Skip to main content

RunnerLauncher

Trait RunnerLauncher 

Source
pub trait RunnerLauncher:
    Debug
    + Send
    + Sync {
    // Required methods
    fn attempts<'life0, 'async_trait>(
        &'life0 self,
    ) -> Pin<Box<dyn Future<Output = Result<Vec<RunnerAttempt>, LaunchFailure>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait;
    fn launch<'life0, 'life1, 'async_trait>(
        &'life0 self,
        request: LaunchRequest<'life1>,
    ) -> Pin<Box<dyn Future<Output = Result<RunnerAttempt, LaunchFailure>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait;
    fn clean<'life0, 'async_trait>(
        &'life0 self,
        attempt: AttemptId,
    ) -> Pin<Box<dyn Future<Output = Result<(), LaunchFailure>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait;

    // Provided method
    fn supervise<'life0, 'life1, 'async_trait>(
        &'life0 self,
        _policy: &'life1 ScalePolicy,
    ) -> Pin<Box<dyn Future<Output = Result<Vec<ReplacementIntent>, LaunchFailure>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait { ... }
}
Expand description

The seam between the decision to start a runner and the act of starting one.

e3 implements this; every test in this file fakes it, which is what makes the whole allocator path decidable with no process, no filesystem and no network.

§Why the attempt set comes through the same port as the launch

b1 makes this argument at HostAllocator::from_attempts and it applies one layer up: the host-wide total (D9) and every per-policy count (D7) are two questions asked of one set, and a design that let the caller supply the set separately from the thing that creates its members is a design in which the two can disagree. Worse, it makes &[] expressible — and an empty attempt set is exactly the shape that drops the - active_owned_runners term, starts a second runner for a job already being served, and reports no error while doing it.

So the launcher is asked, under the allocation lock, immediately before each runtime is created. There is no second supply point and no cached copy.

§The two ways an implementer can say “I hold no attempts”

The argument above closes the hole for a caller. It stayed open one level down for the implementer, in two shapes that both oversubscribe the machine and neither of which reports anything:

  • By failing. attempts() used to be infallible, which left e3 — which reads a journal off a disk — a choice between panicking and answering vec![] on an I/O error. An empty set is indistinguishable from an idle host, so a transient read failure reads as “nothing is running” and the next pass allocates the whole machine for jobs already being served. It is fallible now, and Reconciler treats a failure the way it treats a lock it could not take: start nothing, say so, try again next pass.
  • By lagging. Self::launch returns the attempt it created rather than its identifier, so the caller can carry it. See that method for the measurement that made this necessary.

Required Methods§

Source

fn attempts<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<Vec<RunnerAttempt>, LaunchFailure>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Every attempt this host holds, across every policy, terminal ones included.

Terminal attempts are included rather than filtered out because the caller needs both answers from one set: AttemptState::counts_against_capacity decides the ceiling, and the terminal ones are what RunnerLauncher::clean is for.

§Errors

LaunchFailure when the set could not be read. Never answer Ok with an empty vector to signal a failure — the caller cannot tell that from an idle host, and the two lead to opposite actions.

Source

fn launch<'life0, 'life1, 'async_trait>( &'life0 self, request: LaunchRequest<'life1>, ) -> Pin<Box<dyn Future<Output = Result<RunnerAttempt, LaunchFailure>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Create exactly one runtime and start one runner, and return the attempt that now exists.

Called once per grant, with the host-wide allocation lock held.

§The attempt is returned, not just its identifier

The host ceiling is enforced against a host-wide total, and that total is recomputed from Self::attempts on every hold. If a launch is not yet visible there when the next policy is allocated for — a journal write that has not landed, an asynchronous store, a cache — then that policy’s grant is computed from a set missing the previous policy’s runners, and it is too large.

That is measured, not hypothetical. With a launcher whose attempts never became visible, two policies on a host of three started six runners, with the allocation lock held correctly throughout: host_capacity=3, started=6, launches=6. Serialisation was never the problem; the arithmetic under it was reading a stale set.

So an implementer should make the new attempt visible to Self::attempts before returning — and the caller does not depend on it. Reconciler carries what this pass created and merges it, by RunnerAttempt::id, with whatever the launcher reports. A launcher that honours the contract is not double-counted, and one that lags cannot oversubscribe the host.

§Every call must return a fresh RunnerAttempt::id

This is a requirement, not a convention, because the merge above is what carries the host ceiling and the merge is keyed on the identifier. Two calls that answer with the same id are two runtimes that the host-wide total counts once, and the machine is then allocated past host_capacity: probed at host_capacity = 3 with one slot already busy and a launcher answering with a duplicate id, the pass started four runners for five occupied slots.

That is a narrower defect than the lagging launcher above — that one needed no bug at all, this one needs a broken id generator — but e3 is the implementor and cannot honour a requirement nobody states. Reconciler::host_attempts carries a debug_assert that fires on a collision, so a development build finds it at the first duplicate rather than through an oversubscribed host.

§Errors

LaunchFailure, carrying the FailureReason e3 recorded.

Source

fn clean<'life0, 'async_trait>( &'life0 self, attempt: AttemptId, ) -> Pin<Box<dyn Future<Output = Result<(), LaunchFailure>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Remove a terminal attempt’s runtime and mark it cleaned.

Never called for a non-terminal attempt: capacity is reclaimed when an attempt reaches a terminal state and at no other time.

§Errors

LaunchFailure, carrying the FailureReason e3 recorded.

Provided Methods§

Source

fn supervise<'life0, 'life1, 'async_trait>( &'life0 self, _policy: &'life1 ScalePolicy, ) -> Pin<Box<dyn Future<Output = Result<Vec<ReplacementIntent>, LaunchFailure>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Reconcile this policy’s existing processes before demand is read and capacity is recomputed. A concluded pre-acceptance attempt thereby becomes an ordinary allocation candidate in this same pass; replacement never bypasses the allocator.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§