redis-scheduler 0.3 → 0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README +22 -14
- data/lib/redis-scheduler.rb +104 -50
- metadata +2 -2
data/README
CHANGED
@@ -1,20 +1,28 @@
|
|
1
|
-
|
2
|
-
items to be processed at arbitrary points in time (via #schedule!)
|
3
|
-
|
1
|
+
RedisScheduler is a chronological scheduler for Redis. It allows you to schedule
|
2
|
+
items to be processed at arbitrary points in time (via RedisScheduler#schedule!)
|
3
|
+
and easily retrieve only those items that are due to be processed (via
|
4
|
+
RedisScheduler#each).
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
It does everything you'd want from a production scheduler:
|
6
|
+
It does everything you'd expect from a production scheduler:
|
8
7
|
* You can schedule items at arbitrary times.
|
9
|
-
* It supports multiple simultaneous readers and writers.
|
10
|
-
* An exception causes the in-process item to be rescheduled at the original time.
|
11
|
-
* A crash leaves the item in a separate error queue, from which it can later be recovered.
|
12
8
|
* You can iterate over ready items in either blocking or non-blocking mode.
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
9
|
+
* It supports multiple simultaneous readers and writers.
|
10
|
+
* A Ruby exception causes the item to be rescheduled at the original time.
|
11
|
+
* Work items lost as part of a Ruby crash or segfault are recoverable.
|
12
|
+
|
13
|
+
In non-blocking mode (the default), RedisScheduler#each will iterate only over
|
14
|
+
those work items whose scheduled time is less than or equal to the current time,
|
15
|
+
and then stop. In blocking mode, RedisScheduler#each will iterate over the same
|
16
|
+
items, but will also block until items are available. In this mode, #each will
|
17
|
+
never return.
|
18
|
+
|
19
|
+
For debugging purposes, you can use RedisScheduler#items to iterate over all
|
20
|
+
items in the queue, but note that this method is not guaranteed to be
|
21
|
+
consistent.
|
22
|
+
|
23
|
+
For error recovery purposes, you can use RedisScheduler#processing_set_items
|
24
|
+
to iterate over all the items in the processing set to determine whether any
|
25
|
+
of them are the result of a process crash.
|
18
26
|
|
19
27
|
== Synopsis
|
20
28
|
|
data/lib/redis-scheduler.rb
CHANGED
@@ -1,105 +1,154 @@
|
|
1
|
+
## A basic chronological scheduler for Redis.
|
2
|
+
##
|
3
|
+
## Use #schedule! to add an item to be processed at an arbitrary point in time.
|
4
|
+
## The item will be converted to a string and later returned to you as such.
|
5
|
+
##
|
6
|
+
## Use #each to iterate over those items in the schedule which are ready for
|
7
|
+
## processing. In blocking mode, this call will never terminate. In nonblocking
|
8
|
+
## mode, this call will terminate when there are no items ready for processing.
|
9
|
+
##
|
10
|
+
## Use #items to iterate over all items in the queue, for debugging purposes.
|
11
|
+
##
|
12
|
+
## == Ensuring reliable behavior in the presence of segfaults
|
13
|
+
##
|
14
|
+
## The scheduler maintains a "processing set" of items currently being
|
15
|
+
## processed. If a process dies (i.e. not as a result of a Ruby exception, but
|
16
|
+
## as the result of a segfault), the item will remain in this set but will
|
17
|
+
## not longer appear in the schedule. To avoid losing scheduled work due to
|
18
|
+
## segfaults, you must periodically iterate through this set and recover
|
19
|
+
## any items that have been abandoned, using #processing_set_items. Setting a
|
20
|
+
## proper 'descriptor' argument in #each is suggested.
|
1
21
|
class RedisScheduler
|
2
22
|
include Enumerable
|
3
23
|
|
4
24
|
POLL_DELAY = 1.0 # seconds
|
5
25
|
CAS_DELAY = 0.5 # seconds
|
6
26
|
|
7
|
-
##
|
8
|
-
## * +namespace+: prefix for
|
27
|
+
## Options:
|
28
|
+
## * +namespace+: prefix for Redis keys, e.g. "scheduler/"
|
9
29
|
## * +blocking+: whether #each should block or return immediately if there are items to be processed immediately.
|
10
30
|
##
|
11
|
-
## Note that nonblocking mode may still actually block as part of
|
12
|
-
## check-and-set semantics, i.e. block during contention from multiple
|
13
|
-
## clients. "Nonblocking"
|
14
|
-
##
|
15
|
-
##
|
31
|
+
## Note that nonblocking mode may still actually block momentarily as part of
|
32
|
+
## the check-and-set semantics, i.e. block during contention from multiple
|
33
|
+
## clients. "Nonblocking" refers to whether the scheduler should wait until
|
34
|
+
## events in the schedule are ready, or only return those items that are
|
35
|
+
## ready currently.
|
16
36
|
def initialize redis, opts={}
|
17
37
|
@redis = redis
|
18
38
|
@namespace = opts[:namespace]
|
19
39
|
@blocking = opts[:blocking]
|
20
40
|
|
21
41
|
@queue = [@namespace, "q"].join
|
22
|
-
@
|
42
|
+
@processing_set = [@namespace, "processing"].join
|
23
43
|
@counter = [@namespace, "counter"].join
|
24
44
|
end
|
25
45
|
|
26
|
-
##
|
27
|
-
## string.
|
46
|
+
## Schedule an item at a specific time. item will be converted to a string.
|
28
47
|
def schedule! item, time
|
29
48
|
id = @redis.incr @counter
|
30
49
|
@redis.zadd @queue, time.to_f, "#{id}:#{item}"
|
31
50
|
end
|
32
51
|
|
52
|
+
## Drop all data and reset the schedule entirely.
|
33
53
|
def reset!
|
34
|
-
[@queue, @
|
54
|
+
[@queue, @processing_set, @counter].each { |k| @redis.del k }
|
35
55
|
end
|
36
56
|
|
57
|
+
## Return the total number of items in the schedule.
|
37
58
|
def size; @redis.zcard @queue end
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
##
|
43
|
-
##
|
44
|
-
##
|
45
|
-
|
46
|
-
|
47
|
-
|
59
|
+
|
60
|
+
## Returns the total number of items currently being processed.
|
61
|
+
def processing_set_size; @redis.scard @processing_set end
|
62
|
+
|
63
|
+
## Yields items along with their scheduled times. only returns items on or
|
64
|
+
## after their scheduled times. items are returned as strings. if @blocking is
|
65
|
+
## false, will stop once there are no more items that can be processed
|
66
|
+
## immediately; if it's true, will wait until items become available (and
|
67
|
+
## never terminate).
|
68
|
+
##
|
69
|
+
## +Descriptor+ is an optional string that will be associated with this item
|
70
|
+
## while in the processing set. This is useful for providing whatever
|
71
|
+
## information you need to determine whether the item needs to be recovered
|
72
|
+
## when iterating through the processing set.
|
73
|
+
def each descriptor=nil
|
74
|
+
while(x = get(descriptor))
|
75
|
+
item, processing_descriptor, at = x
|
48
76
|
begin
|
49
77
|
yield item, at
|
50
78
|
rescue Exception # back in the hole!
|
51
79
|
schedule! item, at
|
52
80
|
raise
|
53
81
|
ensure
|
54
|
-
cleanup!
|
82
|
+
cleanup! processing_descriptor
|
55
83
|
end
|
56
84
|
end
|
57
85
|
end
|
58
86
|
|
59
|
-
##
|
60
|
-
##
|
61
|
-
## latest-scheduled
|
62
|
-
##
|
63
|
-
##
|
87
|
+
## Returns an Enumerable of [item, scheduled time] pairs, which can be used
|
88
|
+
## to iterate over all the items in the queue, in order of earliest- to
|
89
|
+
## latest-scheduled, regardless of the schedule time.
|
90
|
+
##
|
91
|
+
## Note that this view is not synchronized with write operations, and thus
|
92
|
+
## may be inconsistent (e.g. return duplicates, miss items, etc) if changes
|
93
|
+
## to the schedule happen while iterating.
|
64
94
|
##
|
65
|
-
##
|
95
|
+
## For these reasons, this is mainly useful for debugging purposes.
|
66
96
|
def items; ItemEnumerator.new(@redis, @queue) end
|
67
97
|
|
98
|
+
## Returns an Array of [item, timestamp, descriptor] tuples representing the
|
99
|
+
## set of in-process items. The timestamp corresponds to the time at which
|
100
|
+
## the item was removed from the schedule for processing.
|
101
|
+
def processing_set_items
|
102
|
+
@redis.smembers(@processing_set).map do |x|
|
103
|
+
item, timestamp, descriptor = Marshal.load(x)
|
104
|
+
[item, Time.at(timestamp), descriptor]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
68
108
|
private
|
69
109
|
|
70
|
-
def get; @blocking ? blocking_get : nonblocking_get end
|
110
|
+
def get descriptor; @blocking ? blocking_get(descriptor) : nonblocking_get(descriptor) end
|
71
111
|
|
72
|
-
def blocking_get
|
73
|
-
sleep POLL_DELAY until(x = nonblocking_get)
|
112
|
+
def blocking_get descriptor
|
113
|
+
sleep POLL_DELAY until(x = nonblocking_get(descriptor))
|
74
114
|
x
|
75
115
|
end
|
76
116
|
|
117
|
+
## Thrown by some RedisScheduler operations if the item in Redis zset
|
118
|
+
## underlying the schedule is not parseable. This should basically never
|
119
|
+
## happen, unless you are naughty and are adding/removing items from that
|
120
|
+
## zset yourself.
|
77
121
|
class InvalidEntryException < StandardError; end
|
78
|
-
def nonblocking_get
|
122
|
+
def nonblocking_get descriptor
|
79
123
|
catch :cas_retry do
|
80
124
|
@redis.watch @queue
|
81
|
-
|
82
|
-
|
83
|
-
|
125
|
+
entry, at = @redis.zrangebyscore @queue, 0, Time.now.to_f, :withscores => true, :limit => [0, 1]
|
126
|
+
if entry
|
127
|
+
entry =~ /^\d+:(\S+)$/ or raise InvalidEntryException, entry
|
128
|
+
item = $1
|
129
|
+
processing_descriptor = Marshal.dump [item, Time.now.to_i, descriptor]
|
84
130
|
@redis.multi do # try and grab it
|
85
|
-
@redis.zrem @queue,
|
86
|
-
@redis.
|
131
|
+
@redis.zrem @queue, entry
|
132
|
+
@redis.sadd @processing_set, processing_descriptor
|
87
133
|
end or begin
|
88
134
|
sleep CAS_DELAY
|
89
135
|
throw :cas_retry
|
90
136
|
end
|
91
|
-
item
|
92
|
-
original = $1
|
93
|
-
[original, item, Time.at(at.to_f)]
|
137
|
+
[item, processing_descriptor, Time.at(at.to_f)]
|
94
138
|
end
|
95
139
|
end
|
96
140
|
end
|
97
141
|
|
98
142
|
def cleanup! item
|
99
|
-
@redis.
|
143
|
+
@redis.srem @processing_set, item
|
100
144
|
end
|
101
145
|
|
102
|
-
##
|
146
|
+
## Enumerable class for iterating over everything in the schedule. Paginates
|
147
|
+
## calls to Redis under the hood (and is thus usable for very large
|
148
|
+
## schedules), but is not synchronized with write traffic and thus may return
|
149
|
+
## duplicates or skip items when paginating.
|
150
|
+
##
|
151
|
+
## Supports random access with #[], with the same caveats as above.
|
103
152
|
class ItemEnumerator
|
104
153
|
include Enumerable
|
105
154
|
def initialize redis, q
|
@@ -107,21 +156,26 @@ private
|
|
107
156
|
@q = q
|
108
157
|
end
|
109
158
|
|
110
|
-
|
159
|
+
PAGE_SIZE = 50
|
111
160
|
def each
|
112
161
|
start = 0
|
113
162
|
while start < size
|
114
|
-
elements =
|
115
|
-
|
116
|
-
elements.each_slice(2) do |item, at| # isgh
|
117
|
-
item =~ /^\d+:(\S+)$/ or raise InvalidEntryException, item
|
118
|
-
item = $1
|
119
|
-
yield item, Time.at(at.to_f)
|
120
|
-
end
|
163
|
+
elements = self[start, PAGE_SIZE]
|
164
|
+
elements.each { |*x| yield(*x) }
|
121
165
|
start += elements.size
|
122
166
|
end
|
123
167
|
end
|
124
168
|
|
169
|
+
def [] start, num=nil
|
170
|
+
elements = @redis.zrange @q, start, start + (num || 0) - 1, :withscores => true
|
171
|
+
v = elements.each_slice(2).map do |item, at|
|
172
|
+
item =~ /^\d+:(\S+)$/ or raise InvalidEntryException, item
|
173
|
+
item = $1
|
174
|
+
[item, Time.at(at.to_f)]
|
175
|
+
end
|
176
|
+
num ? v : v.first
|
177
|
+
end
|
178
|
+
|
125
179
|
def size; @redis.zcard @q end
|
126
180
|
end
|
127
181
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-scheduler
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.4'
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-05-12 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: A basic chronological scheduler for Redis. Add work items to be processed
|
15
15
|
at specific times in the future, and easily retrieve all items that are ready for
|