admit_one 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/README.md +76 -0
- data/Rakefile +10 -0
- data/admit_one.gemspec +18 -0
- data/lib/admit_one/lock_file.rb +42 -0
- data/lib/admit_one/version.rb +3 -0
- data/lib/admit_one.rb +1 -0
- data/test/unit/lock_file_test.rb +35 -0
- metadata +72 -0
data/.gitignore
ADDED
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
AdmitOne
|
2
|
+
========
|
3
|
+
|
4
|
+
A Ruby lock file manager that is immune to race conditions.
|
5
|
+
|
6
|
+
Usage
|
7
|
+
-----
|
8
|
+
|
9
|
+
AdmitOne::LockFile.new(:lock_name) do
|
10
|
+
begin
|
11
|
+
# code that needs to run with confidence
|
12
|
+
# that the same code isn't also running
|
13
|
+
# at the same time in another process
|
14
|
+
rescue AdmitOne::LockFileAlreadyExists
|
15
|
+
# gracefully recover if the lock could not
|
16
|
+
# be established
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
Installation
|
21
|
+
------------
|
22
|
+
|
23
|
+
gem install admit_one
|
24
|
+
|
25
|
+
Testing
|
26
|
+
-------
|
27
|
+
|
28
|
+
rake
|
29
|
+
|
30
|
+
Race Conditions
|
31
|
+
---------------
|
32
|
+
|
33
|
+
All other Ruby lock file libraries I've examined (I'm sure I've missed some,
|
34
|
+
but hey) are susceptible to race conditions, meaning that there's a tiny
|
35
|
+
window of opportunity for two separate processes that want the same lock file
|
36
|
+
to both *think* that they successfully got the lock, defeating the entire
|
37
|
+
purpose of a lock file.
|
38
|
+
|
39
|
+
Usually this is caused by one line of code that checks to see if the lock file
|
40
|
+
already exists, and if not, a second that creates it. This means two processes
|
41
|
+
could each, theoretically, check for the file at the same time, both not find
|
42
|
+
one and think it's safe for each of them to create one, and then both do so,
|
43
|
+
blissfully ignorant of each other.
|
44
|
+
|
45
|
+
Admittedly, this scenario seems highly unlikely, but in some applications
|
46
|
+
it could result in a complete disaster in a variety of wonderful flavors,
|
47
|
+
and you're not likely to be creating lock files in the first place unless
|
48
|
+
it's that very disaster that you absolutely need to prevent.
|
49
|
+
|
50
|
+
Solution
|
51
|
+
--------
|
52
|
+
|
53
|
+
AdmitOne solves this problem by taking advantage of how Ruby opens files in
|
54
|
+
append mode. If the file doesn't already exist, it creates it and then opens
|
55
|
+
it. Otherwise it just opens it. Then, any writes to the file are appended
|
56
|
+
to the end. Two processes can open and write to the file at the same time.
|
57
|
+
|
58
|
+
AdmitOne writes the process id to the file, closes the file and then reopens
|
59
|
+
it, this time in read mode, to compare it's process id with the one on the
|
60
|
+
first line in the file. In the event of a race condition, two process ids
|
61
|
+
will be written to the file (remember, append mode), but *only one* can
|
62
|
+
possibly be on the first line.
|
63
|
+
|
64
|
+
The first process will reopen the lock file in read mode, compare it's process
|
65
|
+
id with the one on the first line, see that they match, and only then
|
66
|
+
execute your block of code that needed a lock. The second process also opens
|
67
|
+
the file, but it will see that the process id on the first line does *not*
|
68
|
+
match it's own, and instead of executing the code block, will raise an
|
69
|
+
exception for your application to catch and handle gracefully according to
|
70
|
+
your preferences (such as trying again later, or triggering a missile launch).
|
71
|
+
|
72
|
+
Contributions
|
73
|
+
-------------
|
74
|
+
|
75
|
+
If you can see some way to improve upon this gem, feel free to fork, commit
|
76
|
+
with tests (if applicable), and then send a pull request. Thank You!
|
data/Rakefile
ADDED
data/admit_one.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require File.expand_path('../lib/admit_one/version', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "admit_one"
|
5
|
+
s.version = AdmitOne::VERSION
|
6
|
+
s.platform = Gem::Platform::RUBY
|
7
|
+
s.authors = ["Jonathan S. Garvin"]
|
8
|
+
s.email = ["jon@5valleys.com"]
|
9
|
+
s.homepage = "https://github.com/jsgarvin/admit_one"
|
10
|
+
s.summary = %q{Lock file manager.}
|
11
|
+
s.description = %q{Lock file manager that is immune to race conditions.}
|
12
|
+
|
13
|
+
s.rubyforge_project = "admit_one"
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module AdmitOne
|
2
|
+
|
3
|
+
class LockFile
|
4
|
+
require 'tempfile'
|
5
|
+
|
6
|
+
attr_accessor :filename
|
7
|
+
|
8
|
+
def initialize(name,&block)
|
9
|
+
@filename = "#{name}.lock"
|
10
|
+
begin
|
11
|
+
lock!
|
12
|
+
yield
|
13
|
+
ensure
|
14
|
+
unlock!
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def full_path
|
19
|
+
"#{Dir.tmpdir}/#{filename}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def lock!
|
23
|
+
File.open(full_path, "a") { |file| file.write("#{Process.pid}\n") }
|
24
|
+
raise LockFileAlreadyExists unless lock_file_acquired?
|
25
|
+
end
|
26
|
+
|
27
|
+
def unlock!
|
28
|
+
begin
|
29
|
+
File.delete(full_path) if lock_file_acquired?
|
30
|
+
rescue
|
31
|
+
raise LockFileMissing
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def lock_file_acquired?
|
36
|
+
File.open(full_path, "r") { |file| file.gets.to_i == Process.pid }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class LockFileAlreadyExists < StandardError; end
|
41
|
+
class LockFileMissing < StandardError; end
|
42
|
+
end
|
data/lib/admit_one.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.expand_path('../admit_one/lock_file', __FILE__)
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require File.expand_path('../../../lib/admit_one', __FILE__)
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
class LockFileTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
File.delete(lock_file_path) if File.exist?(lock_file_path)
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_should_create_and_remove_lockfile
|
11
|
+
AdmitOne::LockFile.new(:admit_one_lock_file_unit_test) do
|
12
|
+
assert(File.exist?(lock_file_path))
|
13
|
+
end
|
14
|
+
assert(!File.exist?(lock_file_path))
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_should_not_clobber_another_lock_file
|
18
|
+
File.open(lock_file_path, "a") { |file| file.write("1\n") }
|
19
|
+
assert_raise(AdmitOne::LockFileAlreadyExists) do
|
20
|
+
AdmitOne::LockFile.new(:admit_one_lock_file_unit_test) do
|
21
|
+
assert false #should never run
|
22
|
+
end
|
23
|
+
end
|
24
|
+
assert(File.exist?(lock_file_path))
|
25
|
+
File.delete(lock_file_path)
|
26
|
+
end
|
27
|
+
|
28
|
+
#######
|
29
|
+
private
|
30
|
+
#######
|
31
|
+
|
32
|
+
def lock_file_path
|
33
|
+
"#{Dir.tmpdir}/admit_one_lock_file_unit_test.lock"
|
34
|
+
end
|
35
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: admit_one
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Jonathan S. Garvin
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-05-19 00:00:00 -06:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: Lock file manager that is immune to race conditions.
|
22
|
+
email:
|
23
|
+
- jon@5valleys.com
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files: []
|
29
|
+
|
30
|
+
files:
|
31
|
+
- .gitignore
|
32
|
+
- README.md
|
33
|
+
- Rakefile
|
34
|
+
- admit_one.gemspec
|
35
|
+
- lib/admit_one.rb
|
36
|
+
- lib/admit_one/lock_file.rb
|
37
|
+
- lib/admit_one/version.rb
|
38
|
+
- test/unit/lock_file_test.rb
|
39
|
+
has_rdoc: true
|
40
|
+
homepage: https://github.com/jsgarvin/admit_one
|
41
|
+
licenses: []
|
42
|
+
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
segments:
|
54
|
+
- 0
|
55
|
+
version: "0"
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
segments:
|
62
|
+
- 0
|
63
|
+
version: "0"
|
64
|
+
requirements: []
|
65
|
+
|
66
|
+
rubyforge_project: admit_one
|
67
|
+
rubygems_version: 1.3.7
|
68
|
+
signing_key:
|
69
|
+
specification_version: 3
|
70
|
+
summary: Lock file manager.
|
71
|
+
test_files:
|
72
|
+
- test/unit/lock_file_test.rb
|