ost 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/README.md +158 -0
  2. data/lib/ost.rb +14 -18
  3. data/ost.gemspec +1 -1
  4. data/test/ost_test.rb +52 -47
  5. metadata +35 -43
  6. data/README.markdown +0 -111
data/README.md ADDED
@@ -0,0 +1,158 @@
1
+ Ost
2
+ ===
3
+
4
+ Redis based queues and workers.
5
+
6
+ ![Ost Cafe, by Arancia Project](http://farm4.static.flickr.com/3255/3161710005_36566b8a9e.jpg)
7
+
8
+ Description
9
+ -----------
10
+
11
+ **Ost** makes it easy to enqueue object ids and process them with
12
+ workers.
13
+
14
+ Say you want to process video uploads. In your application you will
15
+ have something like this:
16
+
17
+ ``` ruby
18
+ Ost[:videos_to_process].push(@video.id)
19
+ ```
20
+
21
+ Then, you will have a worker that will look like this:
22
+
23
+ ``` ruby
24
+ require "ost"
25
+
26
+ Ost[:videos_to_process].each do |id|
27
+ # Do something with it!
28
+ end
29
+ ```
30
+
31
+ Usage
32
+ -----
33
+
34
+ **Ost** connects to Redis automatically with the default options
35
+ (localhost:6379, database 0).
36
+
37
+ You can customize the connection by calling `connect`:
38
+
39
+ ``` ruby
40
+ Ost.connect port: 6380, db: 2
41
+ ```
42
+
43
+ Then you only need to refer to a queue for it to pop into existence:
44
+
45
+ ``` ruby
46
+ Ost[:rss_feeds] << @feed.id
47
+ ```
48
+
49
+ A worker is a Ruby file with this basic code:
50
+
51
+ ``` ruby
52
+ require "ost"
53
+
54
+ Ost[:rss_feeds].each do |id|
55
+ # ...
56
+ end
57
+ ```
58
+
59
+ It will pop items from the queue as soon as they become available. It
60
+ uses BRPOPLPUSH with a timeout that can be specified with the
61
+ `OST_TIMEOUT` environment variable.
62
+
63
+ Note that in these examples we are pushing numbers to the queue. As
64
+ we have unlimited queues, each queue should be specialized and the
65
+ workers must be smart enough to know what to do with the numbers they
66
+ pop.
67
+
68
+ Available methods
69
+ =================
70
+
71
+ `Ost.connect`: configure the connection to Redis. By default, it
72
+ connects to localhost in port 6379 and uses the database 0. It accepts
73
+ the same options as [redis-rb](https://github.com/redis/redis-rb).
74
+
75
+ `Ost.stop`: halt processing for all queues.
76
+
77
+ `Ost[:example].push item`, `Ost[:some_queue] << item`: add `item` to
78
+ the `:example` queue.
79
+
80
+ `Ost[:example].push { |item| ... }`, `Ost[:example].each { |item| ...
81
+ }`: consume `item` from the `:example` queue. If the block doesn't
82
+ complete successfully, the item will be left at a backup queue.
83
+
84
+ `Ost[:example].stop`: halt processing for the `example` queue.
85
+
86
+ Failures
87
+ ========
88
+
89
+ **Ost** stores in-process items in backup queues. That allows the
90
+ developer to deal with exceptions in a way that results adequate
91
+ for his application.
92
+
93
+ There is one backup queue for each worker, with the following
94
+ convention for naming the key in Redis: given a worker using the
95
+ `:events` queue, running in the hostname `domU-12-31-39-04-49-C7`
96
+ with the process id `28431`, the key for the backup queue will be
97
+ `ost:events:domU-12-31-39-04-49-C7:28431`.
98
+
99
+ Here's the explanation for each part:
100
+
101
+ * `ost`: namespace for all **Ost** related keys.
102
+ * `events`: name of the queue.
103
+ * `domU-12-31-39-04-49-C7`: hostname of the worker.
104
+ * `28431`: process id of the worker.
105
+
106
+ Priorities
107
+ ----------
108
+
109
+ There's no concept of priorities, as each queue is specialized and you
110
+ can create as many as you want. For example, nothing prevents the
111
+ creation of the `:example_high_priority` or the
112
+ `:example_low_priority` queues.
113
+
114
+ Differences with Delayed::Job and Resque
115
+ ----------------------------------------
116
+
117
+ Both [Delayed::Job](http://github.com/tobi/delayed_job) and
118
+ [Resque](http://github.com/defunkt/resque) provide queues and workers
119
+ (the latter using Redis). They provide dumb workers that process jobs,
120
+ which are specialized for each task. The specialization takes place
121
+ in the application side, and the job is serialized and pushed into a
122
+ queue.
123
+
124
+ **Ost**, by contrast, just pushes numbers into specialized queues, and
125
+ uses workers that are subscribed to specific queues and know what to
126
+ do with the items they get. The total sum of logic is about the same,
127
+ but there's less communication and less data transfer with **Ost**.
128
+
129
+ Installation
130
+ ------------
131
+
132
+ $ gem install ost
133
+
134
+ License
135
+ -------
136
+
137
+ Copyright (c) 2010, 2011, 2012 Michel Martens
138
+
139
+ Permission is hereby granted, free of charge, to any person
140
+ obtaining a copy of this software and associated documentation
141
+ files (the "Software"), to deal in the Software without
142
+ restriction, including without limitation the rights to use,
143
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
144
+ copies of the Software, and to permit persons to whom the
145
+ Software is furnished to do so, subject to the following
146
+ conditions:
147
+
148
+ The above copyright notice and this permission notice shall be
149
+ included in all copies or substantial portions of the Software.
150
+
151
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
152
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
153
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
154
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
155
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
156
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
157
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
158
+ OTHER DEALINGS IN THE SOFTWARE.
data/lib/ost.rb CHANGED
@@ -1,18 +1,20 @@
1
1
  require "nest"
2
2
 
3
3
  module Ost
4
- VERSION = "0.0.3"
5
- TIMEOUT = ENV["OST_TIMEOUT"] || 2
4
+ VERSION = "0.1.0"
5
+ TIMEOUT = ENV["OST_TIMEOUT"] || 0
6
6
 
7
7
  class Queue
8
- attr :ns
8
+ attr :key
9
+ attr :backup
9
10
 
10
11
  def initialize(name)
11
- @ns = Nest.new(:ost)[name]
12
+ @key = Nest.new(:ost)[name]
13
+ @backup = @key[Socket.gethostname][Process.pid]
12
14
  end
13
15
 
14
16
  def push(value)
15
- redis.lpush(ns, value)
17
+ key.lpush(value)
16
18
  end
17
19
 
18
20
  def each(&block)
@@ -21,28 +23,22 @@ module Ost
21
23
  loop do
22
24
  break if @stopping
23
25
 
24
- _, item = redis.brpop(ns, TIMEOUT)
25
- next if item.nil? or item.empty?
26
+ item = @key.brpoplpush(@backup, TIMEOUT)
26
27
 
27
- begin
28
- block.call(item)
29
- rescue Exception => e
30
- error = "#{Time.now} #{ns[item]} => #{e.inspect}"
28
+ block.call(item)
31
29
 
32
- redis.rpush ns[:errors], error
33
- redis.publish ns[:errors], error
34
- end
30
+ @backup.del
35
31
  end
36
32
  end
37
33
 
38
- def errors
39
- redis.lrange ns[:errors], 0, -1
40
- end
41
-
42
34
  def stop
43
35
  @stopping = true
44
36
  end
45
37
 
38
+ def items
39
+ key.lrange(0, -1)
40
+ end
41
+
46
42
  alias << push
47
43
  alias pop each
48
44
 
data/ost.gemspec CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |s|
11
11
 
12
12
  s.files = Dir[
13
13
  "LICENSE",
14
- "README.markdown",
14
+ "README.md",
15
15
  "Rakefile",
16
16
  "lib/**/*.rb",
17
17
  "*.gemspec",
data/test/ost_test.rb CHANGED
@@ -1,14 +1,20 @@
1
+ ENV["OST_TIMEOUT"] = "1"
2
+
1
3
  require File.expand_path("test_helper", File.dirname(__FILE__))
2
4
 
3
5
  scope do
4
6
  def ost(&job)
5
7
  thread = Thread.new do
6
- Ost[:events].each(&job)
8
+ Ost[:events].each do |item|
9
+ begin
10
+ yield(item)
11
+ ensure
12
+ thread.kill
13
+ end
14
+ end
7
15
  end
8
16
 
9
- sleep 0.1
10
-
11
- thread.kill
17
+ thread.join
12
18
  end
13
19
 
14
20
  def enqueue(id)
@@ -24,9 +30,14 @@ scope do
24
30
  Redis.new
25
31
  end
26
32
 
27
- test "insert items in the queue" do |redis|
33
+ test "allows access to the queued items" do
28
34
  enqueue(1)
29
- assert_equal ["1"], redis.lrange("ost:events", 0, -1)
35
+
36
+ assert_equal ["1"], Ost[:events].items
37
+ end
38
+
39
+ test "allows access to the underlying key" do
40
+ assert_equal 0, Ost[:events].key.llen
30
41
  end
31
42
 
32
43
  test "process items from the queue" do |redis|
@@ -38,50 +49,10 @@ scope do
38
49
  results << item
39
50
  end
40
51
 
41
- assert_equal [], redis.lrange("ost:events", 0, -1)
52
+ assert_equal [], Ost[:events].items
42
53
  assert_equal ["1"], results
43
54
  end
44
55
 
45
- test "add failures to special lists" do |redis|
46
- enqueue(1)
47
-
48
- ost do |item|
49
- raise "Wrong answer"
50
- end
51
-
52
- assert_equal 0, redis.llen("ost:events")
53
- assert_equal 1, redis.llen("ost:events:errors")
54
-
55
- assert redis.rpop("ost:events:errors").match(/ost:events:1 => #<RuntimeError: Wrong answer/)
56
- end
57
-
58
- test "publish the error to a specific channel" do |redis|
59
- enqueue(1)
60
- results = []
61
-
62
- t1 = Thread.new do
63
- redis.subscribe("ost:events:errors") do |on|
64
- on.message do |channel, message|
65
- if message[/ost:events:1 => #<RuntimeError: Wrong answer/]
66
- results << message
67
- redis.unsubscribe
68
- end
69
- end
70
- end
71
- end
72
-
73
- ost do |item|
74
- raise "Wrong answer"
75
- end
76
-
77
- t1.join
78
-
79
- assert_equal 0, redis.llen("ost:events")
80
- assert_equal 1, redis.llen("ost:events:errors")
81
-
82
- assert results.pop.match(/ost:events:1 => #<RuntimeError: Wrong answer/)
83
- end
84
-
85
56
  test "halt processing a queue" do
86
57
  Thread.new do
87
58
  sleep 0.5
@@ -107,4 +78,38 @@ scope do
107
78
 
108
79
  assert true
109
80
  end
81
+
82
+ test "maintains a backup queue for when worker dies" do
83
+ enqueue(1)
84
+
85
+ assert_equal 0, Ost[:events].backup.llen
86
+
87
+ begin
88
+ Ost[:events].each do |item|
89
+ item.some_error
90
+ end
91
+ rescue
92
+ end
93
+
94
+ assert_equal ["1"], Ost[:events].backup.lrange(0, -1)
95
+ end
96
+
97
+ test "cleans up the backup queue on success" do
98
+ enqueue(1)
99
+
100
+ done = false
101
+
102
+ Thread.new do
103
+ Ost[:events].each do |item|
104
+ assert_equal ["1"], Ost[:events].backup.lrange(0, -1)
105
+ done = true
106
+ end
107
+ end
108
+
109
+ until done; end
110
+
111
+ Ost[:events].stop
112
+
113
+ assert_equal 0, Ost[:events].backup.llen
114
+ end
110
115
  end
metadata CHANGED
@@ -1,51 +1,47 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: ost
3
- version: !ruby/object:Gem::Version
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
4
5
  prerelease:
5
- version: 0.0.3
6
6
  platform: ruby
7
- authors:
7
+ authors:
8
8
  - Michel Martens
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
-
13
- date: 2011-05-13 00:00:00 Z
14
- dependencies:
15
- - !ruby/object:Gem::Dependency
12
+ date: 2012-03-17 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
16
15
  name: nest
17
- prerelease: false
18
- requirement: &id001 !ruby/object:Gem::Requirement
16
+ requirement: &2160446460 !ruby/object:Gem::Requirement
19
17
  none: false
20
- requirements:
18
+ requirements:
21
19
  - - ~>
22
- - !ruby/object:Gem::Version
23
- version: "1.0"
20
+ - !ruby/object:Gem::Version
21
+ version: '1.0'
24
22
  type: :runtime
25
- version_requirements: *id001
26
- - !ruby/object:Gem::Dependency
27
- name: cutest
28
23
  prerelease: false
29
- requirement: &id002 !ruby/object:Gem::Requirement
24
+ version_requirements: *2160446460
25
+ - !ruby/object:Gem::Dependency
26
+ name: cutest
27
+ requirement: &2160445920 !ruby/object:Gem::Requirement
30
28
  none: false
31
- requirements:
29
+ requirements:
32
30
  - - ~>
33
- - !ruby/object:Gem::Version
34
- version: "1.0"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
35
33
  type: :development
36
- version_requirements: *id002
34
+ prerelease: false
35
+ version_requirements: *2160445920
37
36
  description: Ost lets you manage queues and workers with Redis.
38
- email:
37
+ email:
39
38
  - michel@soveran.com
40
39
  executables: []
41
-
42
40
  extensions: []
43
-
44
41
  extra_rdoc_files: []
45
-
46
- files:
42
+ files:
47
43
  - LICENSE
48
- - README.markdown
44
+ - README.md
49
45
  - Rakefile
50
46
  - lib/ost.rb
51
47
  - ost.gemspec
@@ -53,30 +49,26 @@ files:
53
49
  - test/test_helper.rb
54
50
  homepage: http://github.com/soveran/ost
55
51
  licenses: []
56
-
57
52
  post_install_message:
58
53
  rdoc_options: []
59
-
60
- require_paths:
54
+ require_paths:
61
55
  - lib
62
- required_ruby_version: !ruby/object:Gem::Requirement
56
+ required_ruby_version: !ruby/object:Gem::Requirement
63
57
  none: false
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: "0"
68
- required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
63
  none: false
70
- requirements:
71
- - - ">="
72
- - !ruby/object:Gem::Version
73
- version: "0"
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
74
68
  requirements: []
75
-
76
69
  rubyforge_project:
77
- rubygems_version: 1.8.1
70
+ rubygems_version: 1.8.10
78
71
  signing_key:
79
72
  specification_version: 3
80
73
  summary: Redis based queues and workers.
81
74
  test_files: []
82
-
data/README.markdown DELETED
@@ -1,111 +0,0 @@
1
- Ost
2
- ===
3
-
4
- Redis based queues and workers.
5
-
6
- ![Ost Cafe, by Arancia Project](http://farm4.static.flickr.com/3255/3161710005_36566b8a9e.jpg)
7
-
8
- Description
9
- -----------
10
-
11
- **Ost** makes it easy to enqueue object ids and process them with workers.
12
-
13
- Say you want to process video uploads. In your application you will have something like this:
14
-
15
- Ost[:videos_to_process].push(@video.id)
16
-
17
- Then, you will have a worker that will look like this:
18
-
19
- require "ost"
20
-
21
- Ost[:videos_to_process].each do |id|
22
- # Do something with it!
23
- end
24
-
25
- Usage
26
- -----
27
-
28
- **Ost** connects to Redis automatically with the default options (localhost:6379, database 0).
29
-
30
- You can customize the connection by calling `connect`:
31
-
32
- Ost.connect port: 6380, db: 2
33
-
34
- Then you only need to refer to a queue for it to pop into existence:
35
-
36
- Ost[:rss_feeds] << @feed.id
37
-
38
- A worker is a Ruby file with this basic code:
39
-
40
- require "ost"
41
-
42
- Ost[:rss_feeds].each do |id|
43
- ...
44
- end
45
-
46
- It will pop items from the queue with a timeout of two seconds and retry indefinitely. If you want to configure the timeout, set the environment variable `OST_TIMEOUT`.
47
-
48
- Available methods for a queue are `push` (aliased to `<<`) and `pop` (aliased to `each`).
49
-
50
- Note that in these examples we are pushing numbers to the queue. As we have unlimited queues, each queue should be specialized and the workers must be smart enough to know what to do with the numbers they pop.
51
-
52
- Failures
53
- ========
54
-
55
- If the block raises an error, it is captured by **Ost** and the exception is logged in Redis.
56
-
57
- Consider this example:
58
-
59
- require "ost"
60
-
61
- Ost[:rss_feeds].each do |id|
62
- ...
63
- raise "Invalid format"
64
- end
65
-
66
- Then, in the console you can do:
67
-
68
- >> Ost[:rss_feeds].push 1
69
- => 1
70
-
71
- >> Ost[:rss_feeds].errors
72
- => ["2010-04-12 21:57:23 -0300 ost:rss_feeds:1 => #<RuntimeError: Invalid format>"]
73
-
74
- Differences with Delayed::Job and Resque
75
- --------------------------------------
76
-
77
- Both [Delayed::Job](http://github.com/tobi/delayed_job) and [Resque](http://github.com/defunkt/resque)
78
- provide queues and workers (the latter using Redis). They provide dumb workers that process jobs, which are specialized for each task. The specialization takes place in the application side, and the job is serialized and pushed into a queue.
79
-
80
- **Ost**, by contrast, just pushes numbers into specialized queues, and uses workers that are subscribed to specific queues and know what to do with the items they get. The total sum of logic is almost the same.
81
-
82
- Installation
83
- ------------
84
-
85
- $ gem install ost
86
-
87
- License
88
- -------
89
-
90
- Copyright (c) 2010 Michel Martens
91
-
92
- Permission is hereby granted, free of charge, to any person
93
- obtaining a copy of this software and associated documentation
94
- files (the "Software"), to deal in the Software without
95
- restriction, including without limitation the rights to use,
96
- copy, modify, merge, publish, distribute, sublicense, and/or sell
97
- copies of the Software, and to permit persons to whom the
98
- Software is furnished to do so, subject to the following
99
- conditions:
100
-
101
- The above copyright notice and this permission notice shall be
102
- included in all copies or substantial portions of the Software.
103
-
104
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
105
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
106
- OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
107
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
108
- HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
109
- WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
110
- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
111
- OTHER DEALINGS IN THE SOFTWARE.