Does `fdb_future_set_callback` guarantee exactly-once execution?

I don’t see any mention of it in the docs, but it feel like fdb_future_set_callback guarantees the callback will be called exactly once. Is this something I can depend on?

Specifically, if callback_parameter is a malloced pointer, and I free it in callback, will I have a memory leak?

1 Like

No, fdb_future_set_callback guarantees at most once. And this can indeed be problematic. Let me try to give some context:

A fdb_future is a C wrapper around a flow future (flow is the actor framework FoundationDB is implemented in). A future can even be set to Never - in which case it is guaranteed that your callback will never be called. In practice, an explicit Never should not propagate to the client code (I think we would consider this as a bug otherwise).

So while having exactly once semantic is very desirable it is not guaranteed. And I would argue that it is in general impossible to guarantee exactly once (this would mean that we would need to guarantee that any operation eventually terminates). In fact we had this exact problem in production (we use multiple threads and block on futures and in our case these threads would just block forever - or at least for several days).

As a workaround I would strongly suggest to set the option transaction_timeout on each transaction that you start. From 6.2 on you can set this on the database level and it will be set for each transaction for you. There is a very strong argument for setting this option by default to something that makes sense (5-10s maybe?) but this makes testing harder (if I remember the discussion correctly - otherwise I believe @ajbeamon might be able to explain this better than I can).

If you set a transaction timeout I think exactly once is guaranteed (unless there are bugs - which of course is possible).

1 Like

It’s true that we can’t guarantee that an operation will complete (and thus call the callback), but I think it’s the case that canceling or destroying the future will cause the callback to complete with an error. Thus you could guarantee that the callback would fire if you canceled or destroyed the future at some point.

Something else that we rely on in some of our bindings is that destroying a transaction will terminate all the futures associated with the transaction. For example, in Java we have explicit APIs for cleaning up native objects (i.e. by calling close on their Java wrapper objects). We don’t require this for future types, though, and the only way for them to be cleaned up is by them being completed. We depend on the user destroying the transaction to clean up any stray ones that haven’t yet fired.

This option is actually available in 6.1, see for example Python API — FoundationDB 7.1.

I think if you choose a default that makes sense for your use-case and test it, it should be fine. My main concerns were around what happens when we’ve chosen the default for you and you don’t test what happens when you encounter it (which is also relevant when the default is no timeout, as it currently is).

Thanks! This sounds like it is good enough for my purposes, I’m not too worried about the halting problem. I just wanted to make sure that there wasn’t a code path where cancellation/destruction of the future would prevent the callback from firing.