Skip to main content

orbit_pool/
policy.rs

1//! The decision: given what the fleet has for a key, what should this
2//! caller do? The pool runs the loop (snapshot, decide, reserve or claim,
3//! retry on a lost race) and the policy makes the choice. No cost model is
4//! built in; a policy that wants one brings its own numbers.
5
6use crate::{Candidate, Key, ResourceId, State};
7
8/// Caller-supplied bounds for one acquisition.
9#[derive(Clone, Copy, Debug)]
10pub struct Limits {
11    /// Fleet-wide ceiling on resources for the key, live plus being made.
12    pub max_live: u32,
13    /// Reservation attempts before the pool gives the decision back as
14    /// [`Plan::Wait`]; a lost race (a candidate turned out busy) costs one.
15    pub attempts: u32,
16}
17
18impl Default for Limits {
19    fn default() -> Self {
20        Self {
21            max_live: 1,
22            attempts: 4,
23        }
24    }
25}
26
27/// What the policy wants done next.
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub enum Decision {
30    /// Reserve capacity on this candidate.
31    Reuse(ResourceId),
32    /// Claim creation budget and make a new resource.
33    Create,
34    /// Nothing usable now; the caller waits for the key to change.
35    Wait,
36    /// Nothing usable and waiting is not the answer.
37    Reject(Reason),
38}
39
40/// Why a policy would not serve the request, for logs and metrics.
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub enum Reason {
43    /// The key has no resources and creation is not allowed.
44    NothingToUse,
45    /// Every resource is at capacity and the caller asked not to wait.
46    Saturated,
47    /// The policy's own rule.
48    Policy(&'static str),
49}
50
51pub trait Policy: Send + Sync {
52    /// `candidates` is a snapshot; `budget` is `(live, creating)` for the
53    /// key. Called again after a lost race with a fresh snapshot.
54    fn decide(
55        &self,
56        key: Key,
57        candidates: &[Candidate],
58        budget: (u32, u32),
59        limits: &Limits,
60    ) -> Decision;
61}
62
63/// The starting policy: a local resource with room, else a remote one with
64/// room (least loaded first), else create within the budget, else wait.
65/// No number from any benchmark is built in; whether remote reuse beats
66/// creation is a measurement the embedder makes and expresses in its own
67/// policy.
68#[derive(Clone, Copy, Debug, Default)]
69pub struct LocalFirst;
70
71impl Policy for LocalFirst {
72    fn decide(
73        &self,
74        _key: Key,
75        candidates: &[Candidate],
76        budget: (u32, u32),
77        limits: &Limits,
78    ) -> Decision {
79        let usable =
80            |candidate: &&Candidate| candidate.state == State::Live && candidate.free() > 0;
81        let least_loaded = |a: &&Candidate, b: &&Candidate| {
82            (a.reserved + a.active, a.last_reserve_ms)
83                .cmp(&(b.reserved + b.active, b.last_reserve_ms))
84        };
85        if let Some(local) = candidates
86            .iter()
87            .filter(|candidate| candidate.local)
88            .filter(usable)
89            .min_by(least_loaded)
90        {
91            return Decision::Reuse(local.id);
92        }
93        if let Some(remote) = candidates
94            .iter()
95            .filter(|candidate| !candidate.local)
96            .filter(usable)
97            .min_by(least_loaded)
98        {
99            return Decision::Reuse(remote.id);
100        }
101        let (live, creating) = budget;
102        if live + creating < limits.max_live {
103            return Decision::Create;
104        }
105        Decision::Wait
106    }
107}
108
109/// Never leaves this process: local reuse, else create, else wait. What a
110/// standalone runtime or a `LocalOnly` profile uses; remote candidates are
111/// invisible to it even when they exist.
112#[derive(Clone, Copy, Debug, Default)]
113pub struct LocalOnly;
114
115impl Policy for LocalOnly {
116    fn decide(
117        &self,
118        _key: Key,
119        candidates: &[Candidate],
120        budget: (u32, u32),
121        limits: &Limits,
122    ) -> Decision {
123        if let Some(local) = candidates
124            .iter()
125            .filter(|candidate| {
126                candidate.local && candidate.state == State::Live && candidate.free() > 0
127            })
128            .min_by_key(|candidate| candidate.reserved + candidate.active)
129        {
130            return Decision::Reuse(local.id);
131        }
132        let (live, creating) = budget;
133        if live + creating < limits.max_live {
134            return Decision::Create;
135        }
136        Decision::Wait
137    }
138}