redis-scheduler 0.3 → 0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README +22 -14
  2. data/lib/redis-scheduler.rb +104 -50
  3. metadata +2 -2
data/README CHANGED
@@ -1,20 +1,28 @@
1
- This is a basic chronological scheduler for Redis. It allows you to schedule
2
- items to be processed at arbitrary points in time (via #schedule!), and then
3
- to easily retrieve only those items that are due to be processed (via #each).
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
- Items are represented as strings.
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
- In non-blocking mode (the default), #each will iterate only over those work items
15
- whose scheduled time is less than or equal to the current time, and then stop.
16
- In blocking mode, #each will iterate over the same items, but will also block
17
- until items are available. In blocking mode, #each will never return.
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
 
@@ -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
- ## options:
8
- ## * +namespace+: prefix for redis data, e.g. "scheduler/"
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 the
12
- ## check-and-set semantics, i.e. block during contention from multiple
13
- ## clients. "Nonblocking" mode just refers to whether the scheduler
14
- ## should wait until events in the schedule are ready, or only return
15
- ## those items that are ready currently.
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
- @error_queue = [@namespace, "errorq"].join
42
+ @processing_set = [@namespace, "processing"].join
23
43
  @counter = [@namespace, "counter"].join
24
44
  end
25
45
 
26
- ## schedule an item at a specific time. item will be converted to a
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, @error_queue, @counter].each { |k| @redis.del k }
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
- def error_queue_size; @redis.llen @error_queue end
39
-
40
- ## yields items along with their scheduled times. only returns items
41
- ## on or after their scheduled times. items returned as strings. if
42
- ## @blocking is false, will stop once there are no more items that can
43
- ## be processed immediately; if it's true, will wait until items
44
- ## become available (and never terminate).
45
- def each
46
- while(x = get)
47
- item, erritem, at = x
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! erritem
82
+ cleanup! processing_descriptor
55
83
  end
56
84
  end
57
85
  end
58
86
 
59
- ## returns an Enumerable of [item, schedule time] pairs, which can be used to
60
- ## easily iterate over all the items in the queue, in order of earliest- to
61
- ## latest-scheduled. note that this view is not coordinated with write
62
- ## operations, and may be inconsistent (e.g. return duplicates, miss items,
63
- ## etc).
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
- ## for these reasons, this operation is mainly useful for debugging purposes.
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
- item, at = @redis.zrangebyscore @queue, 0, Time.now.to_f,
82
- :withscores => true, :limit => [0, 1]
83
- if item
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, item
86
- @redis.lpush @error_queue, item
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 =~ /^\d+:(\S+)$/ or raise InvalidEntryException, 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.lrem @error_queue, 1, item
143
+ @redis.srem @processing_set, item
100
144
  end
101
145
 
102
- ## enumerable for just iterating over everything in the queue
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
- BLOCK_SIZE = 10
159
+ PAGE_SIZE = 50
111
160
  def each
112
161
  start = 0
113
162
  while start < size
114
- elements = @redis.zrange @q, start, start + BLOCK_SIZE,
115
- :withscores => true
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.3'
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-04-29 00:00:00.000000000 Z
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