Skip to content

Commit e56f02d

Browse files
authored
Merge pull request #167 from dblock/once-and-every
Fix timers to run independently, add once_and_every.
2 parents 1b5986f + 46ec601 commit e56f02d

File tree

4 files changed

+94
-18
lines changed

4 files changed

+94
-18
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* [#163](https://github.com/slack-ruby/slack-ruby-bot-server/pull/163): Updated releasing documentation - [@crazyoptimist](https://github.com/crazyoptimist).
66
* [#164](https://github.com/slack-ruby/slack-ruby-bot-server/pull/164): Added support for Rails 6.1 and 7.0 - [@maths22](https://github.com/maths22).
77
* [#168](https://github.com/slack-ruby/slack-ruby-bot-server/pull/168): Added support for Ruby 3.2.1 - [@dblock](https://github.com/dblock).
8+
* [#167](https://github.com/slack-ruby/slack-ruby-bot-server/pull/167): Added `once_and_every` timer - [@dblock](https://github.com/dblock).
89
* Your contribution here.
910

1011
#### 2.0.1 (2023/02/20)

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,7 @@ end
238238

239239
##### Service Timers
240240

241-
You can introduce custom behavior into the service lifecycle on a timer. For example, check whether a team's trial has expired, or periodically cleanup data.
242-
243-
Note that unlike callbacks, timers are global for the entire service.
241+
You can introduce custom behavior into the service lifecycle on a timer. For example, check whether a team's trial has expired, or periodically clean-up data. Timers can run once on start (`once_and_every`) and start running after a certain period (`every`).
244242

245243
```ruby
246244
instance = SlackRubyBotServer::Service.instance
@@ -254,6 +252,10 @@ instance.every :hour do
254252
end
255253
end
256254

255+
instance.once_and_every :minute do
256+
# called once on start, then every minute
257+
end
258+
257259
instance.every :minute do
258260
# called every minute
259261
end
@@ -267,6 +269,8 @@ instance.every 30 do
267269
end
268270
```
269271

272+
Note that, unlike callbacks, timers are global for the entire service. Timers are independent, and a failing timer will not terminate other timers.
273+
270274
##### Extensions
271275

272276
A number of extensions use service manager callbacks and service timers to implement useful functionality.

lib/slack-ruby-bot-server/service.rb

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,15 @@ def on(*types, &block)
2424
end
2525
end
2626

27-
def every(*intervals, &block)
27+
def once_and_every(*intervals, &block)
2828
Array(intervals).each do |interval|
29-
case interval
30-
when :minute
31-
interval = 60
32-
when :hour
33-
interval = 60 * 60
34-
when :day
35-
interval = 60 * 60 * 24
36-
end
37-
raise "Invalid interval \"#{interval}\"." unless interval.is_a?(Integer) && interval > 0
29+
@intervals[_validate_interval(interval)] << [block, { run_on_start: true }]
30+
end
31+
end
3832

39-
@intervals[interval] << block
33+
def every(*intervals, &block)
34+
Array(intervals).each do |interval|
35+
@intervals[_validate_interval(interval)] << [block, {}]
4036
end
4137
end
4238

@@ -83,9 +79,12 @@ def start_from_database!
8379
end
8480

8581
def start_intervals!
86-
@intervals.each_pair do |period, calls|
87-
_every period do
88-
calls.each(&:call)
82+
@intervals.each_pair do |period, calls_with_options|
83+
calls_with_options.each do |call_with_options|
84+
call, options = *call_with_options
85+
_every period, options do
86+
call.call
87+
end
8988
end
9089
end
9190
end
@@ -105,10 +104,28 @@ def self.reset!
105104

106105
private
107106

108-
def _every(tt, &_block)
107+
def _validate_interval(interval)
108+
case interval
109+
when :minute
110+
interval = 60
111+
when :hour
112+
interval = 60 * 60
113+
when :day
114+
interval = 60 * 60 * 24
115+
end
116+
raise "Invalid interval \"#{interval}\"." unless interval.is_a?(Integer) && interval > 0
117+
118+
interval
119+
end
120+
121+
def _every(tt, options = {}, &_block)
109122
::Async::Reactor.run do |task|
110123
loop do
111124
begin
125+
if options[:run_on_start]
126+
options = {}
127+
yield
128+
end
112129
task.sleep tt
113130
yield
114131
rescue StandardError => e

spec/slack-ruby-bot-server/service_spec.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,60 @@
103103
expect(@events.sort.uniq).to eq %w[1-2s 1s]
104104
end
105105
end
106+
context 'with a failing timer' do
107+
before do
108+
@events = []
109+
instance.every 1 do
110+
@events << 'fail'
111+
raise 'error'
112+
end
113+
instance.every 1 do
114+
@events << '1'
115+
end
116+
end
117+
it 'does not abort all timers on failure of the first one' do
118+
Async::Reactor.run do |task|
119+
instance.start_intervals!
120+
task.sleep 3
121+
task.stop
122+
end
123+
expect(@events.sort.uniq).to eq %w[1 fail]
124+
end
125+
end
126+
context 'once_and_every' do
127+
context '5 seconds' do
128+
before do
129+
@events = []
130+
instance.once_and_every 5 do
131+
@events << '1'
132+
end
133+
end
134+
it 'runs the timer once within 3 seconds' do
135+
Async::Reactor.run do |task|
136+
instance.start_intervals!
137+
task.sleep 3
138+
task.stop
139+
end
140+
expect(@events).to eq %w[1]
141+
end
142+
end
143+
context '2 seconds' do
144+
before do
145+
@events = []
146+
instance.once_and_every 2 do
147+
@events << '1'
148+
end
149+
end
150+
it 'runs the timer exactly twice within 3 seconds' do
151+
Async::Reactor.run do |task|
152+
instance.start_intervals!
153+
task.sleep 3
154+
task.stop
155+
end
156+
expect(@events).to eq %w[1 1]
157+
end
158+
end
159+
end
106160
end
107161
context 'overriding service_class' do
108162
let(:service_class) do

0 commit comments

Comments
 (0)