supervisor_machines 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +28 -0
- data/Cargo.lock +286 -0
- data/Cargo.toml +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +140 -0
- data/ext/supervisor_machines_native/core/Cargo.toml +24 -0
- data/ext/supervisor_machines_native/core/src/lib.rs +128 -0
- data/ext/supervisor_machines_native/core/src/supervisor.rs +155 -0
- data/ext/supervisor_machines_native/extconf.rb +3 -0
- data/ext/supervisor_machines_native/ffi/Cargo.toml +19 -0
- data/ext/supervisor_machines_native/ffi/extconf.rb +21 -0
- data/ext/supervisor_machines_native/ffi/src/decider.rs +199 -0
- data/ext/supervisor_machines_native/ffi/src/lib.rs +135 -0
- data/lib/supervisor_machines/errors.rb +9 -0
- data/lib/supervisor_machines/supervisor.rb +320 -0
- data/lib/supervisor_machines/version.rb +5 -0
- data/lib/supervisor_machines.rb +14 -0
- data/sig/supervisor_machines.rbs +173 -0
- metadata +111 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
//! The decision machine itself.
|
|
2
|
+
|
|
3
|
+
use crate::{Decision, Exit, Intensity, Policy, Span, Strategy};
|
|
4
|
+
|
|
5
|
+
/// Holds the strategy, the restart intensity, and the recent restart history.
|
|
6
|
+
///
|
|
7
|
+
/// `CHILDREN` is how many children the supervisor owns, which fixes the
|
|
8
|
+
/// strategy spans. `HISTORY` is the restart timestamps kept; it must exceed the
|
|
9
|
+
/// intensity's restart count, or a single burst could not be measured.
|
|
10
|
+
///
|
|
11
|
+
/// ```
|
|
12
|
+
/// use supervisor_machines::{Decision, Exit, Intensity, Policy, Span, Strategy, Supervisor};
|
|
13
|
+
///
|
|
14
|
+
/// const SECOND: u64 = 1_000_000_000;
|
|
15
|
+
///
|
|
16
|
+
/// // Three children that only work as a set, two restarts per minute allowed.
|
|
17
|
+
/// let mut tree: Supervisor<3, 8> = Supervisor::new(
|
|
18
|
+
/// Strategy::OneForAll,
|
|
19
|
+
/// Intensity::new(2, 60 * SECOND),
|
|
20
|
+
/// );
|
|
21
|
+
///
|
|
22
|
+
/// // The middle child dies badly, so all three come back.
|
|
23
|
+
/// let decision = tree.stopped(1, Policy::Permanent, Exit::Abnormal, 0);
|
|
24
|
+
/// assert_eq!(decision, Decision::Restart(Span { first: 0, last: 2 }));
|
|
25
|
+
///
|
|
26
|
+
/// // A temporary child is never worth restarting.
|
|
27
|
+
/// assert_eq!(
|
|
28
|
+
/// tree.stopped(2, Policy::Temporary, Exit::Abnormal, SECOND),
|
|
29
|
+
/// Decision::Ignore,
|
|
30
|
+
/// );
|
|
31
|
+
/// ```
|
|
32
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
33
|
+
pub struct Supervisor<const CHILDREN: usize, const HISTORY: usize> {
|
|
34
|
+
strategy: Strategy,
|
|
35
|
+
intensity: Intensity,
|
|
36
|
+
history: [u64; HISTORY],
|
|
37
|
+
recorded: usize,
|
|
38
|
+
next: usize,
|
|
39
|
+
terminated: bool,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
impl<const CHILDREN: usize, const HISTORY: usize> Supervisor<CHILDREN, HISTORY> {
|
|
43
|
+
/// Rejects a shape that could not measure its own intensity.
|
|
44
|
+
const SHAPE: () = {
|
|
45
|
+
assert!(
|
|
46
|
+
CHILDREN > 0,
|
|
47
|
+
"a supervisor with no children has nothing to decide"
|
|
48
|
+
);
|
|
49
|
+
assert!(
|
|
50
|
+
HISTORY > 0,
|
|
51
|
+
"restart history needs room for at least one restart"
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
#[must_use]
|
|
56
|
+
pub const fn new(strategy: Strategy, intensity: Intensity) -> Self {
|
|
57
|
+
let () = Self::SHAPE;
|
|
58
|
+
Self {
|
|
59
|
+
strategy,
|
|
60
|
+
intensity,
|
|
61
|
+
history: [0; HISTORY],
|
|
62
|
+
recorded: 0,
|
|
63
|
+
next: 0,
|
|
64
|
+
terminated: false,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[must_use]
|
|
69
|
+
pub const fn strategy(&self) -> Strategy {
|
|
70
|
+
self.strategy
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#[must_use]
|
|
74
|
+
pub const fn intensity(&self) -> Intensity {
|
|
75
|
+
self.intensity
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// True once the supervisor has escalated. It stays true: a supervisor that
|
|
79
|
+
/// gave up does not change its mind.
|
|
80
|
+
#[must_use]
|
|
81
|
+
pub const fn terminated(&self) -> bool {
|
|
82
|
+
self.terminated
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Restarts still inside the window as of `now_ns`.
|
|
86
|
+
#[must_use]
|
|
87
|
+
pub const fn recent_restarts(&self, now_ns: u64) -> u32 {
|
|
88
|
+
let cutoff = now_ns.saturating_sub(self.intensity.window_ns);
|
|
89
|
+
let mut counted = 0;
|
|
90
|
+
let mut index = 0;
|
|
91
|
+
while index < self.recorded {
|
|
92
|
+
if self.history[index] >= cutoff {
|
|
93
|
+
counted += 1;
|
|
94
|
+
}
|
|
95
|
+
index += 1;
|
|
96
|
+
}
|
|
97
|
+
counted
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// The children a failure at `index` takes with it under this strategy.
|
|
101
|
+
#[must_use]
|
|
102
|
+
pub const fn span(&self, index: usize) -> Span {
|
|
103
|
+
match self.strategy {
|
|
104
|
+
Strategy::OneForOne => Span::single(index),
|
|
105
|
+
Strategy::OneForAll => Span {
|
|
106
|
+
first: 0,
|
|
107
|
+
last: CHILDREN - 1,
|
|
108
|
+
},
|
|
109
|
+
Strategy::RestForOne => Span {
|
|
110
|
+
first: index,
|
|
111
|
+
last: CHILDREN - 1,
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const fn record(&mut self, now_ns: u64) {
|
|
117
|
+
self.history[self.next] = now_ns;
|
|
118
|
+
self.next = (self.next + 1) % HISTORY;
|
|
119
|
+
if self.recorded < HISTORY {
|
|
120
|
+
self.recorded += 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Decide what to do about the child at `index`.
|
|
125
|
+
///
|
|
126
|
+
/// One restart is recorded per failure handled, not per child restarted.
|
|
127
|
+
pub const fn stopped(
|
|
128
|
+
&mut self,
|
|
129
|
+
index: usize,
|
|
130
|
+
policy: Policy,
|
|
131
|
+
exit: Exit,
|
|
132
|
+
now_ns: u64,
|
|
133
|
+
) -> Decision {
|
|
134
|
+
if self.terminated {
|
|
135
|
+
return Decision::Escalate;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
match policy {
|
|
139
|
+
Policy::Temporary => return Decision::Ignore,
|
|
140
|
+
Policy::Transient if matches!(exit, Exit::Normal) => return Decision::Ignore,
|
|
141
|
+
Policy::Permanent | Policy::Transient => {}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
self.record(now_ns);
|
|
145
|
+
if self.recent_restarts(now_ns) > self.intensity.restarts {
|
|
146
|
+
self.terminated = true;
|
|
147
|
+
return Decision::Escalate;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
Decision::Restart(self.span(index))
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#[cfg(test)]
|
|
155
|
+
mod tests;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "supervisor_machines_native"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
rust-version.workspace = true
|
|
6
|
+
authors = ["Abdelkader Boudih <oss@seuros.com>"]
|
|
7
|
+
license = "MIT"
|
|
8
|
+
description = "Ruby bindings for the supervisor-machines decision machine"
|
|
9
|
+
publish = false
|
|
10
|
+
|
|
11
|
+
[lib]
|
|
12
|
+
crate-type = ["cdylib"]
|
|
13
|
+
|
|
14
|
+
[lints]
|
|
15
|
+
workspace = true
|
|
16
|
+
|
|
17
|
+
[dependencies]
|
|
18
|
+
supervisor-machines = { workspace = true, features = ["std"] }
|
|
19
|
+
magnus = { version = ">= 0.9.3", default-features = false, features = ["io"] }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mkmf'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
require 'rb_sys/mkmf'
|
|
6
|
+
|
|
7
|
+
create_rust_makefile('supervisor_machines_native/supervisor_machines_native') do |r|
|
|
8
|
+
r.ext_dir = Pathname(__dir__).relative_path_from(Pathname(Dir.pwd)).to_s
|
|
9
|
+
r.profile = ENV.fetch('RB_SYS_CARGO_PROFILE', :release).to_sym
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# rake-compiler builds from tmp/, where the generated relative manifest path no
|
|
13
|
+
# longer resolves.
|
|
14
|
+
makefile = File.join(Dir.pwd, 'Makefile')
|
|
15
|
+
File.write(
|
|
16
|
+
makefile,
|
|
17
|
+
File.read(makefile).gsub(
|
|
18
|
+
/^RB_SYS_CARGO_MANIFEST_DIR \?=.*$/,
|
|
19
|
+
"RB_SYS_CARGO_MANIFEST_DIR ?= #{File.expand_path(__dir__)}"
|
|
20
|
+
)
|
|
21
|
+
)
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
//! `SupervisorMachines::Decider`: the core's decision machine, bound to Ruby.
|
|
2
|
+
//!
|
|
3
|
+
//! `Supervisor` carries its child count and its restart history in the type,
|
|
4
|
+
//! so a runtime child count is dispatched to one monomorphised shape per
|
|
5
|
+
//! supported count and kept behind [`Decide`].
|
|
6
|
+
|
|
7
|
+
use std::cell::RefCell;
|
|
8
|
+
|
|
9
|
+
use magnus::scan_args::{get_kwargs, scan_args};
|
|
10
|
+
use magnus::{Error, RArray, RHash, RModule, Ruby, Value, function, method, prelude::*};
|
|
11
|
+
use supervisor_machines::{Decision, Exit, Intensity, Policy, Strategy, Supervisor};
|
|
12
|
+
|
|
13
|
+
use crate::{Whole, decision, exit, policy, strategy, strategy_name};
|
|
14
|
+
|
|
15
|
+
/// Restart timestamps every shape keeps.
|
|
16
|
+
const HISTORY: usize = 64;
|
|
17
|
+
|
|
18
|
+
/// The core escalates one restart past the intensity, so the history has to
|
|
19
|
+
/// hold one more timestamp than the intensity tolerates.
|
|
20
|
+
#[expect(clippy::cast_possible_truncation)]
|
|
21
|
+
const MAX_RESTARTS: u32 = HISTORY as u32 - 1;
|
|
22
|
+
|
|
23
|
+
/// The child counts a [`Decider`] can be built for.
|
|
24
|
+
const SUPPORTED_CHILDREN: [usize; 17] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 32];
|
|
25
|
+
|
|
26
|
+
/// A [`Supervisor`] with its child count erased. A supervision tree is plain
|
|
27
|
+
/// data, so `Send` costs nothing and is what magnus asks of wrapped types.
|
|
28
|
+
trait Decide: Send {
|
|
29
|
+
fn children(&self) -> usize;
|
|
30
|
+
fn strategy(&self) -> Strategy;
|
|
31
|
+
fn terminated(&self) -> bool;
|
|
32
|
+
fn recent_restarts(&self, now_ns: u64) -> u32;
|
|
33
|
+
fn stopped(&mut self, index: usize, policy: Policy, exit: Exit, now_ns: u64) -> Decision;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// One monomorphised supervision tree. The newtype keeps the trait's method
|
|
37
|
+
/// names from shadowing the core's inherent ones.
|
|
38
|
+
struct Shape<const CHILDREN: usize>(Supervisor<CHILDREN, HISTORY>);
|
|
39
|
+
|
|
40
|
+
impl<const CHILDREN: usize> Decide for Shape<CHILDREN> {
|
|
41
|
+
fn children(&self) -> usize {
|
|
42
|
+
CHILDREN
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn strategy(&self) -> Strategy {
|
|
46
|
+
self.0.strategy()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn terminated(&self) -> bool {
|
|
50
|
+
self.0.terminated()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn recent_restarts(&self, now_ns: u64) -> u32 {
|
|
54
|
+
self.0.recent_restarts(now_ns)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn stopped(&mut self, index: usize, policy: Policy, exit: Exit, now_ns: u64) -> Decision {
|
|
58
|
+
self.0.stopped(index, policy, exit, now_ns)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Each shape is built behind its own call frame. Inlined, a debug build sizes
|
|
63
|
+
/// one frame for every arm of the match in [`Wrapper::new`] at once, which
|
|
64
|
+
/// overflows Ruby's fiber stack.
|
|
65
|
+
#[inline(never)]
|
|
66
|
+
fn boxed<const CHILDREN: usize>(strategy: Strategy, intensity: Intensity) -> Box<dyn Decide> {
|
|
67
|
+
Box::new(Shape(Supervisor::<CHILDREN, HISTORY>::new(
|
|
68
|
+
strategy, intensity,
|
|
69
|
+
)))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn unsupported(ruby: &Ruby, children: usize) -> Error {
|
|
73
|
+
Error::new(
|
|
74
|
+
ruby.exception_arg_error(),
|
|
75
|
+
format!(
|
|
76
|
+
"{children} children is not a supported shape, expected one of {SUPPORTED_CHILDREN:?}"
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[magnus::wrap(class = "SupervisorMachines::Decider", free_immediately, size)]
|
|
82
|
+
struct Wrapper(RefCell<Box<dyn Decide>>);
|
|
83
|
+
|
|
84
|
+
impl Wrapper {
|
|
85
|
+
fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
|
|
86
|
+
let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
|
|
87
|
+
let (Whole(children), name, Whole(restarts), Whole(window_ns)) =
|
|
88
|
+
get_kwargs::<_, (Whole<usize>, Value, Whole<u32>, Whole<u64>), (), ()>(
|
|
89
|
+
args.keywords,
|
|
90
|
+
&["children", "strategy", "restarts", "window_ns"],
|
|
91
|
+
&[],
|
|
92
|
+
)?
|
|
93
|
+
.required;
|
|
94
|
+
if restarts > MAX_RESTARTS {
|
|
95
|
+
return Err(Error::new(
|
|
96
|
+
ruby.exception_arg_error(),
|
|
97
|
+
format!("{restarts} restarts cannot escalate, the most is {MAX_RESTARTS}"),
|
|
98
|
+
));
|
|
99
|
+
}
|
|
100
|
+
let strategy = strategy(ruby, name)?;
|
|
101
|
+
let intensity = Intensity::new(restarts, window_ns);
|
|
102
|
+
let tree = match children {
|
|
103
|
+
1 => boxed::<1>(strategy, intensity),
|
|
104
|
+
2 => boxed::<2>(strategy, intensity),
|
|
105
|
+
3 => boxed::<3>(strategy, intensity),
|
|
106
|
+
4 => boxed::<4>(strategy, intensity),
|
|
107
|
+
5 => boxed::<5>(strategy, intensity),
|
|
108
|
+
6 => boxed::<6>(strategy, intensity),
|
|
109
|
+
7 => boxed::<7>(strategy, intensity),
|
|
110
|
+
8 => boxed::<8>(strategy, intensity),
|
|
111
|
+
9 => boxed::<9>(strategy, intensity),
|
|
112
|
+
10 => boxed::<10>(strategy, intensity),
|
|
113
|
+
11 => boxed::<11>(strategy, intensity),
|
|
114
|
+
12 => boxed::<12>(strategy, intensity),
|
|
115
|
+
13 => boxed::<13>(strategy, intensity),
|
|
116
|
+
14 => boxed::<14>(strategy, intensity),
|
|
117
|
+
15 => boxed::<15>(strategy, intensity),
|
|
118
|
+
16 => boxed::<16>(strategy, intensity),
|
|
119
|
+
32 => boxed::<32>(strategy, intensity),
|
|
120
|
+
_ => return Err(unsupported(ruby, children)),
|
|
121
|
+
};
|
|
122
|
+
Ok(Self(RefCell::new(tree)))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Feeds one stopped child to the core and returns what it decided:
|
|
126
|
+
/// `:ignore`, `:escalate`, or the inclusive range of children to restart.
|
|
127
|
+
fn stopped(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Value, Error> {
|
|
128
|
+
let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
|
|
129
|
+
let (Whole(index), name, ending, Whole(now_ns)) =
|
|
130
|
+
get_kwargs::<_, (Whole<usize>, Value, Value, Whole<u64>), (), ()>(
|
|
131
|
+
args.keywords,
|
|
132
|
+
&["index", "policy", "exit", "now_ns"],
|
|
133
|
+
&[],
|
|
134
|
+
)?
|
|
135
|
+
.required;
|
|
136
|
+
let (policy, ending) = (policy(ruby, name)?, exit(ruby, ending)?);
|
|
137
|
+
let children = rb_self.0.borrow().children();
|
|
138
|
+
if index >= children {
|
|
139
|
+
return Err(Error::new(
|
|
140
|
+
ruby.exception_arg_error(),
|
|
141
|
+
format!("index {index} is outside the {children} children"),
|
|
142
|
+
));
|
|
143
|
+
}
|
|
144
|
+
let verdict = rb_self
|
|
145
|
+
.0
|
|
146
|
+
.borrow_mut()
|
|
147
|
+
.stopped(index, policy, ending, now_ns);
|
|
148
|
+
decision(ruby, verdict)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
fn children(&self) -> usize {
|
|
152
|
+
self.0.borrow().children()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fn strategy(ruby: &Ruby, rb_self: &Self) -> Value {
|
|
156
|
+
strategy_name(ruby, rb_self.0.borrow().strategy()).as_value()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fn terminated(&self) -> bool {
|
|
160
|
+
self.0.borrow().terminated()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fn recent_restarts(&self, Whole(now_ns): Whole<u64>) -> u32 {
|
|
164
|
+
self.0.borrow().recent_restarts(now_ns)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fn inspect(ruby: &Ruby, rb_self: &Self) -> String {
|
|
168
|
+
format!(
|
|
169
|
+
"#<SupervisorMachines::Decider strategy={} children={} terminated={}>",
|
|
170
|
+
Self::strategy(ruby, rb_self).inspect(),
|
|
171
|
+
rb_self.children(),
|
|
172
|
+
rb_self.terminated()
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
|
|
178
|
+
let class = module.define_class("Decider", ruby.class_object())?;
|
|
179
|
+
class.const_set("HISTORY", HISTORY)?;
|
|
180
|
+
class.const_set("MAX_RESTARTS", MAX_RESTARTS)?;
|
|
181
|
+
class.const_set("SUPPORTED_CHILDREN", supported_children(ruby)?)?;
|
|
182
|
+
class.define_singleton_method("new", function!(Wrapper::new, -1))?;
|
|
183
|
+
class.define_method("stopped", method!(Wrapper::stopped, -1))?;
|
|
184
|
+
class.define_method("children", method!(Wrapper::children, 0))?;
|
|
185
|
+
class.define_method("strategy", method!(Wrapper::strategy, 0))?;
|
|
186
|
+
class.define_method("terminated?", method!(Wrapper::terminated, 0))?;
|
|
187
|
+
class.define_method("recent_restarts", method!(Wrapper::recent_restarts, 1))?;
|
|
188
|
+
class.define_method("inspect", method!(Wrapper::inspect, 0))?;
|
|
189
|
+
Ok(())
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fn supported_children(ruby: &Ruby) -> Result<RArray, Error> {
|
|
193
|
+
let array = ruby.ary_new_capa(SUPPORTED_CHILDREN.len());
|
|
194
|
+
for children in SUPPORTED_CHILDREN {
|
|
195
|
+
array.push(children)?;
|
|
196
|
+
}
|
|
197
|
+
array.freeze();
|
|
198
|
+
Ok(array)
|
|
199
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
//! Ruby bindings for the `supervisor-machines` decision machine.
|
|
2
|
+
//!
|
|
3
|
+
//! The core decides and nothing else: this crate converts Ruby symbols into
|
|
4
|
+
//! its enums, hands back a `Decision` as a symbol or a `Range`, and leaves
|
|
5
|
+
//! spawning, joining and killing to `SupervisorMachines::Supervisor`.
|
|
6
|
+
//!
|
|
7
|
+
//! Durations cross into the core as nanosecond integers. Seconds belong to the
|
|
8
|
+
//! Ruby surface, which converts them with [`NS_PER_SECOND`].
|
|
9
|
+
|
|
10
|
+
#![allow(clippy::needless_pass_by_value)]
|
|
11
|
+
|
|
12
|
+
mod decider;
|
|
13
|
+
|
|
14
|
+
use magnus::{Error, Integer, RArray, Ruby, Symbol, TryConvert, Value, prelude::*};
|
|
15
|
+
use supervisor_machines::{DEFAULT_RESTARTS, DEFAULT_WINDOW_NS, Decision, Exit, Policy, Strategy};
|
|
16
|
+
|
|
17
|
+
/// Nanoseconds in a second.
|
|
18
|
+
pub(crate) const NS_PER_SECOND: u32 = 1_000_000_000;
|
|
19
|
+
|
|
20
|
+
/// A whole number of things: a child count, a child index, a restart
|
|
21
|
+
/// intensity, a nanosecond reading.
|
|
22
|
+
///
|
|
23
|
+
/// magnus reads an integer through Ruby's `NUM2*` family, which truncates a
|
|
24
|
+
/// `Float` towards zero instead of refusing it, so an argument that means an
|
|
25
|
+
/// integer is read as one of these and anything but an `Integer` is turned
|
|
26
|
+
/// away. An `Integer` outside the target width still comes back as the
|
|
27
|
+
/// `RangeError` magnus raises for it. Seconds belong to the Ruby surface,
|
|
28
|
+
/// which converts them before they reach here.
|
|
29
|
+
pub(crate) struct Whole<T>(pub(crate) T);
|
|
30
|
+
|
|
31
|
+
impl<T: TryConvert> TryConvert for Whole<T> {
|
|
32
|
+
fn try_convert(value: Value) -> Result<Self, Error> {
|
|
33
|
+
if Integer::from_value(value).is_none() {
|
|
34
|
+
return Err(not_whole(value));
|
|
35
|
+
}
|
|
36
|
+
T::try_convert(value).map(Self)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fn not_whole(value: Value) -> Error {
|
|
41
|
+
let ruby = Ruby::get_with(value);
|
|
42
|
+
Error::new(
|
|
43
|
+
ruby.exception_type_error(),
|
|
44
|
+
format!(
|
|
45
|
+
"no implicit conversion of {} into Integer: {}",
|
|
46
|
+
value.class(),
|
|
47
|
+
value.inspect()
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Every [`Strategy`], in the order the core declares them.
|
|
53
|
+
pub(crate) const STRATEGIES: [(&str, Strategy); 3] = [
|
|
54
|
+
("one_for_one", Strategy::OneForOne),
|
|
55
|
+
("one_for_all", Strategy::OneForAll),
|
|
56
|
+
("rest_for_one", Strategy::RestForOne),
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/// Every [`Policy`], in the order the core declares them.
|
|
60
|
+
pub(crate) const POLICIES: [(&str, Policy); 3] = [
|
|
61
|
+
("permanent", Policy::Permanent),
|
|
62
|
+
("transient", Policy::Transient),
|
|
63
|
+
("temporary", Policy::Temporary),
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/// Every [`Exit`], in the order the core declares them.
|
|
67
|
+
pub(crate) const EXITS: [(&str, Exit); 2] =
|
|
68
|
+
[("normal", Exit::Normal), ("abnormal", Exit::Abnormal)];
|
|
69
|
+
|
|
70
|
+
/// Resolves a symbol against one of the closed sets above.
|
|
71
|
+
fn member<T: Copy>(ruby: &Ruby, kind: &str, value: Value, set: &[(&str, T)]) -> Result<T, Error> {
|
|
72
|
+
let name = Symbol::from_value(value).and_then(|symbol| symbol.name().ok());
|
|
73
|
+
name.as_deref()
|
|
74
|
+
.and_then(|name| set.iter().find(|(known, _)| *known == name))
|
|
75
|
+
.map(|&(_, member)| member)
|
|
76
|
+
.ok_or_else(|| {
|
|
77
|
+
Error::new(
|
|
78
|
+
ruby.exception_arg_error(),
|
|
79
|
+
format!("unknown {kind}: {}", value.inspect()),
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pub(crate) fn strategy(ruby: &Ruby, value: Value) -> Result<Strategy, Error> {
|
|
85
|
+
member(ruby, "strategy", value, &STRATEGIES)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
pub(crate) fn policy(ruby: &Ruby, value: Value) -> Result<Policy, Error> {
|
|
89
|
+
member(ruby, "policy", value, &POLICIES)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pub(crate) fn exit(ruby: &Ruby, value: Value) -> Result<Exit, Error> {
|
|
93
|
+
member(ruby, "exit", value, &EXITS)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
pub(crate) fn strategy_name(ruby: &Ruby, strategy: Strategy) -> Symbol {
|
|
97
|
+
let name = STRATEGIES
|
|
98
|
+
.iter()
|
|
99
|
+
.find(|&&(_, known)| known == strategy)
|
|
100
|
+
.map_or("", |&(name, _)| name);
|
|
101
|
+
ruby.to_symbol(name)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// A [`Decision`] as Ruby reads it: `:ignore`, `:escalate`, or the inclusive
|
|
105
|
+
/// range of child indices to restart.
|
|
106
|
+
pub(crate) fn decision(ruby: &Ruby, decision: Decision) -> Result<Value, Error> {
|
|
107
|
+
match decision {
|
|
108
|
+
Decision::Ignore => Ok(ruby.to_symbol("ignore").as_value()),
|
|
109
|
+
Decision::Escalate => Ok(ruby.to_symbol("escalate").as_value()),
|
|
110
|
+
Decision::Restart(span) => ruby
|
|
111
|
+
.range_new(span.first, span.last, false)
|
|
112
|
+
.map(magnus::value::ReprValue::as_value),
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// A frozen array of the symbols naming a closed set.
|
|
117
|
+
fn names<T>(ruby: &Ruby, set: &[(&str, T)]) -> Result<RArray, Error> {
|
|
118
|
+
let array = ruby.ary_new_capa(set.len());
|
|
119
|
+
for &(name, _) in set {
|
|
120
|
+
array.push(ruby.to_symbol(name))?;
|
|
121
|
+
}
|
|
122
|
+
array.freeze();
|
|
123
|
+
Ok(array)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#[magnus::init]
|
|
127
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
128
|
+
let module = ruby.define_module("SupervisorMachines")?;
|
|
129
|
+
module.const_set("NS_PER_SECOND", NS_PER_SECOND)?;
|
|
130
|
+
module.const_set("DEFAULT_RESTARTS", DEFAULT_RESTARTS)?;
|
|
131
|
+
module.const_set("DEFAULT_WINDOW_NS", DEFAULT_WINDOW_NS)?;
|
|
132
|
+
module.const_set("STRATEGIES", names(ruby, &STRATEGIES)?)?;
|
|
133
|
+
module.const_set("POLICIES", names(ruby, &POLICIES)?)?;
|
|
134
|
+
decider::define(ruby, module)
|
|
135
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SupervisorMachines
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
# Raised once a supervisor has spent its restart intensity. Its children are
|
|
7
|
+
# down and the decision now belongs to whatever supervises the supervisor.
|
|
8
|
+
class Escalated < Error; end
|
|
9
|
+
end
|