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}