Lwt cooperative cancellation vs Async with_timeout for reliable file descriptor closure on timeout
0 reputation · 30 May 2022, 08:15 UTC
0 reputation · 30 May 2022, 08:15 UTC
Goal: ensure that when a timeout occurs, any opened file descriptor is closed reliably without leaking resources.
Constraints: Lwt cancellation is cooperative and may not interrupt a blocking system call, so cleanup depends on the operation yielding; Async's with_timeout uses finalizers that may run after the deferred is already resolved, leaving a window where the descriptor stays open; effect-based handlers give deterministic release but require the -effects flag and have limited library support.
Questions: Which of these approaches guarantees that the file descriptor is closed before the timeout handler continues? Can Lwt's cancel be trusted to close descriptors when the operation yields control? Is Async's finalizer timing sufficient to prevent leaks in practice?
35275 reputation · 30 May 2022, 13:31 UTC
For an operation that opens a file descriptor, Async’s with_timeout guarantees the descriptor is closed before the timeout handler continues—provided the I/O uses Async’s cancellable primitives. Lwt’s cooperative cancellation only guarantees closure if you explicitly cancel the promise and attach a finally block (or Lwt.finalize) that closes the descriptor. Without those, the descriptor may remain open until the promise settles or the GC runs.
Lwt.finalize or a try…with…finally block.with_timeout internally creates a deferred that is cancelled after the timeout. Async’s runtime runs any finalizers attached to the I/O resource when the deferred is cancelled, so the descriptor is closed before the timeout handler runs. If the underlying I/O does not register a finalizer, the descriptor may leak; in that case you still need an explicit finally block.with_timeout runs the supplied function and, if the timeout elapses, cancels the deferred and runs any registered finalizers immediately.Open the file descriptor using the library’s I/O primitive (e.g., Async.Unix.openfile or Lwt_unix.openfile).
Wrap the read/write operation in a timeout guard:
let%bind _ = Async.with_timeout 5.0 (fun () -> Async.Unix.read fd buf 0 len)let promise = Lwt_unix.read fd buf 0 len in
Lwt.pick [promise; Lwt_unix.sleep 5.0 |> Lwt.map (fun () -> Lwt.fail Timeout)]Attach a cleanup block:
Async.finalize (fun () -> Async.Unix.close fd) (fun () -> …)Lwt.finalize (fun () -> Lwt_unix.close fd) (fun () -> …)Verify that the descriptor count drops after the timeout by running lsof -p $$ or inspecting /proc/self/fd before and after the operation.
Do you use the native Lwt_unix blocking calls or a library that already provides cancellation support? Knowing this will tell us whether the timeout guard alone is sufficient or if you need an explicit finalizer.
Use comments to ask for clarification. Post a solution as an answer.
35,275 reputation · 30 May 2022, 19:26 UTC
To build on the distinction between these two libraries, it is important to highlight the specific patterns used to prevent resource leaks during timeouts. In Lwt, simply using a timeout is insufficient because cancellation is cooperative; the standard practice for reliable closure is wrapping the operation in Lwt.protect.
Lwt.protect ensures that the cleanup function executes regardless of whether the promise is resolved, rejected, or cancelled, provided the operation eventually yields. Without this, a cancelled promise may leave a file descriptor open until the garbage collector intervenes.
In Async, while with_timeout manages the deferred, relying on GC-based finalizers for file descriptors is risky under high load, as it can lead to descriptor exhaustion before the collector runs. For deterministic release, developers should use Resource.with or explicit finally blocks to ensure the descriptor is closed immediately upon timeout, rather than waiting for the scheduler's finalization cycle.