shed_machines 0.1.1 → 0.1.2
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 +4 -4
- data/CHANGELOG.md +8 -0
- data/Cargo.lock +2 -2
- data/README.md +21 -3
- data/ext/shed_machines_native/core/Cargo.toml +1 -1
- data/ext/shed_machines_native/core/src/vegas.rs +82 -10
- data/ext/shed_machines_native/ffi/Cargo.toml +1 -1
- data/ext/shed_machines_native/ffi/src/vegas.rs +14 -3
- data/lib/shed_machines/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c1963be072e29aa88de44cf7a973c02baa5e4a56f7df026c35d289cce6d396f2
|
|
4
|
+
data.tar.gz: 79b9212417e38b688989b41e65dd49853d3a1c96ddad46b543d2d305dcba3cb3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 66a0334e48d91bae8ff3c0b1ced6b2482ca6a19bd42662de5659d869bf401d4077b1dacfe892d80e114263a2d2843e058132aae9032e3b828981a4f09418809b
|
|
7
|
+
data.tar.gz: b3f93302bac8c313c4ce1b2c02257f0e9172949361dca756c32f6e6fa21554f8deec40a626dd2fd314a3ac4d2cd8a9ac9d28faf590dca6c04a848ea544608fb0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.2](https://github.com/seuros/shed_machines/compare/shed_machines/v0.1.1...shed_machines/v0.1.2) (2026-11-01)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* a republished platform gem is skipped, not fatal ([35352a1](https://github.com/seuros/shed_machines/commit/35352a16dd73e19cf7e7661ce362f96e016a2cac))
|
|
9
|
+
* window the Vegas baseline so it cannot pin to one lucky call ([9cea8fa](https://github.com/seuros/shed_machines/commit/9cea8fa82f1fd9ab99f0bcf7a67a36ee24cd61ec))
|
|
10
|
+
|
|
3
11
|
## [0.1.1](https://github.com/seuros/shed_machines/compare/shed_machines-v0.1.0...shed_machines/v0.1.1) (2026-11-01)
|
|
4
12
|
|
|
5
13
|
|
data/Cargo.lock
CHANGED
|
@@ -240,11 +240,11 @@ checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
|
|
|
240
240
|
|
|
241
241
|
[[package]]
|
|
242
242
|
name = "shed-machines"
|
|
243
|
-
version = "0.2.
|
|
243
|
+
version = "0.2.1"
|
|
244
244
|
|
|
245
245
|
[[package]]
|
|
246
246
|
name = "shed_machines_native"
|
|
247
|
-
version = "0.1.
|
|
247
|
+
version = "0.1.2"
|
|
248
248
|
dependencies = [
|
|
249
249
|
"magnus",
|
|
250
250
|
"shed-machines",
|
data/README.md
CHANGED
|
@@ -70,15 +70,32 @@ control.limit # => 8
|
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
`Vegas` infers queue depth from the gap between the current round-trip time and
|
|
73
|
-
the
|
|
74
|
-
from a deep queue. It grows while fewer than `alpha` calls look
|
|
75
|
-
above `beta`, and holds in between.
|
|
73
|
+
the smallest one it has seen recently, because latency alone cannot tell a slow
|
|
74
|
+
dependency from a deep queue. It grows while fewer than `alpha` calls look
|
|
75
|
+
queued, shrinks above `beta`, and holds in between.
|
|
76
76
|
|
|
77
77
|
```ruby
|
|
78
78
|
control = ShedMachines::Vegas.new(limit: 10, min: 1, max: 200, alpha: 3, beta: 6)
|
|
79
79
|
control.best_rtt # => nil, until the first completed call
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
+
That inference only holds when any two calls would take about as long on an idle
|
|
83
|
+
dependency. Over a mix whose service times differ by orders of magnitude the
|
|
84
|
+
baseline settles on the fastest kind of work, every slower call reads as a deep
|
|
85
|
+
queue, and the limit sinks to `min` and stays there — tuning `alpha` and `beta`
|
|
86
|
+
cannot fix it, because the signal is measuring the wrong thing. Use `Aimd` for
|
|
87
|
+
heterogeneous work, since it steers on outcomes rather than latency, or give
|
|
88
|
+
each class of work its own `Vegas`.
|
|
89
|
+
|
|
90
|
+
The baseline is the smallest round-trip time in the current window of
|
|
91
|
+
`DEFAULT_BASELINE_WINDOW` samples or the one before it, so one unusually fast
|
|
92
|
+
call cannot pin it for the life of the process and a dependency that
|
|
93
|
+
legitimately slows down is believed within two windows.
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
control = ShedMachines::Vegas.new(limit: 10).baseline_window(250)
|
|
97
|
+
```
|
|
98
|
+
|
|
82
99
|
Both take `outcome: :ok` or `outcome: :overload`, where overload means the call
|
|
83
100
|
timed out, was rejected, or otherwise says the dependency is loaded. Anything
|
|
84
101
|
else raises `ArgumentError`. Both clamp a starting limit into `min..max`, which
|
|
@@ -152,6 +169,7 @@ The Ruby constants mirror the crate's, with the same names and values.
|
|
|
152
169
|
| `Vegas::DEFAULT_ALPHA` | 3 |
|
|
153
170
|
| `Vegas::DEFAULT_BETA` | 6 |
|
|
154
171
|
| `Vegas::OVERLOAD_DIVISOR` | 2 |
|
|
172
|
+
| `Vegas::DEFAULT_BASELINE_WINDOW` | 1000 |
|
|
155
173
|
| `Codel::DEFAULT_TARGET_NS` | 5_000_000 |
|
|
156
174
|
| `Codel::DEFAULT_INTERVAL_NS` | 100_000_000 |
|
|
157
175
|
| `Budget::DEFAULT_MULTIPLIER` | 2.0 |
|
|
@@ -14,12 +14,41 @@ pub const DEFAULT_BETA: u32 = 6;
|
|
|
14
14
|
/// Denominator of the fraction kept after an overload.
|
|
15
15
|
pub const OVERLOAD_DIVISOR: u32 = 2;
|
|
16
16
|
|
|
17
|
+
/// Samples one baseline window spans. The baseline is the smallest round-trip
|
|
18
|
+
/// time in the current window or the one before it, so it always reflects
|
|
19
|
+
/// between this many recent calls and twice as many.
|
|
20
|
+
pub const DEFAULT_BASELINE_WINDOW: u32 = 1_000;
|
|
21
|
+
|
|
17
22
|
/// Calls added or removed per sample.
|
|
18
23
|
const STEP: u32 = 1;
|
|
19
24
|
|
|
20
25
|
/// Estimates how many calls are queued rather than working, from the gap
|
|
21
|
-
/// between the current round-trip time and the
|
|
22
|
-
/// the limit to keep that estimate inside a band.
|
|
26
|
+
/// between the current round-trip time and the smallest one seen recently, then
|
|
27
|
+
/// steers the limit to keep that estimate inside a band.
|
|
28
|
+
///
|
|
29
|
+
/// # The work has to be homogeneous
|
|
30
|
+
///
|
|
31
|
+
/// The queue estimate reads every nanosecond above the baseline as time spent
|
|
32
|
+
/// waiting, which only holds when any two calls would take about as long on an
|
|
33
|
+
/// idle dependency. Over a mix whose service times differ by orders of
|
|
34
|
+
/// magnitude the baseline settles on the fastest kind of work, every slower
|
|
35
|
+
/// call looks like a deep queue, and the limit sinks to [`Bounds`]' floor and
|
|
36
|
+
/// stays there — no amount of tuning `alpha` and `beta` fixes it, because the
|
|
37
|
+
/// signal itself is measuring the wrong thing. Give heterogeneous work
|
|
38
|
+
/// [`Aimd`](crate::Aimd) instead: it steers on outcomes rather than latency, so
|
|
39
|
+
/// a slow call is not mistaken for a queued one. One `Vegas` per class of work
|
|
40
|
+
/// is the other way out.
|
|
41
|
+
///
|
|
42
|
+
/// # The baseline is windowed
|
|
43
|
+
///
|
|
44
|
+
/// A baseline that only ever shrank would pin itself to the luckiest call the
|
|
45
|
+
/// process ever made and never recover, so one unusually fast sample, or a
|
|
46
|
+
/// dependency that has since legitimately slowed down, would hold the limit at
|
|
47
|
+
/// the floor for good. The baseline is instead the smallest round-trip time in
|
|
48
|
+
/// the current window of [`DEFAULT_BASELINE_WINDOW`] samples or in the one
|
|
49
|
+
/// before it, which keeps it stable while traffic is steady and lets it rise
|
|
50
|
+
/// within two windows when the dependency changes. Set the span with
|
|
51
|
+
/// [`baseline_window`](Self::baseline_window).
|
|
23
52
|
///
|
|
24
53
|
/// ```
|
|
25
54
|
/// use shed_machines::{Bounds, Controller, Sample, Vegas};
|
|
@@ -38,7 +67,13 @@ const STEP: u32 = 1;
|
|
|
38
67
|
pub struct Vegas {
|
|
39
68
|
bounds: Bounds,
|
|
40
69
|
limit: u32,
|
|
41
|
-
|
|
70
|
+
/// Smallest round-trip time in the window being filled, and in the one
|
|
71
|
+
/// before it. [`u64::MAX`] stands for a window with nothing in it.
|
|
72
|
+
current_ns: u64,
|
|
73
|
+
previous_ns: u64,
|
|
74
|
+
/// Samples into the current window, and how many it holds.
|
|
75
|
+
seen: u32,
|
|
76
|
+
window: u32,
|
|
42
77
|
alpha: u32,
|
|
43
78
|
beta: u32,
|
|
44
79
|
}
|
|
@@ -49,7 +84,10 @@ impl Vegas {
|
|
|
49
84
|
Self {
|
|
50
85
|
bounds,
|
|
51
86
|
limit: bounds.clamp(limit),
|
|
52
|
-
|
|
87
|
+
current_ns: u64::MAX,
|
|
88
|
+
previous_ns: u64::MAX,
|
|
89
|
+
seen: 0,
|
|
90
|
+
window: DEFAULT_BASELINE_WINDOW,
|
|
53
91
|
alpha: DEFAULT_ALPHA,
|
|
54
92
|
beta: DEFAULT_BETA,
|
|
55
93
|
}
|
|
@@ -63,23 +101,57 @@ impl Vegas {
|
|
|
63
101
|
self
|
|
64
102
|
}
|
|
65
103
|
|
|
66
|
-
///
|
|
104
|
+
/// Samples one baseline window spans, at least one. A shorter window
|
|
105
|
+
/// follows a dependency whose idle latency drifts; a longer one holds a
|
|
106
|
+
/// steadier baseline when traffic is bursty.
|
|
107
|
+
#[must_use]
|
|
108
|
+
pub const fn baseline_window(mut self, samples: u32) -> Self {
|
|
109
|
+
self.window = samples;
|
|
110
|
+
self
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// The smallest round-trip time in the last window or two, the stand-in for
|
|
114
|
+
/// zero queueing.
|
|
67
115
|
#[must_use]
|
|
68
116
|
pub const fn best_rtt_ns(&self) -> Option<u64> {
|
|
69
|
-
|
|
117
|
+
let baseline = self.baseline();
|
|
118
|
+
if baseline == u64::MAX {
|
|
70
119
|
None
|
|
71
120
|
} else {
|
|
72
|
-
Some(
|
|
121
|
+
Some(baseline)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const fn baseline(&self) -> u64 {
|
|
126
|
+
if self.current_ns < self.previous_ns {
|
|
127
|
+
self.current_ns
|
|
128
|
+
} else {
|
|
129
|
+
self.previous_ns
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Takes `rtt_ns` into the window being filled, retiring that window once
|
|
134
|
+
/// it is full so the baseline cannot outlive the traffic it came from.
|
|
135
|
+
const fn rebase(&mut self, rtt_ns: u64) {
|
|
136
|
+
if rtt_ns < self.current_ns {
|
|
137
|
+
self.current_ns = rtt_ns;
|
|
138
|
+
}
|
|
139
|
+
self.seen += 1;
|
|
140
|
+
if self.seen >= self.window {
|
|
141
|
+
self.previous_ns = self.current_ns;
|
|
142
|
+
self.current_ns = u64::MAX;
|
|
143
|
+
self.seen = 0;
|
|
73
144
|
}
|
|
74
145
|
}
|
|
75
146
|
|
|
76
147
|
/// `limit * (rtt - best) / rtt`, the Vegas estimate of how many calls in
|
|
77
148
|
/// flight are waiting rather than working.
|
|
78
149
|
fn queued(&self, rtt_ns: u64) -> u64 {
|
|
79
|
-
|
|
150
|
+
let baseline = self.baseline();
|
|
151
|
+
if rtt_ns == 0 || baseline >= rtt_ns {
|
|
80
152
|
return 0;
|
|
81
153
|
}
|
|
82
|
-
let excess = rtt_ns -
|
|
154
|
+
let excess = rtt_ns - baseline;
|
|
83
155
|
let scaled = u128::from(self.limit) * u128::from(excess) / u128::from(rtt_ns);
|
|
84
156
|
u64::try_from(scaled).unwrap_or(u64::MAX)
|
|
85
157
|
}
|
|
@@ -92,7 +164,7 @@ impl Controller for Vegas {
|
|
|
92
164
|
return;
|
|
93
165
|
}
|
|
94
166
|
|
|
95
|
-
self.
|
|
167
|
+
self.rebase(sample.rtt_ns);
|
|
96
168
|
let queued = self.queued(sample.rtt_ns);
|
|
97
169
|
|
|
98
170
|
self.limit = if queued < u64::from(self.alpha) {
|
|
@@ -5,10 +5,12 @@ use std::cell::RefCell;
|
|
|
5
5
|
use magnus::scan_args::scan_args;
|
|
6
6
|
use magnus::typed_data::Obj;
|
|
7
7
|
use magnus::{Error, RHash, RModule, Ruby, Value, function, method, prelude::*};
|
|
8
|
-
use shed_machines::vegas::{
|
|
8
|
+
use shed_machines::vegas::{
|
|
9
|
+
DEFAULT_ALPHA, DEFAULT_BASELINE_WINDOW, DEFAULT_BETA, OVERLOAD_DIVISOR,
|
|
10
|
+
};
|
|
9
11
|
use shed_machines::{Controller, Vegas};
|
|
10
12
|
|
|
11
|
-
use crate::{observe, opening, to_seconds};
|
|
13
|
+
use crate::{Whole, observe, opening, to_seconds};
|
|
12
14
|
|
|
13
15
|
#[magnus::wrap(class = "ShedMachines::Vegas", free_immediately, size)]
|
|
14
16
|
struct Wrapper(RefCell<Vegas>);
|
|
@@ -31,7 +33,14 @@ impl Wrapper {
|
|
|
31
33
|
self.0.borrow().limit()
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
///
|
|
36
|
+
/// Samples one baseline window spans. Chainable, like the Rust builder.
|
|
37
|
+
fn baseline_window(rb_self: Obj<Self>, Whole(samples): Whole<u32>) -> Obj<Self> {
|
|
38
|
+
let windowed = rb_self.0.borrow().baseline_window(samples);
|
|
39
|
+
*rb_self.0.borrow_mut() = windowed;
|
|
40
|
+
rb_self
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// The smallest round-trip time in the last window or two, in seconds.
|
|
35
44
|
fn best_rtt(&self) -> Option<f64> {
|
|
36
45
|
self.0.borrow().best_rtt_ns().map(to_seconds)
|
|
37
46
|
}
|
|
@@ -52,9 +61,11 @@ pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
|
|
|
52
61
|
class.const_set("DEFAULT_ALPHA", DEFAULT_ALPHA)?;
|
|
53
62
|
class.const_set("DEFAULT_BETA", DEFAULT_BETA)?;
|
|
54
63
|
class.const_set("OVERLOAD_DIVISOR", OVERLOAD_DIVISOR)?;
|
|
64
|
+
class.const_set("DEFAULT_BASELINE_WINDOW", DEFAULT_BASELINE_WINDOW)?;
|
|
55
65
|
class.define_singleton_method("new", function!(Wrapper::new, -1))?;
|
|
56
66
|
class.define_method("observe", method!(Wrapper::observe, -1))?;
|
|
57
67
|
class.define_method("limit", method!(Wrapper::limit, 0))?;
|
|
68
|
+
class.define_method("baseline_window", method!(Wrapper::baseline_window, 1))?;
|
|
58
69
|
class.define_method("best_rtt", method!(Wrapper::best_rtt, 0))?;
|
|
59
70
|
class.define_method("inspect", method!(Wrapper::inspect, 0))?;
|
|
60
71
|
Ok(())
|