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.
@@ -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.
@@ -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: []