Skip to content

Commit b4baad3

Browse files
authored
fix: Stop repeating tasks promptly instead of waiting out the sleep (#432)
1 parent af79a96 commit b4baad3

2 files changed

Lines changed: 99 additions & 5 deletions

File tree

lib/ldclient-rb/impl/repeating_task.rb

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,39 @@
44

55
module LaunchDarkly
66
module Impl
7+
#
8+
# Runs a task again and again on a worker thread, with a fixed interval
9+
# between runs.
10+
#
11+
# The worker waits on an event instead of calling `sleep`, so `stop` can
12+
# wake it at once even if `stop` runs before the worker starts waiting.
13+
#
14+
# @private
15+
#
716
class RepeatingTask
817
attr_reader :name
918

19+
#
20+
# @param interval [Numeric] seconds between the start of one run and the start of the next
21+
# @param start_delay [Numeric, nil] seconds to wait before the first run
22+
# @param task [Proc] the code to run
23+
# @param logger [Logger]
24+
# @param name [String] the name given to the worker thread
25+
#
1026
def initialize(interval, start_delay, task, logger, name)
1127
@interval = interval
1228
@start_delay = start_delay
1329
@task = task
1430
@logger = logger
1531
@stopped = Concurrent::AtomicBoolean.new(false)
32+
@stop_event = Concurrent::Event.new
1633
@worker = nil
1734
@name = name
1835
end
1936

2037
def start
2138
@worker = Thread.new do
22-
sleep(@start_delay) unless @start_delay.nil? || @start_delay == 0
39+
@stop_event.wait(@start_delay) unless @start_delay.nil? || @start_delay == 0
2340

2441
until @stopped.value do
2542
started_at = Time.now
@@ -29,19 +46,23 @@ def start
2946
Impl::Util.log_exception(@logger, "Uncaught exception from repeating task", e)
3047
end
3148
delta = @interval - (Time.now - started_at)
32-
if delta > 0
33-
sleep(delta)
34-
end
49+
@stop_event.wait(delta) if delta > 0
3550
end
3651
end
3752

3853
@worker.name = @name
3954
end
4055

56+
#
57+
# Stops the worker thread and waits for it to finish.
58+
#
59+
# This method is safe to call more than once, before `start`, and from
60+
# inside the task itself.
61+
#
4162
def stop
4263
if @stopped.make_true
64+
@stop_event.set
4365
if @worker && @worker.alive? && @worker != Thread.current
44-
@worker.run # causes the thread to wake up if it's currently in a sleep
4566
@worker.join
4667
end
4768
end

spec/impl/repeating_task_spec.rb

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,79 @@ def null_logger
5959
expect(no_more_items).to be true
6060
end
6161

62+
it "stops promptly when stopped during a long start delay" do
63+
ran = Concurrent::Event.new
64+
task = RepeatingTask.new(10, 10, -> { ran.set }, null_logger, "test")
65+
task.start
66+
started_at = Time.now
67+
task.stop
68+
elapsed = Time.now - started_at
69+
70+
expect(elapsed).to be < 1
71+
expect(ran.set?).to be false
72+
end
73+
74+
it "stops promptly when stopped while waiting between runs" do
75+
ran = Concurrent::Event.new
76+
task = RepeatingTask.new(10, 0, -> { ran.set }, null_logger, "test")
77+
begin
78+
task.start
79+
expect(ran.wait(1)).to be true
80+
started_at = Time.now
81+
task.stop
82+
elapsed = Time.now - started_at
83+
84+
expect(elapsed).to be < 1
85+
ensure
86+
task.stop
87+
end
88+
end
89+
90+
it "can be stopped before it is started" do
91+
task = RepeatingTask.new(0.01, 0, -> {}, null_logger, "test")
92+
93+
expect { task.stop }.not_to raise_error
94+
end
95+
96+
it "does not run the task if stopped before it is started" do
97+
ran = Concurrent::Event.new
98+
task = RepeatingTask.new(0.01, 0, -> { ran.set }, null_logger, "test")
99+
task.stop
100+
task.start
101+
begin
102+
expect(ran.wait(0.1)).to be false
103+
ensure
104+
task.stop
105+
end
106+
end
107+
108+
it "can be stopped more than once" do
109+
task = RepeatingTask.new(10, 0, -> {}, null_logger, "test")
110+
task.start
111+
task.stop
112+
113+
expect { task.stop }.not_to raise_error
114+
end
115+
116+
it "keeps running after the task raises an exception" do
117+
queue = Queue.new
118+
calls = 0
119+
task = RepeatingTask.new(0.01, 0,
120+
-> {
121+
calls += 1
122+
queue << calls
123+
raise "boom" if calls == 1
124+
},
125+
null_logger, "test")
126+
begin
127+
task.start
128+
expect(queue.pop).to eq(1)
129+
expect(queue.pop).to eq(2)
130+
ensure
131+
task.stop
132+
end
133+
end
134+
62135
it "can be stopped from within the task" do
63136
counter = 0
64137
stopped = Concurrent::Event.new

0 commit comments

Comments
 (0)