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 lefte3— which reads a journal off a disk — a choice between panicking and answeringvec![]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, andReconcilertreats a failure the way it treats a lock it could not take: start nothing, say so, try again next pass. - By lagging.
Self::launchreturns 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§
Sourcefn 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 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.
Sourcefn 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 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.
Sourcefn 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,
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§
Sourcefn 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,
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".