singleton_process 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in singleton_process.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Robert Jackson
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # SingletonProcess
2
+
3
+ Ensure that a given process is only running once. Helpful for ensure that scheduled tasks do not overlap if they run longer than the scheduled interval.
4
+
5
+ Prior attempts simply used a pid file, and checked if the process specified was still running (by calling `Process.kill(0, pid)`), but
6
+ since the system reuses PID's you can get false positives. This project uses a locked pid file to ensure that the process is truly still
7
+ running. So basically, if the file is locked the process is still running.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'singleton_process'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install singleton_process
22
+
23
+ ## Usage
24
+
25
+ The basic usage is quite simple: just supply a process name (will show up in `ps ax` output) and call `#lock`.
26
+
27
+ ```ruby
28
+ SingletonProcess.new('long_running_process').lock
29
+ ```
30
+
31
+ By default the lock file will be removed when the process exits, but if you need to clear the lock earlier you can call #unlock.
32
+
33
+ ```ruby
34
+ process = SingletonProcess.new('long_running_process')
35
+ process.lock
36
+ # your process here
37
+ process.unlock
38
+ ```
39
+
40
+ If you want only a specific block of code to be locked call `#run!` with the block.
41
+
42
+ ```ruby
43
+ SingletonProcess.new('long_running_process_name').run! do
44
+ # some long running code here
45
+ end
46
+ ```
47
+
48
+ ## Contributing
49
+
50
+ 1. Fork it
51
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
52
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
53
+ 4. Push to the branch (`git push origin my-new-feature`)
54
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,3 @@
1
+ class SingletonProcess
2
+ VERSION = "0.0.1" unless defined?(VERSION)
3
+ end
@@ -0,0 +1,91 @@
1
+ require "singleton_process/version"
2
+
3
+ require 'pathname'
4
+
5
+ class SingletonProcess
6
+ class AlreadyRunningError < RuntimeError; end
7
+
8
+ attr_accessor :name, :root_path, :app_name
9
+ private :name=
10
+
11
+ def initialize(name, options = {})
12
+ self.name = name
13
+ self.root_path = options.fetch(:root_path, nil)
14
+ self.app_name = options.fetch(:app_name, nil)
15
+ end
16
+
17
+ def root_path=(value)
18
+ @root_path = Pathname.new(value) if value
19
+ end
20
+ private :root_path=
21
+
22
+ def root_path
23
+ @root_path ||= defined?(Rails) ? Rails.root : Pathname.new('.')
24
+ end
25
+
26
+ def pidfile_path
27
+ pidfile_directory.join("#{name}.pid")
28
+ end
29
+
30
+ def lock
31
+ write_pidfile
32
+ at_exit { delete_pidfile }
33
+ $0 = "#{app_name} | #{name} | started #{Time.now}"
34
+ end
35
+
36
+ def run!
37
+ lock
38
+ yield if block_given?
39
+ unlock
40
+ end
41
+
42
+ def unlock
43
+ delete_pidfile
44
+ !running?
45
+ end
46
+
47
+ def running?
48
+ if pidfile_path.exist?
49
+ pidfile = pidfile_path.open('r')
50
+ !pidfile.flock(File::LOCK_EX | File::LOCK_NB)
51
+ else
52
+ false
53
+ end
54
+ ensure
55
+ if pidfile
56
+ pidfile.flock(File::LOCK_UN)
57
+ pidfile.close
58
+ end
59
+ end
60
+
61
+ def pid
62
+ running? ? pidfile_path.read.to_i : nil
63
+ end
64
+
65
+ private
66
+
67
+ def pidfile
68
+ @pidfile ||= pidfile_path.open('a')
69
+ end
70
+
71
+ def write_pidfile
72
+ if pidfile.flock(File::LOCK_EX | File::LOCK_NB)
73
+ pidfile.truncate(0)
74
+ pidfile.write("#{Process.pid}\n")
75
+ pidfile.fsync
76
+ else
77
+ raise AlreadyRunningError.new("Process already running: #{pid}")
78
+ end
79
+ end
80
+
81
+ def delete_pidfile
82
+ pidfile_path.unlink if pidfile_path.exist?
83
+ end
84
+
85
+ def pidfile_directory
86
+ dir = Pathname.new(root_path.join('tmp/pids'))
87
+ dir.mkpath unless dir.directory?
88
+ dir
89
+ end
90
+ end
91
+
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'singleton_process/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "singleton_process"
8
+ gem.version = SingletonProcess::VERSION
9
+ gem.authors = ["Robert Jackson"]
10
+ gem.email = ["robert.w.jackson@me.com"]
11
+ gem.description = %q{Ensure that a given process is only running once. Helpful for ensure that scheduled tasks do not overlap if they run longer than the scheduled interval.}
12
+ gem.summary = %q{Ensure that a given process is only running once.}
13
+ gem.homepage = "https://github.com/rjackson/singleton_process"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_development_dependency 'rspec', '~>2.12.0'
21
+ end
@@ -0,0 +1,233 @@
1
+ require 'timeout'
2
+ require 'spec_helper'
3
+
4
+ require_relative '../lib/singleton_process'
5
+
6
+ describe SingletonProcess do
7
+ let(:random_pid) { rand(99999)}
8
+ let(:process_name) {'testing'}
9
+ let(:pidfile_path) {Pathname.new("tmp/pids/#{process_name}.pid")}
10
+
11
+ before do
12
+ @original_name = $PROGRAM_NAME
13
+ end
14
+
15
+ after do
16
+ $PROGRAM_NAME = @original_name
17
+ singleton.pidfile_path.unlink if singleton.pidfile_path.exist?
18
+ end
19
+
20
+ def write_random_pid
21
+ pidfile_path.open('w'){|io| io.write "#{random_pid}\n"}
22
+ end
23
+
24
+ describe '.new' do
25
+ let(:tmp_pathname) {Pathname.new('/tmp')}
26
+
27
+ it "should accept a name." do
28
+ instance = described_class.new(process_name)
29
+ instance.name.should eql(process_name)
30
+ end
31
+
32
+ it "should accept a root_path." do
33
+ instance = described_class.new(process_name, root_path: tmp_pathname)
34
+ instance.root_path.should eql(tmp_pathname)
35
+ end
36
+
37
+ it "should convert root_path to a Pathname." do
38
+ instance = described_class.new(process_name, root_path: '/tmp')
39
+ instance.root_path.should eql(tmp_pathname)
40
+ end
41
+
42
+ it "should set root_path to Rails.root if not specified." do
43
+ stub_const('Rails', double('rails',:root => tmp_pathname))
44
+ instance = described_class.new(process_name)
45
+ instance.root_path.should eql(tmp_pathname)
46
+ end
47
+
48
+ it "should accept and save an application name." do
49
+ instance = described_class.new(process_name, app_name: 'blah')
50
+ instance.app_name.should eql('blah')
51
+ end
52
+ end
53
+
54
+ subject(:singleton) {described_class.new(process_name)}
55
+
56
+ describe "#name=" do
57
+ it "should be private." do
58
+ singleton.respond_to?(:name=).should be_false
59
+ singleton.respond_to?(:name=, true).should be_true
60
+ end
61
+ end
62
+
63
+ describe "lock" do
64
+ let(:expected_error) {SingletonProcess::AlreadyRunningError}
65
+
66
+ context "when it is not already running" do
67
+ it "should write the current PID to the pidfile." do
68
+ singleton.lock
69
+ written_pid = singleton.pidfile_path.read.to_i
70
+ written_pid.should eql(Process.pid)
71
+ end
72
+
73
+ it "should set the $PROGRAM_NAME." do
74
+ singleton.lock
75
+ $PROGRAM_NAME.should_not eql(@original_name)
76
+ end
77
+ end
78
+
79
+ context "when it is already running" do
80
+ context "in the same process" do
81
+ it "should raise an error." do
82
+ instance1 = described_class.new('blah')
83
+ instance2 = described_class.new('blah')
84
+
85
+ instance1.lock
86
+ expect{instance2.lock}.to raise_error(expected_error)
87
+ end
88
+ end
89
+
90
+ context "in separate processes" do
91
+ let(:successful_output) {'Ran successfully!'}
92
+ let(:process_name) {'spawn_test'}
93
+
94
+ let(:spawn_command) do
95
+ [ File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME')),
96
+ "-I", File.expand_path("../../lib", __FILE__),
97
+ "-r", "singleton_process",
98
+ "-e", """
99
+ trap(:INT) {puts '#{successful_output}'; exit;};
100
+ SingletonProcess.new('#{process_name}').lock;
101
+ while true; sleep 0.01; end;
102
+ """
103
+ ]
104
+ end
105
+
106
+ before do
107
+ require 'open3'
108
+ require 'io/wait'
109
+ end
110
+
111
+ after do
112
+ Process.kill(:KILL, @pid1) rescue nil
113
+ Process.kill(:KILL, @pid2) rescue nil
114
+ end
115
+
116
+ it "should raise an error." do
117
+ pidfile_path.exist?.should be_false
118
+ stdin1, stdout1, stderr1, wait_thr1 = Open3.popen3(*spawn_command)
119
+ @pid1 = wait_thr1[:pid]
120
+
121
+ start_time = Time.now
122
+ while Time.now - start_time < 1
123
+ break if pidfile_path.exist?
124
+ sleep 0.01
125
+ end
126
+ pidfile_path.exist?.should be_true
127
+
128
+ stdin2, stdout2, stderr2, wait_thr2 = Open3.popen3(*spawn_command)
129
+ @pid2 = wait_thr2[:pid]
130
+
131
+ begin
132
+ exit_status = Timeout.timeout(5) { wait_thr2.value }
133
+ rescue Timeout::Error
134
+ raise "Child process didn't exit properly."
135
+ end
136
+
137
+ stderr2.read.should match(/AlreadyRunningError/)
138
+ exit_status.success?.should be_false
139
+
140
+ stdin2.close; stdout2.close; stderr2.close
141
+
142
+ Process.kill(:INT, @pid1)
143
+
144
+ begin
145
+ exit_status = Timeout.timeout(5) { wait_thr1.value }
146
+ rescue Timeout::Error
147
+ raise "Child process didn't exit properly."
148
+ end
149
+
150
+ stdout1.read.should eql("#{successful_output}\n")
151
+ stdin1.close; stdout1.close; stderr1.close
152
+ exit_status.success?.should be_true
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ describe "run!" do
159
+ it "should yield." do
160
+ ran = false
161
+ singleton.run! { ran = true }
162
+ ran.should be_true
163
+ end
164
+
165
+ it "should call #lock." do
166
+ singleton.should_receive(:lock)
167
+ singleton.run!
168
+ end
169
+
170
+ it "should call #unlock." do
171
+ singleton.should_receive(:unlock)
172
+ singleton.run!
173
+ end
174
+ end
175
+
176
+ describe "#unlock" do
177
+ it "should delete the pid file." do
178
+ singleton.lock
179
+ singleton.running?.should be_true
180
+ singleton.unlock
181
+ singleton.running?.should be_false
182
+ end
183
+ end
184
+
185
+ describe "#running?" do
186
+ context "when the pid file exists" do
187
+ let(:pidfile) {pidfile_path.open('r')}
188
+
189
+ before do
190
+ write_random_pid
191
+ end
192
+
193
+ after do
194
+ pidfile_path.unlink
195
+ end
196
+
197
+ it "should return true when the pid file is locked." do
198
+ pidfile.flock(File::LOCK_EX | File::LOCK_NB).should be_true
199
+
200
+ singleton.running?.should be_true
201
+ end
202
+
203
+ it "should return false when there is no lock." do
204
+ singleton.running?.should be_false
205
+ end
206
+ end
207
+
208
+ it "should return false when the process is not running." do
209
+ pidfile_path.unlink if pidfile_path.exist?
210
+ singleton.running?.should be_false
211
+ end
212
+ end
213
+
214
+ describe "#pid" do
215
+
216
+ it "should return the pid from pidfile_path." do
217
+ pidfile_path.open('w') {|io| io.write "#{random_pid}\n" }
218
+ singleton.should_receive(:running?).and_return(true)
219
+ singleton.pid.should eql(random_pid)
220
+ end
221
+
222
+ it "should return nil unless the process is running?." do
223
+ singleton.pid.should be_nil
224
+ end
225
+ end
226
+
227
+ describe "#pidfile_path" do
228
+ it "should return the proper path based on the processes name." do
229
+ singleton.pidfile_path.expand_path.should eql(pidfile_path.expand_path)
230
+ end
231
+ end
232
+ end
233
+
@@ -0,0 +1,7 @@
1
+ RSpec.configure do |config|
2
+ config.treat_symbols_as_metadata_keys_with_true_values = true
3
+ config.run_all_when_everything_filtered = true
4
+ config.filter_run :focus
5
+
6
+ config.order = 'random'
7
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: singleton_process
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Robert Jackson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-12 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ prerelease: false
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 2.12.0
22
+ none: false
23
+ type: :development
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ version: 2.12.0
29
+ none: false
30
+ description: Ensure that a given process is only running once. Helpful for ensure
31
+ that scheduled tasks do not overlap if they run longer than the scheduled interval.
32
+ email:
33
+ - robert.w.jackson@me.com
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - .gitignore
39
+ - .rspec
40
+ - Gemfile
41
+ - LICENSE.txt
42
+ - README.md
43
+ - Rakefile
44
+ - lib/singleton_process.rb
45
+ - lib/singleton_process/version.rb
46
+ - singleton_process.gemspec
47
+ - spec/singleton_process_spec.rb
48
+ - spec/spec_helper.rb
49
+ homepage: https://github.com/rjackson/singleton_process
50
+ licenses: []
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ! '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ none: false
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ none: false
67
+ requirements: []
68
+ rubyforge_project:
69
+ rubygems_version: 1.8.24
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: Ensure that a given process is only running once.
73
+ test_files:
74
+ - spec/singleton_process_spec.rb
75
+ - spec/spec_helper.rb
76
+ has_rdoc: