admit_one 0.1.0
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.
- 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
|