redis-locker 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ Gemfile.lock
2
+ *.gem
3
+ coverage
4
+ .bundle
5
+ pkg
6
+ spec/config/redis.yml
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "redis"
4
+ gem "logger"
5
+
6
+ group :test do
7
+ gem 'rspec'
8
+ end
9
+
10
+ group :test, :development do
11
+ gem 'rake'
12
+ end
@@ -0,0 +1,53 @@
1
+ # redis-locker
2
+
3
+ A super-FAST and super-ROBUST LOCKING mechanism.
4
+ Builds queue of concurrent code blocks using Redis.
5
+
6
+ If Redis fails at some point (cleared, filled with wrong data, stuck) it will be trying to stay alive anyway. This is supported by additional key-control.
7
+
8
+ ## Installing
9
+ ```
10
+ $ gem install redis-locker
11
+ ```
12
+
13
+ Or put in your gemfile for latest version:
14
+ ```ruby
15
+ gem 'redis-locker', git: 'git://github.com/einzige/redis-locker.git'
16
+ ```
17
+
18
+ ## Using
19
+ ```ruby
20
+ # Throws an error if transaction will not be finished in 10 seconds
21
+ RedisLocker.new('payment_transaction').run!(10.seconds) do
22
+ # Any concurrent code.
23
+ end
24
+
25
+ # Throws an error if transaction will not be finished in 10 seconds
26
+ # Clears all stale tasks which were not performed within 10 seconds
27
+ RedisLocker.new('payment_transaction', 10.seconds).run! do
28
+ # Any concurrent code.
29
+ end
30
+
31
+ # Does not throw any error, but clears all stale tasks which were not performed within 10 seconds
32
+ RedisLocker.new('payment_transaction', 10.seconds).run do
33
+ # Any concurrent code.
34
+ end
35
+ ```
36
+
37
+ ## Running specs
38
+ - Clone the repo
39
+ - run `bundle exec rake spec`
40
+
41
+ ## Contributing to redis-locker
42
+
43
+ - Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
44
+ - Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
45
+ - Fork the project
46
+ - Start a feature/bugfix branch
47
+ - Commit and push until you are happy with your contribution
48
+ - Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
49
+ - Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
50
+
51
+ ## Copyright
52
+
53
+ Copyright (c) 2013 Sergei Zinin. No LICENSE for details :)
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env rake
2
+
3
+ ENV['BUNDLE_GEMFILE'] = 'Gemfile'
4
+ gem_root = File.expand_path(File.dirname(__FILE__))
5
+
6
+ require 'rubygems'
7
+ require 'bundler'
8
+ require 'rake'
9
+ require 'rake/testtask'
10
+ require 'rspec'
11
+ require 'rspec/core/rake_task'
12
+
13
+ task default: :spec
14
+
15
+ desc "Run the test suite"
16
+ task spec: ['spec:setup', 'spec:lib']
17
+
18
+ namespace :spec do
19
+ desc "Setup the test environment"
20
+ task :setup do
21
+ system "cd #{gem_root} && bundle install && mkdir db"
22
+ end
23
+
24
+ desc "Test the RedisLocker lib"
25
+ RSpec::Core::RakeTask.new(:lib) do |task|
26
+ task.pattern = File.join(gem_root, '/spec/lib/**/*_spec.rb')
27
+ end
28
+ end
@@ -0,0 +1,173 @@
1
+ require 'logger'
2
+ require 'redis'
3
+ require 'timeout'
4
+
5
+
6
+ class RedisLocker
7
+ attr_reader :key, :running, :timestamp, :timestamp_key, :time_limit
8
+
9
+ # @param [String] key
10
+ # @param [Integer] time_limit Number of seconds when locker will be expired
11
+ def initialize(key, time_limit = 5)
12
+ @key = key
13
+ @time_limit = time_limit
14
+ @running = false
15
+ end
16
+
17
+ # @return [true, false]
18
+ def current?
19
+ concurrent_timestamp == timestamp
20
+ end
21
+
22
+ # Puts running block information in Redis
23
+ # This information will be used to place running block in a specific position of its queue
24
+ def enter_queue
25
+ logger.info("Entering #@key")
26
+ raise 'This block is already in the queue' if running?
27
+
28
+ @running = true
29
+ self.timestamp = generate_timestamp.to_s
30
+
31
+ redis.set timestamp_key, '1'
32
+ redis.expire timestamp_key, time_limit
33
+ redis.rpush key, timestamp
34
+ end
35
+
36
+ # Clears all data from queue related to this block
37
+ def exit_queue
38
+ logger.info("Leaving #@key")
39
+ redis.del timestamp_key
40
+ redis.lrem key, 1, timestamp
41
+ @running = false
42
+ end
43
+
44
+ # Returns true if block is ready to run
45
+ # @return [true, false]
46
+ def get_ready
47
+ if ready?
48
+ concurrent_timestamp.nil? ? start_queue : make_current
49
+ true
50
+ else
51
+ current?
52
+ end
53
+ end
54
+
55
+ def ready?
56
+ concurrent_timestamp.nil? || current? ||
57
+ (generate_timestamp - concurrent_timestamp.to_f >= time_limit) ||
58
+ redis.get(generate_timestamp_key(concurrent_timestamp)).nil?
59
+ end
60
+
61
+ def redis
62
+ self.class.redis
63
+ end
64
+
65
+ # Waits for the queue and evaluates the block
66
+ def run(&block)
67
+ logger.info("Running queue #@key")
68
+
69
+ enter_queue
70
+ wait
71
+ begin
72
+ block.call
73
+ ensure
74
+ exit_queue
75
+ end
76
+ end
77
+
78
+ # @param [Integer] time_limit Number of seconds after we throw a Timeout::Error
79
+ # @param [true, false] clear_queue_on_timeout
80
+ # @raise [Timeout::Error]
81
+ def run!(time_limit = @time_limit, clear_queue_on_timeout = false, &block)
82
+ Timeout::timeout(time_limit) { run(&block) }
83
+ rescue Timeout::Error => error
84
+ logger.error("Failed by timeout #{time_limit}s on #@key")
85
+
86
+ if clear_queue_on_timeout
87
+ logger.info("Clearing queue #@key")
88
+ clear_queue
89
+ end
90
+
91
+ raise error
92
+ end
93
+
94
+ def running?
95
+ @running
96
+ end
97
+
98
+ def self.logger
99
+ @logger ||= Logger.new(STDOUT)
100
+ end
101
+
102
+ def self.logger=(logger)
103
+ @logger = logger
104
+ end
105
+
106
+ def self.redis
107
+ @redis
108
+ end
109
+
110
+ def self.redis=(adapter)
111
+ @redis = adapter
112
+ end
113
+
114
+ protected
115
+
116
+ # @return [Float]
117
+ def generate_timestamp
118
+ Time.now.to_f
119
+ end
120
+
121
+ private
122
+
123
+ def clear_queue
124
+ redis.del key
125
+ end
126
+
127
+ # @return [String]
128
+ def concurrent_timestamp
129
+ @concurrent_timestamp ||= fetch_concurrent_timestamp
130
+ end
131
+
132
+ # Fetches next concurrent thread ID from the queue
133
+ def fetch_concurrent_timestamp
134
+ redis.lindex(key, 0)
135
+ end
136
+
137
+ # @param [String, Float] timestamp
138
+ def generate_timestamp_key(timestamp = @timestamp)
139
+ "Locker::__key_#{timestamp}"
140
+ end
141
+
142
+ # @return [Logger]
143
+ def logger
144
+ self.class.logger
145
+ end
146
+
147
+ # Replaces concurrent timestamp
148
+ def make_current
149
+ redis.lrem key, 0, timestamp
150
+ redis.lpop key
151
+ redis.lpush key, timestamp
152
+ end
153
+ alias_method :replace_concurrent_timestamp, :make_current
154
+
155
+ # Builds queue starting from self
156
+ def start_queue
157
+ redis.lpush key, timestamp
158
+ end
159
+
160
+ # @param [Float] value
161
+ def timestamp=(value)
162
+ @timestamp = value
163
+ @timestamp_key = generate_timestamp_key(@timestamp)
164
+ @timestamp
165
+ end
166
+
167
+ # Locking itself
168
+ def wait
169
+ begin
170
+ @concurrent_timestamp = fetch_concurrent_timestamp
171
+ end until get_ready
172
+ end
173
+ end
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{redis-locker}
5
+ s.version = "0.0.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Sergei Zinin"]
9
+ s.date = %q{2013-08-13}
10
+ s.description = %q{A locking mechanism. Builds queue of concurrent code blocks using Redis.}
11
+ s.email = %q{szinin@partyearth.com}
12
+ s.extra_rdoc_files = [ "README.md" ]
13
+ s.files = `git ls-files`.split("\n")
14
+ s.homepage = %q{http://github.com/einzige/redis-locker}
15
+ s.licenses = ["MIT"]
16
+ s.require_paths = ["lib"]
17
+ s.rubygems_version = %q{1.6.2}
18
+ s.summary = %q{Destroys the concurrency of your code.}
19
+
20
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0')
21
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
22
+ s.add_runtime_dependency(%q<logger>, [">= 1.2.8"])
23
+ s.add_runtime_dependency(%q<redis>, [">= 3.0.3"])
24
+ else
25
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
26
+ s.add_dependency(%q<logger>, [">= 1.2.8"])
27
+ s.add_dependency(%q<redis>, [">= 3.0.3"])
28
+ end
29
+ end
@@ -0,0 +1,2 @@
1
+ host: 127.0.0.1
2
+ port: 6379
@@ -0,0 +1,156 @@
1
+ require 'spec_helper'
2
+
3
+
4
+ describe RedisLocker do
5
+ let(:queue_name) { 'queue_name' }
6
+ subject { described_class.new(queue_name, 6) }
7
+
8
+ describe "#initialize" do
9
+ it 'assigns key' do
10
+ subject.key.should == queue_name
11
+ end
12
+
13
+ it 'assigns time_limit' do
14
+ subject.time_limit.should == 6
15
+ end
16
+ end
17
+
18
+ describe "#enter_queue" do
19
+ context "calling multiple times" do
20
+ before { subject.enter_queue }
21
+ its(:running?) { should be_true }
22
+ it { expect { subject.enter_queue }.to raise_error("This block is already in the queue") }
23
+
24
+ context "after exit" do
25
+ before { subject.exit_queue }
26
+ its(:running?) { should be_false }
27
+ it { expect { subject.enter_queue }.not_to raise_error }
28
+ end
29
+ end
30
+ end
31
+
32
+ describe "#ready?" do
33
+ its(:ready?) { should be_true }
34
+
35
+ context "trash in a queue" do
36
+ before do
37
+ described_class.redis.lpush queue_name, 'whatever'
38
+ end
39
+
40
+ its(:current?) { should be_false }
41
+ its(:ready?) { should be_true }
42
+ end
43
+
44
+ context "pending timestamp in a queue" do
45
+ before do
46
+ subject.enter_queue
47
+ end
48
+
49
+ its(:current?) { should be_true }
50
+
51
+ context "stale" do
52
+ before do
53
+ described_class.redis.lpush queue_name, subject.timestamp.to_f + 10000
54
+ end
55
+
56
+ its(:current?) { should be_false }
57
+ its(:ready?) { should be_true }
58
+ end
59
+
60
+ context "not stale" do
61
+ context "same time" do
62
+ before do
63
+ described_class.redis.lpush queue_name, subject.timestamp.to_f
64
+ end
65
+
66
+ its(:current?) { should be_true }
67
+ its(:ready?) { should be_true }
68
+ end
69
+
70
+ context "later time" do
71
+ context "same time" do
72
+ before do
73
+ described_class.redis.lpush queue_name, subject.timestamp.to_f + 0.00001
74
+ end
75
+
76
+ its(:current?) { should be_false }
77
+ its(:ready?) { should be_true }
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ context "concurrent threads" do
84
+ let(:concurrent_locker_1) { described_class.new(queue_name) }
85
+ let(:concurrent_locker_2) { described_class.new(queue_name) }
86
+ let(:unrelated_locker) { described_class.new('unrelated') }
87
+
88
+ before do
89
+ concurrent_locker_1.enter_queue
90
+ concurrent_locker_2.enter_queue
91
+ unrelated_locker.enter_queue
92
+ end
93
+
94
+ it 'allows first locker in the queue to be run' do
95
+ concurrent_locker_1.ready?.should be_true
96
+ end
97
+
98
+ it 'makes second locker in the queue wait' do
99
+ concurrent_locker_2.ready?.should be_false
100
+ end
101
+
102
+ it 'does not have an impact on a separate queue' do
103
+ unrelated_locker.ready?.should be_true
104
+ end
105
+ end
106
+ end
107
+
108
+ describe "#run" do
109
+ context "with breaking block" do
110
+ before do
111
+ subject.should_receive(:exit_queue).once
112
+ end
113
+
114
+ it 'exits queue even if something fails' do
115
+ expect { subject.run { raise 'PIZDEC!' } }.to raise_error('PIZDEC!')
116
+ end
117
+ end
118
+
119
+ describe "locking", integrational: true do
120
+ it 'locks' do
121
+ looser = nil
122
+ winner = nil
123
+
124
+ concurrent_request = proc do |id|
125
+ proc do
126
+ described_class.new(queue_name).run do
127
+ looser = id
128
+
129
+ if winner
130
+ winner.should_not == looser
131
+ else
132
+ winner = id
133
+ sleep(5)
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ thr_1 = Thread.new &concurrent_request.call(1)
140
+ thr_2 = Thread.new &concurrent_request.call(2)
141
+
142
+ thr_1.join
143
+ thr_2.join
144
+ end
145
+ end
146
+ end
147
+
148
+ describe "#run!" do
149
+ context "reaching deadline" do
150
+ before do
151
+ subject.should_receive(:clear_queue)
152
+ end
153
+ it { expect { subject.run!(0.0000000001, true) { sleep(0.00001) } }.to raise_error(Timeout::Error) }
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,16 @@
1
+ require "rspec"
2
+ require "redis-locker"
3
+ require "yaml"
4
+
5
+ RSpec.configure { |config| config.mock_with :rspec }
6
+
7
+ redis = Redis.new(YAML.load_file("spec/config/redis.yml"))
8
+ RedisLocker.redis = redis
9
+ RedisLocker.logger.level = Logger::WARN
10
+
11
+ RSpec.configure do |config|
12
+ config.filter_run_excluding integrational: true
13
+
14
+ config.before { redis.flushdb }
15
+ config.after { redis.flushdb }
16
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis-locker
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Sergei Zinin
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-08-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.0.0
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 1.0.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: logger
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: 1.2.8
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 1.2.8
46
+ - !ruby/object:Gem::Dependency
47
+ name: redis
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: 3.0.3
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: 3.0.3
62
+ description: A locking mechanism. Builds queue of concurrent code blocks using Redis.
63
+ email: szinin@partyearth.com
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files:
67
+ - README.md
68
+ files:
69
+ - .gitignore
70
+ - Gemfile
71
+ - README.md
72
+ - Rakefile
73
+ - lib/redis-locker.rb
74
+ - redis-locker.gemspec
75
+ - spec/config/redis.yml
76
+ - spec/config/redis.yml.example
77
+ - spec/lib/redis_locker_spec.rb
78
+ - spec/spec_helper.rb
79
+ homepage: http://github.com/einzige/redis-locker
80
+ licenses:
81
+ - MIT
82
+ post_install_message:
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ! '>='
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - ! '>='
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubyforge_project:
100
+ rubygems_version: 1.8.25
101
+ signing_key:
102
+ specification_version: 3
103
+ summary: Destroys the concurrency of your code.
104
+ test_files: []