Skip to content

Context: publisher-owned async work can deadlock close (close/publish dependency cycle) #13

Description

@marcio-absmartly

Split out of #11 review discussion so it can get its own design and review.

Problem

Context.closeAsync() awaits every in-flight publish before finalizing. If work owned by a ContextPublisher calls closeAsync() (or close()) and only completes its transport future after that close settles, the two futures wait on each other and neither ever completes. The context is left permanently closing.

Reproduced by @Pedro-Revez-Silva with a publisher that starts a worker thread, returns an incomplete transport future, has the worker call context.closeAsync(), and completes the transport only once close finishes. An executor callback reaches the same state. The direct same-thread variant (publisher calls closeAsync() inline) is the degenerate case of the same cycle.

This is pre-existing on main, not introduced by #11.

Why the obvious fix does not work

A same-thread guard was attempted on #11 and reverted (436ba37). Rejecting close when the calling thread is inside publish() only catches the inline case; any delegated thread walks straight past it. Chasing the worker thread moves the hole rather than closing it, and the tolerance machinery built around it (same-thread exclusion, reopening closed_/closing_ from the publish failure path, pending-count rechecks before finalization) produced its own defects: close completing successfully while isClosed() == false with events queued, a late publisher failure resurrecting an already-closed context and emitting a second Close, and restoration clearing closing_ letting a concurrent close overwrite closingFuture_.

Suggested direction

Attack the dependency rather than the callback, per @Pedro-Revez-Silva: redesign the close/publish dependency so publisher-owned asynchronous work cannot form this cycle.

Close only awaits in-flight publishes because a failed publish restores its batch (pendingCount_.addAndGet(...) in flush()'s failure handler) and close must not finalize with events pending. Break that edge and no thread — inline, worker, or executor — can form the cycle:

  • have restoration treat events as undeliverable once close has finalized, rather than requiring close to wait; or
  • give close a bounded wait, after which it finalizes and reports the undelivered batch honestly.

Either removes the need for a contract, for reentrancy detection, and for the failure modes above.

Constraints

  • Java 1.6 source/bytecode floor (Android); java8.util.concurrent.CompletableFuture, anonymous inner classes only in main/java.
  • Must preserve the invariant Java SDK: All tests passing (294/294 unit, 138/138 cross-SDK) #11 established: a Context must never end up closed with events pending, and close must never hang.
  • Needs regression coverage for a delegated/executor callback, not just the inline case.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions