Skip to content

Commit 3576c74

Browse files
committed
Add more flexible timeout.
1 parent a5b59e1 commit 3576c74

3 files changed

Lines changed: 135 additions & 3 deletions

File tree

lib/async/scheduler.rb

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
require_relative "clock"
99
require_relative "task"
10+
require_relative "timeout"
1011
require_relative "worker_pool"
1112

1213
require "io/event"
@@ -539,7 +540,7 @@ def fiber(...)
539540
# @parameter duration [Numeric] The time in seconds, in which the task should complete.
540541
# @parameter exception [Class] The exception class to raise.
541542
# @parameter message [String] The message to pass to the exception.
542-
# @yields {|duration| ...} The block to execute with a timeout.
543+
# @yields {|timeout| ...} The block to execute with a timeout.
543544
def with_timeout(duration, exception = TimeoutError, message = "execution expired", &block)
544545
fiber = Fiber.current
545546

@@ -549,7 +550,11 @@ def with_timeout(duration, exception = TimeoutError, message = "execution expire
549550
end
550551
end
551552

552-
yield timer
553+
if block.arity.zero?
554+
yield
555+
else
556+
yield Timeout.new(@timers, timer, duration)
557+
end
553558
ensure
554559
timer&.cancel!
555560
end
@@ -564,7 +569,7 @@ def with_timeout(duration, exception = TimeoutError, message = "execution expire
564569
# @parameter message [String] The message to pass to the exception.
565570
# @yields {|duration| ...} The block to execute with a timeout.
566571
def timeout_after(duration, exception, message, &block)
567-
with_timeout(duration, exception, message) do |timer|
572+
with_timeout(duration, exception, message) do
568573
yield duration
569574
end
570575
end

lib/async/timeout.rb

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
module Async
7+
# Represents a flexible timeout that can be rescheduled or extended.
8+
# @public Since *Async v2.24*.
9+
class Timeout
10+
# Initialize a new timeout.
11+
def initialize(timers, handle, duration = nil)
12+
@timers = timers
13+
@handle = handle
14+
@duration = duration || (handle.time - timers.now)
15+
end
16+
17+
# @attribute [Numeric] The duration of the timeout.
18+
attr :duration
19+
20+
# Update the duration of the timeout, rescheduling it if necessary.
21+
#
22+
# The duration is relative to the time the timeout was created.
23+
#
24+
# @parameter value [Numeric] The new duration to assign to the timeout.
25+
def duration=(value)
26+
delta = value - @duration
27+
self.reschedule(time + delta, value)
28+
end
29+
30+
# Adjust the timeout by the specified duration, rescheduling it if necessary.
31+
#
32+
# @parameter duration [Numeric] The duration to adjust the timeout by.
33+
# @returns [Numeric] The new time at which the timeout will occur.
34+
def adjust(duration)
35+
self.reschedule(time + duration, @duration + duration)
36+
end
37+
38+
# @returns [Numeric] The time at which the timeout will occur.
39+
def time
40+
@handle.time
41+
end
42+
43+
# Assign a new time to the timeout, rescheduling it if necessary.
44+
#
45+
# @parameter value [Numeric] The new time to assign to the timeout.
46+
# @returns [Numeric] The new time at which the timeout will occur.
47+
def time=(value)
48+
self.reschedule(value)
49+
end
50+
51+
# Cancel the timeout.
52+
def cancel!
53+
@handle.cancel!
54+
end
55+
56+
# @returns [Boolean] Whether the timeout has been cancelled.
57+
def cancelled?
58+
@handle.cancelled?
59+
end
60+
61+
# Reschedule the timeout to occur at the specified time.
62+
#
63+
# @parameter time [Numeric] The new time to schedule the timeout for.
64+
# @parameter duration [Numeric | Nil] The new duration to assign to the timeout.
65+
# @returns [Numeric] The new time at which the timeout will occur.
66+
private def reschedule(time, duration = nil)
67+
if block = @handle&.block
68+
@handle.cancel!
69+
70+
@duration = duration || (time - @timers.now)
71+
@handle = @timers.schedule(time, block)
72+
73+
return time
74+
else
75+
raise RuntimeError, "Cannot reschedule a cancelled timeout!"
76+
end
77+
end
78+
end
79+
end

test/async/timeout.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
require "sus/fixtures/async"
2+
3+
describe Async::Timeout do
4+
include Sus::Fixtures::Async::ReactorContext
5+
6+
it "can schedule a timeout" do
7+
scheduler.with_timeout(1) do |timeout|
8+
expect(timeout.time).to be >= 0
9+
expect(timeout.duration).to be == 1
10+
end
11+
end
12+
13+
with "#adjust" do
14+
it "can adjust the timeout" do
15+
scheduler.with_timeout(1) do |timeout|
16+
timeout.adjust(1)
17+
expect(timeout.duration).to be == 2
18+
end
19+
end
20+
end
21+
22+
with "#duration=" do
23+
it "can set the timeout duration" do
24+
scheduler.with_timeout(1) do |timeout|
25+
timeout.duration = 2
26+
expect(timeout.duration).to be == 2
27+
end
28+
end
29+
end
30+
31+
with "#time=" do
32+
it "can set the timeout time" do
33+
scheduler.with_timeout(1) do |timeout|
34+
timeout.time = timeout.time + 1
35+
expect(timeout.duration).to (be > 1).and(be < 2)
36+
end
37+
end
38+
end
39+
40+
with "#cancel!" do
41+
it "can cancel the timeout" do
42+
scheduler.with_timeout(1) do |timeout|
43+
timeout.cancel!
44+
expect(timeout).to be(:cancelled?)
45+
end
46+
end
47+
end
48+
end

0 commit comments

Comments
 (0)