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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 061e0fb85399ec23ed73e673a6ca55d406b3c1a775a6c3fade1a84e836bcb615
4
- data.tar.gz: e02e410d9a5909bf71101a5d5cbe80cfc70e1aa6a42e0a6c3eb6509115078cb5
3
+ metadata.gz: c1963be072e29aa88de44cf7a973c02baa5e4a56f7df026c35d289cce6d396f2
4
+ data.tar.gz: 79b9212417e38b688989b41e65dd49853d3a1c96ddad46b543d2d305dcba3cb3
5
5
  SHA512:
6
- metadata.gz: 274e603824e19a217e2762cec86d6c43b1756ac955b753afb1061b9b5ed4dcd96a745b92c10c96fe6b80d5221fb95be4d3b109ce652d857d76d2557d096c0b78
7
- data.tar.gz: 6e928923121a47f7c38f963c3f5c09b4e77c408c7b7a6d122f18b1587e54983949c2067cd2d73b3e4d16784bc51b03ae2a5a7bd29967f4685ec6dc40366781ab
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.0"
243
+ version = "0.2.1"
244
244
 
245
245
  [[package]]
246
246
  name = "shed_machines_native"
247
- version = "0.1.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 best one it has seen, because latency alone cannot tell a slow dependency
74
- from a deep queue. It grows while fewer than `alpha` calls look queued, shrinks
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 |
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "shed-machines"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  edition.workspace = true
5
5
  rust-version.workspace = true
6
6
  authors = ["Abdelkader Boudih <oss@seuros.com>"]
@@ -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 best one ever seen, then steers
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
- best_rtt_ns: u64,
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
- best_rtt_ns: u64::MAX,
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
- /// The best round-trip time seen so far, the stand-in for zero queueing.
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
- if self.best_rtt_ns == u64::MAX {
117
+ let baseline = self.baseline();
118
+ if baseline == u64::MAX {
70
119
  None
71
120
  } else {
72
- Some(self.best_rtt_ns)
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
- if rtt_ns == 0 || self.best_rtt_ns >= rtt_ns {
150
+ let baseline = self.baseline();
151
+ if rtt_ns == 0 || baseline >= rtt_ns {
80
152
  return 0;
81
153
  }
82
- let excess = rtt_ns - self.best_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.best_rtt_ns = self.best_rtt_ns.min(sample.rtt_ns);
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) {
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "shed_machines_native"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  edition.workspace = true
5
5
  rust-version.workspace = true
6
6
  authors = ["Abdelkader Boudih <oss@seuros.com>"]
@@ -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::{DEFAULT_ALPHA, DEFAULT_BETA, OVERLOAD_DIVISOR};
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
- /// The best round-trip time seen so far, in seconds.
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(())
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ShedMachines
4
- VERSION = '0.1.1'
4
+ VERSION = '0.1.2'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shed_machines
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih