perfectsched 0.8.10 → 0.8.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -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