redis_safe_queue 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []