Skip to content

Fix single-shot blocking-command effect and unsettled span on interrupt#154

Merged
ghostdogpr merged 1 commit into
mainfrom
fix/blocking-lease-single-shot
Jul 2, 2026
Merged

Fix single-shot blocking-command effect and unsettled span on interrupt#154
ghostdogpr merged 1 commit into
mainfrom
fix/blocking-lease-single-shot

Conversation

@ghostdogpr

Copy link
Copy Markdown
Owner

Two regressions from #152 in the blocking-command path, across all three runtimes.

H1 — a blocking-command effect is single-shot; re-running it hangs

val lease = new DedicatedPool.Lease was evaluated when run(command) was called, so one lease got captured inside the returned effect value. CIO.ensure's finalizer runs cancel() on every exit — including success — driving that lease to the terminal Cancelled state. Re-executing the same value (.retry, .repeat, .forever, …) made Lease.attach fail its CAS, discard the freshly acquired connection, skip the submit, and never invoke the callback: the fiber hangs silently.

It was self-triggering in shipped code: Paged.consume re-runs its single tailNew effect value each poll round, so xConsume delivered one blocking round and then stalled forever on the zio/ce/kyo backends.

Fix: acquire a fresh lease per execution in the standalone, cluster, and master-replica runtimes, expressed as acquireReleaseWith with the lease as the acquired resource (CIO.defer for the acquire — value is strict on the CE backend and would allocate once).

M1 — interrupting an in-flight blocking command never settles observability

The cancel path released the connection but never invoked the tracked callback, so the trace span settled zero times and no CommandCompleted fired.

Fix: thread an onInterrupt settle action through the lease. cancel runs it for a held connection; leaseAndSubmit runs it directly when attach finds the lease already cancelled — an interrupt in the offloaded acquire window or between redirect attempts, where cancel saw no held connection and so could not settle the callback itself. The state CAS keeps this exclusive with the reply's finish, so the callback fires exactly once. It uses ConnectionLost(mayHaveExecuted = true), which in the cluster reply path terminally completes (Fault.Lost(true)) rather than triggering a retry.

Tests

  • ZioSmokeSuite: re-running the same blocking effect value twice succeeds each round (fails by hanging without the fix).
  • DedicatedPoolSpec: interrupt of an in-flight command settles + discards the slot; interrupt before attach (offloaded-acquire window) still settles; a cancel after a normal reply does not re-fire.

All six client backend cells compile; DedicatedPoolSpec (20) and ZioSmokeSuite (12) pass; scalafmt clean.

A blocking-command effect allocated its DedicatedPool.Lease when run(command)
was called, so one lease was captured inside the returned effect value.
CIO.ensure's finalizer cancels the lease on every exit (including success),
driving it to the terminal Cancelled state, so re-executing the same value
made Lease.attach fail its CAS, discard the connection, skip the submit, and
never invoke the callback: the fiber hung silently. This was self-triggering
via Paged.consume, which re-runs its single tailNew value each poll round, so
xConsume stalled after one blocking round on the zio/ce/kyo backends.

Acquire a fresh lease per execution in all three runtimes (standalone,
cluster, master-replica) via acquireReleaseWith with the lease as the acquired
resource.

Also settle the tracked callback on interrupt so a started span still fires
CommandCompleted: the cancel path released the connection but never invoked
the callback. cancel now runs the settle action for a held connection, and
leaseAndSubmit settles directly when attach finds the lease already cancelled
(an interrupt in the offloaded acquire window or between redirect attempts,
where cancel saw no held connection). The state CAS keeps this exclusive with
the reply's finish, so the callback fires exactly once.

Regression tests: re-running a blocking effect value twice (ZioSmokeSuite),
and the interrupt/pre-attach settle paths (DedicatedPoolSpec).
@ghostdogpr ghostdogpr merged commit 2126080 into main Jul 2, 2026
9 checks passed
@ghostdogpr ghostdogpr deleted the fix/blocking-lease-single-shot branch July 2, 2026 08:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant