redis_safe_queue 0.0.1
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 +105 -0
- data/example.rb +35 -0
- data/lib/redis_safe_queue.rb +97 -0
- data/redis_safe_queue.gemspec +21 -0
- metadata +64 -0
data/README.md
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
redis_safe_queue
|
2
|
+
================
|
3
|
+
|
4
|
+
RedisSafeQueue is a transactional queue for ruby/redis. It guarantees at least once
|
5
|
+
semantics.
|
6
|
+
|
7
|
+
The queue may be used with multiple producers and consumers, each job is removed
|
8
|
+
in a open/commit transaction; even if a worker dies while processing a job, it is
|
9
|
+
automatically requeued after the specified `timeout`.
|
10
|
+
|
11
|
+
You can create multiple instances of the RedisSafeQueue class with the same
|
12
|
+
`queue_id` (i.e. on multiple machines), they will share the same queue and
|
13
|
+
synchronize.
|
14
|
+
|
15
|
+
Each queue provides:
|
16
|
+
|
17
|
+
+ A `work` method that processes jobs in a loop and returns when the queue is empty.
|
18
|
+
You may provide a maximum number of jobs as an integer argument and/or invoke the
|
19
|
+
`work` method in a loop to run a continuous queue.
|
20
|
+
|
21
|
+
+ A `finish` method that blocks until the queue is empty and then yields only once.
|
22
|
+
When multiple instances of the RedisSafeQueue class with the same `queue_id` exist
|
23
|
+
(i.e. on multiple machines) only one instance will yield and the others will return
|
24
|
+
without doing anything.
|
25
|
+
|
26
|
+
|
27
|
+
### Example
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
require "rubygems"
|
31
|
+
require "redis_safe_queue"
|
32
|
+
|
33
|
+
$processed = 0
|
34
|
+
|
35
|
+
# create a new queue 'my_queue_1' with a 1-minute timeout, if a worker
|
36
|
+
# exceeds the timeout while processing a job it is implicitly requeued
|
37
|
+
my_queue = RedisSafeQueue.new(
|
38
|
+
:redis => Redis.new,
|
39
|
+
:queue_id => "my_queue_1",
|
40
|
+
:timeout => 60
|
41
|
+
)
|
42
|
+
|
43
|
+
# push 10.000 items to the queue
|
44
|
+
10_000.times do |i|
|
45
|
+
my_queue.push("fnord-#{rand(123)}")
|
46
|
+
end
|
47
|
+
|
48
|
+
# start 10 workers/threads
|
49
|
+
10.times { Thread.new {
|
50
|
+
|
51
|
+
# this will process jobs in parallel until the queue is empty
|
52
|
+
my_queue.work do |job|
|
53
|
+
$processed += 1
|
54
|
+
end
|
55
|
+
|
56
|
+
# this will yield only once, on one of the 10 threads
|
57
|
+
my_queue.finish do
|
58
|
+
puts "queue finished, processed #{$processed} jobs on 10 threads"
|
59
|
+
exit 0
|
60
|
+
end
|
61
|
+
|
62
|
+
} }
|
63
|
+
```
|
64
|
+
|
65
|
+
|
66
|
+
Limitations
|
67
|
+
-----------
|
68
|
+
|
69
|
+
+ A job may be processed more than once if a worker takes longer than `timeout` to process a job (the transaction is only commited after the block passed to `work` returns)
|
70
|
+
+ A job may be processed more than once if a redis network roundtrip takes longer than `TX_COMMIT_TIMEOUT` (default: 10 seconds) during the `start_tx` procedure
|
71
|
+
+ Since queue appends are not synchronized there is a known race condition that occurs when producing and consuming from the same queue (same `queue_id`) at the same time; in this case the `work` method may return even though there are still jobs left in the queue. This can be mitigated by calling the `work` method in a loop and never invoking `finish`.
|
72
|
+
+ In a distributed setup the system clocks must be in sync on all machines since redis_safe_queue implements a timestamp-based lock expiration mechanism
|
73
|
+
|
74
|
+
|
75
|
+
|
76
|
+
Installation
|
77
|
+
------------
|
78
|
+
|
79
|
+
gem install redis_safe_queue
|
80
|
+
|
81
|
+
or in your Gemfile:
|
82
|
+
|
83
|
+
gem 'redis_safe_queue', '~> 0.1'
|
84
|
+
|
85
|
+
|
86
|
+
License
|
87
|
+
-------
|
88
|
+
|
89
|
+
Copyright (c) 2011 Paul Asmuth
|
90
|
+
|
91
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
92
|
+
a copy of this software and associated documentation files (the
|
93
|
+
"Software"), to use, copy and modify copies of the Software, subject
|
94
|
+
to the following conditions:
|
95
|
+
|
96
|
+
The above copyright notice and this permission notice shall be
|
97
|
+
included in all copies or substantial portions of the Software.
|
98
|
+
|
99
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
100
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
101
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
102
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
103
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
104
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
105
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/example.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
$: << ::File.expand_path("../lib", __FILE__)
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "redis_safe_queue"
|
5
|
+
|
6
|
+
QUEUE = RedisSafeQueue.new(
|
7
|
+
:redis => Redis.new,
|
8
|
+
:redis_prefix => "fnordbar",
|
9
|
+
:queue_id => rand(8**16).to_s(36),
|
10
|
+
:timeout => 60
|
11
|
+
)
|
12
|
+
|
13
|
+
10_000.times do |i|
|
14
|
+
QUEUE.push("fnord-#{rand(123)}")
|
15
|
+
end
|
16
|
+
|
17
|
+
$processed = 0
|
18
|
+
|
19
|
+
10.times { Thread.new {
|
20
|
+
|
21
|
+
QUEUE.work do |job|
|
22
|
+
$processed += 1
|
23
|
+
end
|
24
|
+
|
25
|
+
QUEUE.finish do
|
26
|
+
puts "queue finished, processed #{$processed} jobs on 10 threads"
|
27
|
+
exit 0
|
28
|
+
end
|
29
|
+
|
30
|
+
} }
|
31
|
+
|
32
|
+
loop do
|
33
|
+
puts "size: #{QUEUE.size}, processed: #{$processed}, iterations: #{QUEUE.iterations}"
|
34
|
+
sleep 1
|
35
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "redis"
|
3
|
+
|
4
|
+
class RedisSafeQueue
|
5
|
+
|
6
|
+
# redis must process all commands within 10s
|
7
|
+
TX_COMMIT_TIMEOUT = 10
|
8
|
+
|
9
|
+
attr_accessor :timeout, :queue_id, :prefix, :redis, :iterations
|
10
|
+
|
11
|
+
def initialize(opts)
|
12
|
+
@redis = opts.fetch(:redis)
|
13
|
+
@prefix = opts[:redis_prefix] || :redis_safe_queue
|
14
|
+
@queue_id = opts.fetch(:queue_id)
|
15
|
+
@timeout = opts.fetch(:timeout).to_i
|
16
|
+
@iterations = 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def push(job_data)
|
20
|
+
job_id = unique_id
|
21
|
+
@redis.set(key(job_id, :job), job_data)
|
22
|
+
@redis.sadd(key, job_id)
|
23
|
+
end
|
24
|
+
|
25
|
+
def work(max_jobs = nil)
|
26
|
+
loop do
|
27
|
+
break if max_jobs == 0
|
28
|
+
if job_id = start_tx
|
29
|
+
job = get_job(job_id)
|
30
|
+
yield(job)
|
31
|
+
commit_tx(job_id)
|
32
|
+
max_jobs -= 1 if max_jobs
|
33
|
+
else
|
34
|
+
break
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def finish
|
40
|
+
sleep 1 while size > 0
|
41
|
+
|
42
|
+
lock = @redis.setnx(key(:finished), 1)
|
43
|
+
yield if lock
|
44
|
+
|
45
|
+
@redis.expire(key, @timeout)
|
46
|
+
@redis.expire(key(:finished), @timeout)
|
47
|
+
end
|
48
|
+
|
49
|
+
def size
|
50
|
+
@redis.scard(key)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def start_tx
|
56
|
+
job_id = nil
|
57
|
+
|
58
|
+
loop do
|
59
|
+
@iterations +=1
|
60
|
+
|
61
|
+
candidate = @redis.srandmember(key)
|
62
|
+
return unless candidate
|
63
|
+
|
64
|
+
if lock = @redis.setnx(key(candidate, :lock), Time.now.to_i)
|
65
|
+
@redis.expire(key(candidate, :lock), @timeout)
|
66
|
+
return candidate
|
67
|
+
else
|
68
|
+
lock_time = @redis.get(key(candidate, :lock)).to_i
|
69
|
+
|
70
|
+
if (Time.now.to_i - lock_time) > @timeout
|
71
|
+
@redis.del(key(candidate, :lock))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def commit_tx(job_id)
|
78
|
+
@redis.pipelined do
|
79
|
+
@redis.expire(key(job_id, :lock), TX_COMMIT_TIMEOUT)
|
80
|
+
@redis.srem(key, job_id)
|
81
|
+
@redis.del(key(job_id, :job))
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def unique_id
|
86
|
+
rand(8**32).to_s(36)
|
87
|
+
end
|
88
|
+
|
89
|
+
def get_job(job_id)
|
90
|
+
@redis.get(key(job_id, :job))
|
91
|
+
end
|
92
|
+
|
93
|
+
def key(*append)
|
94
|
+
[@prefix, @queue_id, append].flatten.compact * ":"
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "redis_safe_queue"
|
6
|
+
s.version = "0.0.1"
|
7
|
+
s.date = Date.today.to_s
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Paul Asmuth"]
|
10
|
+
s.email = ["paul@paulasmuth.com"]
|
11
|
+
s.homepage = "http://github.com/paulasmuth/redis_safe_queue"
|
12
|
+
s.summary = %q{transactional queue for redis (at least once semantics)}
|
13
|
+
s.description = %q{RedisSafeQueue is a transactional queue for ruby/redis. It guarantees at least once semantics. The queue may be used with multiple producers and consumers, each job is removed in a open/commit transaction; even if a worker dies while processing a job, it is automatically requeued.}
|
14
|
+
s.licenses = ["MIT"]
|
15
|
+
|
16
|
+
s.add_dependency "redis", ">= 2.2.2"
|
17
|
+
|
18
|
+
s.files = `git ls-files`.split("\n") - [".gitignore", ".rspec", ".travis.yml"]
|
19
|
+
s.test_files = `git ls-files -- spec/*`.split("\n")
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: redis_safe_queue
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Paul Asmuth
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-11-06 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: redis
|
16
|
+
requirement: &6216580 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 2.2.2
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *6216580
|
25
|
+
description: RedisSafeQueue is a transactional queue for ruby/redis. It guarantees
|
26
|
+
at least once semantics. The queue may be used with multiple producers and consumers,
|
27
|
+
each job is removed in a open/commit transaction; even if a worker dies while processing
|
28
|
+
a job, it is automatically requeued.
|
29
|
+
email:
|
30
|
+
- paul@paulasmuth.com
|
31
|
+
executables: []
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- README.md
|
36
|
+
- example.rb
|
37
|
+
- lib/redis_safe_queue.rb
|
38
|
+
- redis_safe_queue.gemspec
|
39
|
+
homepage: http://github.com/paulasmuth/redis_safe_queue
|
40
|
+
licenses:
|
41
|
+
- MIT
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options: []
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
none: false
|
48
|
+
requirements:
|
49
|
+
- - ! '>='
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ! '>='
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '0'
|
58
|
+
requirements: []
|
59
|
+
rubyforge_project:
|
60
|
+
rubygems_version: 1.8.17
|
61
|
+
signing_key:
|
62
|
+
specification_version: 3
|
63
|
+
summary: transactional queue for redis (at least once semantics)
|
64
|
+
test_files: []
|