later 0.1.3 → 0.2.0

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.
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