chrono_machines 0.5.1-aarch64-linux

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3184a28d31c4f22e7e445670e62c4a1912fe8c989bafd3626513547cf2ea4434
4
+ data.tar.gz: 0a628e34a0e801ea30bbc09b72eac8834d083be7ed5717a89def1aa765bace41
5
+ SHA512:
6
+ metadata.gz: 8a4bcbacf0d62e161cd2907bfe7b507717467eefb69d4d50f99b74f369b0ee6d82c3a494c86608b4d96a3ccd1bcac7b53d9d3f564ffb7296e5cf2d41cd734308
7
+ data.tar.gz: cde6a4494eecdeb1b9b7a95f522db9a7c55085b70f1507a4c5876fa0fe03fcc7b76b3b5bd56db39e5bbb1fc9a27af3a218fe2f6dc32e1dabf5cd80c98c33290f
data/CHANGELOG.md ADDED
@@ -0,0 +1,86 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.5.1](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.5.0...chrono_machines/v0.5.1) (2026-08-07)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * package workspace Cargo manifest so source builds work ([#17](https://github.com/seuros/chrono_machines/issues/17)) ([ac0ddad](https://github.com/seuros/chrono_machines/commit/ac0ddad15d26528771825240a05ed8c84fb61520))
9
+
10
+ ## [0.5.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.4.4...chrono_machines/v0.5.0) (2026-06-24)
11
+
12
+
13
+ ### Features
14
+
15
+ * **native:** make core crate no_std capable ([#15](https://github.com/seuros/chrono_machines/issues/15)) ([76129a3](https://github.com/seuros/chrono_machines/commit/76129a31ad75142641e4a29a82424ad26fb9ecd6))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **native:** use workspace dependency for chrono-machines in ffi ([fdeca2f](https://github.com/seuros/chrono_machines/commit/fdeca2f938e2647107793cfcf4c5c7c77e62e09b))
21
+
22
+ ## [0.4.4](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.4.3...chrono_machines/v0.4.4) (2026-06-16)
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * Upgrade setup-java action to version 5 ([f6dcecc](https://github.com/seuros/chrono_machines/commit/f6dcecc233d4d4f74bacad8bfe893eff981a4737))
28
+
29
+ ## [0.4.3](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.4.2...chrono_machines/v0.4.3) (2026-06-15)
30
+
31
+
32
+ ### Bug Fixes
33
+
34
+ * **native:** deduplicate backoff jitter, fibonacci, and retry-failure logic ([46f861a](https://github.com/seuros/chrono_machines/commit/46f861acc7e29024fc35dafb39aaec22db33440d))
35
+
36
+ ## [0.4.2](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.4.1...chrono_machines/v0.4.2) (2026-05-25)
37
+
38
+
39
+ ### Bug Fixes
40
+
41
+ * remove unused Magnus embed feature ([3b0877a](https://github.com/seuros/chrono_machines/commit/3b0877a11f768a135c877980320cae53fa58cd3d))
42
+
43
+ ## [0.4.1](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.4.0...chrono_machines/v0.4.1) (2026-03-19)
44
+
45
+
46
+ ### Bug Fixes
47
+
48
+ * **native:** upgrade rand to 0.10 ([#10](https://github.com/seuros/chrono_machines/issues/10)) ([d7e2738](https://github.com/seuros/chrono_machines/commit/d7e2738552a98e4f528055f671cfdfbdb02b0c7a))
49
+
50
+ ## [0.4.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.3.0...chrono_machines/v0.4.0) (2025-12-19)
51
+
52
+
53
+ ### Features
54
+
55
+ * add async support, error classification ([#6](https://github.com/seuros/chrono_machines/issues/6)) ([3d6d2c6](https://github.com/seuros/chrono_machines/commit/3d6d2c6e06de58247c74cdcef94e78a099b11e31))
56
+ * add constant and fibonacci backoff strategies with native acceleration ([582ba9c](https://github.com/seuros/chrono_machines/commit/582ba9ccd84072ec4ccf5d3340eced27cc4d1925))
57
+
58
+
59
+ ### Bug Fixes
60
+
61
+ * add Ruby 4.0.0-preview3 to test matrix ([#9](https://github.com/seuros/chrono_machines/issues/9)) ([df8a3a2](https://github.com/seuros/chrono_machines/commit/df8a3a23acf5ea37b115ab7d05936331b4ae2018))
62
+
63
+ ## [0.3.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.2.0...chrono_machines/v0.3.0) (2025-10-14)
64
+
65
+
66
+ ### Features
67
+
68
+ * add COSS (Contribution Open Source Standard) metadata ([de5b64a](https://github.com/seuros/chrono_machines/commit/de5b64af5c443d8987909199a69030fa332fa323))
69
+ * add GitHub Packages native compilation workflow ([0d7e950](https://github.com/seuros/chrono_machines/commit/0d7e9502d13a8de6b47756c27d682339435fe663))
70
+ * Add Rust native speedup with fluent retry DSL ([#4](https://github.com/seuros/chrono_machines/issues/4)) ([ac980bf](https://github.com/seuros/chrono_machines/commit/ac980bfda450b4e63739e4dc3c19513dd989e819))
71
+
72
+
73
+ ### Bug Fixes
74
+
75
+ * CI compilation using extconf.rb instead of rb_sys task ([bd844a5](https://github.com/seuros/chrono_machines/commit/bd844a594f25745edb407695ef36f7e214a29347))
76
+
77
+ ## [0.2.0](https://github.com/seuros/chrono_machines/compare/chrono_machines-v0.1.0...chrono_machines/v0.2.0) (2025-07-20)
78
+
79
+
80
+ ### Features
81
+
82
+ * migrate to Zeitwerk autoloading and improve Ruby compatibility ([#1](https://github.com/seuros/chrono_machines/issues/1)) ([162a1cb](https://github.com/seuros/chrono_machines/commit/162a1cb6ad81c3c59778dcd325cebdfe637c22cb))
83
+
84
+ ## [0.1.0] - 2025-07-12
85
+
86
+ - Initial release
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,618 @@
1
+ # ChronoMachines
2
+
3
+ > The temporal manipulation engine that rewrites the rules of retry!
4
+
5
+ A sophisticated Ruby implementation of exponential backoff and retry mechanisms, built for temporal precision in distributed systems where time itself is your greatest ally.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ gem 'chrono_machines'
11
+ ```
12
+
13
+ ```ruby
14
+ class PaymentService
15
+ include ChronoMachines::DSL
16
+
17
+ chrono_policy :stripe_payment, max_attempts: 5, base_delay: 0.1, multiplier: 2
18
+
19
+ def charge(amount)
20
+ with_chrono_policy(:stripe_payment) do
21
+ Stripe::Charge.create(amount: amount)
22
+ end
23
+ end
24
+ end
25
+
26
+ # Or use it directly
27
+ result = ChronoMachines.retry(max_attempts: 3) do
28
+ perform_risky_operation
29
+ end
30
+ ```
31
+
32
+ ## A Message from the Time Lords
33
+
34
+ So your microservices are failing faster than your deployment pipeline can recover, and you're stuck in an infinite loop of "let's just add more retries"?
35
+
36
+ Welcome to the temporal wasteland—where every millisecond matters, exponential backoff is law, and jitter isn't just a feeling you get when watching your error rates spike.
37
+
38
+ Still here? Excellent. Because in the fabric of spacetime, nobody can hear your servers screaming about cascading failures. It's all just timing and patience.
39
+
40
+ ### The Pattern Time Forgot
41
+
42
+ Built for Ruby 3.2+ with fiber-aware sleep and full jitter implementation, because when you're manipulating time itself, precision matters.
43
+
44
+ ## Features
45
+
46
+ - **Temporal Precision** - Full jitter exponential backoff with microsecond accuracy
47
+ - **Advanced Retry Logic** - Configurable retryable exceptions and intelligent failure handling
48
+ - **Rich Instrumentation** - Success, retry, and failure callbacks with contextual data
49
+ - **Fallback Mechanisms** - Execute alternative logic when all retries are exhausted
50
+ - **Declarative DSL** - Clean policy definitions with builder patterns
51
+ - **Fiber-Safe Operations** - Async-aware sleep handling for modern Ruby
52
+ - **Custom Exceptions** - MaxRetriesExceededError with original exception context
53
+ - **Policy Management** - Named retry policies with inheritance and overrides
54
+ - **Robust Error Handling** - Interrupt-preserving sleep with graceful degradation
55
+
56
+ ## The Temporal Manifesto
57
+
58
+ ### You Think: "I'll just add `retry` and call it resilience!"
59
+ ### Reality: You're creating a time paradox that crashes your entire fleet
60
+
61
+ When your payment service fails, you don't want to hammer it into submission. You want to approach it like a time traveler—carefully, with exponential patience, and a healthy respect for the butterfly effect.
62
+
63
+ ## Core Usage Patterns
64
+
65
+ ### Direct Retry with Options
66
+
67
+ ```ruby
68
+ # Simple retry with exponential backoff
69
+ result = ChronoMachines.retry(max_attempts: 3, base_delay: 0.1) do
70
+ fetch_external_data
71
+ end
72
+
73
+ # Advanced configuration
74
+ result = ChronoMachines.retry(
75
+ max_attempts: 5,
76
+ base_delay: 0.2,
77
+ multiplier: 3,
78
+ max_delay: 30,
79
+ retryable_exceptions: [Net::TimeoutError, HTTPError],
80
+ on_retry: ->(exception:, attempt:, next_delay:) {
81
+ Rails.logger.warn "Retry #{attempt}: #{exception.message}, waiting #{next_delay}s"
82
+ },
83
+ on_failure: ->(exception:, attempts:) {
84
+ Metrics.increment('api.retry.exhausted', tags: ["attempts:#{attempts}"])
85
+ }
86
+ ) do
87
+ external_api_call
88
+ end
89
+ ```
90
+
91
+ ### Policy-Based Configuration
92
+
93
+ ```ruby
94
+ # Configure global policies
95
+ ChronoMachines.configure do |config|
96
+ config.define_policy(:aggressive, max_attempts: 10, base_delay: 0.01, multiplier: 1.5)
97
+ config.define_policy(:conservative, max_attempts: 3, base_delay: 1.0, multiplier: 2)
98
+ config.define_policy(:database, max_attempts: 5, retryable_exceptions: [ActiveRecord::ConnectionError])
99
+ end
100
+
101
+ # Use named policies
102
+ result = ChronoMachines.retry(:database) do
103
+ User.find(user_id)
104
+ end
105
+ ```
106
+
107
+ ### DSL Integration
108
+
109
+ ```ruby
110
+ class ApiClient
111
+ include ChronoMachines::DSL
112
+
113
+ # Define policies at class level
114
+ chrono_policy :standard_api, max_attempts: 5, base_delay: 0.1, multiplier: 2
115
+ chrono_policy :critical_api, max_attempts: 10, base_delay: 0.05, max_delay: 5
116
+
117
+ def fetch_user_data(id)
118
+ with_chrono_policy(:standard_api) do
119
+ api_request("/users/#{id}")
120
+ end
121
+ end
122
+
123
+ def emergency_shutdown
124
+ # Use inline options for one-off scenarios
125
+ with_chrono_policy(max_attempts: 1, base_delay: 0) do
126
+ shutdown_api_call
127
+ end
128
+ end
129
+ end
130
+ ```
131
+
132
+ ## Advanced Temporal Mechanics
133
+
134
+ ### Callback Instrumentation
135
+
136
+ ```ruby
137
+ # Monitor retry patterns
138
+ policy_options = {
139
+ max_attempts: 5,
140
+ on_success: ->(result:, attempts:) {
141
+ Metrics.histogram('operation.attempts', attempts)
142
+ Rails.logger.info "Operation succeeded after #{attempts} attempts"
143
+ },
144
+
145
+ on_retry: ->(exception:, attempt:, next_delay:) {
146
+ Metrics.increment('operation.retry', tags: ["attempt:#{attempt}"])
147
+ Honeybadger.notify(exception, context: { attempt: attempt, next_delay: next_delay })
148
+ },
149
+
150
+ on_failure: ->(exception:, attempts:) {
151
+ Metrics.increment('operation.failure', tags: ["final_attempts:#{attempts}"])
152
+ PagerDuty.trigger("Operation failed after #{attempts} attempts: #{exception.message}")
153
+ }
154
+ }
155
+
156
+ ChronoMachines.retry(policy_options) do
157
+ critical_operation
158
+ end
159
+ ```
160
+
161
+ ### Exception Handling
162
+
163
+ ```ruby
164
+ begin
165
+ ChronoMachines.retry(max_attempts: 3) do
166
+ risky_operation
167
+ end
168
+ rescue ChronoMachines::MaxRetriesExceededError => e
169
+ # Access the original exception and retry context
170
+ Rails.logger.error "Failed after #{e.attempts} attempts: #{e.original_exception.message}"
171
+
172
+ # The original exception is preserved
173
+ case e.original_exception
174
+ when Net::TimeoutError
175
+ handle_timeout_failure
176
+ when HTTPError
177
+ handle_http_failure
178
+ end
179
+ end
180
+ ```
181
+
182
+ ### Fallback Mechanisms
183
+
184
+ ```ruby
185
+ # Execute fallback logic when retries are exhausted
186
+ ChronoMachines.retry(
187
+ max_attempts: 3,
188
+ on_failure: ->(exception:, attempts:) {
189
+ # Fallback doesn't throw - original exception is still raised
190
+ Rails.cache.write("fallback_data_#{user_id}", cached_response, expires_in: 5.minutes)
191
+ SlackNotifier.notify("API down, serving cached data for user #{user_id}")
192
+ }
193
+ ) do
194
+ fetch_fresh_user_data
195
+ end
196
+ ```
197
+
198
+ ## The Science of Temporal Jitter
199
+
200
+ ChronoMachines implements **full jitter** exponential backoff:
201
+
202
+ ```ruby
203
+ # Instead of predictable delays that create thundering herds:
204
+ # Attempt 1: 100ms
205
+ # Attempt 2: 200ms
206
+ # Attempt 3: 400ms
207
+
208
+ # ChronoMachines uses full jitter:
209
+ # Attempt 1: random(0, 100ms)
210
+ # Attempt 2: random(0, 200ms)
211
+ # Attempt 3: random(0, 400ms)
212
+ ```
213
+
214
+ This prevents the "thundering herd" problem where multiple clients retry simultaneously, overwhelming recovering services.
215
+
216
+ ## Configuration Reference
217
+
218
+ ### Policy Options
219
+
220
+ | Option | Default | Description |
221
+ |--------|---------|-------------|
222
+ | `max_attempts` | `3` | Maximum number of retry attempts |
223
+ | `base_delay` | `0.1` | Initial delay in seconds |
224
+ | `multiplier` | `2` | Exponential backoff multiplier |
225
+ | `max_delay` | `10` | Maximum delay cap in seconds |
226
+ | `retryable_exceptions` | `[StandardError]` | Array of exception classes to retry |
227
+ | `on_success` | `nil` | Success callback: `(result:, attempts:)` |
228
+ | `on_retry` | `nil` | Retry callback: `(exception:, attempt:, next_delay:)` |
229
+ | `on_failure` | `nil` | Failure callback: `(exception:, attempts:)` |
230
+
231
+ ### DSL Methods
232
+
233
+ | Method | Scope | Description |
234
+ |--------|-------|-------------|
235
+ | `chrono_policy(name, options)` | Class | Define a named retry policy |
236
+ | `with_chrono_policy(policy_or_options, &block)` | Instance | Execute block with retry policy |
237
+
238
+ ## Real-World Examples
239
+
240
+ ### Database Connection Resilience
241
+
242
+ ```ruby
243
+ class DatabaseService
244
+ include ChronoMachines::DSL
245
+
246
+ chrono_policy :db_connection,
247
+ max_attempts: 5,
248
+ base_delay: 0.1,
249
+ retryable_exceptions: [
250
+ ActiveRecord::ConnectionTimeoutError,
251
+ ActiveRecord::DisconnectedError,
252
+ PG::ConnectionBad
253
+ ],
254
+ on_retry: ->(exception:, attempt:, next_delay:) {
255
+ Rails.logger.warn "DB retry #{attempt}: #{exception.class}"
256
+ }
257
+
258
+ def find_user(id)
259
+ with_chrono_policy(:db_connection) do
260
+ User.find(id)
261
+ end
262
+ end
263
+ end
264
+ ```
265
+
266
+ ### HTTP API Integration
267
+
268
+ ```ruby
269
+ class WeatherService
270
+ include ChronoMachines::DSL
271
+
272
+ chrono_policy :weather_api,
273
+ max_attempts: 4,
274
+ base_delay: 0.2,
275
+ max_delay: 10,
276
+ retryable_exceptions: [Net::TimeoutError, Net::HTTPServerError],
277
+ on_failure: ->(exception:, attempts:) {
278
+ # Serve stale data when API is completely down
279
+ Rails.cache.write("weather_service_down", true, expires_in: 5.minutes)
280
+ }
281
+
282
+ def current_weather(location)
283
+ with_chrono_policy(:weather_api) do
284
+ response = HTTP.timeout(connect: 2, read: 5)
285
+ .get("https://api.weather.com/#{location}")
286
+ JSON.parse(response.body)
287
+ end
288
+ rescue ChronoMachines::MaxRetriesExceededError
289
+ # Return cached data if available
290
+ Rails.cache.fetch("weather_#{location}", expires_in: 1.hour) do
291
+ { temperature: "Unknown", status: "Service Unavailable" }
292
+ end
293
+ end
294
+ end
295
+ ```
296
+
297
+ ### Background Job Retry Logic
298
+
299
+ ```ruby
300
+ class EmailDeliveryJob
301
+ include ChronoMachines::DSL
302
+
303
+ chrono_policy :email_delivery,
304
+ max_attempts: 8,
305
+ base_delay: 1,
306
+ multiplier: 1.5,
307
+ max_delay: 300, # 5 minutes max
308
+ retryable_exceptions: [Net::SMTPServerBusy, Net::SMTPTemporaryError],
309
+ on_failure: ->(exception:, attempts:) {
310
+ # Move to dead letter queue after all retries
311
+ DeadLetterQueue.push(job_data, reason: exception.message)
312
+ }
313
+
314
+ def perform(email_data)
315
+ with_chrono_policy(:email_delivery) do
316
+ EmailService.deliver(email_data)
317
+ end
318
+ end
319
+ end
320
+ ```
321
+
322
+ ## Testing Strategies
323
+
324
+ ### Mocking Time and Retries
325
+
326
+ ```ruby
327
+ require "minitest/autorun"
328
+ require "mocha/minitest"
329
+
330
+ class PaymentServiceTest < Minitest::Test
331
+ def setup
332
+ @service = PaymentService.new
333
+ end
334
+
335
+ def test_retries_payment_on_timeout
336
+ charge_response = { id: "ch_123", amount: 100 }
337
+
338
+ Stripe::Charge.expects(:create)
339
+ .raises(Net::TimeoutError).once
340
+ .then.returns(charge_response)
341
+
342
+ # Mock sleep to avoid test delays
343
+ ChronoMachines::Executor.any_instance.expects(:robust_sleep).at_least_once
344
+
345
+ result = @service.charge(100)
346
+ assert_equal charge_response, result
347
+ end
348
+
349
+ def test_respects_max_attempts
350
+ Stripe::Charge.expects(:create)
351
+ .raises(Net::TimeoutError).times(3)
352
+
353
+ assert_raises(ChronoMachines::MaxRetriesExceededError) do
354
+ @service.charge(100)
355
+ end
356
+ end
357
+
358
+ def test_preserves_original_exception
359
+ original_error = Net::TimeoutError.new("Connection timed out")
360
+ Stripe::Charge.expects(:create).raises(original_error).times(3)
361
+
362
+ begin
363
+ @service.charge(100)
364
+ flunk "Expected MaxRetriesExceededError to be raised"
365
+ rescue ChronoMachines::MaxRetriesExceededError => e
366
+ assert_equal 3, e.attempts
367
+ assert_equal original_error, e.original_exception
368
+ assert_equal "Connection timed out", e.original_exception.message
369
+ end
370
+ end
371
+ end
372
+ ```
373
+
374
+ ### Testing Callbacks
375
+
376
+ ```ruby
377
+ class CallbackTest < Minitest::Test
378
+ def test_calls_retry_callback_with_correct_context
379
+ retry_calls = []
380
+ call_count = 0
381
+
382
+ result = ChronoMachines.retry(
383
+ max_attempts: 3,
384
+ base_delay: 0.001, # Short delay for tests
385
+ on_retry: ->(exception:, attempt:, next_delay:) {
386
+ retry_calls << {
387
+ attempt: attempt,
388
+ delay: next_delay,
389
+ exception_message: exception.message
390
+ }
391
+ }
392
+ ) do
393
+ call_count += 1
394
+ raise "Fail" if call_count < 2
395
+ "Success"
396
+ end
397
+
398
+ assert_equal "Success", result
399
+ assert_equal 1, retry_calls.length
400
+ assert_equal 1, retry_calls.first[:attempt]
401
+ assert retry_calls.first[:delay] > 0
402
+ assert_equal "Fail", retry_calls.first[:exception_message]
403
+ end
404
+
405
+ def test_calls_success_callback
406
+ success_called = false
407
+ result_captured = nil
408
+ attempts_captured = nil
409
+
410
+ result = ChronoMachines.retry(
411
+ on_success: ->(result:, attempts:) {
412
+ success_called = true
413
+ result_captured = result
414
+ attempts_captured = attempts
415
+ }
416
+ ) do
417
+ "Operation succeeded"
418
+ end
419
+
420
+ assert success_called
421
+ assert_equal "Operation succeeded", result_captured
422
+ assert_equal 1, attempts_captured
423
+ end
424
+
425
+ def test_calls_failure_callback
426
+ failure_called = false
427
+ exception_captured = nil
428
+
429
+ assert_raises(ChronoMachines::MaxRetriesExceededError) do
430
+ ChronoMachines.retry(
431
+ max_attempts: 2,
432
+ on_failure: ->(exception:, attempts:) {
433
+ failure_called = true
434
+ exception_captured = exception
435
+ }
436
+ ) do
437
+ raise "Always fails"
438
+ end
439
+ end
440
+
441
+ assert failure_called
442
+ assert_equal "Always fails", exception_captured.message
443
+ end
444
+ end
445
+ ```
446
+
447
+ ### Testing DSL Integration
448
+
449
+ ```ruby
450
+ class DSLTestExample < Minitest::Test
451
+ class TestService
452
+ include ChronoMachines::DSL
453
+
454
+ chrono_policy :test_policy, max_attempts: 2, base_delay: 0.001
455
+
456
+ def risky_operation
457
+ with_chrono_policy(:test_policy) do
458
+ # Simulated operation
459
+ yield if block_given?
460
+ end
461
+ end
462
+ end
463
+
464
+ def test_dsl_policy_definition
465
+ service = TestService.new
466
+
467
+ call_count = 0
468
+ result = service.risky_operation do
469
+ call_count += 1
470
+ raise "Fail" if call_count < 2
471
+ "Success"
472
+ end
473
+
474
+ assert_equal "Success", result
475
+ assert_equal 2, call_count
476
+ end
477
+
478
+ def test_dsl_with_inline_options
479
+ service = TestService.new
480
+
481
+ assert_raises(ChronoMachines::MaxRetriesExceededError) do
482
+ service.with_chrono_policy(max_attempts: 1) do
483
+ raise "Always fails"
484
+ end
485
+ end
486
+ end
487
+ end
488
+ ```
489
+
490
+ ## TestHelper for Library Authors
491
+
492
+ ChronoMachines provides a test helper module for library authors who want to integrate ChronoMachines testing utilities into their own test suites.
493
+
494
+ ### Setup
495
+
496
+ ```ruby
497
+ require 'chrono_machines/test_helper'
498
+
499
+ class MyLibraryTest < Minitest::Test
500
+ include ChronoMachines::TestHelper
501
+
502
+ def setup
503
+ super # Important: calls ChronoMachines config reset
504
+ # Your setup code here
505
+ end
506
+ end
507
+ ```
508
+
509
+ ### Features
510
+
511
+ **Configuration Reset**: The TestHelper automatically resets ChronoMachines configuration before each test, ensuring test isolation.
512
+
513
+ **Custom Assertions**: Provides specialized assertions for testing delay ranges:
514
+
515
+ ```ruby
516
+ def test_delay_calculation
517
+ executor = ChronoMachines::Executor.new(base_delay: 0.1, multiplier: 2)
518
+ delay = executor.send(:calculate_delay, 1)
519
+
520
+ # Assert delay is within expected jitter range
521
+ assert_cm_delay_range(delay, 0.0, 0.1, "First attempt delay out of range")
522
+ end
523
+ ```
524
+
525
+ **Available Assertions**:
526
+ - `assert_cm_delay_range(delay, min, max, message = nil)` - Assert delay falls within expected range
527
+
528
+ ### Integration Example
529
+
530
+ ```ruby
531
+ # In your gem's test_helper.rb
532
+ require 'minitest/autorun'
533
+ require 'chrono_machines/test_helper'
534
+
535
+ class TestBase < Minitest::Test
536
+ include ChronoMachines::TestHelper
537
+
538
+ def setup
539
+ super
540
+ # Reset any additional state
541
+ end
542
+ end
543
+
544
+ # In your specific tests
545
+ class RetryServiceTest < TestBase
546
+ def test_retry_with_custom_policy
547
+ # ChronoMachines config is automatically reset
548
+ # You can safely define test-specific policies
549
+
550
+ ChronoMachines.configure do |config|
551
+ config.define_policy(:test_policy, max_attempts: 2)
552
+ end
553
+
554
+ result = ChronoMachines.retry(:test_policy) do
555
+ "success"
556
+ end
557
+
558
+ assert_equal "success", result
559
+ end
560
+ end
561
+ ```
562
+
563
+ ## Why ChronoMachines?
564
+
565
+ ### Built for Modern Ruby
566
+ - **Ruby 3.2+ Support**: Fiber-aware sleep handling
567
+ - **Clean Architecture**: Separation of concerns with configurable policies
568
+ - **Rich Instrumentation**: Comprehensive callback system for monitoring
569
+ - **Battle-Tested**: Full jitter implementation prevents thundering herds
570
+
571
+ ### Time-Tested Patterns
572
+ - **Exponential Backoff**: Industry-standard retry timing
573
+ - **Circuit Breaking**: Fail-fast when upstream is down
574
+ - **Fallback Support**: Graceful degradation strategies
575
+ - **Exception Preservation**: Original errors aren't lost in retry logic
576
+
577
+ ## A Word from the Time Corps Engineering Division
578
+
579
+ *The Temporal Commentary Engine activates:*
580
+
581
+ "Time isn't linear—especially when your payment processor is having 'a moment.'
582
+
583
+ The universe doesn't care about your startup's burn rate or your post on X about 'building in public.' It cares about one immutable law:
584
+
585
+ **Does your system handle failure gracefully across the fourth dimension?**
586
+
587
+ If not, welcome to the Time Corps. We have exponential backoff.
588
+
589
+ Remember: The goal isn't to prevent temporal anomalies—it's to fail fast, fail smart, and retry with mathematical precision.
590
+
591
+ As I always say when debugging production: 'Time heals all wounds, but jitter prevents thundering herds.'"
592
+
593
+ *— Temporal Commentary Engine, Log Entry ∞*
594
+
595
+ ## Contributing to the Timeline
596
+
597
+ 1. Fork it (like it's 2005, but with better temporal mechanics)
598
+ 2. Create your feature branch (`git checkout -b feature/quantum-retries`)
599
+ 3. Commit your changes (`git commit -am 'Add temporal stabilization'`)
600
+ 4. Push to the branch (`git push origin feature/quantum-retries`)
601
+ 5. Create a new Pull Request (and wait for the Time Lords to review)
602
+
603
+ ## License
604
+
605
+ MIT License. See [LICENSE](LICENSE) file for details.
606
+
607
+ ## Acknowledgments
608
+
609
+ - The Ruby community - For building a language worth retrying for
610
+ - Every timeout that ever taught us patience - You made us stronger
611
+ - The Time Corps - For maintaining temporal stability
612
+ - The universe - For being deterministically random
613
+
614
+ ## Author
615
+
616
+ Built with time and Brawndo (it's got electrolytes) by temporal engineers fighting entropy one retry at a time.
617
+
618
+ **Remember: In the fabric of spacetime, nobody can hear your API timeout. But they can feel your exponential backoff working as intended.**
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ffi/extconf'
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file provides optional integration with the Async gem.
4
+ # It will only patch if Async is available.
5
+
6
+ async_available = defined?(Async) || begin
7
+ require 'async'
8
+ true
9
+ rescue LoadError
10
+ false
11
+ end
12
+
13
+ if async_available
14
+ module ChronoMachines
15
+ # Patch the Executor's robust_sleep method to use Async's non-blocking sleep
16
+ # if an Async task is currently running.
17
+ class Executor
18
+ alias original_robust_sleep robust_sleep
19
+
20
+ def robust_sleep(delay)
21
+ # Check if we're in an Async context and safely call current
22
+ current_task = begin
23
+ Async::Task.current
24
+ rescue RuntimeError
25
+ # No async task available
26
+ nil
27
+ end
28
+
29
+ if current_task
30
+ # Use Kernel#sleep which is now fiber-aware in async 2.x
31
+ sleep(delay)
32
+ else
33
+ original_robust_sleep(delay)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChronoMachines
4
+ class Configuration
5
+ DEFAULT_POLICY_NAME = :default
6
+
7
+ attr_reader :policies
8
+ attr_accessor :engine_override
9
+
10
+ def initialize
11
+ @engine_override = nil # nil = auto-detect, :ruby = force Ruby, :native = force native
12
+ @policies = {
13
+ DEFAULT_POLICY_NAME => {
14
+ backoff_strategy: :exponential, # :exponential, :constant, or :fibonacci
15
+ max_attempts: 3,
16
+ base_delay: 0.1, # seconds
17
+ multiplier: 2, # For exponential backoff
18
+ max_delay: 10, # seconds
19
+ jitter_factor: 1.0, # 1.0 = full jitter (recommended), 0.0 = no jitter
20
+ retryable_exceptions: [StandardError],
21
+ on_failure: nil, # Fallback block when all retries are exhausted
22
+ on_retry: nil, # Callback block when a retry occurs
23
+ on_success: nil # Callback block when operation succeeds
24
+ }
25
+ }
26
+ end
27
+
28
+ def define_policy(name, options)
29
+ # Deep copy the default policy to avoid shared mutable references
30
+ default_copy = @policies[DEFAULT_POLICY_NAME].transform_values do |value|
31
+ case value
32
+ when Array
33
+ value.dup
34
+ when Proc
35
+ value # Procs are immutable, no need to dup
36
+ else
37
+ value # Primitives (numbers, symbols, nil) are immutable
38
+ end
39
+ end
40
+
41
+ @policies[name.to_sym] = default_copy.merge(options)
42
+ end
43
+
44
+ def get_policy(name)
45
+ @policies[name.to_sym] || raise(ArgumentError, "Policy '#{name}' not found.")
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChronoMachines
4
+ module DSL
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ def chrono_policy(name, options = {})
11
+ ChronoMachines.configure do |config|
12
+ config.define_policy(name, options)
13
+ end
14
+ end
15
+ end
16
+
17
+ def with_chrono_policy(policy_name_or_options, &)
18
+ ChronoMachines.retry(policy_name_or_options, &)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChronoMachines
4
+ class Error < StandardError; end
5
+
6
+ class MaxRetriesExceededError < Error
7
+ attr_reader :original_exception, :attempts
8
+
9
+ def initialize(original_exception, attempts)
10
+ @original_exception = original_exception
11
+ @attempts = attempts
12
+ super("Max retries (#{attempts}) exceeded. Original error: #{original_exception.class}: #{original_exception.message}")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChronoMachines
4
+ class Executor
5
+ def initialize(policy_or_options = {})
6
+ policy_options = if policy_or_options.is_a?(Symbol)
7
+ ChronoMachines.config.get_policy(policy_or_options)
8
+ else
9
+ ChronoMachines.config.get_policy(Configuration::DEFAULT_POLICY_NAME).merge(policy_or_options)
10
+ end
11
+
12
+ @backoff_strategy = policy_options[:backoff_strategy] || :exponential
13
+ @max_attempts = policy_options[:max_attempts]
14
+ @base_delay = policy_options[:base_delay]
15
+ @multiplier = policy_options[:multiplier] || 2
16
+ @max_delay = policy_options[:max_delay]
17
+ @jitter_factor = policy_options[:jitter_factor]
18
+ @retryable_exceptions = policy_options[:retryable_exceptions]
19
+ @on_failure = policy_options[:on_failure]
20
+ @on_retry = policy_options[:on_retry]
21
+ @on_success = policy_options[:on_success]
22
+ end
23
+
24
+ def call
25
+ attempts = 0
26
+
27
+ begin
28
+ attempts += 1
29
+ result = yield
30
+
31
+ # Call success callback if defined
32
+ @on_success&.call(result: result, attempts: attempts)
33
+
34
+ result
35
+ rescue StandardError => e
36
+ # Check if exception is retryable
37
+ unless @retryable_exceptions.any? { |ex| e.is_a?(ex) }
38
+ # Non-retryable exception - call failure callback and re-raise
39
+ handle_final_failure(e, attempts)
40
+ raise e
41
+ end
42
+
43
+ # Check if we've exhausted all attempts
44
+ if attempts >= @max_attempts
45
+ handle_final_failure(e, attempts)
46
+ raise MaxRetriesExceededError.new(e, attempts)
47
+ end
48
+
49
+ # Calculate delay
50
+ delay = calculate_delay(attempts)
51
+
52
+ # Call retry callback if defined
53
+ @on_retry&.call(exception: e, attempt: attempts, next_delay: delay)
54
+
55
+ # Execute delay with robust sleep
56
+ robust_sleep(delay)
57
+ retry
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ # Pure Ruby implementation of delay calculation (exponential backoff)
64
+ def ruby_calculate_delay_exponential(attempts)
65
+ base_exponential_delay = [@base_delay * (@multiplier**(attempts - 1)), @max_delay].min
66
+ jitter_factor = normalized_jitter_factor
67
+ base_exponential_delay * (1 - jitter_factor + (rand * jitter_factor))
68
+ end
69
+
70
+ # Pure Ruby implementation of constant backoff
71
+ def ruby_calculate_delay_constant(_attempts)
72
+ jitter_factor = normalized_jitter_factor
73
+ @base_delay * (1 - jitter_factor + (rand * jitter_factor))
74
+ end
75
+
76
+ # Pure Ruby implementation of Fibonacci backoff
77
+ def ruby_calculate_delay_fibonacci(attempts)
78
+ fib = fibonacci(attempts)
79
+ base_delay = [@base_delay * fib, @max_delay].min
80
+ jitter_factor = normalized_jitter_factor
81
+ base_delay * (1 - jitter_factor + (rand * jitter_factor))
82
+ end
83
+
84
+ # Calculate Fibonacci number (1-indexed)
85
+ def fibonacci(n)
86
+ return 0 if n.zero?
87
+ return 1 if n <= 2
88
+
89
+ a, b = 1, 1
90
+ (2...n).each do
91
+ a, b = b, a + b
92
+ end
93
+ b
94
+ end
95
+
96
+ # Main calculate_delay dispatches to the appropriate strategy
97
+ def ruby_calculate_delay(attempts)
98
+ case @backoff_strategy
99
+ when :exponential
100
+ ruby_calculate_delay_exponential(attempts)
101
+ when :constant
102
+ ruby_calculate_delay_constant(attempts)
103
+ when :fibonacci
104
+ ruby_calculate_delay_fibonacci(attempts)
105
+ else
106
+ raise ArgumentError, "Unknown backoff strategy: #{@backoff_strategy}"
107
+ end
108
+ end
109
+
110
+ # By default, use Ruby implementation (may be overridden by native extension)
111
+ alias_method :calculate_delay, :ruby_calculate_delay
112
+
113
+ def robust_sleep(delay)
114
+ # Handle potential interruptions to sleep
115
+ # In Ruby 3.2+, Kernel.sleep is fiber-aware
116
+ return if delay <= 0
117
+
118
+ begin
119
+ sleep(delay)
120
+ rescue Interrupt
121
+ # Re-raise interrupt signals
122
+ raise
123
+ rescue StandardError
124
+ # Log or handle other sleep interruptions, but continue
125
+ # In most cases, we want to proceed with the retry
126
+ end
127
+ end
128
+
129
+ def handle_final_failure(exception, attempts)
130
+ # Execute fallback block if defined
131
+ return unless @on_failure
132
+
133
+ begin
134
+ @on_failure.call(exception: exception, attempts: attempts)
135
+ rescue StandardError
136
+ # Don't let fallback errors mask the original error
137
+ # Could log this or handle as needed
138
+ end
139
+ end
140
+
141
+ def normalized_jitter_factor
142
+ factor = Float(@jitter_factor)
143
+ raise ArgumentError, 'jitter_factor cannot be NaN' if factor.nan?
144
+
145
+ factor.clamp(0.0, 1.0)
146
+ rescue ArgumentError, TypeError
147
+ raise ArgumentError, 'jitter_factor must be a numeric value'
148
+ end
149
+ end
150
+ end
151
+
152
+ # Load optional native speedup if available
153
+ begin
154
+ require_relative 'native_speedup'
155
+ rescue LoadError
156
+ # Native extension not available, continue with pure Ruby implementation
157
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Optional native speedup for ChronoMachines
4
+ # This file is loaded conditionally and monkey-patches the Executor
5
+ # to use the native Rust implementation for delay calculations.
6
+
7
+ begin
8
+ # Try to load the native extension
9
+ # Prefer local development version if it exists
10
+ local_ext_path = File.expand_path('../../ext/chrono_machines_native/chrono_machines_native', __dir__)
11
+ if File.exist?("#{local_ext_path}.bundle") || File.exist?("#{local_ext_path}.so")
12
+ require local_ext_path
13
+ else
14
+ require 'chrono_machines_native/chrono_machines_native'
15
+ end
16
+
17
+ module ChronoMachines
18
+ # Native speedup module that overrides backoff calculations
19
+ # The original Ruby implementations remain available as ruby_calculate_delay_*
20
+ module NativeExecutor
21
+ private
22
+
23
+ # Override calculate_delay to dispatch to native implementations
24
+ def calculate_delay(attempts)
25
+ case @backoff_strategy
26
+ when :exponential
27
+ ChronoMachinesNative.exponential_delay(
28
+ attempts,
29
+ @base_delay,
30
+ @multiplier,
31
+ @max_delay,
32
+ normalized_jitter_factor
33
+ )
34
+ when :constant
35
+ ChronoMachinesNative.constant_delay(
36
+ attempts,
37
+ @base_delay,
38
+ normalized_jitter_factor
39
+ )
40
+ when :fibonacci
41
+ ChronoMachinesNative.fibonacci_delay(
42
+ attempts,
43
+ @base_delay,
44
+ @max_delay,
45
+ normalized_jitter_factor
46
+ )
47
+ else
48
+ # Unknown strategy, fall back to Ruby
49
+ super
50
+ end
51
+ end
52
+ end
53
+
54
+ class Executor
55
+ prepend NativeExecutor
56
+ end
57
+ end
58
+ rescue LoadError
59
+ # Native extension not available (e.g., on JRuby or failed compilation)
60
+ # Silently fall back to pure Ruby implementation
61
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+
5
+ module ChronoMachines
6
+ module TestHelper
7
+ def setup
8
+ super
9
+ # Reset configuration before each test
10
+ ChronoMachines.instance_variable_set(:@config, ChronoMachines::Configuration.new)
11
+ end
12
+
13
+ module Assertions
14
+ def assert_cm_delay_range(delay, expected_min, expected_max, message = nil)
15
+ assert_operator(delay, :>=, expected_min, "Expected delay #{delay} to be >= #{expected_min}. #{message}")
16
+ assert_operator(delay, :<=, expected_max, "Expected delay #{delay} to be <= #{expected_max}. #{message}")
17
+ end
18
+ end
19
+
20
+ include Assertions
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChronoMachines
4
+ VERSION = '0.5.1'
5
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zeitwerk'
4
+
5
+ loader = Zeitwerk::Loader.for_gem
6
+ loader.ignore("#{__dir__}/chrono_machines/errors.rb")
7
+ loader.ignore("#{__dir__}/chrono_machines/async_support.rb")
8
+ loader.ignore("#{__dir__}/chrono_machines/test_helper.rb")
9
+ loader.inflector.inflect('dsl' => 'DSL')
10
+ loader.setup
11
+
12
+ require_relative 'chrono_machines/errors'
13
+
14
+ module ChronoMachines
15
+ # Global configuration instance
16
+ @config = Configuration.new
17
+
18
+ def self.configure
19
+ yield @config
20
+ end
21
+
22
+ def self.config
23
+ @config
24
+ end
25
+
26
+ def self.retry(policy_name_or_options = {}, &)
27
+ Executor.new(policy_name_or_options).call(&)
28
+ end
29
+ end
30
+
31
+ # Load optional async support after core is loaded
32
+ begin
33
+ require_relative 'chrono_machines/async_support'
34
+ rescue LoadError
35
+ # Async gem not available, skip async support
36
+ end
@@ -0,0 +1,4 @@
1
+ module ChronoMachines
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chrono_machines
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.1
5
+ platform: aarch64-linux
6
+ authors:
7
+ - Abdelkader Boudih
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-08-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: zeitwerk
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.7'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.7'
27
+ description: ChronoMachines offers a flexible and configurable solution for handling
28
+ transient failures in distributed Ruby applications. It provides powerful retry
29
+ strategies, including exponential backoff and full jitter, with customizable callbacks.
30
+ Features optional Rust-powered native performance on CRuby with pure Ruby fallback
31
+ for JRuby compatibility.
32
+ email:
33
+ - terminale@gmail.com
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - CHANGELOG.md
39
+ - LICENSE.txt
40
+ - README.md
41
+ - ext/chrono_machines_native/extconf.rb
42
+ - lib/chrono_machines.rb
43
+ - lib/chrono_machines/async_support.rb
44
+ - lib/chrono_machines/configuration.rb
45
+ - lib/chrono_machines/dsl.rb
46
+ - lib/chrono_machines/errors.rb
47
+ - lib/chrono_machines/executor.rb
48
+ - lib/chrono_machines/native_speedup.rb
49
+ - lib/chrono_machines/test_helper.rb
50
+ - lib/chrono_machines/version.rb
51
+ - lib/chrono_machines_native/3.4/chrono_machines_native.so
52
+ - lib/chrono_machines_native/4.0/chrono_machines_native.so
53
+ - sig/chrono_machines.rbs
54
+ homepage: https://github.com/seuros/chrono_machines
55
+ licenses:
56
+ - MIT
57
+ metadata:
58
+ homepage_uri: https://github.com/seuros/chrono_machines
59
+ source_code_uri: https://github.com/seuros/chrono_machines
60
+ changelog_uri: https://github.com/seuros/chrono_machines/blob/main/CHANGELOG.md
61
+ github_repo: ssh://github.com/seuros/chrono_machines
62
+ rubygems_mfa_required: 'true'
63
+ cargo_crate_name: chrono_machines_native
64
+ cargo_manifest_path: ext/chrono_machines_native/ffi/Cargo.toml
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '3.4'
74
+ - - "<"
75
+ - !ruby/object:Gem::Version
76
+ version: 4.1.dev
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubygems_version: 3.5.23
84
+ signing_key:
85
+ specification_version: 4
86
+ summary: A robust Ruby gem for implementing retry mechanisms with exponential backoff
87
+ and jitter.
88
+ test_files: []