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.
@@ -0,0 +1,101 @@
1
+ //! Client-side adaptive throttling, so retries cannot amplify an outage.
2
+
3
+ /// Each [`Budget::decay`] halves both counts.
4
+ const DECAY_DIVISOR: u64 = 2;
5
+
6
+ /// Denominator bias from the formula in Google's SRE book.
7
+ const DENOMINATOR_BIAS: f64 = 1.0;
8
+
9
+ /// Requests each accepted one entitles the client to, from Google's SRE book.
10
+ pub const DEFAULT_MULTIPLIER: f64 = 2.0;
11
+
12
+ /// Reports how much outbound work to drop before it leaves the process.
13
+ ///
14
+ /// ```
15
+ /// use shed_machines::Budget;
16
+ ///
17
+ /// let mut budget = Budget::new(2.0);
18
+ ///
19
+ /// // Everything is being accepted, so nothing is held back.
20
+ /// for _ in 0..100 {
21
+ /// budget.requested();
22
+ /// budget.accepted();
23
+ /// }
24
+ /// assert_eq!(budget.shed_probability(), 0.0);
25
+ ///
26
+ /// // The dependency stops accepting and the client keeps asking.
27
+ /// for _ in 0..300 {
28
+ /// budget.requested();
29
+ /// }
30
+ /// assert!(budget.shed_probability() > 0.0);
31
+ /// ```
32
+ #[derive(Debug, Clone, Copy, PartialEq)]
33
+ pub struct Budget {
34
+ requests: u64,
35
+ accepts: u64,
36
+ multiplier: f64,
37
+ }
38
+
39
+ impl Budget {
40
+ /// `multiplier` is how many requests each accepted one entitles the client
41
+ /// to. See [`DEFAULT_MULTIPLIER`].
42
+ #[must_use]
43
+ pub const fn new(multiplier: f64) -> Self {
44
+ Self {
45
+ requests: 0,
46
+ accepts: 0,
47
+ multiplier,
48
+ }
49
+ }
50
+
51
+ pub const fn requested(&mut self) {
52
+ self.requests = self.requests.saturating_add(1);
53
+ }
54
+
55
+ pub const fn accepted(&mut self) {
56
+ self.accepts = self.accepts.saturating_add(1);
57
+ }
58
+
59
+ /// Halve both counts.
60
+ pub const fn decay(&mut self) {
61
+ self.requests /= DECAY_DIVISOR;
62
+ self.accepts /= DECAY_DIVISOR;
63
+ }
64
+
65
+ #[must_use]
66
+ pub const fn multiplier(&self) -> f64 {
67
+ self.multiplier
68
+ }
69
+
70
+ #[must_use]
71
+ pub const fn requests(&self) -> u64 {
72
+ self.requests
73
+ }
74
+
75
+ #[must_use]
76
+ pub const fn accepts(&self) -> u64 {
77
+ self.accepts
78
+ }
79
+
80
+ /// The fraction of outbound work to drop locally, in `0.0..1.0`.
81
+ #[must_use]
82
+ #[expect(clippy::cast_precision_loss)]
83
+ pub fn shed_probability(&self) -> f64 {
84
+ let requests = self.requests as f64;
85
+ let entitled = self.accepts as f64 * self.multiplier;
86
+ let excess = requests - entitled;
87
+ if excess <= 0.0 {
88
+ return 0.0;
89
+ }
90
+ excess / (requests + DENOMINATOR_BIAS)
91
+ }
92
+ }
93
+
94
+ impl Default for Budget {
95
+ fn default() -> Self {
96
+ Self::new(DEFAULT_MULTIPLIER)
97
+ }
98
+ }
99
+
100
+ #[cfg(test)]
101
+ mod tests;
@@ -0,0 +1,122 @@
1
+ //! Controlled delay: shed by how long work waited, not by how much there is.
2
+
3
+ /// Acceptable waiting time, from the `CoDel` paper.
4
+ pub const DEFAULT_TARGET_NS: u64 = 5_000_000;
5
+
6
+ /// How long the target may be exceeded before shedding starts, from the paper.
7
+ pub const DEFAULT_INTERVAL_NS: u64 = 100_000_000;
8
+
9
+ /// What to do with a piece of work that has been waiting.
10
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
11
+ pub enum Verdict {
12
+ /// Run it.
13
+ Admit,
14
+ /// Drop it. The queue has been slow for longer than one interval.
15
+ Shed,
16
+ }
17
+
18
+ /// Sheds by how long work waited rather than by how much of it there is.
19
+ ///
20
+ /// ```
21
+ /// use shed_machines::{Codel, Verdict};
22
+ ///
23
+ /// const MS: u64 = 1_000_000;
24
+ /// let mut queue = Codel::new(5 * MS, 100 * MS);
25
+ ///
26
+ /// // A burst above target is tolerated until the interval elapses.
27
+ /// assert_eq!(queue.admit(20 * MS, 0), Verdict::Admit);
28
+ /// assert_eq!(queue.admit(20 * MS, 50 * MS), Verdict::Admit);
29
+ /// assert_eq!(queue.admit(20 * MS, 150 * MS), Verdict::Shed);
30
+ ///
31
+ /// // Dropping back under target clears the state.
32
+ /// assert_eq!(queue.admit(1 * MS, 200 * MS), Verdict::Admit);
33
+ /// ```
34
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
35
+ pub struct Codel {
36
+ target_ns: u64,
37
+ interval_ns: u64,
38
+ above_since_ns: Option<u64>,
39
+ next_shed_at_ns: u64,
40
+ shed_count: u64,
41
+ shedding: bool,
42
+ }
43
+
44
+ impl Codel {
45
+ /// See [`DEFAULT_TARGET_NS`] and [`DEFAULT_INTERVAL_NS`] for the values the
46
+ /// paper settled on.
47
+ #[must_use]
48
+ pub const fn new(target_ns: u64, interval_ns: u64) -> Self {
49
+ Self {
50
+ target_ns,
51
+ interval_ns,
52
+ above_since_ns: None,
53
+ next_shed_at_ns: 0,
54
+ shed_count: 0,
55
+ shedding: false,
56
+ }
57
+ }
58
+
59
+ #[must_use]
60
+ pub const fn target_ns(&self) -> u64 {
61
+ self.target_ns
62
+ }
63
+
64
+ #[must_use]
65
+ pub const fn interval_ns(&self) -> u64 {
66
+ self.interval_ns
67
+ }
68
+
69
+ #[must_use]
70
+ pub const fn shedding(&self) -> bool {
71
+ self.shedding
72
+ }
73
+
74
+ /// Successive sheds come faster, as the inverse square root of how many
75
+ /// have happened.
76
+ const fn shed_gap_ns(&self) -> u64 {
77
+ let count = if self.shed_count == 0 {
78
+ 1
79
+ } else {
80
+ self.shed_count
81
+ };
82
+ let root = count.isqrt();
83
+ self.interval_ns / root
84
+ }
85
+
86
+ /// `sojourn_ns` is how long this work waited before being considered.
87
+ pub const fn admit(&mut self, sojourn_ns: u64, now_ns: u64) -> Verdict {
88
+ if sojourn_ns < self.target_ns {
89
+ self.above_since_ns = None;
90
+ self.shedding = false;
91
+ self.shed_count = 0;
92
+ return Verdict::Admit;
93
+ }
94
+
95
+ let Some(since) = self.above_since_ns else {
96
+ self.above_since_ns = Some(now_ns);
97
+ return Verdict::Admit;
98
+ };
99
+
100
+ if now_ns.saturating_sub(since) < self.interval_ns {
101
+ return Verdict::Admit;
102
+ }
103
+
104
+ if self.shedding && now_ns < self.next_shed_at_ns {
105
+ return Verdict::Admit;
106
+ }
107
+
108
+ self.shedding = true;
109
+ self.shed_count = self.shed_count.saturating_add(1);
110
+ self.next_shed_at_ns = now_ns.saturating_add(self.shed_gap_ns());
111
+ Verdict::Shed
112
+ }
113
+ }
114
+
115
+ impl Default for Codel {
116
+ fn default() -> Self {
117
+ Self::new(DEFAULT_TARGET_NS, DEFAULT_INTERVAL_NS)
118
+ }
119
+ }
120
+
121
+ #[cfg(test)]
122
+ mod tests;
@@ -0,0 +1,118 @@
1
+ #![cfg_attr(not(feature = "std"), no_std)]
2
+
3
+ //! Adaptive concurrency limits and load shedding.
4
+ //!
5
+ //! A [`Controller`] watches completed calls and moves a concurrency limit. A
6
+ //! [`Codel`] decides whether to admit work based on how long it waited. A
7
+ //! [`Budget`] stops a client from amplifying an outage with retries.
8
+ //!
9
+ //! Nothing here enforces anything. These are the control loops; the caller owns
10
+ //! the semaphore, the queue and the dice.
11
+
12
+ /// Compiles and runs the README's examples as doctests.
13
+ #[cfg(doctest)]
14
+ #[doc = include_str!("../README.md")]
15
+ struct ReadmeDoctests;
16
+
17
+ pub mod aimd;
18
+ pub mod budget;
19
+ pub mod codel;
20
+ pub mod vegas;
21
+
22
+ pub use aimd::Aimd;
23
+ pub use budget::Budget;
24
+ pub use codel::{Codel, Verdict};
25
+ pub use vegas::Vegas;
26
+
27
+ /// How a completed call ended, from the limiter's point of view.
28
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
29
+ pub enum Outcome {
30
+ /// It finished. The round-trip time is a usable signal.
31
+ Ok,
32
+ /// It timed out, was rejected, or otherwise says the dependency is loaded.
33
+ Overload,
34
+ }
35
+
36
+ /// One completed call.
37
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
38
+ pub struct Sample {
39
+ /// Round-trip time in nanoseconds.
40
+ pub rtt_ns: u64,
41
+ /// Calls in flight when this one started, including itself.
42
+ pub in_flight: u32,
43
+ /// How it ended.
44
+ pub outcome: Outcome,
45
+ }
46
+
47
+ impl Sample {
48
+ #[must_use]
49
+ pub const fn ok(rtt_ns: u64, in_flight: u32) -> Self {
50
+ Self {
51
+ rtt_ns,
52
+ in_flight,
53
+ outcome: Outcome::Ok,
54
+ }
55
+ }
56
+
57
+ #[must_use]
58
+ pub const fn overload(rtt_ns: u64, in_flight: u32) -> Self {
59
+ Self {
60
+ rtt_ns,
61
+ in_flight,
62
+ outcome: Outcome::Overload,
63
+ }
64
+ }
65
+ }
66
+
67
+ /// A control loop over a concurrency limit.
68
+ pub trait Controller {
69
+ /// Fold one completed call into the limit.
70
+ fn observe(&mut self, sample: Sample);
71
+
72
+ /// Calls allowed in flight right now.
73
+ fn limit(&self) -> u32;
74
+ }
75
+
76
+ /// The bounds every controller moves its limit within.
77
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
78
+ pub struct Bounds {
79
+ /// Never go below this. A limit of zero would wedge the caller shut.
80
+ pub min: u32,
81
+ /// Never go above this.
82
+ pub max: u32,
83
+ }
84
+
85
+ impl Bounds {
86
+ #[must_use]
87
+ pub const fn new(min: u32, max: u32) -> Self {
88
+ Self { min, max }
89
+ }
90
+
91
+ #[must_use]
92
+ pub const fn clamp(&self, limit: u32) -> u32 {
93
+ if limit < self.min {
94
+ self.min
95
+ } else if limit > self.max {
96
+ self.max
97
+ } else {
98
+ limit
99
+ }
100
+ }
101
+ }
102
+
103
+ /// A limit of zero would wedge the caller shut, so one is the lowest floor
104
+ /// that still makes progress.
105
+ pub const LOWEST_FLOOR: u32 = 1;
106
+
107
+ /// Arbitrary, and deliberately so: there is no principled default ceiling for
108
+ /// an unknown dependency. Set it from what the pool behind it can take.
109
+ pub const DEFAULT_CEILING: u32 = 1_000;
110
+
111
+ impl Default for Bounds {
112
+ fn default() -> Self {
113
+ Self::new(LOWEST_FLOOR, DEFAULT_CEILING)
114
+ }
115
+ }
116
+
117
+ #[cfg(test)]
118
+ mod tests;
@@ -0,0 +1,113 @@
1
+ //! TCP Vegas, applied to a concurrency limit instead of a window.
2
+
3
+ use crate::{Bounds, Controller, Outcome, Sample};
4
+
5
+ /// Grow while fewer than this many calls look queued. From the Vegas paper,
6
+ /// which uses one to three packets; a concurrency limit tolerates the top of
7
+ /// that range.
8
+ pub const DEFAULT_ALPHA: u32 = 3;
9
+
10
+ /// Shrink once more than this many look queued. Twice `DEFAULT_ALPHA`, so the
11
+ /// hold band is wide enough that a single slow call does not trigger a cut.
12
+ pub const DEFAULT_BETA: u32 = 6;
13
+
14
+ /// Denominator of the fraction kept after an overload.
15
+ pub const OVERLOAD_DIVISOR: u32 = 2;
16
+
17
+ /// Calls added or removed per sample.
18
+ const STEP: u32 = 1;
19
+
20
+ /// 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.
23
+ ///
24
+ /// ```
25
+ /// use shed_machines::{Bounds, Controller, Sample, Vegas};
26
+ ///
27
+ /// let mut control = Vegas::new(Bounds::new(1, 100), 10);
28
+ ///
29
+ /// // Establishes the baseline, then grows while the queue estimate stays low.
30
+ /// control.observe(Sample::ok(1_000_000, 10));
31
+ /// assert_eq!(control.limit(), 11);
32
+ ///
33
+ /// // Ten times the baseline latency means the queue is deep.
34
+ /// control.observe(Sample::ok(10_000_000, 11));
35
+ /// assert!(control.limit() < 11);
36
+ /// ```
37
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
38
+ pub struct Vegas {
39
+ bounds: Bounds,
40
+ limit: u32,
41
+ best_rtt_ns: u64,
42
+ alpha: u32,
43
+ beta: u32,
44
+ }
45
+
46
+ impl Vegas {
47
+ #[must_use]
48
+ pub const fn new(bounds: Bounds, limit: u32) -> Self {
49
+ Self {
50
+ bounds,
51
+ limit: bounds.clamp(limit),
52
+ best_rtt_ns: u64::MAX,
53
+ alpha: DEFAULT_ALPHA,
54
+ beta: DEFAULT_BETA,
55
+ }
56
+ }
57
+
58
+ /// Grow below `alpha` queued calls, shrink above `beta`, hold between.
59
+ #[must_use]
60
+ pub const fn band(mut self, alpha: u32, beta: u32) -> Self {
61
+ self.alpha = alpha;
62
+ self.beta = beta;
63
+ self
64
+ }
65
+
66
+ /// The best round-trip time seen so far, the stand-in for zero queueing.
67
+ #[must_use]
68
+ pub const fn best_rtt_ns(&self) -> Option<u64> {
69
+ if self.best_rtt_ns == u64::MAX {
70
+ None
71
+ } else {
72
+ Some(self.best_rtt_ns)
73
+ }
74
+ }
75
+
76
+ /// `limit * (rtt - best) / rtt`, the Vegas estimate of how many calls in
77
+ /// flight are waiting rather than working.
78
+ fn queued(&self, rtt_ns: u64) -> u64 {
79
+ if rtt_ns == 0 || self.best_rtt_ns >= rtt_ns {
80
+ return 0;
81
+ }
82
+ let excess = rtt_ns - self.best_rtt_ns;
83
+ let scaled = u128::from(self.limit) * u128::from(excess) / u128::from(rtt_ns);
84
+ u64::try_from(scaled).unwrap_or(u64::MAX)
85
+ }
86
+ }
87
+
88
+ impl Controller for Vegas {
89
+ fn observe(&mut self, sample: Sample) {
90
+ if sample.outcome == Outcome::Overload {
91
+ self.limit = self.bounds.clamp(self.limit / OVERLOAD_DIVISOR);
92
+ return;
93
+ }
94
+
95
+ self.best_rtt_ns = self.best_rtt_ns.min(sample.rtt_ns);
96
+ let queued = self.queued(sample.rtt_ns);
97
+
98
+ self.limit = if queued < u64::from(self.alpha) {
99
+ self.bounds.clamp(self.limit.saturating_add(STEP))
100
+ } else if queued > u64::from(self.beta) {
101
+ self.bounds.clamp(self.limit.saturating_sub(STEP))
102
+ } else {
103
+ self.limit
104
+ };
105
+ }
106
+
107
+ fn limit(&self) -> u32 {
108
+ self.limit
109
+ }
110
+ }
111
+
112
+ #[cfg(test)]
113
+ mod tests;
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ffi/extconf'
@@ -0,0 +1,19 @@
1
+ [package]
2
+ name = "shed_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 shed-machines control laws"
9
+ publish = false
10
+
11
+ [lib]
12
+ crate-type = ["cdylib"]
13
+
14
+ [lints]
15
+ workspace = true
16
+
17
+ [dependencies]
18
+ shed-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('shed_machines_native/shed_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,50 @@
1
+ //! `ShedMachines::Aimd`: grow by one per saturated success, cut on overload.
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::aimd::{DEFAULT_BACKOFF_KEPT, DEFAULT_BACKOFF_OF};
9
+ use shed_machines::{Aimd, Controller};
10
+
11
+ use crate::{observe, opening};
12
+
13
+ #[magnus::wrap(class = "ShedMachines::Aimd", free_immediately, size)]
14
+ struct Wrapper(RefCell<Aimd>);
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, [kept, of]) = opening(ruby, args.keywords, ["kept", "of"])?;
20
+ let control = Aimd::new(bounds, limit).backoff(
21
+ kept.unwrap_or(DEFAULT_BACKOFF_KEPT),
22
+ of.unwrap_or(DEFAULT_BACKOFF_OF),
23
+ );
24
+ Ok(Self(RefCell::new(control)))
25
+ }
26
+
27
+ fn observe(ruby: &Ruby, rb_self: Obj<Self>, args: &[Value]) -> Result<Obj<Self>, Error> {
28
+ observe(ruby, &rb_self.0, args)?;
29
+ Ok(rb_self)
30
+ }
31
+
32
+ fn limit(&self) -> u32 {
33
+ self.0.borrow().limit()
34
+ }
35
+
36
+ fn inspect(&self) -> String {
37
+ format!("#<ShedMachines::Aimd limit={}>", self.limit())
38
+ }
39
+ }
40
+
41
+ pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
42
+ let class = module.define_class("Aimd", ruby.class_object())?;
43
+ class.const_set("DEFAULT_BACKOFF_KEPT", DEFAULT_BACKOFF_KEPT)?;
44
+ class.const_set("DEFAULT_BACKOFF_OF", DEFAULT_BACKOFF_OF)?;
45
+ class.define_singleton_method("new", function!(Wrapper::new, -1))?;
46
+ class.define_method("observe", method!(Wrapper::observe, -1))?;
47
+ class.define_method("limit", method!(Wrapper::limit, 0))?;
48
+ class.define_method("inspect", method!(Wrapper::inspect, 0))?;
49
+ Ok(())
50
+ }
@@ -0,0 +1,79 @@
1
+ //! `ShedMachines::Budget`: how much outbound work to drop before it leaves.
2
+
3
+ use std::cell::RefCell;
4
+
5
+ use magnus::scan_args::{get_kwargs, scan_args};
6
+ use magnus::typed_data::Obj;
7
+ use magnus::{Error, RHash, RModule, Ruby, Value, function, method, prelude::*};
8
+ use shed_machines::Budget;
9
+ use shed_machines::budget::DEFAULT_MULTIPLIER;
10
+
11
+ #[magnus::wrap(class = "ShedMachines::Budget", free_immediately, size)]
12
+ struct Wrapper(RefCell<Budget>);
13
+
14
+ impl Wrapper {
15
+ fn new(args: &[Value]) -> Result<Self, Error> {
16
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
17
+ let (multiplier,) =
18
+ get_kwargs::<_, (), (Option<f64>,), ()>(args.keywords, &[], &["multiplier"])?.optional;
19
+ let budget = Budget::new(multiplier.unwrap_or(DEFAULT_MULTIPLIER));
20
+ Ok(Self(RefCell::new(budget)))
21
+ }
22
+
23
+ fn requested(rb_self: Obj<Self>) -> Obj<Self> {
24
+ rb_self.0.borrow_mut().requested();
25
+ rb_self
26
+ }
27
+
28
+ fn accepted(rb_self: Obj<Self>) -> Obj<Self> {
29
+ rb_self.0.borrow_mut().accepted();
30
+ rb_self
31
+ }
32
+
33
+ /// Halves both counts, so the budget forgets an outage.
34
+ fn decay(rb_self: Obj<Self>) -> Obj<Self> {
35
+ rb_self.0.borrow_mut().decay();
36
+ rb_self
37
+ }
38
+
39
+ fn multiplier(&self) -> f64 {
40
+ self.0.borrow().multiplier()
41
+ }
42
+
43
+ fn requests(&self) -> u64 {
44
+ self.0.borrow().requests()
45
+ }
46
+
47
+ fn accepts(&self) -> u64 {
48
+ self.0.borrow().accepts()
49
+ }
50
+
51
+ /// The fraction of outbound work to drop locally, in `0.0..1.0`.
52
+ fn shed_probability(&self) -> f64 {
53
+ self.0.borrow().shed_probability()
54
+ }
55
+
56
+ fn inspect(&self) -> String {
57
+ format!(
58
+ "#<ShedMachines::Budget requests={} accepts={} shed_probability={}>",
59
+ self.requests(),
60
+ self.accepts(),
61
+ self.shed_probability()
62
+ )
63
+ }
64
+ }
65
+
66
+ pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
67
+ let class = module.define_class("Budget", ruby.class_object())?;
68
+ class.const_set("DEFAULT_MULTIPLIER", DEFAULT_MULTIPLIER)?;
69
+ class.define_singleton_method("new", function!(Wrapper::new, -1))?;
70
+ class.define_method("requested", method!(Wrapper::requested, 0))?;
71
+ class.define_method("accepted", method!(Wrapper::accepted, 0))?;
72
+ class.define_method("decay", method!(Wrapper::decay, 0))?;
73
+ class.define_method("multiplier", method!(Wrapper::multiplier, 0))?;
74
+ class.define_method("requests", method!(Wrapper::requests, 0))?;
75
+ class.define_method("accepts", method!(Wrapper::accepts, 0))?;
76
+ class.define_method("shed_probability", method!(Wrapper::shed_probability, 0))?;
77
+ class.define_method("inspect", method!(Wrapper::inspect, 0))?;
78
+ Ok(())
79
+ }