locker 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/.travis.yml +15 -0
- data/Gemfile +1 -1
- data/README.md +19 -1
- data/gemfiles/rails2.gemfile +15 -0
- data/gemfiles/rails3.gemfile +15 -0
- data/lib/generators/locker/templates/migration.rb +3 -1
- data/lib/locker/locker.rb +14 -6
- data/lib/locker/version.rb +1 -1
- data/locker.gemspec +0 -1
- data/spec/locker/locker_spec.rb +19 -0
- data/spec/spec_helper.rb +22 -1
- metadata +9 -19
data/.gitignore
CHANGED
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,9 +1,19 @@
|
|
1
|
-
# Locker
|
1
|
+
# Locker [![Build Status](https://travis-ci.org/zencoder/locker.png)](https://travis-ci.org/zencoder/locker)
|
2
2
|
|
3
3
|
Locker is a locking mechanism for limiting the concurrency of ruby code using the database.
|
4
4
|
|
5
5
|
Locker is dependent on Postgres and the ActiveRecord (>= 2.3.14) gem.
|
6
6
|
|
7
|
+
**NOTE:** In the next minor version (0.1.0), the generated Locker migration has changed to include a bigint field named `sequence` with a default of zero. Since I'm pretty sure Zencoder is the only one using this gem at the moment, I opted to not include an upgrade path. If you really must, however:
|
8
|
+
|
9
|
+
In Rails 2.3.x+:
|
10
|
+
|
11
|
+
script/generate migration add_sequence_to_locks sequence:bigint
|
12
|
+
|
13
|
+
In Rails 3.x+:
|
14
|
+
|
15
|
+
script/rails generate migration add_sequence_to_locks sequence:bigint
|
16
|
+
|
7
17
|
## The Basics
|
8
18
|
|
9
19
|
In its simplest form it can be used as follows:
|
@@ -137,6 +147,14 @@ Locker.run("some-unique-key", :model => SomeOtherLockModel) do
|
|
137
147
|
end
|
138
148
|
```
|
139
149
|
|
150
|
+
If you need to know how many times the lock has been acquired, this is available as well.
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
Locker.run("some-unique-key") do |sequence|
|
154
|
+
# sequence is the number of times the lock has been acquired
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
140
158
|
## A Common pattern
|
141
159
|
|
142
160
|
In our use we've settled on a common pattern, one that lets us distribute the load of our processes between our application and/or utility servers while making sure we have no single point of failure. This means that no single server going down (except the database) will stop the code from executing. Continuing from the code above, we'll use the example of the RSS/Atom feed checker, `FeedChecker.check_for_new_feeds`. To improve on the previous examples, we'll make the code rotate among our servers, so over a long enough time period each server will have spent an equal amount of time running the task.
|
@@ -1,9 +1,10 @@
|
|
1
1
|
class Create<%= plural_name.camelize %> < ActiveRecord::Migration
|
2
|
-
<%- if ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR
|
2
|
+
<%- if ActiveRecord::VERSION::MAJOR > 3 || ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR >= 1 -%>
|
3
3
|
def change
|
4
4
|
create_table :<%= plural_name %> do |t|
|
5
5
|
t.string :locked_by
|
6
6
|
t.string :key
|
7
|
+
t.integer :sequence, :default => 0, :limit => 8
|
7
8
|
t.datetime :locked_at
|
8
9
|
t.datetime :locked_until
|
9
10
|
end
|
@@ -15,6 +16,7 @@ class Create<%= plural_name.camelize %> < ActiveRecord::Migration
|
|
15
16
|
create_table :<%= plural_name %> do |t|
|
16
17
|
t.string :locked_by
|
17
18
|
t.string :key
|
19
|
+
t.integer :sequence, :default => 0, :limit => 8
|
18
20
|
t.datetime :locked_at
|
19
21
|
t.datetime :locked_until
|
20
22
|
end
|
data/lib/locker/locker.rb
CHANGED
@@ -1,10 +1,9 @@
|
|
1
|
+
require "socket"
|
2
|
+
require "securerandom"
|
3
|
+
|
1
4
|
class Locker
|
2
5
|
class LockStolen < StandardError; end
|
3
6
|
|
4
|
-
if !defined?(SecureRandom)
|
5
|
-
SecureRandom = ActiveSupport::SecureRandom
|
6
|
-
end
|
7
|
-
|
8
7
|
attr_accessor :identifier, :key, :renew_every, :lock_for, :model, :locked, :blocking
|
9
8
|
|
10
9
|
class << self
|
@@ -48,7 +47,7 @@ class Locker
|
|
48
47
|
end
|
49
48
|
end
|
50
49
|
|
51
|
-
block.call
|
50
|
+
block.call(sequence)
|
52
51
|
ensure
|
53
52
|
renewer.exit rescue nil
|
54
53
|
release if @locked
|
@@ -61,7 +60,7 @@ class Locker
|
|
61
60
|
end
|
62
61
|
|
63
62
|
def get
|
64
|
-
@locked = update_all(["locked_by = ?, locked_at = clock_timestamp() at time zone 'UTC', locked_until = clock_timestamp() at time zone 'UTC' + #{lock_interval}", @identifier],
|
63
|
+
@locked = update_all(["locked_by = ?, locked_at = clock_timestamp() at time zone 'UTC', locked_until = clock_timestamp() at time zone 'UTC' + #{lock_interval}, sequence = sequence + 1", @identifier],
|
65
64
|
["key = ? AND (locked_by IS NULL OR locked_by = ? OR locked_until < clock_timestamp() at time zone 'UTC')", @key, @identifier])
|
66
65
|
end
|
67
66
|
|
@@ -75,6 +74,15 @@ class Locker
|
|
75
74
|
@locked
|
76
75
|
end
|
77
76
|
|
77
|
+
def sequence
|
78
|
+
if @sequence
|
79
|
+
@sequence
|
80
|
+
else
|
81
|
+
record = model.find_by_key_and_locked_by(@key, @identifier)
|
82
|
+
@sequence = record && record.sequence
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
78
86
|
private
|
79
87
|
|
80
88
|
def lock_interval
|
data/lib/locker/version.rb
CHANGED
data/locker.gemspec
CHANGED
data/spec/locker/locker_spec.rb
CHANGED
@@ -85,6 +85,25 @@ describe Locker do
|
|
85
85
|
end
|
86
86
|
end.to raise_error(Locker::LockStolen)
|
87
87
|
end
|
88
|
+
|
89
|
+
it "should call the passed in block when the lock is aquired" do
|
90
|
+
value = nil
|
91
|
+
Locker.run("foo") do
|
92
|
+
value = 1
|
93
|
+
end
|
94
|
+
|
95
|
+
expect(value).to eq(1)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should pass in a sequence number representing the number of times the lock has been locked" do
|
99
|
+
(1..10).each do |i|
|
100
|
+
value = nil
|
101
|
+
Locker.run("foo") do |sequence|
|
102
|
+
value = sequence
|
103
|
+
end
|
104
|
+
expect(value).to eq(i)
|
105
|
+
end
|
106
|
+
end
|
88
107
|
end
|
89
108
|
|
90
109
|
describe "blocking" do
|
data/spec/spec_helper.rb
CHANGED
@@ -10,7 +10,19 @@ require 'locker'
|
|
10
10
|
ActiveRecord::Base.time_zone_aware_attributes = true
|
11
11
|
ActiveRecord::Base.default_timezone = "UTC"
|
12
12
|
|
13
|
-
|
13
|
+
if File.exist?(File.join(File.dirname(__FILE__), 'database.yml'))
|
14
|
+
config = YAML.load_file(File.join(File.dirname(__FILE__), 'database.yml'))
|
15
|
+
else
|
16
|
+
puts "database.yml did not exist, using defaults"
|
17
|
+
config = { "reconnect" => false,
|
18
|
+
"database" => "locker_test",
|
19
|
+
"adapter" => "postgresql",
|
20
|
+
"password" => nil,
|
21
|
+
"user" => "postgres",
|
22
|
+
"encoding" => "utf8",
|
23
|
+
"pool" => 5 }
|
24
|
+
end
|
25
|
+
|
14
26
|
begin
|
15
27
|
ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public'))
|
16
28
|
ActiveRecord::Base.connection.create_database(config['database'], config.merge("encoding" => config['encoding'] || ENV['CHARSET'] || 'utf8'))
|
@@ -24,6 +36,7 @@ ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS locks")
|
|
24
36
|
ActiveRecord::Base.connection.create_table(:locks) do |t|
|
25
37
|
t.string :locked_by
|
26
38
|
t.string :key
|
39
|
+
t.integer :sequence, :default => 0
|
27
40
|
t.datetime :locked_at
|
28
41
|
t.datetime :locked_until
|
29
42
|
end
|
@@ -42,6 +55,14 @@ class FakeLock
|
|
42
55
|
fake_locks[key]
|
43
56
|
end
|
44
57
|
|
58
|
+
def self.find_by_key_and_locked_by(key, locked_by)
|
59
|
+
lock = fake_locks[key]
|
60
|
+
|
61
|
+
if lock && lock.locked_by == locked_by
|
62
|
+
lock
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
45
66
|
def self.create(attributes={})
|
46
67
|
fake_locks[attributes[:key]] = new(attributes)
|
47
68
|
true
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: locker
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 27
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
+
- 1
|
8
9
|
- 0
|
9
|
-
|
10
|
-
version: 0.0.3
|
10
|
+
version: 0.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Nathan Sutton
|
@@ -16,7 +16,7 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date:
|
19
|
+
date: 2013-04-04 00:00:00 Z
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
22
22
|
name: activerecord
|
@@ -62,20 +62,6 @@ dependencies:
|
|
62
62
|
version: "0"
|
63
63
|
type: :development
|
64
64
|
version_requirements: *id003
|
65
|
-
- !ruby/object:Gem::Dependency
|
66
|
-
name: autotest
|
67
|
-
prerelease: false
|
68
|
-
requirement: &id004 !ruby/object:Gem::Requirement
|
69
|
-
none: false
|
70
|
-
requirements:
|
71
|
-
- - ">="
|
72
|
-
- !ruby/object:Gem::Version
|
73
|
-
hash: 3
|
74
|
-
segments:
|
75
|
-
- 0
|
76
|
-
version: "0"
|
77
|
-
type: :development
|
78
|
-
version_requirements: *id004
|
79
65
|
description: Locker is a locking mechanism for limiting the concurrency of ruby code using the database. It presently only works with PostgreSQL.
|
80
66
|
email:
|
81
67
|
- nate@zencoder.com
|
@@ -88,11 +74,14 @@ extra_rdoc_files: []
|
|
88
74
|
|
89
75
|
files:
|
90
76
|
- .gitignore
|
77
|
+
- .travis.yml
|
91
78
|
- Gemfile
|
92
79
|
- LICENSE
|
93
80
|
- README.md
|
94
81
|
- Rakefile
|
95
82
|
- autotest/discover.rb
|
83
|
+
- gemfiles/rails2.gemfile
|
84
|
+
- gemfiles/rails3.gemfile
|
96
85
|
- generators/locker/USAGE
|
97
86
|
- generators/locker/locker_generator.rb
|
98
87
|
- generators/locker/templates/migration.rb
|
@@ -136,10 +125,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
136
125
|
requirements: []
|
137
126
|
|
138
127
|
rubyforge_project: locker
|
139
|
-
rubygems_version: 1.8.
|
128
|
+
rubygems_version: 1.8.25
|
140
129
|
signing_key:
|
141
130
|
specification_version: 3
|
142
131
|
summary: Locker is a locking mechanism for limiting the concurrency of ruby code using the database.
|
143
132
|
test_files:
|
144
133
|
- spec/locker/locker_spec.rb
|
145
134
|
- spec/spec_helper.rb
|
135
|
+
has_rdoc:
|