philiprehberger-timeout_kit 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 439206c3312c3911ae57be460c6d4c6fdae897dc334307b5a7ad2fb08f5ad3f2
4
+ data.tar.gz: dcdf01577d2efaec47d7ca37505baf755900902aba28d758347b41266b1f3b4e
5
+ SHA512:
6
+ metadata.gz: 77126a25c3e6d01c0d1dc9be4998314d04c3be99a2b8c9cd1154626ed05834ef507b1adaaefd4336b47ef307512ef080b20478218dca3b7459a4e41f41e3f9b3
7
+ data.tar.gz: 1ca69dbfd8a5d414b01329c691a07cf04fc91f5b676985b7737b8d907b0f292a33d08aeac9a6990684274fd4b4fac0a8675542402579259fa2c9a4a91c37b9d1
data/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ All notable changes to this gem will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-03-22
11
+
12
+ ### Added
13
+ - Initial release
14
+ - Cooperative deadline with nested support and remaining time tracking
15
+ - Cooperative timeout with explicit cancellation checks
16
+ - DeadlineExceeded error for expired deadlines
17
+ - Thread-local deadline stack for nested contexts
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 philiprehberger
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # philiprehberger-timeout_kit
2
+
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
+ [![Gem Version](https://badge.fury.io/rb/philiprehberger-timeout_kit.svg)](https://rubygems.org/gems/philiprehberger-timeout_kit)
5
+ [![License](https://img.shields.io/github/license/philiprehberger/rb-timeout-kit)](LICENSE)
6
+
7
+ Safe timeout patterns without Thread.raise
8
+
9
+ ## Requirements
10
+
11
+ - Ruby >= 3.1
12
+
13
+ ## Installation
14
+
15
+ Add to your Gemfile:
16
+
17
+ ```ruby
18
+ gem "philiprehberger-timeout_kit"
19
+ ```
20
+
21
+ Or install directly:
22
+
23
+ ```bash
24
+ gem install philiprehberger-timeout_kit
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```ruby
30
+ require "philiprehberger/timeout_kit"
31
+
32
+ Philiprehberger::TimeoutKit.deadline(5) do |d|
33
+ loop do
34
+ d.check! # raises DeadlineExceeded if time is up
35
+ process_next_item
36
+ end
37
+ end
38
+ ```
39
+
40
+ ### Remaining Time
41
+
42
+ ```ruby
43
+ Philiprehberger::TimeoutKit.deadline(10) do |d|
44
+ while d.remaining > 1
45
+ do_work
46
+ d.check!
47
+ end
48
+ puts "Only #{d.remaining}s left, wrapping up"
49
+ end
50
+ ```
51
+
52
+ ### Nested Deadlines
53
+
54
+ ```ruby
55
+ Philiprehberger::TimeoutKit.deadline(30) do |outer|
56
+ # Inner deadline is tighter, so it takes precedence
57
+ Philiprehberger::TimeoutKit.deadline(5) do |inner|
58
+ inner.check!
59
+ puts inner.remaining # <= 5
60
+ end
61
+
62
+ # After inner block, outer deadline is restored
63
+ outer.check!
64
+ puts outer.remaining # <= 30
65
+ end
66
+ ```
67
+
68
+ ### Cooperative Timeout
69
+
70
+ ```ruby
71
+ Philiprehberger::TimeoutKit.cooperative(5) do |t|
72
+ items.each do |item|
73
+ t.check!
74
+ process(item)
75
+ end
76
+ end
77
+ ```
78
+
79
+ ### Current Deadline
80
+
81
+ ```ruby
82
+ Philiprehberger::TimeoutKit.deadline(10) do |_d|
83
+ current = Philiprehberger::TimeoutKit.current_deadline
84
+ puts current.remaining
85
+ end
86
+ ```
87
+
88
+ ## API
89
+
90
+ | Method | Description |
91
+ |--------|-------------|
92
+ | `.deadline(seconds) { \|d\| }` | Execute a block with a cooperative deadline |
93
+ | `.cooperative(seconds) { \|t\| }` | Execute a block with a simple cooperative timeout |
94
+ | `.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 |
98
+ | `DeadlineExceeded` | Raised when a deadline or timeout expires |
99
+
100
+ ## Development
101
+
102
+ ```bash
103
+ bundle install
104
+ bundle exec rspec
105
+ bundle exec rubocop
106
+ ```
107
+
108
+ ## License
109
+
110
+ MIT
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module TimeoutKit
5
+ # A cooperative deadline that tracks remaining time and supports nesting.
6
+ #
7
+ # Deadlines do not use Thread.raise. Instead, callers must explicitly
8
+ # call {#check!} at safe cancellation points.
9
+ class Deadline
10
+ # @return [Float] the absolute monotonic time when the deadline expires
11
+ attr_reader :expires_at
12
+
13
+ # Create a new deadline.
14
+ #
15
+ # @param seconds [Numeric] the number of seconds until the deadline expires
16
+ def initialize(seconds)
17
+ @expires_at = now + seconds
18
+ end
19
+
20
+ # Check whether the deadline has expired.
21
+ #
22
+ # @raise [DeadlineExceeded] if the deadline has passed
23
+ # @return [void]
24
+ def check!
25
+ raise DeadlineExceeded, "Deadline exceeded" if expired?
26
+ end
27
+
28
+ # Return the remaining time in seconds.
29
+ #
30
+ # @return [Float] seconds remaining (0.0 if expired)
31
+ def remaining
32
+ r = @expires_at - now
33
+ r.negative? ? 0.0 : r
34
+ end
35
+
36
+ # Whether the deadline has expired.
37
+ #
38
+ # @return [Boolean]
39
+ def expired?
40
+ now >= @expires_at
41
+ end
42
+
43
+ private
44
+
45
+ def now
46
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module TimeoutKit
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module TimeoutKit
5
+ class Error < StandardError; end
6
+
7
+ # Raised when a deadline or cooperative timeout expires.
8
+ class DeadlineExceeded < Error; end
9
+
10
+ # Execute a block with a deadline. The block receives a {Deadline} object
11
+ # that can be used to check remaining time and whether the deadline has passed.
12
+ #
13
+ # Deadlines can be nested. The innermost deadline is always the tightest
14
+ # constraint, but outer deadlines are also checked.
15
+ #
16
+ # @param seconds [Numeric] the number of seconds for the deadline
17
+ # @yield [deadline] the block to execute within the deadline
18
+ # @yieldparam deadline [Deadline] the deadline context
19
+ # @return the block's return value
20
+ # @raise [DeadlineExceeded] if the deadline is exceeded and {Deadline#check!} is called
21
+ def self.deadline(seconds, &block)
22
+ dl = Deadline.new(seconds)
23
+
24
+ # Support nested deadlines: use the tightest constraint
25
+ parent = current_deadline
26
+ effective = if parent && parent.expires_at < dl.expires_at
27
+ parent
28
+ else
29
+ dl
30
+ end
31
+
32
+ push_deadline(effective)
33
+ begin
34
+ block.call(effective)
35
+ ensure
36
+ pop_deadline
37
+ end
38
+ end
39
+
40
+ # Execute a block with a cooperative timeout. The block receives a timeout
41
+ # context that can be checked at safe cancellation points.
42
+ #
43
+ # Unlike {.deadline}, this is a simpler wrapper that does not support nesting.
44
+ #
45
+ # @param seconds [Numeric] the number of seconds for the timeout
46
+ # @yield [timeout] the block to execute within the timeout
47
+ # @yieldparam timeout [Deadline] a deadline-like context for checking timeout
48
+ # @return the block's return value
49
+ # @raise [DeadlineExceeded] if the timeout is exceeded and {Deadline#check!} is called
50
+ def self.cooperative(seconds)
51
+ dl = Deadline.new(seconds)
52
+ yield dl
53
+ end
54
+
55
+ # Return the current innermost deadline, or nil if none is active.
56
+ #
57
+ # @return [Deadline, nil]
58
+ def self.current_deadline
59
+ stack = Thread.current[:philiprehberger_timeout_kit_deadlines]
60
+ stack&.last
61
+ end
62
+
63
+ # @api private
64
+ def self.push_deadline(deadline)
65
+ Thread.current[:philiprehberger_timeout_kit_deadlines] ||= []
66
+ Thread.current[:philiprehberger_timeout_kit_deadlines].push(deadline)
67
+ end
68
+ private_class_method :push_deadline
69
+
70
+ # @api private
71
+ def self.pop_deadline
72
+ stack = Thread.current[:philiprehberger_timeout_kit_deadlines]
73
+ stack&.pop
74
+ Thread.current[:philiprehberger_timeout_kit_deadlines] = nil if stack&.empty?
75
+ end
76
+ private_class_method :pop_deadline
77
+ end
78
+ end
79
+
80
+ require_relative "timeout_kit/version"
81
+ require_relative "timeout_kit/deadline"
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: philiprehberger-timeout_kit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Philip Rehberger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-03-22 00:00:00.000000000 Z
12
+ dependencies: []
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.
16
+ email:
17
+ - me@philiprehberger.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - CHANGELOG.md
23
+ - LICENSE
24
+ - README.md
25
+ - lib/philiprehberger/timeout_kit.rb
26
+ - lib/philiprehberger/timeout_kit/deadline.rb
27
+ - lib/philiprehberger/timeout_kit/version.rb
28
+ homepage: https://github.com/philiprehberger/rb-timeout-kit
29
+ licenses:
30
+ - MIT
31
+ metadata:
32
+ homepage_uri: https://github.com/philiprehberger/rb-timeout-kit
33
+ source_code_uri: https://github.com/philiprehberger/rb-timeout-kit
34
+ changelog_uri: https://github.com/philiprehberger/rb-timeout-kit/blob/main/CHANGELOG.md
35
+ bug_tracker_uri: https://github.com/philiprehberger/rb-timeout-kit/issues
36
+ rubygems_mfa_required: 'true'
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: 3.1.0
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubygems_version: 3.5.22
53
+ signing_key:
54
+ specification_version: 4
55
+ summary: Safe timeout patterns without Thread.raise
56
+ test_files: []