continuity 0.0.1
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.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +62 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/continuity.gemspec +71 -0
- data/examples/worker.rb +17 -0
- data/lib/continuity.rb +5 -0
- data/lib/continuity/cron_entry.rb +91 -0
- data/lib/continuity/periodic_entry.rb +22 -0
- data/lib/continuity/redis_backend.rb +79 -0
- data/lib/continuity/scheduler.rb +78 -0
- data/test/helper.rb +34 -0
- data/test/redis.conf +421 -0
- data/test/test_cron_entry.rb +191 -0
- data/test/test_periodic_entry.rb +109 -0
- data/test/test_race_issues.rb +62 -0
- data/test/test_redis_backend.rb +146 -0
- data/test/test_scheduler.rb +55 -0
- metadata +136 -0
@@ -0,0 +1,191 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'continuity'
|
4
|
+
|
5
|
+
include Continuity
|
6
|
+
|
7
|
+
describe CronEntry do
|
8
|
+
describe "0 * * * * * (every minute)" do
|
9
|
+
before do
|
10
|
+
@ce_short = CronEntry.new("* * * * *")
|
11
|
+
@ce = CronEntry.new("0 * * * * *")
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should run at 2010-12-20 00:00:00" do
|
15
|
+
@ce_short.at?(Time.parse("2010-12-20 00:00:00")).must_equal true
|
16
|
+
@ce.at?(Time.parse("2010-12-20 00:00:00")).must_equal true
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should not run at 2010-12-20 00:00:01" do
|
20
|
+
@ce_short.at?(Time.parse("2010-12-20 00:00:01")).must_equal false
|
21
|
+
@ce.at?(Time.parse("2010-12-20 00:00:01")).must_equal false
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should run 60 times in a one hour period" do
|
25
|
+
count = 0
|
26
|
+
start = Time.parse("2010-12-20 00:00:00").to_i
|
27
|
+
start.upto(start + 3599) do |t|
|
28
|
+
count += 1 if @ce_short.at?(Time.at(t))
|
29
|
+
count += 1 if @ce.at?(Time.at(t))
|
30
|
+
end
|
31
|
+
count.must_equal 120
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "0 1 * * * * (every hour)" do
|
36
|
+
before do
|
37
|
+
@ce = CronEntry.new("0 1 * * * *")
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should run at 2010-12-20 01:01:00" do
|
41
|
+
@ce.at?(Time.parse("2010-12-20 01:01:00")).must_equal true
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should not run at 2010-12-20 00:01:01" do
|
45
|
+
@ce.at?(Time.parse("2010-12-20 00:01:01")).must_equal false
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should run 24 times in a one day period" do
|
49
|
+
count = 0
|
50
|
+
start = Time.parse("2010-12-20 00:01:00").to_i
|
51
|
+
start.upto(start + 86399) do |t|
|
52
|
+
count += 1 if @ce.at?(Time.at(t))
|
53
|
+
end
|
54
|
+
count.must_equal 24
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "0 0 0 1 * * (first of the month)" do
|
59
|
+
before do
|
60
|
+
@ce = CronEntry.new("0 0 0 1 * *")
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should run at 2010-12-01 00:00:00" do
|
64
|
+
@ce.at?(Time.parse("2010-12-01 00:00:00")).must_equal true
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should run at 2010-08-01 00:00:00" do
|
68
|
+
@ce.at?(Time.parse("2010-08-01 00:00:00")).must_equal true
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should not run at 2010-12-02 00:00:00" do
|
72
|
+
@ce.at?(Time.parse("2010-12-02 00:00:00")).must_equal false
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should not run at 2010-12-01 01:00:00" do
|
76
|
+
@ce.at?(Time.parse("2010-12-01 01:00:00")).must_equal false
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should run 12 times in a year" do
|
80
|
+
count = 0
|
81
|
+
start = Time.parse("2010-01-01 00:00:00").to_i
|
82
|
+
year_end = Time.parse("2010-12-31 23:59:59").to_i
|
83
|
+
start.step(year_end, 3600) do |t|
|
84
|
+
if @ce.at?(Time.at(t))
|
85
|
+
count += 1
|
86
|
+
end
|
87
|
+
end
|
88
|
+
count.must_equal 12
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "0 0 0 1 1,4,7,10 * (quarterly months)" do
|
93
|
+
before do
|
94
|
+
@ce = CronEntry.new("0 0 0 1 1,4,7,10 *")
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should run at 2010-04-01 00:00:00" do
|
98
|
+
@ce.at?(Time.parse("2010-04-01 00:00:00")).must_equal true
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should not run at 2010-03-01 00:00:00" do
|
102
|
+
@ce.at?(Time.parse("2010-03-01 00:00:00")).must_equal false
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should run 4 times in a year" do
|
106
|
+
count = 0
|
107
|
+
start = Time.parse("2010-01-01 00:00:00").to_i
|
108
|
+
year_end = Time.parse("2010-12-31 23:59:59").to_i
|
109
|
+
start.step(year_end, 3600) do |t|
|
110
|
+
if @ce.at?(Time.at(t))
|
111
|
+
count += 1
|
112
|
+
end
|
113
|
+
end
|
114
|
+
count.must_equal 4
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "0 0 7 * * 0 (sunday @ 7am)" do
|
119
|
+
before do
|
120
|
+
@ce = CronEntry.new("0 0 7 * * 0")
|
121
|
+
end
|
122
|
+
|
123
|
+
it "should run at 2010-12-19 07:00:00" do
|
124
|
+
@ce.at?(Time.parse("2010-12-19 07:00:00")).must_equal true
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should run at 2010-12-26 07:00:00" do
|
128
|
+
@ce.at?(Time.parse("2010-12-26 07:00:00")).must_equal true
|
129
|
+
end
|
130
|
+
|
131
|
+
it "should not run at 2010-12-27 07:00:00" do
|
132
|
+
@ce.at?(Time.parse("2010-12-27 07:00:00")).must_equal false
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should not run at 2010-12-26 06:00:00" do
|
136
|
+
@ce.at?(Time.parse("2010-12-26 06:00:00")).must_equal false
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
describe "ranges w/intervals" do
|
141
|
+
before do
|
142
|
+
# 10 * 20 * 3 = 600 times in a day
|
143
|
+
@ce = CronEntry.new("0-9/2,20-39/4 40-59/1 2,4,8 * * *")
|
144
|
+
end
|
145
|
+
|
146
|
+
it "should run at 2010-12-20 04:40:06" do
|
147
|
+
@ce.at?(Time.parse("2010-12-20 04:40:06")).must_equal true
|
148
|
+
end
|
149
|
+
|
150
|
+
it "should run at 2010-12-20 02:50:24" do
|
151
|
+
@ce.at?(Time.parse("2010-12-20 02:50:24")).must_equal true
|
152
|
+
end
|
153
|
+
|
154
|
+
it "should not run at 2010-12-20 03:50:22" do
|
155
|
+
@ce.at?(Time.parse("2010-12-20 03:50:22")).must_equal false
|
156
|
+
end
|
157
|
+
|
158
|
+
it "should run 600 times in a day" do
|
159
|
+
count = 0
|
160
|
+
start = Time.parse("2010-12-20 02:40:09").to_i
|
161
|
+
start.upto(start + 86399) do |t|
|
162
|
+
if @ce.at?(Time.at(t))
|
163
|
+
count += 1
|
164
|
+
end
|
165
|
+
end
|
166
|
+
count.must_equal 600
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
describe "invalid intervals" do
|
171
|
+
it "should raise CronFormatError on '1 2 3'" do
|
172
|
+
lambda { CronEntry.new("1 2 3") }.must_raise CronFormatError
|
173
|
+
end
|
174
|
+
|
175
|
+
it "should raise CronFormatError on '1 2 3 4 5 6 7'" do
|
176
|
+
lambda { CronEntry.new("1 2 3 4 5 6 7") }.must_raise CronFormatError
|
177
|
+
end
|
178
|
+
|
179
|
+
it "should raise CronFormatError on 'im not a cron task'" do
|
180
|
+
lambda { CronEntry.new("im not a cron task") }.must_raise CronFormatError
|
181
|
+
end
|
182
|
+
|
183
|
+
it "should raise CronFormatError on 'SrslyFake'" do
|
184
|
+
lambda { CronEntry.new("SrslyFake") }.must_raise CronFormatError
|
185
|
+
end
|
186
|
+
|
187
|
+
it "should raise CronFormatError on '0 0 0 0 0 0'" do
|
188
|
+
lambda { CronEntry.new("0 0 0 0 0 0") }.must_raise CronFormatError
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'continuity'
|
4
|
+
|
5
|
+
include Continuity
|
6
|
+
|
7
|
+
describe PeriodicEntry do
|
8
|
+
describe "10s" do
|
9
|
+
before do
|
10
|
+
@pe = PeriodicEntry.new("10s")
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should trigger at Time.at(10)" do
|
14
|
+
assert @pe.at?(Time.at(10))
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should trigger 6 times in a minute" do
|
18
|
+
times = 0
|
19
|
+
|
20
|
+
time = Time.parse("2010-12-21 00:01:00").to_i
|
21
|
+
time.upto(time + 59) do |n|
|
22
|
+
times += 1 if @pe.at?(Time.at(n))
|
23
|
+
end
|
24
|
+
|
25
|
+
times.must_equal 6
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "1m" do
|
30
|
+
before do
|
31
|
+
@pe = PeriodicEntry.new("1m")
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should trigger on the minute" do
|
35
|
+
time = Time.parse("2010-12-21 00:01:00").to_i
|
36
|
+
|
37
|
+
assert @pe.at?(time)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should trigger 60 times in a hour" do
|
41
|
+
times = 0
|
42
|
+
|
43
|
+
time = Time.parse("2010-12-21 00:00:00").to_i
|
44
|
+
time.upto(time + 3599) do |n|
|
45
|
+
times += 1 if @pe.at?(Time.at(n))
|
46
|
+
end
|
47
|
+
|
48
|
+
times.must_equal 60
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "2h" do
|
53
|
+
before do
|
54
|
+
@pe = PeriodicEntry.new("2h")
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should trigger at 2am" do
|
58
|
+
time = Time.parse("2010-12-21 02:00:00").to_i
|
59
|
+
|
60
|
+
assert @pe.at?(time)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should trigger 12 times in a 24hour period" do
|
64
|
+
times = 0
|
65
|
+
|
66
|
+
time = Time.parse("2010-12-21 00:00:00").to_i
|
67
|
+
time.upto(time + 86399) do |n|
|
68
|
+
times += 1 if @pe.at?(Time.at(n))
|
69
|
+
end
|
70
|
+
|
71
|
+
times.must_equal 12
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "2d" do
|
76
|
+
before do
|
77
|
+
@pe = PeriodicEntry.new("2d")
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should trigger 5 times in a 10day period" do
|
81
|
+
times = 0
|
82
|
+
|
83
|
+
time = Time.parse("2010-12-21 00:00:00").to_i
|
84
|
+
time.upto(time + (86400 * 10) - 1) do |n|
|
85
|
+
times += 1 if @pe.at?(Time.at(n))
|
86
|
+
end
|
87
|
+
|
88
|
+
times.must_equal 5
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "1w" do
|
93
|
+
before do
|
94
|
+
@pe = PeriodicEntry.new("1w")
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should trigger 4 times in 28 days" do
|
98
|
+
times = 0
|
99
|
+
|
100
|
+
time = Time.parse("2010-12-01 00:00:00").to_i
|
101
|
+
time_end = Time.parse("2010-12-27 23:59:59").to_i
|
102
|
+
time.upto(time_end) do |n|
|
103
|
+
times += 1 if @pe.at?(Time.at(n))
|
104
|
+
end
|
105
|
+
|
106
|
+
times.must_equal 4
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'continuity'
|
4
|
+
|
5
|
+
include Continuity
|
6
|
+
|
7
|
+
def build_scheduler
|
8
|
+
redis = Redis.new(:port => 16379)
|
9
|
+
scheduler = Continuity::Scheduler.new_using_redis(redis)
|
10
|
+
scheduler.cron("*/10 * * * * *") do
|
11
|
+
end
|
12
|
+
|
13
|
+
scheduler.every("1m") do
|
14
|
+
end
|
15
|
+
|
16
|
+
scheduler
|
17
|
+
end
|
18
|
+
|
19
|
+
WORKER_COUNT = 100
|
20
|
+
MUTEX = Mutex.new
|
21
|
+
TEST_LENGTH = 60
|
22
|
+
|
23
|
+
describe "simulation" do
|
24
|
+
before do
|
25
|
+
redis = Redis.new(:port => 16379)
|
26
|
+
redis.flushall
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should schedule a continuous range" do
|
30
|
+
@workers = []
|
31
|
+
last_scheduled = nil
|
32
|
+
|
33
|
+
WORKER_COUNT.times do
|
34
|
+
|
35
|
+
@workers << Thread.new do
|
36
|
+
s = build_scheduler
|
37
|
+
|
38
|
+
loop do
|
39
|
+
sleep rand(5)
|
40
|
+
range = s.maybe_schedule
|
41
|
+
|
42
|
+
if range
|
43
|
+
MUTEX.synchronize do
|
44
|
+
if last_scheduled.nil?
|
45
|
+
last_scheduled = range.last
|
46
|
+
else
|
47
|
+
assert_equal (last_scheduled + 1), range.first
|
48
|
+
last_scheduled = range.last
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
sleep TEST_LENGTH
|
59
|
+
@workers.each { |w| w.terminate }
|
60
|
+
assert last_scheduled > Time.now.to_i - 20
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'continuity'
|
4
|
+
|
5
|
+
describe Continuity::RedisBackend do
|
6
|
+
before do
|
7
|
+
@rb = Continuity::RedisBackend.new(redis_clean, 10, 30)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "bootstrapping" do
|
11
|
+
it "should set current time as the last scheduled at time" do
|
12
|
+
now = Time.now.to_i
|
13
|
+
@rb.lock_for_scheduling(now) {}
|
14
|
+
last_scheduled_at = @rb.lock_for_scheduling(now+1) {}
|
15
|
+
last_scheduled_at.must_equal now
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should yield" do
|
19
|
+
yielded = false
|
20
|
+
|
21
|
+
now = Time.now.to_i
|
22
|
+
@rb.lock_for_scheduling(now) { yielded = true }
|
23
|
+
|
24
|
+
yielded.must_equal true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "trying to lock before scheduling period is over" do
|
29
|
+
before do
|
30
|
+
now = Time.now.to_i
|
31
|
+
@rb.lock_for_scheduling(now) {}
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should not yield" do
|
35
|
+
now = Time.now.to_i
|
36
|
+
@rb.lock_for_scheduling(now) {}
|
37
|
+
|
38
|
+
yielded = false
|
39
|
+
|
40
|
+
@rb.lock_for_scheduling(now+5) { yielded = true }
|
41
|
+
|
42
|
+
yielded.must_equal false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "trying to lock while another client holds to lock" do
|
47
|
+
it "should not yield" do
|
48
|
+
first_yields = false
|
49
|
+
second_yields = false
|
50
|
+
now = Time.now.to_i
|
51
|
+
@rb.lock_for_scheduling(now) {}
|
52
|
+
|
53
|
+
@rb.lock_for_scheduling(now+30) do
|
54
|
+
first_yields = true
|
55
|
+
20.times do
|
56
|
+
@rb.lock_for_scheduling(now+35) do
|
57
|
+
second_yields = true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
first_yields.must_equal true
|
63
|
+
second_yields.must_equal false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "throwing exception in the block given to lock" do
|
68
|
+
it "should release lock" do
|
69
|
+
now = Time.now.to_i
|
70
|
+
@rb.lock_for_scheduling(now) {}
|
71
|
+
|
72
|
+
begin
|
73
|
+
@rb.lock_for_scheduling(now+30) do
|
74
|
+
raise StandardError
|
75
|
+
end
|
76
|
+
rescue
|
77
|
+
end
|
78
|
+
|
79
|
+
acquired_lock = false
|
80
|
+
@rb.lock_for_scheduling(now+30) do
|
81
|
+
acquired_lock = true
|
82
|
+
end
|
83
|
+
|
84
|
+
acquired_lock.must_equal true
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe "expired lock" do
|
89
|
+
it "should be acquireable" do
|
90
|
+
now = Time.now.to_i
|
91
|
+
@rb.lock_for_scheduling(now) {}
|
92
|
+
|
93
|
+
acquired_lock_early = false
|
94
|
+
acquired_lock = false
|
95
|
+
|
96
|
+
@rb.lock_for_scheduling(now+30) do
|
97
|
+
@rb.lock_for_scheduling(now+22) do
|
98
|
+
acquired_lock_early = true
|
99
|
+
end
|
100
|
+
@rb.lock_for_scheduling(now+62) do
|
101
|
+
acquired_lock = true
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
acquired_lock.must_equal true
|
106
|
+
acquired_lock_early.must_equal false
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe "locking after waiting period is up" do
|
111
|
+
it "should yield" do
|
112
|
+
now = Time.now.to_i
|
113
|
+
@rb.lock_for_scheduling(now) {}
|
114
|
+
|
115
|
+
yielded = false
|
116
|
+
@rb.lock_for_scheduling(now+15) { yielded = true }
|
117
|
+
yielded.must_equal true
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should yield last scheduled time" do
|
121
|
+
now = Time.now.to_i
|
122
|
+
@rb.lock_for_scheduling(now) {}
|
123
|
+
|
124
|
+
yielded = false
|
125
|
+
@rb.lock_for_scheduling(now+11) { |t|
|
126
|
+
t.must_equal now
|
127
|
+
yielded = true
|
128
|
+
}
|
129
|
+
yielded.must_equal true
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should continue to yield as time goes on" do
|
133
|
+
yielded_count = 0
|
134
|
+
now = Time.now.to_i
|
135
|
+
@rb.lock_for_scheduling(now) {}
|
136
|
+
|
137
|
+
10.times do |n|
|
138
|
+
@rb.lock_for_scheduling(now+((n+1)*30)) do
|
139
|
+
yielded_count += 1
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
yielded_count.must_equal 10
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|