Skip to content

Race borrowed pool-slot permits without per-slot Arc cloning #646

Description

@leynos

Summary

Make the pool's multi-slot permit race borrow stable slot storage for the duration of one acquisition instead of cloning every slot into 'static boxed futures.

This is the second implementation slice for ADR 013 proposal #638 and follows the PoolCore work in #645.

Problem

The current acquisition path:

  1. clones every Arc<PoolSlot> into a newly allocated ordered vector;
  2. moves each clone into a boxed future;
  3. erases the future lifetime, which defaults the trait object to 'static;
  4. races the futures with select_all;
  5. returns the winning slot Arc and permit.

The permit race cannot outlive the acquisition call. The 'static ownership is therefore self-imposed by the type alias rather than required by Tokio.

Proposed design

After #645 gives the pool stable slot storage, represent selection order as indices and race futures borrowing those slots.

A lifetime-aware shape may look like:

type AcquirePermitFuture<'a> = Pin<
    Box<dyn Future<Output = Result<(usize, OwnedSemaphorePermit), ClientError>> + Send + 'a>
>;

Alternatively, use FuturesUnordered, an iterator of concrete futures, or another scoped collection that avoids boxing if the resulting code remains comprehensible.

The winner returns:

  • the stable slot index;
  • the owned permit.

Losing futures are dropped when the winner is selected. They must not leak or retain permits.

Selection order

Preserve the current round-robin starting-point behaviour without allocating and cloning a vector of slots.

Preferred approaches include:

  • iterate 0..len and map logical offset to (start + offset) % len;
  • use a small index iterator that wraps once;
  • collect indices only if a collection materially simplifies cancellation and benchmark evidence justifies it.

Do not rotate or clone the slot objects themselves.

Cancellation safety

Verify the semaphore acquisition operation remains cancellation-safe for losing futures. The race must not:

  • consume a permit without returning it;
  • leave a queued waiter permanently ahead of later acquisitions;
  • grant multiple permits to one acquisition;
  • keep the pool core alive after the caller and scheduler release it.

If Tokio's owned permit acquisition queue semantics make racing every slot undesirable, document and implement a different scoped strategy, such as probing immediate permits before awaiting one selected slot. Preserve fairness at the logical-handle scheduler layer.

Acceptance criteria

  • Permit-race futures carry a scope lifetime rather than defaulting to 'static merely for type erasure.
  • No acquisition clones every slot or every slot Arc.
  • Selection returns (slot_index, OwnedSemaphorePermit) or an equivalent non-owning slot identity.
  • Starting-slot rotation remains observable for pool sizes greater than one.
  • Losing futures release all queue registrations and acquired resources correctly.
  • Cancellation of the outer acquire future leaves every slot usable.
  • The implementation performs no heap allocation proportional to pool size on the uncontended fast path, unless benchmark evidence and a documented trade-off justify one.
  • Pool-size 1, 4, and 16 cases from Establish runtime ownership and task-churn baselines #639 record before/after latency and allocations.
  • Mutation testing: client pool lifecycle and slot rotation survivors #593's slot-rotation mutation is killed by behavioural coverage.

Tests

  • Pool sizes 2 and 4 with all but one slot saturated; assert the free slot wins.
  • Repeated acquisitions verify the rotating start index changes the first attempted slot.
  • Cancel an acquisition while all slots are saturated, release capacity, and assert a later acquisition succeeds without a lost permit.
  • Make two slots become ready together and assert exactly one permit enters the lease.
  • Close the pool while the race is pending and assert prompt typed failure.
  • Include a deterministic or loom-style model for registration/drop races where practical.

Non-goals

  • Replacing the central scheduler; tracked separately.
  • Changing public fairness policy.
  • Removing OwnedSemaphorePermit or the semaphore's legitimate internal shared ownership.
  • Adding dynamic pool resizing.

Dependencies

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    concurrencyenhancementNew feature or requestlowAin't annoying anyone but the QA departmentmediumCould be disruptive, but might not happenperformancerefactor

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions