philiprehberger-timeout_kit 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 439206c3312c3911ae57be460c6d4c6fdae897dc334307b5a7ad2fb08f5ad3f2
4
- data.tar.gz: dcdf01577d2efaec47d7ca37505baf755900902aba28d758347b41266b1f3b4e
3
+ metadata.gz: 38648273212e92534eb7dceadb874fdff11a9f79537322caccce2b80265eb227
4
+ data.tar.gz: 630c54614f2001126fe6a4f251421245b83832513e1c7b7ac865866a4cf5c77a
5
5
  SHA512:
6
- metadata.gz: 77126a25c3e6d01c0d1dc9be4998314d04c3be99a2b8c9cd1154626ed05834ef507b1adaaefd4336b47ef307512ef080b20478218dca3b7459a4e41f41e3f9b3
7
- data.tar.gz: 1ca69dbfd8a5d414b01329c691a07cf04fc91f5b676985b7737b8d907b0f292a33d08aeac9a6990684274fd4b4fac0a8675542402579259fa2c9a4a91c37b9d1
6
+ metadata.gz: 21f72a37fd9180990963331e97658273e1eee1e5492ecab899fb5540c4843cd55fa2cfd2345920e6840c4b47446c711fb599e4b367a22dc4f7b04ae7de86704b
7
+ data.tar.gz: 1850ebc59963836b441862c3794151f225247604462f43eaa3252ac1efc46efa0b2f5eb25622e992fed1d8c200b430f89f4253e1fbd671a29bf00ca24b17944e
data/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-03-29
11
+
12
+ ### Added
13
+ - Deadline callbacks via `on_expire:` keyword argument or `Deadline#on_expire` block registration, fired once on first expiry detection
14
+ - Grace period via `grace:` keyword argument with `Deadline#in_grace?` and `Deadline#grace_remaining` accessors
15
+ - Deadline naming via `name:` keyword argument with `Deadline#name` accessor, included in error messages
16
+ - Full README compliance with 8 badges, Support section, and all standard sections
17
+ - GitHub issue templates (bug report, feature request), dependabot config, and PR template
18
+
19
+ ## [0.1.1] - 2026-03-22
20
+
21
+ ### Added
22
+ - Expanded test suite from 19 to 30+ examples covering edge cases, error hierarchy, zero/large timeouts, nested deadlines, and cooperative timeout loops
23
+
10
24
  ## [0.1.0] - 2026-03-22
11
25
 
12
26
  ### Added
data/README.md CHANGED
@@ -2,7 +2,12 @@
2
2
 
3
3
  [![Tests](https://github.com/philiprehberger/rb-timeout-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/philiprehberger/rb-timeout-kit/actions/workflows/ci.yml)
4
4
  [![Gem Version](https://badge.fury.io/rb/philiprehberger-timeout_kit.svg)](https://rubygems.org/gems/philiprehberger-timeout_kit)
5
+ [![GitHub release](https://img.shields.io/github/v/release/philiprehberger/rb-timeout-kit)](https://github.com/philiprehberger/rb-timeout-kit/releases)
6
+ [![Last updated](https://img.shields.io/github/last-commit/philiprehberger/rb-timeout-kit)](https://github.com/philiprehberger/rb-timeout-kit/commits/main)
5
7
  [![License](https://img.shields.io/github/license/philiprehberger/rb-timeout-kit)](LICENSE)
8
+ [![Bug Reports](https://img.shields.io/github/issues/philiprehberger/rb-timeout-kit/bug)](https://github.com/philiprehberger/rb-timeout-kit/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
9
+ [![Feature Requests](https://img.shields.io/github/issues/philiprehberger/rb-timeout-kit/enhancement)](https://github.com/philiprehberger/rb-timeout-kit/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)
10
+ [![Sponsor](https://img.shields.io/badge/sponsor-GitHub%20Sponsors-ec6cb9)](https://github.com/sponsors/philiprehberger)
6
11
 
7
12
  Safe timeout patterns without Thread.raise
8
13
 
@@ -65,6 +70,52 @@ Philiprehberger::TimeoutKit.deadline(30) do |outer|
65
70
  end
66
71
  ```
67
72
 
73
+ ### Deadline Naming
74
+
75
+ ```ruby
76
+ Philiprehberger::TimeoutKit.deadline(10, name: 'db_query') do |d|
77
+ d.check! # raises "Deadline 'db_query' exceeded" if expired
78
+ puts d.name # => "db_query"
79
+ end
80
+ ```
81
+
82
+ ### Deadline Callbacks
83
+
84
+ ```ruby
85
+ Philiprehberger::TimeoutKit.deadline(10, on_expire: -> { cleanup() }) do |d|
86
+ loop do
87
+ d.check! # fires callback once on first expiry detection
88
+ process_next_item
89
+ end
90
+ end
91
+
92
+ # Or register via block
93
+ Philiprehberger::TimeoutKit.deadline(10) do |d|
94
+ d.on_expire { cleanup() }
95
+ loop do
96
+ d.check!
97
+ process_next_item
98
+ end
99
+ end
100
+ ```
101
+
102
+ ### Grace Period
103
+
104
+ ```ruby
105
+ Philiprehberger::TimeoutKit.deadline(10, grace: 2) do |d|
106
+ loop do
107
+ d.check! # does not raise during 2s grace period
108
+ break if d.expired?
109
+
110
+ process_next_item
111
+ end
112
+
113
+ if d.in_grace?
114
+ puts "Grace period: #{d.grace_remaining}s left to wrap up"
115
+ end
116
+ end
117
+ ```
118
+
68
119
  ### Cooperative Timeout
69
120
 
70
121
  ```ruby
@@ -89,12 +140,16 @@ end
89
140
 
90
141
  | Method | Description |
91
142
  |--------|-------------|
92
- | `.deadline(seconds) { \|d\| }` | Execute a block with a cooperative deadline |
143
+ | `.deadline(seconds, name:, grace:, on_expire:) { \|d\| }` | Execute a block with a cooperative deadline |
93
144
  | `.cooperative(seconds) { \|t\| }` | Execute a block with a simple cooperative timeout |
94
145
  | `.current_deadline` | Return the current active deadline or nil |
95
- | `Deadline#check!` | Raise `DeadlineExceeded` if the deadline has passed |
96
- | `Deadline#remaining` | Seconds remaining until the deadline (0.0 if expired) |
97
- | `Deadline#expired?` | Whether the deadline has passed |
146
+ | `Deadline#check!` | Raise `DeadlineExceeded` if the deadline has passed (respects grace period) |
147
+ | `Deadline#remaining` | Seconds remaining until the primary deadline (negative during grace) |
148
+ | `Deadline#expired?` | Whether the primary deadline has passed |
149
+ | `Deadline#name` | The human-readable name for this deadline (nil if not set) |
150
+ | `Deadline#in_grace?` | Whether the deadline is in the grace period |
151
+ | `Deadline#grace_remaining` | Seconds remaining in the grace period (0.0 if none) |
152
+ | `Deadline#on_expire { }` | Register a callback that fires once on expiry detection |
98
153
  | `DeadlineExceeded` | Raised when a deadline or timeout expires |
99
154
 
100
155
  ## Development
@@ -105,6 +160,13 @@ bundle exec rspec
105
160
  bundle exec rubocop
106
161
  ```
107
162
 
163
+ ## Support
164
+
165
+ If you find this package useful, consider giving it a star on GitHub — it helps motivate continued maintenance and development.
166
+
167
+ [![LinkedIn](https://img.shields.io/badge/Philip%20Rehberger-LinkedIn-0A66C2?logo=linkedin)](https://www.linkedin.com/in/philiprehberger)
168
+ [![More packages](https://img.shields.io/badge/more-open%20source%20packages-blue)](https://philiprehberger.com/open-source-packages)
169
+
108
170
  ## License
109
171
 
110
- MIT
172
+ [MIT](LICENSE)
@@ -10,38 +10,97 @@ module Philiprehberger
10
10
  # @return [Float] the absolute monotonic time when the deadline expires
11
11
  attr_reader :expires_at
12
12
 
13
+ # @return [String, nil] the human-readable name for this deadline
14
+ attr_reader :name
15
+
13
16
  # Create a new deadline.
14
17
  #
15
18
  # @param seconds [Numeric] the number of seconds until the deadline expires
16
- def initialize(seconds)
19
+ # @param name [String, nil] optional human-readable name for the deadline
20
+ # @param grace [Numeric, nil] optional grace period in seconds after the primary deadline
21
+ # @param on_expire [Proc, nil] optional callback that fires once when expiry is detected
22
+ def initialize(seconds, name: nil, grace: nil, on_expire: nil)
17
23
  @expires_at = now + seconds
24
+ @name = name
25
+ @grace_seconds = grace
26
+ @grace_expires_at = @grace_seconds ? @expires_at + @grace_seconds : nil
27
+ @on_expire = on_expire
28
+ @expire_callback_fired = false
29
+ end
30
+
31
+ # Register a callback that fires once when expiry is detected.
32
+ #
33
+ # @yield the block to call on expiry
34
+ # @return [void]
35
+ def on_expire(&block)
36
+ @on_expire = block
18
37
  end
19
38
 
20
39
  # Check whether the deadline has expired.
21
40
  #
22
- # @raise [DeadlineExceeded] if the deadline has passed
41
+ # @raise [DeadlineExceeded] if the deadline has passed (and grace period, if any, has also passed)
23
42
  # @return [void]
24
43
  def check!
25
- raise DeadlineExceeded, "Deadline exceeded" if expired?
44
+ return unless expired?
45
+
46
+ fire_expire_callback
47
+
48
+ # If we have a grace period and are still within it, don't raise
49
+ return if in_grace?
50
+
51
+ message = @name ? "Deadline '#{@name}' exceeded" : 'Deadline exceeded'
52
+ raise DeadlineExceeded, message
26
53
  end
27
54
 
28
- # Return the remaining time in seconds.
55
+ # Return the remaining time in seconds until the primary deadline.
56
+ # Can be negative during the grace period.
29
57
  #
30
- # @return [Float] seconds remaining (0.0 if expired)
58
+ # @return [Float] seconds remaining (negative if past primary deadline)
31
59
  def remaining
32
60
  r = @expires_at - now
61
+ if @grace_seconds
62
+ r
63
+ else
64
+ r.negative? ? 0.0 : r
65
+ end
66
+ end
67
+
68
+ # Return the remaining time in the grace period.
69
+ #
70
+ # @return [Float] seconds remaining in grace period (0.0 if no grace period or grace expired)
71
+ def grace_remaining
72
+ return 0.0 unless @grace_expires_at
73
+
74
+ r = @grace_expires_at - now
33
75
  r.negative? ? 0.0 : r
34
76
  end
35
77
 
36
- # Whether the deadline has expired.
78
+ # Whether the primary deadline has expired.
37
79
  #
38
80
  # @return [Boolean]
39
81
  def expired?
40
82
  now >= @expires_at
41
83
  end
42
84
 
85
+ # Whether the deadline is currently in the grace period.
86
+ # True only when the primary deadline has expired but the grace period has not.
87
+ #
88
+ # @return [Boolean]
89
+ def in_grace?
90
+ return false unless @grace_expires_at
91
+
92
+ expired? && now < @grace_expires_at
93
+ end
94
+
43
95
  private
44
96
 
97
+ def fire_expire_callback
98
+ return if @expire_callback_fired || @on_expire.nil?
99
+
100
+ @expire_callback_fired = true
101
+ @on_expire.call
102
+ end
103
+
45
104
  def now
46
105
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
47
106
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module TimeoutKit
5
- VERSION = "0.1.0"
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
@@ -14,20 +14,23 @@ module Philiprehberger
14
14
  # constraint, but outer deadlines are also checked.
15
15
  #
16
16
  # @param seconds [Numeric] the number of seconds for the deadline
17
+ # @param name [String, nil] optional human-readable name for the deadline
18
+ # @param grace [Numeric, nil] optional grace period in seconds after the primary deadline
19
+ # @param on_expire [Proc, nil] optional callback that fires once when expiry is detected
17
20
  # @yield [deadline] the block to execute within the deadline
18
21
  # @yieldparam deadline [Deadline] the deadline context
19
22
  # @return the block's return value
20
23
  # @raise [DeadlineExceeded] if the deadline is exceeded and {Deadline#check!} is called
21
- def self.deadline(seconds, &block)
22
- dl = Deadline.new(seconds)
24
+ def self.deadline(seconds, name: nil, grace: nil, on_expire: nil, &block)
25
+ dl = Deadline.new(seconds, name: name, grace: grace, on_expire: on_expire)
23
26
 
24
27
  # Support nested deadlines: use the tightest constraint
25
28
  parent = current_deadline
26
29
  effective = if parent && parent.expires_at < dl.expires_at
27
- parent
28
- else
29
- dl
30
- end
30
+ parent
31
+ else
32
+ dl
33
+ end
31
34
 
32
35
  push_deadline(effective)
33
36
  begin
@@ -77,5 +80,5 @@ module Philiprehberger
77
80
  end
78
81
  end
79
82
 
80
- require_relative "timeout_kit/version"
81
- require_relative "timeout_kit/deadline"
83
+ require_relative 'timeout_kit/version'
84
+ require_relative 'timeout_kit/deadline'
metadata CHANGED
@@ -1,18 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-timeout_kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Philip Rehberger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-22 00:00:00.000000000 Z
11
+ date: 2026-03-30 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A cooperative timeout library providing deadline and timeout patterns
14
- that avoid Thread.raise, with nested deadline support and explicit cancellation
15
- checks.
14
+ that avoid Thread.raise, with nested deadline support, grace periods, callbacks,
15
+ and explicit cancellation checks.
16
16
  email:
17
17
  - me@philiprehberger.com
18
18
  executables: []