shed_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 +42 -0
- data/Cargo.lock +286 -0
- data/Cargo.toml +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +174 -0
- data/ext/shed_machines_native/core/Cargo.toml +24 -0
- data/ext/shed_machines_native/core/src/aimd.rs +89 -0
- data/ext/shed_machines_native/core/src/budget.rs +101 -0
- data/ext/shed_machines_native/core/src/codel.rs +122 -0
- data/ext/shed_machines_native/core/src/lib.rs +118 -0
- data/ext/shed_machines_native/core/src/vegas.rs +113 -0
- data/ext/shed_machines_native/extconf.rb +3 -0
- data/ext/shed_machines_native/ffi/Cargo.toml +19 -0
- data/ext/shed_machines_native/ffi/extconf.rb +21 -0
- data/ext/shed_machines_native/ffi/src/aimd.rs +50 -0
- data/ext/shed_machines_native/ffi/src/budget.rs +79 -0
- data/ext/shed_machines_native/ffi/src/codel.rs +80 -0
- data/ext/shed_machines_native/ffi/src/lib.rs +161 -0
- data/ext/shed_machines_native/ffi/src/vegas.rs +61 -0
- data/lib/shed_machines/version.rb +5 -0
- data/lib/shed_machines.rb +7 -0
- data/sig/shed_machines.rbs +131 -0
- metadata +86 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
//! `ShedMachines::Codel`: shed by how long work waited.
|
|
2
|
+
|
|
3
|
+
use std::cell::RefCell;
|
|
4
|
+
|
|
5
|
+
use magnus::scan_args::{get_kwargs, scan_args};
|
|
6
|
+
use magnus::{Error, RHash, RModule, Ruby, Symbol, Value, function, method, prelude::*};
|
|
7
|
+
use shed_machines::Codel;
|
|
8
|
+
use shed_machines::codel::{DEFAULT_INTERVAL_NS, DEFAULT_TARGET_NS};
|
|
9
|
+
|
|
10
|
+
use crate::{duration, to_seconds, verdict};
|
|
11
|
+
|
|
12
|
+
#[magnus::wrap(class = "ShedMachines::Codel", free_immediately, size)]
|
|
13
|
+
struct Wrapper(RefCell<Codel>);
|
|
14
|
+
|
|
15
|
+
impl Wrapper {
|
|
16
|
+
fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
|
|
17
|
+
let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
|
|
18
|
+
let (target, interval) = get_kwargs::<_, (), (Option<f64>, Option<f64>), ()>(
|
|
19
|
+
args.keywords,
|
|
20
|
+
&[],
|
|
21
|
+
&["target", "interval"],
|
|
22
|
+
)?
|
|
23
|
+
.optional;
|
|
24
|
+
let queue = Codel::new(
|
|
25
|
+
target.map_or(Ok(DEFAULT_TARGET_NS), |seconds| {
|
|
26
|
+
duration(ruby, "target", seconds)
|
|
27
|
+
})?,
|
|
28
|
+
interval.map_or(Ok(DEFAULT_INTERVAL_NS), |seconds| {
|
|
29
|
+
duration(ruby, "interval", seconds)
|
|
30
|
+
})?,
|
|
31
|
+
);
|
|
32
|
+
Ok(Self(RefCell::new(queue)))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// `sojourn:` is how long this work waited, `now:` the current time, both
|
|
36
|
+
/// in seconds. Answers `:admit` or `:shed`.
|
|
37
|
+
fn admit(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Symbol, Error> {
|
|
38
|
+
let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
|
|
39
|
+
let (sojourn, now) =
|
|
40
|
+
get_kwargs::<_, (f64, f64), (), ()>(args.keywords, &["sojourn", "now"], &[])?.required;
|
|
41
|
+
let sojourn_ns = duration(ruby, "sojourn", sojourn)?;
|
|
42
|
+
let now_ns = duration(ruby, "now", now)?;
|
|
43
|
+
let answer = rb_self.0.borrow_mut().admit(sojourn_ns, now_ns);
|
|
44
|
+
Ok(verdict(ruby, answer))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// The acceptable waiting time, in seconds.
|
|
48
|
+
fn interval(&self) -> f64 {
|
|
49
|
+
to_seconds(self.0.borrow().interval_ns())
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn target(&self) -> f64 {
|
|
53
|
+
to_seconds(self.0.borrow().target_ns())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn shedding(&self) -> bool {
|
|
57
|
+
self.0.borrow().shedding()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fn inspect(&self) -> String {
|
|
61
|
+
format!(
|
|
62
|
+
"#<ShedMachines::Codel target={} shedding={}>",
|
|
63
|
+
self.target(),
|
|
64
|
+
self.shedding()
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
|
|
70
|
+
let class = module.define_class("Codel", ruby.class_object())?;
|
|
71
|
+
class.const_set("DEFAULT_TARGET_NS", DEFAULT_TARGET_NS)?;
|
|
72
|
+
class.const_set("DEFAULT_INTERVAL_NS", DEFAULT_INTERVAL_NS)?;
|
|
73
|
+
class.define_singleton_method("new", function!(Wrapper::new, -1))?;
|
|
74
|
+
class.define_method("admit", method!(Wrapper::admit, -1))?;
|
|
75
|
+
class.define_method("target", method!(Wrapper::target, 0))?;
|
|
76
|
+
class.define_method("interval", method!(Wrapper::interval, 0))?;
|
|
77
|
+
class.define_method("shedding?", method!(Wrapper::shedding, 0))?;
|
|
78
|
+
class.define_method("inspect", method!(Wrapper::inspect, 0))?;
|
|
79
|
+
Ok(())
|
|
80
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
//! Ruby bindings for the `shed-machines` control laws.
|
|
2
|
+
//!
|
|
3
|
+
//! Durations cross this boundary as seconds and move through the control laws
|
|
4
|
+
//! as nanoseconds. [`to_ns`] and [`to_seconds`] are the only places either
|
|
5
|
+
//! representation changes.
|
|
6
|
+
|
|
7
|
+
#![allow(clippy::needless_pass_by_value)]
|
|
8
|
+
|
|
9
|
+
mod aimd;
|
|
10
|
+
mod budget;
|
|
11
|
+
mod codel;
|
|
12
|
+
mod vegas;
|
|
13
|
+
|
|
14
|
+
use std::cell::RefCell;
|
|
15
|
+
|
|
16
|
+
use magnus::scan_args::{get_kwargs, scan_args};
|
|
17
|
+
use magnus::{Error, Integer, RHash, Ruby, Symbol, TryConvert, Value, prelude::*};
|
|
18
|
+
use shed_machines::{Bounds, Controller, DEFAULT_CEILING, LOWEST_FLOOR, Outcome, Sample, Verdict};
|
|
19
|
+
|
|
20
|
+
/// Nanoseconds in a second.
|
|
21
|
+
pub(crate) const NS_PER_SECOND: u32 = 1_000_000_000;
|
|
22
|
+
|
|
23
|
+
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
|
24
|
+
pub(crate) fn to_ns(seconds: f64) -> u64 {
|
|
25
|
+
(seconds * f64::from(NS_PER_SECOND)).round() as u64
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#[expect(clippy::cast_precision_loss)]
|
|
29
|
+
pub(crate) fn to_seconds(nanoseconds: u64) -> f64 {
|
|
30
|
+
nanoseconds as f64 / f64::from(NS_PER_SECOND)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// A whole number of things: a limit, a bound, a call count, a ratio's two
|
|
34
|
+
/// sides.
|
|
35
|
+
///
|
|
36
|
+
/// magnus reads an integer through Ruby's `NUM2*` family, which truncates a
|
|
37
|
+
/// `Float` towards zero instead of refusing it, so an argument that counts
|
|
38
|
+
/// rather than measures is read as one of these and anything but an `Integer`
|
|
39
|
+
/// is turned away. An `Integer` outside the target width still comes back as
|
|
40
|
+
/// the `RangeError` magnus raises for it. Seconds stay [`duration`]'s.
|
|
41
|
+
pub(crate) struct Whole<T>(pub(crate) T);
|
|
42
|
+
|
|
43
|
+
impl<T: TryConvert> TryConvert for Whole<T> {
|
|
44
|
+
fn try_convert(value: Value) -> Result<Self, Error> {
|
|
45
|
+
if Integer::from_value(value).is_none() {
|
|
46
|
+
return Err(not_whole(value));
|
|
47
|
+
}
|
|
48
|
+
T::try_convert(value).map(Self)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn not_whole(value: Value) -> Error {
|
|
53
|
+
let ruby = Ruby::get_with(value);
|
|
54
|
+
Error::new(
|
|
55
|
+
ruby.exception_type_error(),
|
|
56
|
+
format!(
|
|
57
|
+
"no implicit conversion of {} into Integer: {}",
|
|
58
|
+
value.class(),
|
|
59
|
+
value.inspect()
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Seconds as nanoseconds, rejecting a negative or non-finite duration.
|
|
65
|
+
pub(crate) fn duration(ruby: &Ruby, name: &str, seconds: f64) -> Result<u64, Error> {
|
|
66
|
+
if !seconds.is_finite() || seconds < 0.0 {
|
|
67
|
+
return Err(Error::new(
|
|
68
|
+
ruby.exception_arg_error(),
|
|
69
|
+
format!("{name} must be finite and not negative, got {seconds}"),
|
|
70
|
+
));
|
|
71
|
+
}
|
|
72
|
+
Ok(to_ns(seconds))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// The keywords every controller is opened with: a starting `limit:`, the
|
|
76
|
+
/// `min:` and `max:` it moves within, and the two values it tunes itself with.
|
|
77
|
+
pub(crate) fn opening(
|
|
78
|
+
ruby: &Ruby,
|
|
79
|
+
keywords: RHash,
|
|
80
|
+
tuning: [&str; 2],
|
|
81
|
+
) -> Result<(Bounds, u32, [Option<u32>; 2]), Error> {
|
|
82
|
+
let keywords = get_kwargs::<
|
|
83
|
+
_,
|
|
84
|
+
(Whole<u32>,),
|
|
85
|
+
(
|
|
86
|
+
Option<Whole<u32>>,
|
|
87
|
+
Option<Whole<u32>>,
|
|
88
|
+
Option<Whole<u32>>,
|
|
89
|
+
Option<Whole<u32>>,
|
|
90
|
+
),
|
|
91
|
+
(),
|
|
92
|
+
>(keywords, &["limit"], &["min", "max", tuning[0], tuning[1]])?;
|
|
93
|
+
let (Whole(limit),) = keywords.required;
|
|
94
|
+
let (min, max, first, second) = keywords.optional;
|
|
95
|
+
let floor = min.map_or(LOWEST_FLOOR, |Whole(min)| min);
|
|
96
|
+
let ceiling = max.map_or(DEFAULT_CEILING, |Whole(max)| max);
|
|
97
|
+
if floor < LOWEST_FLOOR || ceiling < floor {
|
|
98
|
+
return Err(Error::new(
|
|
99
|
+
ruby.exception_arg_error(),
|
|
100
|
+
format!(
|
|
101
|
+
"min must be at least {LOWEST_FLOOR} and max at least min, got {floor}..{ceiling}"
|
|
102
|
+
),
|
|
103
|
+
));
|
|
104
|
+
}
|
|
105
|
+
let tuning = [first, second].map(|given| given.map(|Whole(value)| value));
|
|
106
|
+
Ok((Bounds::new(floor, ceiling), limit, tuning))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Folds one completed call, given as `rtt:` in seconds, `in_flight:` and
|
|
110
|
+
/// `outcome:`, into a controller.
|
|
111
|
+
pub(crate) fn observe<C: Controller>(
|
|
112
|
+
ruby: &Ruby,
|
|
113
|
+
control: &RefCell<C>,
|
|
114
|
+
args: &[Value],
|
|
115
|
+
) -> Result<(), Error> {
|
|
116
|
+
let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
|
|
117
|
+
let (rtt, Whole(in_flight), outcome) = get_kwargs::<_, (f64, Whole<u32>, Value), (), ()>(
|
|
118
|
+
args.keywords,
|
|
119
|
+
&["rtt", "in_flight", "outcome"],
|
|
120
|
+
&[],
|
|
121
|
+
)?
|
|
122
|
+
.required;
|
|
123
|
+
let sample = Sample {
|
|
124
|
+
rtt_ns: duration(ruby, "rtt", rtt)?,
|
|
125
|
+
in_flight,
|
|
126
|
+
outcome: self::outcome(ruby, outcome)?,
|
|
127
|
+
};
|
|
128
|
+
control.borrow_mut().observe(sample);
|
|
129
|
+
Ok(())
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fn outcome(ruby: &Ruby, value: Value) -> Result<Outcome, Error> {
|
|
133
|
+
let name = Symbol::from_value(value).and_then(|symbol| symbol.name().ok());
|
|
134
|
+
match name.as_deref() {
|
|
135
|
+
Some("ok") => Ok(Outcome::Ok),
|
|
136
|
+
Some("overload") => Ok(Outcome::Overload),
|
|
137
|
+
_ => Err(Error::new(
|
|
138
|
+
ruby.exception_arg_error(),
|
|
139
|
+
format!("unknown outcome: {}", value.inspect()),
|
|
140
|
+
)),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
pub(crate) fn verdict(ruby: &Ruby, verdict: Verdict) -> Symbol {
|
|
145
|
+
match verdict {
|
|
146
|
+
Verdict::Admit => ruby.to_symbol("admit"),
|
|
147
|
+
Verdict::Shed => ruby.to_symbol("shed"),
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#[magnus::init]
|
|
152
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
153
|
+
let module = ruby.define_module("ShedMachines")?;
|
|
154
|
+
module.const_set("NS_PER_SECOND", NS_PER_SECOND)?;
|
|
155
|
+
module.const_set("LOWEST_FLOOR", LOWEST_FLOOR)?;
|
|
156
|
+
module.const_set("DEFAULT_CEILING", DEFAULT_CEILING)?;
|
|
157
|
+
aimd::define(ruby, module)?;
|
|
158
|
+
vegas::define(ruby, module)?;
|
|
159
|
+
codel::define(ruby, module)?;
|
|
160
|
+
budget::define(ruby, module)
|
|
161
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//! `ShedMachines::Vegas`: steer the limit by how many calls look queued.
|
|
2
|
+
|
|
3
|
+
use std::cell::RefCell;
|
|
4
|
+
|
|
5
|
+
use magnus::scan_args::scan_args;
|
|
6
|
+
use magnus::typed_data::Obj;
|
|
7
|
+
use magnus::{Error, RHash, RModule, Ruby, Value, function, method, prelude::*};
|
|
8
|
+
use shed_machines::vegas::{DEFAULT_ALPHA, DEFAULT_BETA, OVERLOAD_DIVISOR};
|
|
9
|
+
use shed_machines::{Controller, Vegas};
|
|
10
|
+
|
|
11
|
+
use crate::{observe, opening, to_seconds};
|
|
12
|
+
|
|
13
|
+
#[magnus::wrap(class = "ShedMachines::Vegas", free_immediately, size)]
|
|
14
|
+
struct Wrapper(RefCell<Vegas>);
|
|
15
|
+
|
|
16
|
+
impl Wrapper {
|
|
17
|
+
fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
|
|
18
|
+
let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
|
|
19
|
+
let (bounds, limit, [alpha, beta]) = opening(ruby, args.keywords, ["alpha", "beta"])?;
|
|
20
|
+
let control = Vegas::new(bounds, limit)
|
|
21
|
+
.band(alpha.unwrap_or(DEFAULT_ALPHA), beta.unwrap_or(DEFAULT_BETA));
|
|
22
|
+
Ok(Self(RefCell::new(control)))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn observe(ruby: &Ruby, rb_self: Obj<Self>, args: &[Value]) -> Result<Obj<Self>, Error> {
|
|
26
|
+
observe(ruby, &rb_self.0, args)?;
|
|
27
|
+
Ok(rb_self)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fn limit(&self) -> u32 {
|
|
31
|
+
self.0.borrow().limit()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// The best round-trip time seen so far, in seconds.
|
|
35
|
+
fn best_rtt(&self) -> Option<f64> {
|
|
36
|
+
self.0.borrow().best_rtt_ns().map(to_seconds)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fn inspect(&self) -> String {
|
|
40
|
+
let best_rtt = self
|
|
41
|
+
.best_rtt()
|
|
42
|
+
.map_or_else(|| "nil".to_owned(), |rtt| rtt.to_string());
|
|
43
|
+
format!(
|
|
44
|
+
"#<ShedMachines::Vegas limit={} best_rtt={best_rtt}>",
|
|
45
|
+
self.limit()
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
|
|
51
|
+
let class = module.define_class("Vegas", ruby.class_object())?;
|
|
52
|
+
class.const_set("DEFAULT_ALPHA", DEFAULT_ALPHA)?;
|
|
53
|
+
class.const_set("DEFAULT_BETA", DEFAULT_BETA)?;
|
|
54
|
+
class.const_set("OVERLOAD_DIVISOR", OVERLOAD_DIVISOR)?;
|
|
55
|
+
class.define_singleton_method("new", function!(Wrapper::new, -1))?;
|
|
56
|
+
class.define_method("observe", method!(Wrapper::observe, -1))?;
|
|
57
|
+
class.define_method("limit", method!(Wrapper::limit, 0))?;
|
|
58
|
+
class.define_method("best_rtt", method!(Wrapper::best_rtt, 0))?;
|
|
59
|
+
class.define_method("inspect", method!(Wrapper::inspect, 0))?;
|
|
60
|
+
Ok(())
|
|
61
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Adaptive concurrency limits and load shedding. Every class here is defined by
|
|
2
|
+
# the native extension, which binds the `shed-machines` crate.
|
|
3
|
+
#
|
|
4
|
+
# Nothing here enforces anything: these are the control loops, and the caller
|
|
5
|
+
# owns the semaphore, the queue and the dice.
|
|
6
|
+
module ShedMachines
|
|
7
|
+
VERSION: String
|
|
8
|
+
|
|
9
|
+
# Nanoseconds in a second. Durations cross this boundary as seconds and move
|
|
10
|
+
# through the control laws as nanoseconds.
|
|
11
|
+
NS_PER_SECOND: Integer
|
|
12
|
+
|
|
13
|
+
# A limit of zero would wedge the caller shut, so one is the lowest floor that
|
|
14
|
+
# still makes progress.
|
|
15
|
+
LOWEST_FLOOR: Integer
|
|
16
|
+
|
|
17
|
+
# Arbitrary, and deliberately so: there is no principled default ceiling for
|
|
18
|
+
# an unknown dependency. Set it from what the pool behind it can take.
|
|
19
|
+
DEFAULT_CEILING: Integer
|
|
20
|
+
|
|
21
|
+
# How a completed call ended, from the limiter's point of view. `:overload`
|
|
22
|
+
# means it timed out, was rejected, or otherwise says the dependency is
|
|
23
|
+
# loaded. Anything else raises `ArgumentError`.
|
|
24
|
+
type outcome = :ok | :overload
|
|
25
|
+
|
|
26
|
+
# What to do with a piece of work that has been waiting.
|
|
27
|
+
type verdict = :admit | :shed
|
|
28
|
+
|
|
29
|
+
# Additive increase, multiplicative decrease: grows the limit by one whenever
|
|
30
|
+
# a saturated call succeeds, and keeps `kept / of` of it on any overload.
|
|
31
|
+
class Aimd
|
|
32
|
+
DEFAULT_BACKOFF_KEPT: Integer
|
|
33
|
+
DEFAULT_BACKOFF_OF: Integer
|
|
34
|
+
|
|
35
|
+
# `limit` outside `min..max` is pulled inside it.
|
|
36
|
+
def self.new: (
|
|
37
|
+
limit: Integer,
|
|
38
|
+
?min: Integer,
|
|
39
|
+
?max: Integer,
|
|
40
|
+
?kept: Integer,
|
|
41
|
+
?of: Integer
|
|
42
|
+
) -> instance
|
|
43
|
+
|
|
44
|
+
# Folds one completed call into the limit. `rtt` is in seconds, `in_flight`
|
|
45
|
+
# counts the calls in flight when this one started, including itself.
|
|
46
|
+
def observe: (rtt: Float, in_flight: Integer, outcome: outcome) -> self
|
|
47
|
+
|
|
48
|
+
# Calls allowed in flight right now.
|
|
49
|
+
def limit: () -> Integer
|
|
50
|
+
|
|
51
|
+
def inspect: () -> String
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# TCP Vegas applied to a concurrency limit: estimates how many calls are
|
|
55
|
+
# queued rather than working from the gap between the current round-trip time
|
|
56
|
+
# and the best one seen, then steers the limit to keep that estimate between
|
|
57
|
+
# `alpha` and `beta`.
|
|
58
|
+
class Vegas
|
|
59
|
+
DEFAULT_ALPHA: Integer
|
|
60
|
+
DEFAULT_BETA: Integer
|
|
61
|
+
OVERLOAD_DIVISOR: Integer
|
|
62
|
+
|
|
63
|
+
def self.new: (
|
|
64
|
+
limit: Integer,
|
|
65
|
+
?min: Integer,
|
|
66
|
+
?max: Integer,
|
|
67
|
+
?alpha: Integer,
|
|
68
|
+
?beta: Integer
|
|
69
|
+
) -> instance
|
|
70
|
+
|
|
71
|
+
def observe: (rtt: Float, in_flight: Integer, outcome: outcome) -> self
|
|
72
|
+
|
|
73
|
+
def limit: () -> Integer
|
|
74
|
+
|
|
75
|
+
# The best round-trip time seen so far in seconds, the stand-in for zero
|
|
76
|
+
# queueing. `nil` until the first completed call.
|
|
77
|
+
def best_rtt: () -> Float?
|
|
78
|
+
|
|
79
|
+
def inspect: () -> String
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Controlled delay: sheds by how long work waited rather than by how much of
|
|
83
|
+
# it there is, and only once the wait stays above target for a full interval.
|
|
84
|
+
class Codel
|
|
85
|
+
DEFAULT_TARGET_NS: Integer
|
|
86
|
+
DEFAULT_INTERVAL_NS: Integer
|
|
87
|
+
|
|
88
|
+
# Both in seconds, defaulting to the values the CoDel paper settled on.
|
|
89
|
+
def self.new: (?target: Float, ?interval: Float) -> instance
|
|
90
|
+
|
|
91
|
+
# `sojourn` is how long this work waited before being considered, `now` is
|
|
92
|
+
# the current time, both in seconds.
|
|
93
|
+
def admit: (sojourn: Float, now: Float) -> verdict
|
|
94
|
+
|
|
95
|
+
# The acceptable waiting time, in seconds.
|
|
96
|
+
def target: () -> Float
|
|
97
|
+
def interval: () -> Float
|
|
98
|
+
|
|
99
|
+
def shedding?: () -> bool
|
|
100
|
+
|
|
101
|
+
def inspect: () -> String
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Client-side adaptive throttling, so retries cannot amplify an outage.
|
|
105
|
+
class Budget
|
|
106
|
+
DEFAULT_MULTIPLIER: Float
|
|
107
|
+
|
|
108
|
+
# `multiplier` is how many requests each accepted one entitles the client
|
|
109
|
+
# to.
|
|
110
|
+
def self.new: (?multiplier: Float) -> instance
|
|
111
|
+
|
|
112
|
+
def requested: () -> self
|
|
113
|
+
|
|
114
|
+
def accepted: () -> self
|
|
115
|
+
|
|
116
|
+
# Halves both counts, so the budget forgets an outage instead of holding it
|
|
117
|
+
# against the dependency forever.
|
|
118
|
+
def decay: () -> self
|
|
119
|
+
|
|
120
|
+
def multiplier: () -> Float
|
|
121
|
+
def requests: () -> Integer
|
|
122
|
+
|
|
123
|
+
def accepts: () -> Integer
|
|
124
|
+
|
|
125
|
+
# The fraction of outbound work to drop locally, in `0.0..1.0`. A fraction
|
|
126
|
+
# rather than a decision: the caller owns the dice.
|
|
127
|
+
def shed_probability: () -> Float
|
|
128
|
+
|
|
129
|
+
def inspect: () -> String
|
|
130
|
+
end
|
|
131
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: shed_machines
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Abdelkader Boudih
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rb_sys
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.9'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.9'
|
|
26
|
+
description: 'AIMD and Vegas move a concurrency limit from what completed calls report,
|
|
27
|
+
CoDel sheds queued work by how long it waited, and a retry budget stops a client
|
|
28
|
+
from amplifying an outage. Bound to Ruby from the no_std shed-machines crate: the
|
|
29
|
+
control loops only, with the semaphore, the queue and the dice left to the caller.'
|
|
30
|
+
email:
|
|
31
|
+
- oss@seuros.com
|
|
32
|
+
executables: []
|
|
33
|
+
extensions:
|
|
34
|
+
- ext/shed_machines_native/extconf.rb
|
|
35
|
+
extra_rdoc_files: []
|
|
36
|
+
files:
|
|
37
|
+
- CHANGELOG.md
|
|
38
|
+
- Cargo.lock
|
|
39
|
+
- Cargo.toml
|
|
40
|
+
- LICENSE.txt
|
|
41
|
+
- README.md
|
|
42
|
+
- ext/shed_machines_native/core/Cargo.toml
|
|
43
|
+
- ext/shed_machines_native/core/src/aimd.rs
|
|
44
|
+
- ext/shed_machines_native/core/src/budget.rs
|
|
45
|
+
- ext/shed_machines_native/core/src/codel.rs
|
|
46
|
+
- ext/shed_machines_native/core/src/lib.rs
|
|
47
|
+
- ext/shed_machines_native/core/src/vegas.rs
|
|
48
|
+
- ext/shed_machines_native/extconf.rb
|
|
49
|
+
- ext/shed_machines_native/ffi/Cargo.toml
|
|
50
|
+
- ext/shed_machines_native/ffi/extconf.rb
|
|
51
|
+
- ext/shed_machines_native/ffi/src/aimd.rs
|
|
52
|
+
- ext/shed_machines_native/ffi/src/budget.rs
|
|
53
|
+
- ext/shed_machines_native/ffi/src/codel.rs
|
|
54
|
+
- ext/shed_machines_native/ffi/src/lib.rs
|
|
55
|
+
- ext/shed_machines_native/ffi/src/vegas.rs
|
|
56
|
+
- lib/shed_machines.rb
|
|
57
|
+
- lib/shed_machines/version.rb
|
|
58
|
+
- sig/shed_machines.rbs
|
|
59
|
+
homepage: https://github.com/seuros/shed_machines
|
|
60
|
+
licenses:
|
|
61
|
+
- MIT
|
|
62
|
+
metadata:
|
|
63
|
+
source_code_uri: https://github.com/seuros/shed_machines
|
|
64
|
+
changelog_uri: https://github.com/seuros/shed_machines/blob/main/CHANGELOG.md
|
|
65
|
+
bug_tracker_uri: https://github.com/seuros/shed_machines/issues
|
|
66
|
+
rubygems_mfa_required: 'true'
|
|
67
|
+
cargo_crate_name: shed_machines_native
|
|
68
|
+
cargo_manifest_path: ext/shed_machines_native/ffi/Cargo.toml
|
|
69
|
+
rdoc_options: []
|
|
70
|
+
require_paths:
|
|
71
|
+
- lib
|
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: 4.0.0
|
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
requirements: []
|
|
83
|
+
rubygems_version: 4.0.20
|
|
84
|
+
specification_version: 4
|
|
85
|
+
summary: Adaptive concurrency limits and load shedding for Ruby
|
|
86
|
+
test_files: []
|