perfectsched 0.8.10 → 0.8.11

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.
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+
3
+ describe PerfectSched::Engine do
4
+ let (:logger){ double('logger').as_null_object }
5
+ let (:runner){ double('runner') }
6
+ let (:scheds){ double('scheds') }
7
+ let (:config){ {logger: logger} }
8
+ let (:engine) do
9
+ Engine.new(runner, config)
10
+ end
11
+ before do
12
+ expect(PerfectSched).to receive(:open).with(config).and_return(scheds)
13
+ end
14
+
15
+ describe '.new' do
16
+ it 'returns an Engine' do
17
+ engine = Engine.new(runner, config)
18
+ expect(engine).to be_an_instance_of(Engine)
19
+ expect(engine.instance_variable_get(:@runner)).to eq(runner)
20
+ expect(engine.instance_variable_get(:@poll_interval)).to eq(1.0)
21
+ expect(engine.instance_variable_get(:@log)).to eq(logger)
22
+ expect(engine.instance_variable_get(:@running_flag)).to be_a(BlockingFlag)
23
+ expect(engine.instance_variable_get(:@finish_flag)).to be_a(BlockingFlag)
24
+ expect(engine.instance_variable_get(:@scheds)).to be_a(PerfectSched)
25
+ end
26
+ end
27
+
28
+ describe '#run' do
29
+ it 'runs until stopped' do
30
+ rflag = engine.instance_variable_get(:@running_flag)
31
+ fflag = engine.instance_variable_get(:@finish_flag)
32
+ task1 = double('task1')
33
+ task2 = double('task2')
34
+ allow(scheds).to receive(:poll).and_return(task1, nil, task2)
35
+ expect(runner).to receive(:new).exactly(:twice) do |task|
36
+ expect(rflag.set?).to be true
37
+ r = double('r')
38
+ case task
39
+ when task1
40
+ expect(r).to receive(:run)
41
+ when task2
42
+ expect(r).to receive(:run){ fflag.set! }
43
+ else
44
+ raise ArgumentError
45
+ end
46
+ r
47
+ end
48
+ expect(engine.run).to eq(engine)
49
+ expect(rflag.set?).to be false
50
+ end
51
+ end
52
+
53
+ describe '#stop' do
54
+ it 'sets finish_flag' do
55
+ expect(engine.stop).to eq(engine)
56
+ expect(engine.instance_variable_get(:@finish_flag).set?).to eq true
57
+ end
58
+ end
59
+
60
+ describe '#join' do
61
+ it 'waits running flag is set' do
62
+ expect(engine.join).to eq(engine)
63
+ expect(engine.instance_variable_get(:@running_flag).set?).to eq false
64
+ end
65
+ end
66
+
67
+ describe '#close' do
68
+ it 'closes scheds' do
69
+ expect(scheds).to receive(:close)
70
+ expect(engine.close).to eq(engine)
71
+ end
72
+ end
73
+
74
+ describe '#shutdown' do
75
+ it 'calls stop, join, and close' do
76
+ expect(engine).to receive(:stop)
77
+ expect(engine).to receive(:join)
78
+ expect(engine).to receive(:close).and_return(engine)
79
+ expect(engine.shutdown).to eq(engine)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe PerfectSched::Model do
4
+ let (:config){ double('config') }
5
+ let (:client){ double('client', config: config) }
6
+ let (:klass){ Class.new{include PerfectSched::Model} }
7
+ let (:model){ klass.new(client) }
8
+ describe '.new' do
9
+ it 'creates an instance' do
10
+ expect(model).to be_a(PerfectSched::Model)
11
+ expect(model.instance_variable_get(:@client)).to eq(client)
12
+ end
13
+ end
14
+ describe '#client' do
15
+ it 'returns its client' do
16
+ expect(model.client).to eq(client)
17
+ end
18
+ end
19
+ describe '#config' do
20
+ it 'returns its client.config' do
21
+ expect(model.config).to eq(client.config)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+
3
+ describe PerfectSched do
4
+ describe '.open' do
5
+ let (:config){ double('config') }
6
+ let (:client){ double('client') }
7
+ let (:schedule_collection){ double('schedule_collection') }
8
+ before do
9
+ expect(Client).to receive(:new).with(config).and_return(client)
10
+ expect(ScheduleCollection).to receive(:new).with(client).and_return(schedule_collection)
11
+ end
12
+ it 'returns an instance without block' do
13
+ expect(client).not_to receive(:close)
14
+ expect(PerfectSched.open(config)).to eq(schedule_collection)
15
+ end
16
+ it 'yields block if given' do
17
+ ret = double('ret')
18
+ expect(client).to receive(:close)
19
+ r = PerfectSched.open(config) do |sc|
20
+ expect(sc).to eq(schedule_collection)
21
+ ret
22
+ end
23
+ expect(r).to eq(ret)
24
+ end
25
+ end
26
+
27
+ describe '.cron_time' do
28
+ it do
29
+ ts = PerfectSched.cron_time('0 * * * *', 1, nil)
30
+ expect(ts).to eq 3600
31
+ end
32
+ it do
33
+ expect{PerfectSched.cron_time('0 * * * *', 0, 'JST-9')}.to raise_error(ArgumentError)
34
+ end
35
+ it do
36
+ ts = PerfectSched.cron_time('0,30 * * * *', 1, nil)
37
+ expect(ts).to eq 1800
38
+ end
39
+ it do
40
+ ts = PerfectSched.cron_time('*/7 * * * *', 1, nil)
41
+ expect(ts).to eq 420
42
+ end
43
+ it 'supports @hourly' do
44
+ ts = PerfectSched.cron_time('@hourly', 1, nil)
45
+ expect(ts).to eq 3600
46
+ end
47
+ it 'supports @daily' do
48
+ ts = PerfectSched.cron_time('@daily', 1, nil)
49
+ expect(ts).to eq 86400
50
+ end
51
+ it 'supports @monthly' do
52
+ ts = PerfectSched.cron_time('@monthly', 1, nil)
53
+ expect(ts).to eq 2678400
54
+ end
55
+ end
56
+
57
+ describe '.next_time' do
58
+ it 'can run hourly cron' do
59
+ ts = PerfectSched.next_time(' 0 * * * * ', 0, nil)
60
+ expect(ts).to eq 3600
61
+ end
62
+ it 'calculates 4 years quickly' do
63
+ t = Time.utc(2012,2,29)
64
+ ts = PerfectSched.next_time('0 0 29 2 *', t.to_i, nil)
65
+ expect(ts).to eq(Time.utc(2016,2,29).to_i)
66
+ end
67
+ it 'raises error on unsupported timezone' do
68
+ expect{PerfectSched.next_time('0 * * * *', 0, 'JST-9')}.to raise_error(ArgumentError)
69
+ end
70
+ it 'returns next run time of given time' do
71
+ t0 = Time.new(2015, 12, 3, 1, 0, 0, 9*3600)
72
+ t1 = Time.new(2015, 12, 3, 2, 0, 0, 9*3600)
73
+ ts = PerfectSched.next_time('0 * * * *', t0.to_i, 'Asia/Tokyo')
74
+ expect(ts).to eq(t1.to_i)
75
+ end
76
+ it 'returns next run time of given time' do
77
+ t0 = Time.new(2015, 12, 3, 1, 59, 59, 9*3600)
78
+ t1 = Time.new(2015, 12, 3, 2, 0, 0, 9*3600)
79
+ ts = PerfectSched.next_time('0 * * * *', t0.to_i, 'Asia/Tokyo')
80
+ expect(ts).to eq(t1.to_i)
81
+ end
82
+ it 'returns next run time with day of week (0=Sun)' do
83
+ t0 = Time.new(2015, 12, 3, 0, 0, 0, 9*3600)
84
+ t1 = Time.new(2015, 12, 6, 0, 0, 0, 9*3600)
85
+ ts = PerfectSched.next_time('0 0 * * 0', t0.to_i, 'Asia/Tokyo')
86
+ expect(ts).to eq(t1.to_i)
87
+ end
88
+ context 'DST 2015' do
89
+ it 'skips task on 8 Mar because 1:59:59 PST -> 3:00:00 PDT' do
90
+ t0 = Time.new(2015, 3, 7, 2, 0, 0, -8*3600)
91
+ # 2015-03-08T02:00:00 doesn't exist
92
+ t1 = Time.new(2015, 3, 9, 2, 0, 0, -7*3600)
93
+ ts = PerfectSched.next_time('0 2 * * *', t0.to_i, 'America/Los_Angeles')
94
+ expect(ts).to eq(t1.to_i)
95
+ end
96
+ it 'runs twice on Nov 11 because 1:59:59 PDT -> 1:00:00 PST' do
97
+ # 2015-11-01T01:00:00 exists twice
98
+ t0 = Time.new(2015, 11, 1, 1, 0, 0, -7*3600)
99
+ t1 = Time.new(2015, 11, 1, 1, 0, 0, -8*3600)
100
+ ts = PerfectSched.next_time('0 1 * * *', t0.to_i, 'America/Los_Angeles')
101
+ expect(ts).to eq(t1.to_i)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -2,40 +2,300 @@ require 'spec_helper'
2
2
  require 'perfectsched/backend/rdb_compat'
3
3
 
4
4
  describe Backend::RDBCompatBackend do
5
- let :sc do
6
- FileUtils.rm_f 'spec/test.db'
7
- sc = PerfectSched.open({:type=>'rdb_compat', :url=>'sqlite://spec/test.db', :table=>'test_scheds'})
8
- sc.client.init_database
9
- sc
5
+ let (:now){ Time.now.to_i }
6
+ let (:client){ double('client') }
7
+ let (:config){ {url: 'sqlite://spec/test.db', table: 'test_scheds'} }
8
+ let (:db) do
9
+ d = Backend::RDBCompatBackend.new(client, config)
10
+ s = d.db
11
+ s.tables.each{|t| s.drop_table(t) }
12
+ d.init_database(nil)
13
+ d
10
14
  end
11
15
 
12
- let :client do
13
- sc.client
16
+ context 'compatibility' do
17
+ let :sc do
18
+ FileUtils.rm_f 'spec/test.db'
19
+ sc = PerfectSched.open({:type=>'rdb_compat', :url=>'sqlite://spec/test.db', :table=>'test_scheds'})
20
+ sc.client.init_database
21
+ sc
22
+ end
23
+
24
+ let :client do
25
+ sc.client
26
+ end
27
+
28
+ let :backend do
29
+ client.backend
30
+ end
31
+
32
+ it 'backward compatibility 1' do
33
+ backend.db["INSERT INTO test_scheds (id, timeout, next_time, cron, delay, data, timezone) VALUES (?, ?, ?, ?, ?, ?, ?)", "maint_sched.1.do_hourly", 1339812000, 1339812000, "0 * * * *", 0, {"account_id"=>1}.to_json, "UTC"].insert
34
+ ts = backend.acquire(60, 1, {:now=>1339812003})
35
+ expect(ts).not_to eq(nil)
36
+ t = ts[0]
37
+ expect(t.data).to eq({'account_id'=>1})
38
+ expect(t.type).to eq('maint_sched')
39
+ expect(t.key).to eq('maint_sched.1.do_hourly')
40
+ expect(t.next_time).to eq(1339812000)
41
+ end
42
+
43
+ it 'backward compatibility 2' do
44
+ backend.db["INSERT INTO test_scheds (id, timeout, next_time, cron, delay, data, timezone) VALUES (?, ?, ?, ?, ?, ?, ?)", "merge", 1339812060, 1339812000, "@hourly", 60, '', "Asia/Tokyo"].insert
45
+ ts = backend.acquire(60, 1, {:now=>1339812060})
46
+ t = ts[0]
47
+ expect(t.data).to eq({})
48
+ expect(t.type).to eq('merge')
49
+ expect(t.key).to eq('merge')
50
+ expect(t.next_time).to eq(1339812000)
51
+ end
14
52
  end
15
53
 
16
- let :backend do
17
- client.backend
54
+ context '.new' do
55
+ let (:client){ double('client') }
56
+ let (:table){ double('table') }
57
+ it 'raises error unless url' do
58
+ expect{Backend::RDBCompatBackend.new(client, {})}.to raise_error(ConfigError)
59
+ end
60
+ it 'raises error unless table' do
61
+ expect{Backend::RDBCompatBackend.new(client, {url: ''})}.to raise_error(ConfigError)
62
+ end
63
+ it 'supports sqlite' do
64
+ config = {url: 'sqlite://localhost', table: table}
65
+ expect(Backend::RDBCompatBackend.new(client, config)).to be_an_instance_of(Backend::RDBCompatBackend)
66
+ end
67
+ it 'supports mysql' do
68
+ config = {url: 'mysql://root:@localhost/perfectsched_test', table: table}
69
+ expect(Backend::RDBCompatBackend.new(client, config)).to be_an_instance_of(Backend::RDBCompatBackend)
70
+ end
71
+ it 'doesn\'t support postgres' do
72
+ config = {url: 'postgres://localhost', table: table}
73
+ expect{Backend::RDBCompatBackend.new(client, config)}.to raise_error(ConfigError)
74
+ end
18
75
  end
19
76
 
20
- it 'backward compatibility 1' do
21
- backend.db["INSERT INTO test_scheds (id, timeout, next_time, cron, delay, data, timezone) VALUES (?, ?, ?, ?, ?, ?, ?)", "maint_sched.1.do_hourly", 1339812000, 1339812000, "0 * * * *", 0, {"account_id"=>1}.to_json, "UTC"].insert
22
- ts = backend.acquire(60, 1, {:now=>1339812003})
23
- ts.should_not == nil
24
- t = ts[0]
25
- t.data.should == {'account_id'=>1}
26
- t.type.should == 'maint_sched'
27
- t.key.should == 'maint_sched.1.do_hourly'
28
- t.next_time.should == 1339812000
77
+ context '#init_database' do
78
+ it 'creates the table' do
79
+ db.add('key', 'test', '* * * * *', 0, 'Asia/Tokyo', {}, now, now, {})
80
+ end
29
81
  end
30
82
 
31
- it 'backward compatibility 2' do
32
- backend.db["INSERT INTO test_scheds (id, timeout, next_time, cron, delay, data, timezone) VALUES (?, ?, ?, ?, ?, ?, ?)", "merge", 1339812060, 1339812000, "@hourly", 60, '', "Asia/Tokyo"].insert
33
- ts = backend.acquire(60, 1, {:now=>1339812060})
34
- t = ts[0]
35
- t.data.should == {}
36
- t.type.should == 'merge'
37
- t.key.should == 'merge'
38
- t.next_time.should == 1339812000
83
+ context '#get_schedule_metadata' do
84
+ before do
85
+ db.add('key', 'test', '* * * * *', 0, 'Asia/Tokyo', {}, now, now, {})
86
+ end
87
+ it 'fetches a metadata' do
88
+ expect(db.get_schedule_metadata('key')).to be_an_instance_of(ScheduleWithMetadata)
89
+ end
90
+ it 'raises error if non exist key' do
91
+ expect{db.get_schedule_metadata('nonexistent')}.to raise_error(NotFoundError)
92
+ end
93
+ end
94
+
95
+ context '#list' do
96
+ before do
97
+ db.add('key', 'test', '* * * * *', 0, 'Asia/Tokyo', {}, now, now, {})
98
+ end
99
+ it 'lists a metadata' do
100
+ db.list(nil) do |x|
101
+ expect(x).to be_an_instance_of(PerfectSched::ScheduleWithMetadata)
102
+ expect(x.key).to eq('key')
103
+ end
104
+ end
105
+ end
106
+
107
+ context '#add' do
108
+ it 'adds schedules' do
109
+ db.add('key', 'test', '* * * * *', 0, 'Asia/Tokyo', {}, now, now, {})
110
+ expect{db.add('key', 'test', '* * * * *', 0, 'Asia/Tokyo', {}, now, now, {})}.to raise_error(IdempotentAlreadyExistsError)
111
+ end
39
112
  end
40
- end
41
113
 
114
+ context '#delete' do
115
+ before do
116
+ db.add('key', 'test', '* * * * *', 0, 'Asia/Tokyo', {}, now, now, {})
117
+ end
118
+ it 'deletes schedules' do
119
+ db.delete('key', nil)
120
+ expect{db.delete('key', nil)}.to raise_error(IdempotentNotFoundError)
121
+ end
122
+ end
123
+
124
+ context '#modify' do
125
+ before do
126
+ db.add('key', 'test', '* * * * *', 0, 'Asia/Tokyo', {}, now, now, {})
127
+ end
128
+ it 'returns nil if no keys' do
129
+ expect(db.modify('key', {})).to be_nil
130
+ end
131
+ it 'modifies schedules' do
132
+ db.modify('key', {delay: 1})
133
+ end
134
+ it 'raises if nonexistent' do
135
+ expect{db.modify('nonexistent', {delay: 0})}.to raise_error(NotFoundError)
136
+ end
137
+ end
138
+
139
+ context '#acquire' do
140
+ context 'no tasks' do
141
+ it 'returns nil' do
142
+ expect(db.acquire(0, nil, {})).to be_nil
143
+ end
144
+ end
145
+ context 'some tasks' do
146
+ before do
147
+ db.add('key1', 'test', '* * * * *', 0, 'Asia/Tokyo', {}, now, now, {})
148
+ end
149
+ it 'returns a task' do
150
+ ary = db.acquire(0, nil, {})
151
+ expect(ary).to be_an_instance_of(Array)
152
+ expect(ary[0]).to be_an_instance_of(Task)
153
+ end
154
+ end
155
+ context 'some tasks but conflict with another process' do
156
+ before do
157
+ db.add('key1', 'test', '* * * * *', 0, 'Asia/Tokyo', {}, now, now, {})
158
+ db.add('key2', 'test', '* * * * *', 0, 'Asia/Tokyo', {}, now, now, {})
159
+ db.add('key3', 'test', '* * * * *', 0, 'Asia/Tokyo', {}, now, now, {})
160
+ end
161
+ it 'returns nil' do
162
+ data_set = double('data_set', update: 0)
163
+ allow(db.db).to receive(:[]).and_return(data_set)
164
+ expect(db.acquire(0, nil, {})).to be_nil
165
+ end
166
+ end
167
+ end
168
+
169
+ context '#heartbeat' do
170
+ let (:next_time){ now }
171
+ let (:task_token){ Backend::RDBCompatBackend::Token.new('key', next_time, '* * * * *', 0, 'Asia/Tokyo') }
172
+ context 'have a scheduled task' do
173
+ before do
174
+ db.add('key', 'test', '* * * * *', 0, 'Asia/Tokyo', {}, next_time, next_time, {})
175
+ end
176
+ it 'returns nil if next_run_time is not updated' do
177
+ expect(db.heartbeat(task_token, 0, {now: next_time})).to be_nil
178
+ end
179
+ it 'returns nil even if next_run_time is updated' do
180
+ expect(db.heartbeat(task_token, 1, {})).to be_nil
181
+ end
182
+ end
183
+ context 'no tasks' do
184
+ it 'raises PreemptedError' do
185
+ expect{db.heartbeat(task_token, 0, {})}.to raise_error(PreemptedError)
186
+ end
187
+ end
188
+ end
189
+
190
+ context '#finish' do
191
+ let (:next_time){ now }
192
+ let (:task_token){ Backend::RDBCompatBackend::Token.new('key', next_time, '* * * * *', 0, 'Asia/Tokyo') }
193
+ context 'have the task' do
194
+ before do
195
+ db.add('key', 'test', '* * * * *', 0, 'Asia/Tokyo', {}, next_time, next_time, {})
196
+ end
197
+ it 'returns nil' do
198
+ expect(db.finish(task_token, nil)).to be_nil
199
+ end
200
+ end
201
+ context 'already finished' do
202
+ it 'raises IdempotentAlreadyFinishedError' do
203
+ expect{db.finish(task_token, nil)}.to raise_error(IdempotentAlreadyFinishedError)
204
+ end
205
+ end
206
+ end
207
+
208
+ context '#connect' do
209
+ context 'normal' do
210
+ let (:ret){ double('ret') }
211
+ it 'returns block result' do
212
+ expect(db.__send__(:connect){ ret }).to eq(ret)
213
+ end
214
+ end
215
+ context 'error' do
216
+ it 'returns block result' do
217
+ expect(RuntimeError).to receive(:new).exactly(Backend::RDBCompatBackend::MAX_RETRY).and_call_original
218
+ allow(STDERR).to receive(:puts)
219
+ allow(db).to receive(:sleep)
220
+ expect do
221
+ db.__send__(:connect) do
222
+ raise RuntimeError.new('try restarting transaction')
223
+ end
224
+ end.to raise_error(RuntimeError)
225
+ end
226
+ end
227
+ end
228
+
229
+ context '#create_attributes' do
230
+ let (:data){ Hash.new }
231
+ let (:row) do
232
+ r = double('row')
233
+ allow(r).to receive(:[]){|k| data[k] }
234
+ r
235
+ end
236
+ it 'returns a hash consisting the data of the row' do
237
+ data[:timezone] = timezone = double('timezone')
238
+ data[:delay] = delay = double('delay')
239
+ data[:cron] = cron = double('cron')
240
+ data[:next_time] = next_time = double('next_time')
241
+ data[:timeout] = timeout = double('timeout')
242
+ data[:data] = '{"type":"foo.bar","a":"b"}'
243
+ data[:id] = 'hoge'
244
+ expect(db.__send__(:create_attributes, row)).to eq(
245
+ timezone: timezone,
246
+ delay: delay,
247
+ cron: cron,
248
+ data: {"a"=>"b"},
249
+ next_time: next_time,
250
+ next_run_time: timeout,
251
+ type: 'foo.bar',
252
+ message: nil,
253
+ node: nil,
254
+ )
255
+ end
256
+ it 'returns {} if data\'s JSON is broken' do
257
+ data[:data] = '}{'
258
+ data[:id] = 'foo.bar.baz'
259
+ expect(db.__send__(:create_attributes, row)).to eq(
260
+ timezone: 'UTC',
261
+ delay: 0,
262
+ cron: nil,
263
+ data: {},
264
+ next_time: nil,
265
+ next_run_time: nil,
266
+ type: 'foo',
267
+ message: nil,
268
+ node: nil,
269
+ )
270
+ end
271
+ it 'uses id[/\A[^.]*/] if type is empty string' do
272
+ data[:data] = '{"type":""}'
273
+ data[:id] = 'foo.bar.baz'
274
+ expect(db.__send__(:create_attributes, row)).to eq(
275
+ timezone: 'UTC',
276
+ delay: 0,
277
+ cron: nil,
278
+ data: {},
279
+ next_time: nil,
280
+ next_run_time: nil,
281
+ type: 'foo',
282
+ message: nil,
283
+ node: nil,
284
+ )
285
+ end
286
+ it 'uses id[/\A[^.]*/] if type is nil' do
287
+ data[:id] = 'foo.bar.baz'
288
+ expect(db.__send__(:create_attributes, row)).to eq(
289
+ timezone: 'UTC',
290
+ delay: 0,
291
+ cron: nil,
292
+ data: {},
293
+ next_time: nil,
294
+ next_run_time: nil,
295
+ type: 'foo',
296
+ message: nil,
297
+ node: nil,
298
+ )
299
+ end
300
+ end
301
+ end