Skip to content

feat: adaptive bisection with attempts cap (F6b) - #21

Merged
halaprix merged 2 commits into
mainfrom
feat/f6-adaptive-bisection
Jul 23, 2026
Merged

halaprix merged 2 commits into
mainfrom
feat/f6-adaptive-bisection

Conversation

@halaprix

Copy link
Copy Markdown
Owner

Summary

Completes spec F6 (1.2.0) — adaptive bisection on the concurrency pool:

  • adaptiveBatching (opt-in, default false — JSDoc carries the spec's rate-limit-amplification rationale): a batch-level transport rejection splits the batch and retries both halves through the pool's central queue; at length 1 the failure is terminal (rethrown by run, recorded as a per-call kind: 'batch' DominoCallError by runSettled). Reverts inside resolved results are never retried.
  • Attempts cap per original batch (maxBatchAttempts, default 2·⌈log₂(batchSize)⌉+1): enforced at claim time — every execution counts; exhaustion terminalizes unresolved calls as a coarse group with cause = the last transport error (never wrong data). Review hardening: a rejection that consumes the final attempt terminalizes immediately instead of enqueueing unclaimable children (regression-verified fix for a concurrent-cancellation error-loss race).
  • No deadlock by construction: splitting is a synchronous queue push — workers never await their own children; pool=1 with a poisoned batch of 100 terminates (tested). Fail-fast (a)–(d) generalized with real call offsets in terminal ordering; single-poisoned error identity pinned over 100+ shuffled runs; adaptive-off paths byte-identical to F6a (all 227 prior tests unmodified).

Test evidence

240/240 (13 new: isolation with 99 surviving siblings, attempts-cap coarse groups with exact surviving values, deadlock pool=1, reverts-not-retried, multi-poisoned no-identity, exhausted-on-rejection regression), compat-vs-dist 38/38, full gate build-first. Gzip 12.3KB (<15KB).

Spec: spec.md § 1.2.0 F6 (bisection).

halaprix added 2 commits July 23, 2026 19:21
Bisect a rejecting physical batch (length > 1) into two halves and retry
both through the pool's own central queue instead of failing the whole
batch immediately. Bounded per original batch by maxBatchAttempts
(default 2*ceil(log2(batchSize))+1); on exhaustion the unresolved calls
become terminal with the last transport error. Splitting never awaits
its own children — the permit is released before they're enqueued — so
a pool of size 1 can drain an arbitrarily deep bisection tree without
deadlocking.

Off by default (adaptiveBatching: false) so existing run()/runSettled()
behavior is unchanged byte-for-byte; enabling it lets run() rethrow the
isolated raw transport error and runSettled() record per-call kind:
'batch' failures instead of coarsening the whole batch, while reverts
carried in a resolved RawResult[] are never retried.
A length>1 rejection that consumed an original batch's LAST allowed
attempt was still unconditionally pushing both children onto the
queue. Those children only terminalize once claimed, at the
claim-time cap check — but if a sibling original's cancellation wins
the race first, queued items are never claimed at all, so this item's
own terminal error silently never reached terminalErrors. Re-check
attempts remaining before splitting; when none remain, terminalize
this item synchronously in the same tick as its rejection instead.

Regression test: two original batches, conc=2, maxBatchAttempts=2,
where the lower-index original's final attempt is still in flight when
the higher-index original exhausts and cancels first — the lower
index's last transport error must still win the (batchIndex,
callIndex) selection.
@halaprix
halaprix merged commit 7cc105e into main Jul 23, 2026
2 checks passed
@halaprix
halaprix deleted the feat/f6-adaptive-bisection branch July 23, 2026 17:47
halaprix added a commit that referenced this pull request Jul 24, 2026
* feat: adaptive bisection with attempts cap (F6b)

Bisect a rejecting physical batch (length > 1) into two halves and retry
both through the pool's own central queue instead of failing the whole
batch immediately. Bounded per original batch by maxBatchAttempts
(default 2*ceil(log2(batchSize))+1); on exhaustion the unresolved calls
become terminal with the last transport error. Splitting never awaits
its own children — the permit is released before they're enqueued — so
a pool of size 1 can drain an arbitrarily deep bisection tree without
deadlocking.

Off by default (adaptiveBatching: false) so existing run()/runSettled()
behavior is unchanged byte-for-byte; enabling it lets run() rethrow the
isolated raw transport error and runSettled() record per-call kind:
'batch' failures instead of coarsening the whole batch, while reverts
carried in a resolved RawResult[] are never retried.

* fix: terminalize an exhausted-on-rejection bisection item immediately

A length>1 rejection that consumed an original batch's LAST allowed
attempt was still unconditionally pushing both children onto the
queue. Those children only terminalize once claimed, at the
claim-time cap check — but if a sibling original's cancellation wins
the race first, queued items are never claimed at all, so this item's
own terminal error silently never reached terminalErrors. Re-check
attempts remaining before splitting; when none remain, terminalize
this item synchronously in the same tick as its rejection instead.

Regression test: two original batches, conc=2, maxBatchAttempts=2,
where the lower-index original's final attempt is still in flight when
the higher-index original exhausts and cancels first — the lower
index's last transport error must still win the (batchIndex,
callIndex) selection.

---------

Co-authored-by: halaprix <halaprix@users.noreply.github.com>
halaprix added a commit that referenced this pull request Jul 24, 2026
* feat: adaptive bisection with attempts cap (F6b)

Bisect a rejecting physical batch (length > 1) into two halves and retry
both through the pool's own central queue instead of failing the whole
batch immediately. Bounded per original batch by maxBatchAttempts
(default 2*ceil(log2(batchSize))+1); on exhaustion the unresolved calls
become terminal with the last transport error. Splitting never awaits
its own children — the permit is released before they're enqueued — so
a pool of size 1 can drain an arbitrarily deep bisection tree without
deadlocking.

Off by default (adaptiveBatching: false) so existing run()/runSettled()
behavior is unchanged byte-for-byte; enabling it lets run() rethrow the
isolated raw transport error and runSettled() record per-call kind:
'batch' failures instead of coarsening the whole batch, while reverts
carried in a resolved RawResult[] are never retried.

* fix: terminalize an exhausted-on-rejection bisection item immediately

A length>1 rejection that consumed an original batch's LAST allowed
attempt was still unconditionally pushing both children onto the
queue. Those children only terminalize once claimed, at the
claim-time cap check — but if a sibling original's cancellation wins
the race first, queued items are never claimed at all, so this item's
own terminal error silently never reached terminalErrors. Re-check
attempts remaining before splitting; when none remain, terminalize
this item synchronously in the same tick as its rejection instead.

Regression test: two original batches, conc=2, maxBatchAttempts=2,
where the lower-index original's final attempt is still in flight when
the higher-index original exhausts and cancels first — the lower
index's last transport error must still win the (batchIndex,
callIndex) selection.

---------

Co-authored-by: halaprix <halaprix@users.noreply.github.com>
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