later 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Later
2
2
 
3
- [**Later**](erol.github.com/later) is a lightweight Redis-backed event scheduling library for Ruby.
3
+ [**Later**](erol.github.com/later) is a lean Redis-backed event scheduling library for Ruby.
4
4
 
5
5
  ## Usage
6
6
 
@@ -8,29 +8,30 @@ Later allows you to set unique events on a schedule and run them in the future:
8
8
 
9
9
  require 'later'
10
10
 
11
- Later[:reservations].set 'event-1', Time.now + 60
12
- Later[:reservations].set 'event-2', Time.now + 120
13
- Later[:reservations].set 'event-3', Time.now + 180
11
+ schedule = Later[:schedule]
12
+ schedule.set 'event-1', Time.now + 60
13
+ schedule.set 'event-2', Time.now + 120
14
+ schedule.set 'event-3', Time.now + 180
14
15
 
15
16
  Rescheduling an event is simple:
16
17
 
17
- Later[:reservations].set 'event-1', Time.now + 240
18
+ schedule.set 'event-1', Time.now + 240
18
19
 
19
- And an event can also be unset:
20
+ And an event can unset with equal ease:
20
21
 
21
- Later[key].unset 'event-1'
22
+ schedule.unset 'event-1'
22
23
 
23
24
  You can manage multiple schedules using different keys:
24
25
 
25
- Later[:reservations]
26
- Later[:appointments]
26
+ reservations = Later[:reservations]
27
+ appointments = Later[:appointments]
27
28
 
28
29
  The schedules are stored on the default Redis instance. If you need a schedule which must reside on a different Redis instance, you can pass a [Nest](github.com/soveran/nest) object when referencing a schedule set.
29
30
 
30
31
  redis = Redis.new host: host, port: port
31
- key = Nest.new :reservations, redis
32
+ key = Nest.new 'Reservations', redis
32
33
 
33
- Later[key]
34
+ reservations = Later[key]
34
35
 
35
36
  ### Workers
36
37
 
@@ -38,18 +39,34 @@ Workers are Ruby processes that run forever. They allow you to process event sch
38
39
 
39
40
  require 'later'
40
41
 
41
- Later[:reservations].each do |event|
42
+ Later[:schedule].each do |event|
42
43
  # Do something with the event.
43
44
  end
44
45
 
45
46
  # This line is never reached.
46
47
 
48
+ #### Timeouts, Blocking & Polling
49
+
50
+ `Later::Schedule#each` accepts an optional `timeout` parameter, which has a default value of `1`. Passing an `Integer` will use Redis' blocking mechanism
51
+ to process the schedule and is therefore more efficient. Passing a `Float` will poll Redis using the given timeout, and should
52
+ only be used for events which need to be triggered with millisecond precision.
53
+
54
+ See [BLPOP](http://redis.io/commands/blpop) and [BRPOPLPUSH](http://redis.io/commands/brpoplpush) for more information.
55
+
56
+ The below schedule will be polled every 0.1 seconds:
57
+
58
+ Later[:schedule].each(0.1) do |event|
59
+ # Do something with the event.
60
+ end
61
+
62
+ #### Stopping
63
+
47
64
  If for some reason, a worker has to stop itself from running:
48
65
 
49
- Later[:reservations].each do |event|
66
+ Later[:schedule].each do |event|
50
67
  # Do something with the event.
51
68
 
52
- Later[:reservations].stop! if stop?
69
+ Later[:schedule].stop! if stop?
53
70
  end
54
71
 
55
72
  # This line is reached when stop? is true and Later[:reservations].stop! is called.
data/Rakefile CHANGED
@@ -5,6 +5,6 @@ require 'rake/testtask'
5
5
 
6
6
  Rake::TestTask.new do |t|
7
7
  t.libs << 'test'
8
- t.test_files = FileList['test/lib/*.rb']
8
+ t.test_files = FileList['test/specs/*.rb']
9
9
  t.verbose = true
10
10
  end
@@ -4,8 +4,8 @@ require File.expand_path('../lib/later/version', __FILE__)
4
4
  Gem::Specification.new do |gem|
5
5
  gem.authors = ['Erol Fornoles']
6
6
  gem.email = ['erol.fornoles@gmail.com']
7
- gem.description = %q{Later is a Redis-backed event scheduling library for Ruby}
8
- gem.summary = %q{Later is a Redis-backed event scheduling library for Ruby}
7
+ gem.description = %q{Lean Redis-backed event scheduling library for Ruby}
8
+ gem.summary = %q{Lean Redis-backed event scheduling library for Ruby}
9
9
  gem.homepage = 'http://erol.github.com/later'
10
10
 
11
11
  gem.files = `git ls-files`.split($\)
@@ -18,101 +18,111 @@ module Later
18
18
  end
19
19
  end
20
20
 
21
- # Get the Nest key of the schedule.
21
+ # Returns the Nest key of this schedule.
22
+ #
23
+ # Later[:reservations].key #=> Later:reservations
22
24
  #
23
- # Later[:reservations].key #=> Later::reservations
24
-
25
25
  def key
26
26
  @key
27
27
  end
28
28
 
29
- # Get the Nest key of the schedule exception list.
29
+ # Returns the Nest key of this schedule's exception list.
30
+ #
31
+ # Later[:reservations].exceptions #=> Later:reservations:exceptions
30
32
  #
31
- # Later[:reservations].exceptions #=> Later::reservations::exceptions
32
-
33
33
  def exceptions
34
34
  @exceptions ||= key[:exceptions]
35
35
  end
36
36
 
37
- # Get the schedule time of a unique event.
37
+ # Returns the time of a scheduled unique event.
38
38
  #
39
39
  # Later[:reservations].set 'event-1', Time.parse('2012-09-28 11:36:17 +0800')
40
40
  # Later[:reservations]['event-1'] #=> 2012-09-28 11:36:17 +0800
41
-
41
+ #
42
42
  def [](event)
43
43
  Time.at key[:schedule].zscore(event) rescue nil
44
44
  end
45
45
 
46
- # Gets the number of unique scheduled events.
46
+ # Returns the number of scheduled unique events.
47
47
  #
48
48
  # Later[:reservations].set 'event-1', Time.now + 60
49
49
  # Later[:reservations].set 'event-2', Time.now + 120
50
50
  # Later[:reservations].set 'event-3', Time.now + 180
51
51
  # Later[:reservations].count #=> 3
52
-
52
+ #
53
53
  def count
54
54
  key[:schedule].zcard
55
55
  end
56
56
 
57
- # Set a unique event to the schedule.
58
- #
57
+ # Returns `true` if there are no scheduled unique events. Returns `false` otherwise.
59
58
  # Later[:reservations].set 'event-1', Time.now + 60
59
+ # Later[:reservations].empty? #=> false
60
+ # Later[:reservations].unset 'event-1'
61
+ # Later[:reservations].empty? #=> true
62
+ #
63
+ def empty?
64
+ count.zero?
65
+ end
60
66
 
67
+ # Sets a unique event to this schedule.
68
+ #
69
+ # Later[:reservations].set 'event-1', Time.now + 60
70
+ #
61
71
  def set(event, time)
62
- key[:schedule].zadd time.to_i, event
72
+ key[:schedule].zadd time.to_f, event
63
73
  end
64
74
 
65
- # Unset a unique event from the schedule.
75
+ # Unsets a unique event from this schedule.
76
+ #
77
+ # Later[:reservations].unset 'event-1'
66
78
  #
67
- # Later[:reservations].unset 'event-2'
68
-
69
79
  def unset(event)
70
80
  key[:schedule].zrem event
71
81
  end
72
82
 
73
- # When called inside an `each` block, `stop!` signals the block to halt processing of the schedule.
83
+ # When called inside an `each` block, `stop!` signals the block to halt processing of this schedule.
74
84
  #
75
85
  # Later[:reservations].each do |event|
76
86
  # Later[:reservations].stop!
77
87
  # end
78
-
88
+ #
79
89
  def stop!
80
90
  @stop = true
81
91
  end
82
92
 
83
- # Process each event on the schedule. The block only gets called when an event is due to run based on the current time.
93
+ # Processes each scheduled unique event. The block only gets called when an event is due to run based on the current time.
94
+ #
95
+ # Accepts an optional `timeout` parameter with a default value of `1`. Passing an `Integer` will use Redis' blocking mechanism
96
+ # to process the schedule and is therefore more efficient. Passing a `Float` will poll Redis using the given timeout, and should
97
+ # only be used for events which need to be triggered with millisecond precision.
84
98
  #
85
99
  # Later[:reservations].each do |event|
86
100
  # # Do something with the event
87
101
  # end
88
-
89
- def each(&block)
102
+ #
103
+ # The schedule will be polled every 0.1 seconds:
104
+ #
105
+ # Later[:reservations].each(0.1) do |event|
106
+ # # Do something with the event
107
+ # end
108
+ #
109
+ def each(timeout = 1, &block)
90
110
  @stop = false
91
111
 
92
112
  loop do
93
113
  break if stop?
94
114
 
95
- time = Time.now.to_i
115
+ time = Time.now.to_f
96
116
 
97
- ids = schedule.redis.multi do
98
- schedule.zrangebyscore '-inf', time
99
- schedule.zremrangebyscore '-inf', time
100
- end.first
101
-
102
- key.redis.multi do
103
- ids.each { |id| queue.lpush id }
104
- end
105
-
106
- event = queue.brpoplpush(backup, 1)
107
-
108
- next unless event
117
+ push_to_queue pop_from_schedules(time)
118
+ next unless event = pop_from_queue(timeout)
109
119
 
110
120
  begin
111
121
  block.call event
112
122
  rescue Exception => e
113
123
  exceptions.rpush JSON(time: Time.now, event: event, message: e.inspect)
114
124
  ensure
115
- backup.del
125
+ local.del
116
126
  end
117
127
  end
118
128
  end
@@ -127,22 +137,49 @@ module Later
127
137
  @queue ||= key[:queue]
128
138
  end
129
139
 
130
- def backup
131
- @backup ||= queue[Socket.gethostname][Process.pid]
140
+ def local
141
+ @local ||= queue[Socket.gethostname][Process.pid]
142
+ end
143
+
144
+ def pop_from_schedules(time)
145
+ schedule.redis.multi do
146
+ schedule.zrangebyscore '-inf', time
147
+ schedule.zremrangebyscore '-inf', time
148
+ end.first
149
+ end
150
+
151
+ def push_to_queue(ids)
152
+ key.redis.multi do
153
+ ids.each { |id| queue.lpush id }
154
+ end
155
+ end
156
+
157
+ def pop_from_queue(timeout)
158
+ if timeout.is_a? Integer
159
+ queue.brpoplpush(local, timeout)
160
+ else
161
+ result = queue.rpoplpush(local)
162
+ sleep timeout unless result
163
+ result
164
+ end
132
165
  end
133
166
  end
134
167
 
135
168
  @schedules = {}
136
169
 
170
+ # Returns the Nest key of this module.
171
+ #
172
+ # Later[:reservations].key #=> Later::reservations
173
+ #
137
174
  def self.key
138
175
  Nest.new('Later')
139
176
  end
140
177
 
178
+ # The easiest way to create or reference a schedule. Returns an instance of a Later::Schedule with the given key.
179
+ #
180
+ # Later[:reservations] #=> #<Later::Schedule:0x007faf3b054f50 @key="Later:reservations">
181
+ #
141
182
  def self.[](schedule)
142
- if @schedules[schedule.to_sym]
143
- @schedules[schedule.to_sym]
144
- else
145
- @schedules[schedule.to_sym] = Schedule.new schedule
146
- end
183
+ @schedules[schedule.to_sym] ||= Schedule.new schedule
147
184
  end
148
185
  end
@@ -1,3 +1,3 @@
1
1
  module Later
2
- VERSION = '0.1.3'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -1,17 +1,3 @@
1
1
  require 'bundler/setup'
2
2
  require 'later'
3
3
  require 'minitest/autorun'
4
-
5
- class MiniTest::Unit::TestCase
6
- def setup
7
- system 'redis-server', File.join(File.dirname(__FILE__), 'redis.conf')
8
- until File.exist? 'redis.pid'
9
- sleep 0.1
10
- end
11
- end
12
-
13
- def teardown
14
- system "kill `cat redis.pid`"
15
- sleep 0.1
16
- end
17
- end
@@ -0,0 +1,74 @@
1
+ require_relative '../helper.rb'
2
+
3
+ describe Later::Schedule do
4
+ let(:redis) { Redis.new db: 15 }
5
+ let(:key) { Nest.new 'Events', redis }
6
+ let(:schedule) { Later::Schedule.new key }
7
+
8
+ before do
9
+ redis.flushdb
10
+ end
11
+
12
+ describe '#set' do
13
+ it 'sets a unique event schedule' do
14
+ time = Time.now
15
+
16
+ schedule.set 'event', time
17
+
18
+ assert_equal 1, schedule.count
19
+ assert_in_delta time, schedule['event'], 0.1
20
+ end
21
+ end
22
+
23
+ describe '#unset' do
24
+ it 'unsets a unique event schedule' do
25
+ time = Time.now
26
+
27
+ schedule.set 'event', time
28
+ schedule.unset 'event'
29
+
30
+ assert_equal 0, schedule.count
31
+ assert_equal nil, schedule['event']
32
+ end
33
+ end
34
+
35
+ describe '#count' do
36
+ it 'counts the number of unique event schedules' do
37
+ time = Time.now
38
+
39
+ 1.upto(3) do |i|
40
+ schedule.set i.to_s, time
41
+ assert_equal i, schedule.count
42
+ end
43
+
44
+ 1.upto(3) do |i|
45
+ schedule.unset i.to_s
46
+ assert_equal 3 - i, schedule.count
47
+ end
48
+ end
49
+ end
50
+
51
+ describe '#each' do
52
+ it 'logs exceptions raised within the block' do
53
+ 0.upto(2) do |i|
54
+ schedule.set i, Time.now
55
+ end
56
+
57
+ Thread.new do
58
+ sleep 3
59
+ schedule.stop!
60
+ end
61
+
62
+ schedule.each do |event|
63
+ raise Exception, "an unknown error for #{event} has occurred"
64
+ end
65
+
66
+ exceptions = schedule.exceptions.lrange(0, -1).map{ |e| JSON(e) }
67
+
68
+ 0.upto(2) do |i|
69
+ assert_equal i.to_s, exceptions[i]['event']
70
+ assert_equal "#<Exception: an unknown error for #{i} has occurred>", exceptions[i]['message']
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,15 @@
1
+ require_relative '../helper.rb'
2
+
3
+ describe Later do
4
+ let(:redis) { Redis.new db: 15 }
5
+ let(:key) { Nest.new 'Events', redis }
6
+
7
+ describe '.[]' do
8
+ it 'returns a schedule with the given key' do
9
+ schedule = Later[key]
10
+
11
+ assert_instance_of Later::Schedule, schedule
12
+ assert_equal key, schedule.key
13
+ end
14
+ end
15
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: later
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
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-09-28 00:00:00.000000000 Z
12
+ date: 2012-10-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis
@@ -91,7 +91,7 @@ dependencies:
91
91
  - - ! '>='
92
92
  - !ruby/object:Gem::Version
93
93
  version: '0'
94
- description: Later is a Redis-backed event scheduling library for Ruby
94
+ description: Lean Redis-backed event scheduling library for Ruby
95
95
  email:
96
96
  - erol.fornoles@gmail.com
97
97
  executables: []
@@ -107,8 +107,9 @@ files:
107
107
  - lib/later.rb
108
108
  - lib/later/version.rb
109
109
  - test/helper.rb
110
- - test/lib/later.rb
111
110
  - test/redis.conf
111
+ - test/specs/later-schedule.rb
112
+ - test/specs/later.rb
112
113
  homepage: http://erol.github.com/later
113
114
  licenses: []
114
115
  post_install_message:
@@ -132,8 +133,9 @@ rubyforge_project:
132
133
  rubygems_version: 1.8.24
133
134
  signing_key:
134
135
  specification_version: 3
135
- summary: Later is a Redis-backed event scheduling library for Ruby
136
+ summary: Lean Redis-backed event scheduling library for Ruby
136
137
  test_files:
137
138
  - test/helper.rb
138
- - test/lib/later.rb
139
139
  - test/redis.conf
140
+ - test/specs/later-schedule.rb
141
+ - test/specs/later.rb
@@ -1,103 +0,0 @@
1
- require_relative '../helper.rb'
2
-
3
- class LaterTest < MiniTest::Unit::TestCase
4
- def setup
5
- super
6
-
7
- @redis = Redis.new host: '127.0.0.1', port: 6666
8
- @key = Nest.new 'Events', @redis
9
- end
10
-
11
- def test_set_a_unique_event_schedule
12
- time = Time.now
13
-
14
- Later[@key].set '1', time
15
-
16
- assert_equal 1, Later[@key].count
17
- assert_in_delta time, Later[@key]['1'], 1
18
- end
19
-
20
- def test_unset_a_unique_event_schedule
21
- time = Time.now
22
-
23
- Later[@key].set '1', time
24
- Later[@key].unset '1'
25
-
26
- assert_equal 0, Later[@key].count
27
- assert_equal nil, Later[@key]['1']
28
- end
29
-
30
- def test_count_set_and_unset_event_schedules
31
- time = Time.now
32
-
33
- 1.upto(3) do |i|
34
- Later[@key].set i.to_s, time
35
- assert_equal i, Later[@key].count
36
- end
37
-
38
- 1.upto(3) do |i|
39
- Later[@key].unset i.to_s
40
- assert_equal 3 - i, Later[@key].count
41
- end
42
- end
43
-
44
- def test_process_many_event_schedules
45
- start = Time.now + 2
46
-
47
- times = []
48
- schedules = Hash.new { |h,k| h[k] = [] }
49
-
50
- 1.upto(100) do |i|
51
- time = start + i / 10
52
- times.unshift time
53
- schedules[time].unshift event: i.to_s, time: time
54
- end
55
-
56
- times.map{ |time| schedules[time] }.flatten.shuffle.each do |schedule|
57
- Later[@key].set schedule[:event], schedule[:time]
58
- end
59
-
60
- Thread.new do
61
- sleep 2 + 10 + (start - Time.now).to_i
62
- Later[@key].stop!
63
- end
64
-
65
- Later[@key].each do |event|
66
- time = times.pop
67
-
68
- assert_in_delta time, Time.now, 1.2
69
- assert_includes schedules[time], {event: event, time: time}
70
- end
71
-
72
- assert_equal 0, Later[@key].count
73
- end
74
-
75
- def test_exceptions_raised_within_the_worker_loop_are_handled_and_logged
76
- start = Time.now + 2
77
-
78
- 1.upto(3) do |i|
79
- Later[@key].set i.to_s, start + i
80
- end
81
-
82
- Thread.new do
83
- sleep 2 + 3 + (start - Time.now).to_i
84
- Later[@key].stop!
85
- end
86
-
87
- Later[@key].each do |event|
88
- raise Exception, "an unknown error for #{event} has occurred"
89
- end
90
-
91
- assert_equal 0, Later[@key].count
92
-
93
- exceptions = Later[@key].exceptions.lrange 0, -1
94
- exceptions = exceptions.map{ |e| JSON(e) }
95
-
96
- assert_equal '1', exceptions[0]['event']
97
- assert_equal '#<Exception: an unknown error for 1 has occurred>', exceptions[0]['message']
98
- assert_equal '2', exceptions[1]['event']
99
- assert_equal '#<Exception: an unknown error for 2 has occurred>', exceptions[1]['message']
100
- assert_equal '3', exceptions[2]['event']
101
- assert_equal '#<Exception: an unknown error for 3 has occurred>', exceptions[2]['message']
102
- end
103
- end