deadline_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 +36 -0
- data/Cargo.toml +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +115 -0
- data/ext/deadline_machines_native/core/Cargo.toml +24 -0
- data/ext/deadline_machines_native/core/src/lib.rs +331 -0
- data/lib/deadline_machines/clock.rb +22 -0
- data/lib/deadline_machines/deadline.rb +183 -0
- data/lib/deadline_machines/errors.rb +24 -0
- data/lib/deadline_machines/version.rb +5 -0
- data/lib/deadline_machines.rb +56 -0
- data/sig/deadline_machines.rbs +78 -0
- metadata +59 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c025a93a7c8b92aac8ca2e2ff49e11642a4a2f340e6d53db262234d66fa793c7
|
|
4
|
+
data.tar.gz: d2e0aa5a9c97eb1dfcfd7ed8177ffa92b1d4c2591a7c415cdfb3bd1ab667933d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0a6c0eeeb3c2fe88f63f9298aa5d669a1f2fab1e2c736513df7b4cbdae272a492d3c1df6b9d7eb6927b37561721aef6dbc657f13e3114bf4f54b70926188ec61
|
|
7
|
+
data.tar.gz: b5303a52f3a0e5c82c7eae24d6cc43180803491cae205e1b11f742430e4fdc0ec8385ff329a31ccb259ecc6ceb39241c78b3bf83c6a2bdd99f29a37e78c2e9ae
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.1](https://github.com/seuros/deadline_machines/compare/deadline_machines-v0.1.0...deadline_machines/v0.1.1) (2026-11-01)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **core:** add Scope, RBS signatures, release config and CI ([24018c3](https://github.com/seuros/deadline_machines/commit/24018c39edcb12634f92057d699e1f2ad936ad4a))
|
|
9
|
+
* deadline propagation and cooperative cancellation ([9858496](https://github.com/seuros/deadline_machines/commit/9858496f48cc0eca1edf8df613f9974fa15abba2))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* a block scope lets go of its child's retention too ([e5502c2](https://github.com/seuros/deadline_machines/commit/e5502c29ba21306ef386303e465195d27cb5d643))
|
|
15
|
+
* a cancellation racing a registration no longer strands the child ([e387b71](https://github.com/seuros/deadline_machines/commit/e387b71e6464bb5a6b35ec17b68e898231a8eca7))
|
|
16
|
+
* a failed gem push fails the release ([26678da](https://github.com/seuros/deadline_machines/commit/26678da9039c72be8457e53f3cc4473b6394f5c1))
|
|
17
|
+
* a nested scope without a limit inherits the ambient budget ([f88a0d5](https://github.com/seuros/deadline_machines/commit/f88a0d56497ee199664bd96fa479d6262abcc245))
|
|
18
|
+
* a parent no longer owns every deadline derived from it ([41d609c](https://github.com/seuros/deadline_machines/commit/41d609c623a480b94a3f2789c0d7c3dbf2920ee9))
|
|
19
|
+
* an observer deep in a chain is held all the way to the root ([9abeb8d](https://github.com/seuros/deadline_machines/commit/9abeb8d311f85991d4eed2d4f932b147e59ed574))
|
|
20
|
+
* detachment stops the retention walk, not just its first step ([44ff4d1](https://github.com/seuros/deadline_machines/commit/44ff4d108a6dc139a7731e50432d5010b2f7c2cd))
|
|
21
|
+
* one failing cancel observer no longer silences its peers ([a9ddd70](https://github.com/seuros/deadline_machines/commit/a9ddd70d0d1355aacfc0170d9b874bd9c90e3309))
|
|
22
|
+
* one lock per tree, so a retention chain walk is atomic ([eec49b9](https://github.com/seuros/deadline_machines/commit/eec49b9ff6a79930cfbb4cbb8de398ad7b75efaa))
|
|
23
|
+
* ship the core's test files so the packaged crate builds ([0340bc6](https://github.com/seuros/deadline_machines/commit/0340bc6ec958cb1983212f4cd0b9943cc36aef5c))
|
|
24
|
+
|
|
25
|
+
## 0.1.0
|
|
26
|
+
|
|
27
|
+
- Initial release.
|
|
28
|
+
- `DeadlineMachines::Deadline`: monotonic budgets, `derive` for child scopes that
|
|
29
|
+
cannot outlive their parent, cancellation that flows down a tree, and `check!`
|
|
30
|
+
as the only place an error is raised.
|
|
31
|
+
- `DeadlineMachines.with_deadline` for a fiber-scoped ambient deadline.
|
|
32
|
+
- `DeadlineMachines::TestClock` for deterministic tests across the family.
|
|
33
|
+
- `deadline-machines` core crate: the same arithmetic and cancellation tree in
|
|
34
|
+
`no_std` Rust with zero dependencies, plus `Scope` pairing a budget with a
|
|
35
|
+
cancellation handle. Builds and lints clean on bare metal.
|
|
36
|
+
- RBS signatures for the whole public API.
|
data/Cargo.toml
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[workspace]
|
|
2
|
+
members = ["ext/deadline_machines_native/core"]
|
|
3
|
+
resolver = "3"
|
|
4
|
+
|
|
5
|
+
[workspace.package]
|
|
6
|
+
edition = "2024"
|
|
7
|
+
rust-version = "1.99"
|
|
8
|
+
|
|
9
|
+
[workspace.dependencies]
|
|
10
|
+
deadline-machines = { path = "ext/deadline_machines_native/core" }
|
|
11
|
+
|
|
12
|
+
[workspace.lints.rust]
|
|
13
|
+
unsafe_code = "forbid"
|
|
14
|
+
unused_must_use = "warn"
|
|
15
|
+
|
|
16
|
+
[workspace.lints.clippy]
|
|
17
|
+
all = { level = "warn", priority = -1 }
|
|
18
|
+
pedantic = { level = "warn", priority = -1 }
|
|
19
|
+
nursery = { level = "warn", priority = -1 }
|
|
20
|
+
expect_used = "deny"
|
|
21
|
+
unwrap_used = "deny"
|
|
22
|
+
uninlined_format_args = "deny"
|
|
23
|
+
needless_borrow = "deny"
|
|
24
|
+
redundant_clone = "deny"
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Abdelkader Boudih
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# DeadlineMachines ⏱️
|
|
2
|
+
|
|
3
|
+
> **One clock for the whole fleet.** The tender that hands every ship the same
|
|
4
|
+
> budget, so nobody spends time the mission no longer has.
|
|
5
|
+
|
|
6
|
+
Deadline propagation and cooperative cancellation for Ruby.
|
|
7
|
+
|
|
8
|
+
`Timeout.timeout` raises wherever the interpreter happens to be, which is why it
|
|
9
|
+
corrupts connection state and why every serious library bans it. A deadline here
|
|
10
|
+
is a value you pass down and check at points you choose. **AMS Layland** carries
|
|
11
|
+
no weapons and trips no failures; it keeps the time the rest of the fleet
|
|
12
|
+
navigates by.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 🚀 Launch Sequence
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Add to your ship's Gemfile
|
|
20
|
+
gem 'deadline_machines'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires **Ruby 4.0+**. Nothing else: no Rust toolchain, no platform gem, no
|
|
24
|
+
compiled extension.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 🎯 Mission Brief
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
DeadlineMachines.with_deadline(5) do |request|
|
|
32
|
+
request.remaining # => 5.0
|
|
33
|
+
|
|
34
|
+
request.derive(1) do |attempt|
|
|
35
|
+
attempt.remaining # => 1.0
|
|
36
|
+
attempt.check! # raises DeadlineMachines::Exceeded when spent
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
DeadlineMachines.with_deadline(60) do
|
|
40
|
+
DeadlineMachines.remaining # => 5.0, a nested scope cannot widen the budget
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
request.cancel('client hung up')
|
|
44
|
+
DeadlineMachines.check! # raises DeadlineMachines::Cancelled
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 🛰️ Why the Fleet Needs It
|
|
51
|
+
|
|
52
|
+
Without a shared budget, a retry policy will happily spend thirty seconds on a
|
|
53
|
+
request the caller abandoned twenty-eight seconds ago. `clamp_sleep` is the hook
|
|
54
|
+
that stops it:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
delay = deadline.clamp_sleep(backoff.next_delay) # never overruns what is left
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`DeadlineMachines.current` is fiber-scoped, so `Async` tasks inherit the ambient
|
|
61
|
+
deadline without being handed one.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 🕰️ The Ship's Chronometer
|
|
66
|
+
|
|
67
|
+
Budgets are built on `CLOCK_MONOTONIC`, so an NTP step cannot stretch them.
|
|
68
|
+
Inject `TestClock` and the whole family becomes deterministic:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
clock = DeadlineMachines::TestClock.new
|
|
72
|
+
DeadlineMachines.clock = clock
|
|
73
|
+
deadline = DeadlineMachines::Deadline.after(1)
|
|
74
|
+
clock.advance(2)
|
|
75
|
+
deadline.expired? # => true
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 🔻 Cancellation Runs Down the Chain of Command
|
|
81
|
+
|
|
82
|
+
Cancellation flows down a tree and never up. A child derived after its parent was
|
|
83
|
+
cancelled is already cancelled. `on_cancel` fires once, and a callback registered
|
|
84
|
+
on an already-cancelled deadline fires immediately. The block form of `derive`
|
|
85
|
+
unlinks the child on exit, so a long-lived parent does not accumulate children.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 🔩 The Rust Core, and Why No Extension
|
|
90
|
+
|
|
91
|
+
The same arithmetic and cancellation tree ships as the `deadline-machines` crate
|
|
92
|
+
under `ext/deadline_machines_native/core`, `no_std` with zero dependencies and
|
|
93
|
+
verified on bare metal. `Scope` there is the peer of `Deadline` here: a budget
|
|
94
|
+
and a cancellation handle as one value.
|
|
95
|
+
|
|
96
|
+
The gem itself is pure Ruby. Reading a monotonic clock is already a vDSO read,
|
|
97
|
+
so there is nothing here worth a native extension. The crate is there for Rust
|
|
98
|
+
consumers, not for this gem's install.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 📡 Credits
|
|
103
|
+
|
|
104
|
+
**AMS Layland**, NCC-0112, *Layland*-class tender.
|
|
105
|
+
|
|
106
|
+
Named for C. L. Liu and James W. Layland's *Scheduling Algorithms for
|
|
107
|
+
Multiprogramming in a Hard-Real-Time Environment* (1973), which gave
|
|
108
|
+
earliest-deadline-first scheduling its proof and made a deadline a value a
|
|
109
|
+
system reasons about rather than an alarm it waits for.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "deadline-machines"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
rust-version.workspace = true
|
|
6
|
+
authors = ["Abdelkader Boudih <oss@seuros.com>"]
|
|
7
|
+
license = "MIT"
|
|
8
|
+
description = "Monotonic deadline arithmetic and cooperative cancellation trees - no_std, zero dependencies"
|
|
9
|
+
repository = "https://github.com/seuros/deadline_machines"
|
|
10
|
+
keywords = ["deadline", "timeout", "cancellation", "resilience", "no-std"]
|
|
11
|
+
categories = ["no-std", "concurrency"]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
|
|
14
|
+
[lib]
|
|
15
|
+
name = "deadline_machines"
|
|
16
|
+
crate-type = ["lib"]
|
|
17
|
+
|
|
18
|
+
[features]
|
|
19
|
+
default = ["std"]
|
|
20
|
+
std = ["alloc"]
|
|
21
|
+
alloc = []
|
|
22
|
+
|
|
23
|
+
[lints]
|
|
24
|
+
workspace = true
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#![cfg_attr(not(feature = "std"), no_std)]
|
|
2
|
+
|
|
3
|
+
#[cfg(feature = "alloc")]
|
|
4
|
+
extern crate alloc;
|
|
5
|
+
|
|
6
|
+
/// Compiles and runs the README's examples as doctests.
|
|
7
|
+
#[cfg(doctest)]
|
|
8
|
+
#[doc = include_str!("../README.md")]
|
|
9
|
+
struct ReadmeDoctests;
|
|
10
|
+
|
|
11
|
+
pub const NS_PER_SECOND: u64 = 1_000_000_000;
|
|
12
|
+
|
|
13
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
14
|
+
pub struct Instant(pub u64);
|
|
15
|
+
|
|
16
|
+
pub trait Clock {
|
|
17
|
+
fn now_ns(&self) -> u64;
|
|
18
|
+
|
|
19
|
+
fn now(&self) -> Instant {
|
|
20
|
+
Instant(self.now_ns())
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
25
|
+
pub struct Deadline {
|
|
26
|
+
expires_at: Option<Instant>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl Deadline {
|
|
30
|
+
#[must_use]
|
|
31
|
+
pub const fn never() -> Self {
|
|
32
|
+
Self { expires_at: None }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[must_use]
|
|
36
|
+
pub const fn at(instant: Instant) -> Self {
|
|
37
|
+
Self {
|
|
38
|
+
expires_at: Some(instant),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[must_use]
|
|
43
|
+
pub const fn after(now: Instant, timeout_ns: u64) -> Self {
|
|
44
|
+
Self::at(Instant(now.0.saturating_add(timeout_ns)))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[must_use]
|
|
48
|
+
pub const fn expires_at(&self) -> Option<Instant> {
|
|
49
|
+
self.expires_at
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[must_use]
|
|
53
|
+
pub const fn remaining_ns(&self, now: Instant) -> Option<u64> {
|
|
54
|
+
match self.expires_at {
|
|
55
|
+
Some(expiry) => Some(expiry.0.saturating_sub(now.0)),
|
|
56
|
+
None => None,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[must_use]
|
|
61
|
+
pub const fn is_expired(&self, now: Instant) -> bool {
|
|
62
|
+
matches!(self.remaining_ns(now), Some(0))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[must_use]
|
|
66
|
+
pub const fn derive(&self, now: Instant, timeout_ns: u64) -> Self {
|
|
67
|
+
let candidate = Instant(now.0.saturating_add(timeout_ns));
|
|
68
|
+
match self.expires_at {
|
|
69
|
+
None => Self::at(candidate),
|
|
70
|
+
Some(parent) if candidate.0 < parent.0 => Self::at(candidate),
|
|
71
|
+
Some(parent) => Self::at(parent),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[must_use]
|
|
76
|
+
pub const fn tighten(&self, other: Self) -> Self {
|
|
77
|
+
match (self.expires_at, other.expires_at) {
|
|
78
|
+
(None, any) | (any, None) => Self { expires_at: any },
|
|
79
|
+
(Some(a), Some(b)) if a.0 < b.0 => Self::at(a),
|
|
80
|
+
(Some(_), Some(b)) => Self::at(b),
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#[must_use]
|
|
85
|
+
pub const fn clamp_sleep_ns(&self, now: Instant, requested_ns: u64) -> u64 {
|
|
86
|
+
match self.remaining_ns(now) {
|
|
87
|
+
Some(left) if left < requested_ns => left,
|
|
88
|
+
_ => requested_ns,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#[cfg(feature = "alloc")]
|
|
94
|
+
pub mod cancel {
|
|
95
|
+
use alloc::sync::Arc;
|
|
96
|
+
use core::sync::atomic::{AtomicBool, Ordering};
|
|
97
|
+
|
|
98
|
+
#[derive(Debug)]
|
|
99
|
+
struct Node {
|
|
100
|
+
cancelled: AtomicBool,
|
|
101
|
+
parent: Option<Arc<Self>>,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
impl Node {
|
|
105
|
+
fn is_cancelled(&self) -> bool {
|
|
106
|
+
self.cancelled.load(Ordering::Acquire)
|
|
107
|
+
|| self
|
|
108
|
+
.parent
|
|
109
|
+
.as_ref()
|
|
110
|
+
.is_some_and(|parent| parent.is_cancelled())
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#[derive(Debug, Clone)]
|
|
115
|
+
pub struct CancelToken {
|
|
116
|
+
node: Arc<Node>,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
impl CancelToken {
|
|
120
|
+
#[must_use]
|
|
121
|
+
pub fn root() -> Self {
|
|
122
|
+
Self {
|
|
123
|
+
node: Arc::new(Node {
|
|
124
|
+
cancelled: AtomicBool::new(false),
|
|
125
|
+
parent: None,
|
|
126
|
+
}),
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[must_use]
|
|
131
|
+
pub fn child(&self) -> Self {
|
|
132
|
+
Self {
|
|
133
|
+
node: Arc::new(Node {
|
|
134
|
+
cancelled: AtomicBool::new(false),
|
|
135
|
+
parent: Some(Arc::clone(&self.node)),
|
|
136
|
+
}),
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
pub fn cancel(&self) {
|
|
141
|
+
self.node.cancelled.store(true, Ordering::Release);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#[must_use]
|
|
145
|
+
pub fn is_cancelled(&self) -> bool {
|
|
146
|
+
self.node.is_cancelled()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
impl Default for CancelToken {
|
|
151
|
+
fn default() -> Self {
|
|
152
|
+
Self::root()
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Why a [`Scope`] stopped admitting work.
|
|
158
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
159
|
+
pub enum Stopped {
|
|
160
|
+
/// The budget ran out.
|
|
161
|
+
Exceeded,
|
|
162
|
+
/// Someone upstream gave up.
|
|
163
|
+
Cancelled,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
impl core::fmt::Display for Stopped {
|
|
167
|
+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
168
|
+
f.write_str(match self {
|
|
169
|
+
Self::Exceeded => "deadline exceeded",
|
|
170
|
+
Self::Cancelled => "deadline cancelled",
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
impl core::error::Error for Stopped {}
|
|
176
|
+
|
|
177
|
+
#[cfg(feature = "alloc")]
|
|
178
|
+
mod scope {
|
|
179
|
+
use super::{Deadline, Instant, Stopped, cancel::CancelToken};
|
|
180
|
+
|
|
181
|
+
/// A budget and a cancellation handle as one value, which is what a retry
|
|
182
|
+
/// policy or a circuit breaker actually needs to consult.
|
|
183
|
+
///
|
|
184
|
+
/// ```
|
|
185
|
+
/// use deadline_machines::{Deadline, Instant, Scope, Stopped};
|
|
186
|
+
///
|
|
187
|
+
/// let now = Instant(0);
|
|
188
|
+
/// let request = Scope::new(Deadline::after(now, 5_000));
|
|
189
|
+
///
|
|
190
|
+
/// let attempt = request.derive(now, 9_000);
|
|
191
|
+
/// assert_eq!(attempt.deadline(), request.deadline());
|
|
192
|
+
///
|
|
193
|
+
/// request.cancel();
|
|
194
|
+
/// assert_eq!(attempt.check(now), Err(Stopped::Cancelled));
|
|
195
|
+
/// ```
|
|
196
|
+
#[derive(Debug, Clone)]
|
|
197
|
+
pub struct Scope {
|
|
198
|
+
deadline: Deadline,
|
|
199
|
+
token: CancelToken,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
impl Scope {
|
|
203
|
+
#[must_use]
|
|
204
|
+
pub fn new(deadline: Deadline) -> Self {
|
|
205
|
+
Self {
|
|
206
|
+
deadline,
|
|
207
|
+
token: CancelToken::root(),
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// A scope with no time limit, cancellable only.
|
|
212
|
+
#[must_use]
|
|
213
|
+
pub fn never() -> Self {
|
|
214
|
+
Self::new(Deadline::never())
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#[must_use]
|
|
218
|
+
pub const fn deadline(&self) -> Deadline {
|
|
219
|
+
self.deadline
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#[must_use]
|
|
223
|
+
pub const fn remaining_ns(&self, now: Instant) -> Option<u64> {
|
|
224
|
+
self.deadline.remaining_ns(now)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#[must_use]
|
|
228
|
+
pub fn is_cancelled(&self) -> bool {
|
|
229
|
+
self.token.is_cancelled()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#[must_use]
|
|
233
|
+
pub const fn is_expired(&self, now: Instant) -> bool {
|
|
234
|
+
self.deadline.is_expired(now)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#[must_use]
|
|
238
|
+
pub fn is_done(&self, now: Instant) -> bool {
|
|
239
|
+
self.is_cancelled() || self.is_expired(now)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/// Cancellation reaches every scope derived from this one, now or later.
|
|
243
|
+
pub fn cancel(&self) {
|
|
244
|
+
self.token.cancel();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// The cooperative checkpoint.
|
|
248
|
+
///
|
|
249
|
+
/// # Errors
|
|
250
|
+
///
|
|
251
|
+
/// [`Stopped::Cancelled`] when this scope or an ancestor was cancelled,
|
|
252
|
+
/// which outranks [`Stopped::Exceeded`] for a spent budget. Matches the
|
|
253
|
+
/// precedence on the Ruby side.
|
|
254
|
+
pub fn check(&self, now: Instant) -> Result<(), Stopped> {
|
|
255
|
+
if self.is_cancelled() {
|
|
256
|
+
return Err(Stopped::Cancelled);
|
|
257
|
+
}
|
|
258
|
+
if self.is_expired(now) {
|
|
259
|
+
return Err(Stopped::Exceeded);
|
|
260
|
+
}
|
|
261
|
+
Ok(())
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/// A child scope whose deadline is the tighter of this one and
|
|
265
|
+
/// `timeout_ns` from `now`.
|
|
266
|
+
#[must_use]
|
|
267
|
+
pub fn derive(&self, now: Instant, timeout_ns: u64) -> Self {
|
|
268
|
+
Self {
|
|
269
|
+
deadline: self.deadline.derive(now, timeout_ns),
|
|
270
|
+
token: self.token.child(),
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// A child scope with the same deadline, cancellable on its own.
|
|
275
|
+
#[must_use]
|
|
276
|
+
pub fn child(&self) -> Self {
|
|
277
|
+
Self {
|
|
278
|
+
deadline: self.deadline,
|
|
279
|
+
token: self.token.child(),
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/// The longest sleep that still fits the budget. A retry policy calls
|
|
284
|
+
/// this before each backoff so it cannot outlive its caller.
|
|
285
|
+
#[must_use]
|
|
286
|
+
pub const fn clamp_sleep_ns(&self, now: Instant, requested_ns: u64) -> u64 {
|
|
287
|
+
self.deadline.clamp_sleep_ns(now, requested_ns)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#[cfg(feature = "alloc")]
|
|
293
|
+
pub use scope::Scope;
|
|
294
|
+
|
|
295
|
+
#[cfg(feature = "std")]
|
|
296
|
+
mod system_clock {
|
|
297
|
+
use super::Clock;
|
|
298
|
+
use std::time::Instant as StdInstant;
|
|
299
|
+
|
|
300
|
+
#[derive(Debug, Clone, Copy)]
|
|
301
|
+
pub struct SystemClock {
|
|
302
|
+
origin: StdInstant,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
impl SystemClock {
|
|
306
|
+
#[must_use]
|
|
307
|
+
pub fn new() -> Self {
|
|
308
|
+
Self {
|
|
309
|
+
origin: StdInstant::now(),
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
impl Default for SystemClock {
|
|
315
|
+
fn default() -> Self {
|
|
316
|
+
Self::new()
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
impl Clock for SystemClock {
|
|
321
|
+
fn now_ns(&self) -> u64 {
|
|
322
|
+
u64::try_from(self.origin.elapsed().as_nanos()).unwrap_or(u64::MAX)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
#[cfg(feature = "std")]
|
|
328
|
+
pub use system_clock::SystemClock;
|
|
329
|
+
|
|
330
|
+
#[cfg(test)]
|
|
331
|
+
mod tests;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DeadlineMachines
|
|
4
|
+
module MonotonicClock
|
|
5
|
+
def self.now_ns
|
|
6
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class TestClock
|
|
11
|
+
attr_accessor :now_ns
|
|
12
|
+
|
|
13
|
+
def initialize(now_ns: 0)
|
|
14
|
+
@now_ns = now_ns
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def advance(seconds)
|
|
18
|
+
@now_ns += (seconds.to_r * 1_000_000_000).round
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DeadlineMachines
|
|
4
|
+
class Deadline
|
|
5
|
+
NS_PER_SECOND = 1_000_000_000
|
|
6
|
+
|
|
7
|
+
attr_reader :expires_at_ns, :parent, :clock, :reason
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def after(seconds, clock: DeadlineMachines.clock)
|
|
11
|
+
new(expires_at_ns: clock.now_ns + to_ns(seconds), clock: clock)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def never(clock: DeadlineMachines.clock)
|
|
15
|
+
new(expires_at_ns: nil, clock: clock)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_ns(seconds)
|
|
19
|
+
(seconds.to_r * NS_PER_SECOND).round
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(expires_at_ns:, clock:, parent: nil)
|
|
24
|
+
@expires_at_ns = expires_at_ns
|
|
25
|
+
@clock = clock
|
|
26
|
+
@parent = parent
|
|
27
|
+
@cancelled = false
|
|
28
|
+
@detached = false
|
|
29
|
+
@reason = nil
|
|
30
|
+
@children = ObjectSpace::WeakMap.new
|
|
31
|
+
@retained = {}
|
|
32
|
+
@callbacks = []
|
|
33
|
+
@mutex = parent ? parent.mutex : Mutex.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def remaining_ns
|
|
37
|
+
return nil unless @expires_at_ns
|
|
38
|
+
|
|
39
|
+
left = @expires_at_ns - @clock.now_ns
|
|
40
|
+
left.negative? ? 0 : left
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def remaining
|
|
44
|
+
ns = remaining_ns
|
|
45
|
+
ns ? ns / NS_PER_SECOND.to_f : Float::INFINITY
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def expired?
|
|
49
|
+
remaining_ns == 0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def cancelled?
|
|
53
|
+
@cancelled || !!@parent&.cancelled?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def done?
|
|
57
|
+
cancelled? || expired?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def check!
|
|
61
|
+
raise Cancelled.new(self, cancel_reason) if cancelled?
|
|
62
|
+
raise Exceeded, self if expired?
|
|
63
|
+
|
|
64
|
+
self
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def clamp_sleep(seconds)
|
|
68
|
+
left = remaining
|
|
69
|
+
seconds < left ? seconds : left
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def cancel(reason = nil)
|
|
73
|
+
callbacks, children = @mutex.synchronize do
|
|
74
|
+
return self if @cancelled
|
|
75
|
+
|
|
76
|
+
@cancelled = true
|
|
77
|
+
@reason = reason
|
|
78
|
+
@parent&.drop(self)
|
|
79
|
+
[@callbacks.dup, @children.keys]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
failures = []
|
|
83
|
+
children.each { |child| notify(failures) { child.cancel(reason) } }
|
|
84
|
+
callbacks.each { |callback| notify(failures) { callback.call(self) } }
|
|
85
|
+
raise failures.first unless failures.empty?
|
|
86
|
+
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def on_cancel(&block)
|
|
91
|
+
already = @mutex.synchronize do
|
|
92
|
+
settled = cancelled?
|
|
93
|
+
unless settled
|
|
94
|
+
@callbacks << block
|
|
95
|
+
@parent&.hold(self) unless @detached
|
|
96
|
+
end
|
|
97
|
+
settled
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
block.call(self) if already
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def derive(seconds = nil)
|
|
105
|
+
child = self.class.new(expires_at_ns: child_expiry_ns(seconds), clock: @clock, parent: self)
|
|
106
|
+
@mutex.synchronize { @children[child] = true }
|
|
107
|
+
return child unless block_given?
|
|
108
|
+
|
|
109
|
+
begin
|
|
110
|
+
yield child
|
|
111
|
+
ensure
|
|
112
|
+
@mutex.synchronize { detach(child) }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def inspect
|
|
117
|
+
state = cancelled? ? 'cancelled' : (expired? ? 'expired' : format('%.3fs left', remaining))
|
|
118
|
+
"#<#{self.class} #{state}>"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
protected
|
|
122
|
+
|
|
123
|
+
attr_reader :mutex
|
|
124
|
+
attr_writer :detached
|
|
125
|
+
|
|
126
|
+
# A derived deadline is the caller's to keep, so the parent follows it
|
|
127
|
+
# weakly. Something to deliver is the exception, and it has to be held all
|
|
128
|
+
# the way to the root: an intermediate deadline nobody else references is
|
|
129
|
+
# the one link whose collection would strand the delivery below it.
|
|
130
|
+
#
|
|
131
|
+
# Holding and dropping walk the chain, so they cannot be made atomic out of
|
|
132
|
+
# one lock per link: every deadline in a tree shares the root's lock, and
|
|
133
|
+
# both run inside a section the caller already holds.
|
|
134
|
+
def hold(child)
|
|
135
|
+
@retained[child] = true
|
|
136
|
+
@parent.hold(self) if @parent && !@detached
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def drop(child)
|
|
140
|
+
@retained.delete(child)
|
|
141
|
+
@parent.drop(self) if @parent && !pending?
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# A scope that has ended owns its child outright: the tree will not reach
|
|
145
|
+
# it again, so the parent holds nothing for it and an observer registered
|
|
146
|
+
# afterwards is the caller's own to keep alive.
|
|
147
|
+
def detach(child)
|
|
148
|
+
@children.delete(child)
|
|
149
|
+
child.detached = true
|
|
150
|
+
drop(child)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Whether anything is still owed through this deadline.
|
|
154
|
+
def pending?
|
|
155
|
+
!@cancelled && !(@callbacks.empty? && @retained.empty?)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
# Every observer and descendant is told, then the first failure is raised.
|
|
161
|
+
# One raising observer must not silence its peers.
|
|
162
|
+
def notify(failures)
|
|
163
|
+
yield
|
|
164
|
+
rescue StandardError => e
|
|
165
|
+
failures << e
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
protected
|
|
169
|
+
|
|
170
|
+
def cancel_reason
|
|
171
|
+
@reason || @parent&.cancel_reason
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
def child_expiry_ns(seconds)
|
|
177
|
+
return @expires_at_ns unless seconds
|
|
178
|
+
|
|
179
|
+
candidate = @clock.now_ns + self.class.to_ns(seconds)
|
|
180
|
+
@expires_at_ns ? [candidate, @expires_at_ns].min : candidate
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DeadlineMachines
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class Exceeded < Error
|
|
7
|
+
attr_reader :deadline
|
|
8
|
+
|
|
9
|
+
def initialize(deadline)
|
|
10
|
+
@deadline = deadline
|
|
11
|
+
super('deadline exceeded')
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class Cancelled < Error
|
|
16
|
+
attr_reader :deadline, :reason
|
|
17
|
+
|
|
18
|
+
def initialize(deadline, reason = nil)
|
|
19
|
+
@deadline = deadline
|
|
20
|
+
@reason = reason
|
|
21
|
+
super(reason ? "deadline cancelled: #{reason}" : 'deadline cancelled')
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'deadline_machines/version'
|
|
4
|
+
require_relative 'deadline_machines/errors'
|
|
5
|
+
require_relative 'deadline_machines/clock'
|
|
6
|
+
require_relative 'deadline_machines/deadline'
|
|
7
|
+
|
|
8
|
+
module DeadlineMachines
|
|
9
|
+
FIBER_KEY = :deadline_machines_deadline
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
attr_writer :clock
|
|
13
|
+
|
|
14
|
+
def clock
|
|
15
|
+
@clock ||= MonotonicClock
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def current
|
|
19
|
+
Fiber[FIBER_KEY]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def with_deadline(seconds = nil, &block)
|
|
23
|
+
ambient = current
|
|
24
|
+
return ambient.derive(seconds) { |child| bind(child, &block) } if ambient
|
|
25
|
+
return bind(Deadline.after(seconds, clock: clock), &block) if seconds
|
|
26
|
+
|
|
27
|
+
bind(Deadline.never(clock: clock), &block)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def bind(deadline)
|
|
31
|
+
previous = Fiber[FIBER_KEY]
|
|
32
|
+
Fiber[FIBER_KEY] = deadline
|
|
33
|
+
begin
|
|
34
|
+
yield deadline
|
|
35
|
+
ensure
|
|
36
|
+
Fiber[FIBER_KEY] = previous
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def check!
|
|
41
|
+
current&.check!
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def remaining
|
|
45
|
+
current&.remaining || Float::INFINITY
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def clamp_sleep(seconds)
|
|
49
|
+
current ? current.clamp_sleep(seconds) : seconds
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def deadline?(object)
|
|
53
|
+
object.respond_to?(:remaining) && object.respond_to?(:done?) && object.respond_to?(:check!)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module DeadlineMachines
|
|
2
|
+
VERSION: String
|
|
3
|
+
FIBER_KEY: Symbol
|
|
4
|
+
|
|
5
|
+
# Any monotonic nanosecond time source.
|
|
6
|
+
interface _Clock
|
|
7
|
+
def now_ns: () -> Integer
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class Error < StandardError
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class Exceeded < Error
|
|
14
|
+
attr_reader deadline: Deadline
|
|
15
|
+
def initialize: (Deadline) -> void
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Cancelled < Error
|
|
19
|
+
attr_reader deadline: Deadline
|
|
20
|
+
attr_reader reason: untyped
|
|
21
|
+
def initialize: (Deadline, ?untyped) -> void
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module MonotonicClock
|
|
25
|
+
def self.now_ns: () -> Integer
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class TestClock
|
|
29
|
+
attr_accessor now_ns: Integer
|
|
30
|
+
def initialize: (?now_ns: Integer) -> void
|
|
31
|
+
def advance: (Numeric) -> self
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Deadline
|
|
35
|
+
NS_PER_SECOND: Integer
|
|
36
|
+
|
|
37
|
+
attr_reader expires_at_ns: Integer?
|
|
38
|
+
attr_reader parent: Deadline?
|
|
39
|
+
attr_reader clock: _Clock
|
|
40
|
+
attr_reader reason: untyped
|
|
41
|
+
|
|
42
|
+
def self.after: (Numeric, ?clock: _Clock) -> Deadline
|
|
43
|
+
def self.never: (?clock: _Clock) -> Deadline
|
|
44
|
+
def self.to_ns: (Numeric) -> Integer
|
|
45
|
+
|
|
46
|
+
def initialize: (expires_at_ns: Integer?, clock: _Clock, ?parent: Deadline?) -> void
|
|
47
|
+
|
|
48
|
+
# nil when there is no time limit.
|
|
49
|
+
def remaining_ns: () -> Integer?
|
|
50
|
+
def remaining: () -> Float
|
|
51
|
+
def expired?: () -> bool
|
|
52
|
+
def cancelled?: () -> bool
|
|
53
|
+
def done?: () -> bool
|
|
54
|
+
def check!: () -> self
|
|
55
|
+
def clamp_sleep: (Numeric) -> Numeric
|
|
56
|
+
def cancel: (?untyped) -> self
|
|
57
|
+
def on_cancel: () { (Deadline) -> void } -> self
|
|
58
|
+
|
|
59
|
+
# Without a block the child is returned; with one it is unlinked on exit
|
|
60
|
+
# and the block's value is returned.
|
|
61
|
+
def derive: (?Numeric?) -> Deadline
|
|
62
|
+
| [T] (?Numeric?) { (Deadline) -> T } -> T
|
|
63
|
+
|
|
64
|
+
def inspect: () -> String
|
|
65
|
+
|
|
66
|
+
private def child_expiry_ns: (Numeric?) -> Integer?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.clock: () -> _Clock
|
|
70
|
+
def self.clock=: (_Clock?) -> _Clock?
|
|
71
|
+
def self.current: () -> Deadline?
|
|
72
|
+
def self.with_deadline: [T] (?Numeric?) { (Deadline) -> T } -> T
|
|
73
|
+
def self.bind: [T] (Deadline) { (Deadline) -> T } -> T
|
|
74
|
+
def self.check!: () -> Deadline?
|
|
75
|
+
def self.remaining: () -> Float
|
|
76
|
+
def self.clamp_sleep: (Numeric) -> Numeric
|
|
77
|
+
def self.deadline?: (untyped) -> bool
|
|
78
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: deadline_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
|
+
description: 'A monotonic deadline token that derives child budgets, cancels down
|
|
13
|
+
its tree, and raises only at checkpoints you choose. The shared time contract for
|
|
14
|
+
the machines family: retries, circuit breakers and throttles read one budget instead
|
|
15
|
+
of each keeping its own clock.'
|
|
16
|
+
email:
|
|
17
|
+
- oss@seuros.com
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- CHANGELOG.md
|
|
23
|
+
- Cargo.toml
|
|
24
|
+
- LICENSE.txt
|
|
25
|
+
- README.md
|
|
26
|
+
- ext/deadline_machines_native/core/Cargo.toml
|
|
27
|
+
- ext/deadline_machines_native/core/src/lib.rs
|
|
28
|
+
- lib/deadline_machines.rb
|
|
29
|
+
- lib/deadline_machines/clock.rb
|
|
30
|
+
- lib/deadline_machines/deadline.rb
|
|
31
|
+
- lib/deadline_machines/errors.rb
|
|
32
|
+
- lib/deadline_machines/version.rb
|
|
33
|
+
- sig/deadline_machines.rbs
|
|
34
|
+
homepage: https://github.com/seuros/deadline_machines
|
|
35
|
+
licenses:
|
|
36
|
+
- MIT
|
|
37
|
+
metadata:
|
|
38
|
+
source_code_uri: https://github.com/seuros/deadline_machines
|
|
39
|
+
changelog_uri: https://github.com/seuros/deadline_machines/blob/main/CHANGELOG.md
|
|
40
|
+
bug_tracker_uri: https://github.com/seuros/deadline_machines/issues
|
|
41
|
+
rubygems_mfa_required: 'true'
|
|
42
|
+
rdoc_options: []
|
|
43
|
+
require_paths:
|
|
44
|
+
- lib
|
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: 4.0.0
|
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
requirements: []
|
|
56
|
+
rubygems_version: 4.0.20
|
|
57
|
+
specification_version: 4
|
|
58
|
+
summary: Deadline propagation and cooperative cancellation for Ruby
|
|
59
|
+
test_files: []
|