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 ADDED
@@ -0,0 +1,3 @@
1
+ *.gem
2
+ .loadpath
3
+ .project
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
@@ -0,0 +1,10 @@
1
+ require 'rake/testtask'
2
+
3
+ task :default => :test
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << 'lib'
7
+ t.libs << 'test'
8
+ t.pattern = 'test/**/*_test.rb'
9
+ t.verbose = true
10
+ end
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
@@ -0,0 +1,3 @@
1
+ module AdmitOne
2
+ VERSION = '0.1.0'
3
+ 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