dont-stall-my-process 0.0.1 → 0.1.1
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.
- checksums.yaml +4 -4
- data/lib/dont-stall-my-process.rb +32 -15
- data/lib/dont-stall-my-process/configuration.rb +36 -0
- data/lib/dont-stall-my-process/exceptions.rb +14 -0
- data/lib/dont-stall-my-process/local/child_process.rb +65 -23
- data/lib/dont-stall-my-process/local/child_process_pool.rb +52 -0
- data/lib/dont-stall-my-process/local/local_proxy.rb +44 -57
- data/lib/dont-stall-my-process/local/process_exit_handler.rb +30 -0
- data/lib/dont-stall-my-process/proxy_registry.rb +29 -0
- data/lib/dont-stall-my-process/remote/remote_application.rb +58 -14
- data/lib/dont-stall-my-process/remote/remote_application_controller.rb +55 -0
- data/lib/dont-stall-my-process/remote/remote_proxy.rb +21 -30
- data/lib/dont-stall-my-process/version.rb +1 -1
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 68d9fbd15fa53138d40bf1c042bbc284421fade2
|
4
|
+
data.tar.gz: 262a38004cf7b4f8ecddbb49d370aae5343d5e6b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7fe43327afdc8e662ae41b40397a3395f097ccf81ff175321e544585d6250c39d57174d989120b4c38ca8be44d6c6a28809136006af2bd2ab20b3f6f636af220
|
7
|
+
data.tar.gz: 9cf418fc795e9382b0849731ffc347debfb427619ef725e05b55b28f4a3c952e6c3cb499906e156ab275957f66f07a6ad67500a4ffc4370165d83528c7a19312
|
@@ -1,26 +1,51 @@
|
|
1
|
+
require 'dont-stall-my-process/configuration'
|
2
|
+
require 'dont-stall-my-process/exceptions'
|
3
|
+
require 'dont-stall-my-process/proxy_registry'
|
4
|
+
require 'dont-stall-my-process/version'
|
1
5
|
require 'dont-stall-my-process/local/child_process'
|
6
|
+
require 'dont-stall-my-process/local/child_process_pool'
|
2
7
|
require 'dont-stall-my-process/local/local_proxy'
|
8
|
+
require 'dont-stall-my-process/local/process_exit_handler'
|
3
9
|
require 'dont-stall-my-process/remote/remote_application'
|
10
|
+
require 'dont-stall-my-process/remote/remote_application_controller'
|
4
11
|
require 'dont-stall-my-process/remote/remote_proxy'
|
5
|
-
require 'dont-stall-my-process/version'
|
6
12
|
|
7
13
|
module DontStallMyProcess
|
8
|
-
|
14
|
+
def self.configure
|
15
|
+
yield Configuration.get if block_given?
|
16
|
+
end
|
9
17
|
|
10
|
-
def self.create(klass, opts = {}
|
18
|
+
def self.create(klass, opts = {})
|
11
19
|
fail 'no klass given' unless klass && klass.is_a?(Class)
|
12
20
|
|
13
21
|
# Set default values and validate configuration.
|
14
22
|
opts = sanitize_options(opts)
|
15
23
|
|
24
|
+
# Start a local DRbServer (unnamed?) for callbacks (blocks!).
|
25
|
+
# Each new DontStallMyProcess object will overwrite the main master DRbServer.
|
26
|
+
# This looks weird, but doesn't affect concurrent uses of multiple
|
27
|
+
# Watchdogs, I tested it. Trust me.
|
28
|
+
DRb.start_service
|
29
|
+
|
16
30
|
# Fork the child process.
|
17
|
-
|
31
|
+
process = Local::ChildProcessPool.alloc
|
32
|
+
|
33
|
+
# Start the DRb service for the main class and create a proxy.
|
34
|
+
proxy = process.start_service(klass, opts)
|
18
35
|
|
19
|
-
#
|
20
|
-
|
36
|
+
# If a block is given, we finalize the service immediately after its return.
|
37
|
+
if block_given?
|
38
|
+
|
39
|
+
yield proxy
|
40
|
+
|
41
|
+
proxy.stop_service!
|
42
|
+
proxy = nil
|
43
|
+
end
|
44
|
+
|
45
|
+
proxy
|
21
46
|
end
|
22
47
|
|
23
|
-
def self.sanitize_options(opts, default_timeout = DEFAULT_TIMEOUT)
|
48
|
+
def self.sanitize_options(opts, default_timeout = Configuration::DEFAULT_TIMEOUT)
|
24
49
|
fail 'opts is not a hash' unless opts.is_a?(Hash)
|
25
50
|
|
26
51
|
opts[:timeout] ||= default_timeout
|
@@ -37,12 +62,4 @@ module DontStallMyProcess
|
|
37
62
|
]
|
38
63
|
}
|
39
64
|
end
|
40
|
-
|
41
|
-
# This exception is raised when the watchdog bites.
|
42
|
-
class TimeoutExceeded < StandardError; end
|
43
|
-
|
44
|
-
# This exception is raised when a forbidden Kernel method is called.
|
45
|
-
# These methods do not get forwarded over the DRb.
|
46
|
-
class KernelMethodCalled < StandardError; end
|
47
|
-
|
48
65
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module DontStallMyProcess
|
2
|
+
class Configuration
|
3
|
+
DEFAULT_TIMEOUT = 300
|
4
|
+
|
5
|
+
ATTRIBUTES = [:sigkill_only, :close_stdio, :restore_all_traps, :skip_at_exit_handlers,
|
6
|
+
:process_pool_size, :subprocess_name, :after_fork_handler]
|
7
|
+
attr_writer *(ATTRIBUTES - [:after_fork_handler])
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@sigkill_only = false
|
11
|
+
@close_stdio = true
|
12
|
+
@restore_all_traps = false
|
13
|
+
@skip_at_exit_handlers = false
|
14
|
+
@process_pool_size = nil
|
15
|
+
@after_fork_handler = Proc.new {}
|
16
|
+
@subprocess_name = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def after_fork(p = nil, &block)
|
20
|
+
fail 'after_fork needs a block or Proc object' unless (p && p.is_a?(Proc)) || block_given?
|
21
|
+
@after_fork_handler = p || block
|
22
|
+
end
|
23
|
+
|
24
|
+
class << self
|
25
|
+
def get
|
26
|
+
@configuration ||= Configuration.new
|
27
|
+
end
|
28
|
+
|
29
|
+
ATTRIBUTES.each do |a|
|
30
|
+
define_method(a) do
|
31
|
+
get.instance_variable_get("@#{a}")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module DontStallMyProcess
|
2
|
+
DontStallMyProcessError = Class.new(StandardError)
|
3
|
+
|
4
|
+
# This exception is raised when the watchdog bites.
|
5
|
+
TimeoutExceeded = Class.new(StandardError)
|
6
|
+
|
7
|
+
# This exception is raised when the subprocess could not be created, or
|
8
|
+
# its initialization failed.
|
9
|
+
SubprocessInitializationFailed = Class.new(StandardError)
|
10
|
+
|
11
|
+
# This exception is raised when a forbidden Kernel method is called.
|
12
|
+
# These methods do not get forwarded over the DRb.
|
13
|
+
KernelMethodCalled = Class.new(StandardError)
|
14
|
+
end
|
@@ -4,48 +4,90 @@ module DontStallMyProcess
|
|
4
4
|
# The ChildProcess class encapsulates the forked subprocess that
|
5
5
|
# provides the DRb service object.
|
6
6
|
class ChildProcess
|
7
|
-
attr_reader :
|
7
|
+
attr_reader :pid
|
8
8
|
|
9
|
-
def initialize
|
9
|
+
def initialize
|
10
10
|
# Start RemoteApplication in child process, connect to it thru pipe.
|
11
11
|
r, w = IO.pipe
|
12
12
|
@pid = fork do
|
13
13
|
r.close
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
|
15
|
+
# Disable process cleanup, otherwise killing the
|
16
|
+
# subprocess (when Configuration.skip_at_exit_handlers is false) will
|
17
|
+
# terminate all the other subprocesses, or at least screw with their
|
18
|
+
# unix sockets.
|
19
|
+
ProcessExitHandler.disable_at_exit
|
20
|
+
|
21
|
+
app = DontStallMyProcess::Remote::RemoteApplication.new(w)
|
22
|
+
app.loop!
|
18
23
|
end
|
19
24
|
w.close
|
20
25
|
|
21
|
-
# Wait for the RemoteApplication to finish its setup
|
22
|
-
|
23
|
-
r
|
26
|
+
# Wait for the RemoteApplication to finish its setup, and retrieve
|
27
|
+
# the URI to the RemoteApplicationController.
|
28
|
+
ctrl_uri = Marshal.load(r)
|
24
29
|
|
25
|
-
# RemoteApplication sends us the
|
26
|
-
|
27
|
-
|
30
|
+
# RemoteApplication sends us the URI or an Exception instance if anything
|
31
|
+
# went wrong.
|
32
|
+
if ctrl_uri.is_a?(Exception)
|
33
|
+
e = SubprocessInitializationFailed.new(ctrl_uri)
|
34
|
+
e.set_backtrace(ctrl_uri.backtrace)
|
35
|
+
raise e
|
36
|
+
end
|
37
|
+
|
38
|
+
# Connect to and store the controller DRb client.
|
39
|
+
@controller = DRbObject.new_with_uri(ctrl_uri)
|
40
|
+
|
41
|
+
# Setup LocalProxy registry for this pid.
|
42
|
+
LocalProxy.setup_proxy_registry(@pid) do
|
43
|
+
# This block will be called when all LocalProxies
|
44
|
+
# of this pid are gone (either garbage-collected or
|
45
|
+
# manually destroyed by 'stop_service!').
|
46
|
+
|
47
|
+
# Hand back ourself to the ChildProcessPool to get new jobs.
|
48
|
+
ChildProcessPool.free(self)
|
49
|
+
end
|
28
50
|
|
29
|
-
|
30
|
-
|
31
|
-
|
51
|
+
@alive = true
|
52
|
+
ensure
|
53
|
+
r.close
|
32
54
|
end
|
33
55
|
|
34
|
-
def
|
35
|
-
|
56
|
+
def start_service(klass, opts)
|
57
|
+
uri = @controller.start_service(klass, opts)
|
58
|
+
|
59
|
+
# Create the main proxy class.
|
60
|
+
Local::LocalProxy.new(uri, self, opts)
|
36
61
|
end
|
37
62
|
|
38
|
-
def
|
39
|
-
|
63
|
+
def alive?
|
64
|
+
@alive && @controller.alive? rescue false
|
40
65
|
end
|
41
66
|
|
42
|
-
def
|
43
|
-
|
67
|
+
def quit
|
68
|
+
@controller.stop_application rescue nil
|
69
|
+
@controller = nil
|
70
|
+
sleep 0.5
|
71
|
+
terminate(false, 0.5)
|
44
72
|
end
|
45
73
|
|
46
|
-
def
|
74
|
+
def terminate(sigkill = true, term_sleep = 5)
|
75
|
+
unless Configuration.sigkill_only
|
76
|
+
Process.kill('TERM', @pid)
|
77
|
+
sleep term_sleep
|
78
|
+
end
|
79
|
+
|
47
80
|
# http://stackoverflow.com/questions/325082/how-can-i-check-from-ruby-whether-a-process-with-a-certain-pid-is-running
|
48
|
-
Process.waitpid(@pid, Process::WNOHANG).nil?
|
81
|
+
Process.kill('KILL', @pid) if sigkill && Process.waitpid(@pid, Process::WNOHANG).nil?
|
82
|
+
|
83
|
+
# Collect process status to not have a zombie hanging around.
|
84
|
+
Process.wait(@pid)
|
85
|
+
|
86
|
+
# Do not reuse this process ever again.
|
87
|
+
@alive = false
|
88
|
+
rescue
|
89
|
+
# Exceptions in these Process.* methods almost always mean the process is already dead.
|
90
|
+
nil
|
49
91
|
end
|
50
92
|
end
|
51
93
|
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module DontStallMyProcess
|
2
|
+
module Local
|
3
|
+
class ChildProcessPool
|
4
|
+
MAX_ALLOC_TRIES = 5
|
5
|
+
SEMAPHORE = Mutex.new
|
6
|
+
|
7
|
+
def self.alloc
|
8
|
+
process = nil
|
9
|
+
tries = MAX_ALLOC_TRIES
|
10
|
+
until (process && process.alive?) || tries == 0
|
11
|
+
tries -= 1
|
12
|
+
SEMAPHORE.synchronize do
|
13
|
+
if !@pool || @pool.empty?
|
14
|
+
process = ChildProcess.new
|
15
|
+
else
|
16
|
+
process = @pool.shift
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
unless process && process.alive?
|
22
|
+
fail SubprocessInitializationFailed, "failed to allocated subprocess (tried #{MAX_ALLOC_TRIES} times)"
|
23
|
+
end
|
24
|
+
|
25
|
+
process
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.free(process)
|
29
|
+
# We do not want killed processes to enter the pool again.
|
30
|
+
return unless process.alive?
|
31
|
+
|
32
|
+
terminate = true
|
33
|
+
|
34
|
+
if Configuration.process_pool_size && Configuration.process_pool_size > 0
|
35
|
+
SEMAPHORE.synchronize do
|
36
|
+
@pool ||= []
|
37
|
+
if @pool.size < Configuration.process_pool_size
|
38
|
+
@pool << process
|
39
|
+
terminate = false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
process.quit if terminate
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.each(&block)
|
48
|
+
@pool.each(&block) if @pool
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -6,14 +6,39 @@ module DontStallMyProcess
|
|
6
6
|
|
7
7
|
# LocalProxy connects to an instance of a class on a child process
|
8
8
|
# and watches the execution of remote procedure calls.
|
9
|
+
# Furthermore, it takes care of automatically ending the remote DRb
|
10
|
+
# service when the proxy object gets garbage-collected.
|
9
11
|
class LocalProxy
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
12
|
+
class << self
|
13
|
+
include DontStallMyProcess::ProxyRegistry
|
14
|
+
|
15
|
+
def gc_finalize_proxy(pid, uri)
|
16
|
+
LocalProxy.unregister(pid, proxy_uri) { |proxy| proxy.__destroy }
|
17
|
+
end
|
18
|
+
|
19
|
+
def gc_finalize_proxy_proc(pid, uri)
|
20
|
+
# http://www.mikeperham.com/2010/02/24/the-trouble-with-ruby-finalizers/
|
21
|
+
proc { LocalProxy.gc_finalize_proxy(pid, uri) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(uri, process, opts)
|
26
|
+
@uri = uri
|
27
|
+
@process = process
|
28
|
+
@opts = opts
|
14
29
|
|
15
30
|
# Get a DRb reference to the remote class.
|
16
|
-
@object
|
31
|
+
@object = DRbObject.new_with_uri(uri)
|
32
|
+
|
33
|
+
# Store this proxy in the registry for book-keeping.
|
34
|
+
LocalProxy.register(process.pid, uri, self)
|
35
|
+
|
36
|
+
# Destroy the proxy on GC.
|
37
|
+
ObjectSpace.define_finalizer(self, self.class.gc_finalize_proxy_proc(process.pid, uri))
|
38
|
+
end
|
39
|
+
|
40
|
+
def stop_service!
|
41
|
+
LocalProxy.each_proxy(@process.pid) { |proxy| proxy.send(:__destroy) }
|
17
42
|
end
|
18
43
|
|
19
44
|
def respond_to?(m, ia = false)
|
@@ -35,18 +60,12 @@ module DontStallMyProcess
|
|
35
60
|
end
|
36
61
|
end
|
37
62
|
|
38
|
-
|
39
|
-
|
40
|
-
def __create_nested_proxy(meth, *args, &block)
|
41
|
-
# Get the DRb URI from the remote.
|
42
|
-
uri = __timed(meth) { @object.public_send(meth, *args, &block) }
|
43
|
-
|
44
|
-
# Create a new local proxy and return that.
|
45
|
-
# Note: We do not need to cache these here, as there can be multiple
|
46
|
-
# clients to a single DRb service.
|
47
|
-
LocalProxy.new(uri, @process, @opts[:methods][meth], @sigkill_only)
|
63
|
+
def inspect
|
64
|
+
self.to_s.gsub('>', " URI=#{@uri} CHILD_PID=#{@process.pid}>")
|
48
65
|
end
|
49
66
|
|
67
|
+
private
|
68
|
+
|
50
69
|
def __delegate_with_timeout(meth, *args, &block)
|
51
70
|
__timed(meth) do
|
52
71
|
@object.public_send(meth, *args, &block)
|
@@ -58,55 +77,23 @@ module DontStallMyProcess
|
|
58
77
|
yield
|
59
78
|
end
|
60
79
|
rescue Timeout::Error
|
61
|
-
|
80
|
+
@process.terminate
|
62
81
|
fail TimeoutExceeded, "Method #{meth} took more than #{@opts[:timeout]} seconds to process! Child process killed."
|
63
82
|
end
|
64
83
|
|
65
|
-
def
|
66
|
-
|
67
|
-
|
68
|
-
@process.term
|
69
|
-
sleep 5
|
70
|
-
end
|
71
|
-
|
72
|
-
@process.detach
|
73
|
-
@process.kill if @process.alive?
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
# MainLocalProxy encapsulates the main DRb client, i.e. the top-level
|
78
|
-
# client class requested by the user. It takes care of initially starting
|
79
|
-
# the DRb service for callbacks and cleaning up child processes on
|
80
|
-
# garbage collection.
|
81
|
-
class MainLocalProxy < LocalProxy
|
82
|
-
def self.register_remote_proxy(main_uri, object)
|
83
|
-
@objects ||= {}
|
84
|
-
@objects[main_uri] = object
|
85
|
-
end
|
86
|
-
|
87
|
-
def self.stop_remote_application(main_uri)
|
88
|
-
@objects[main_uri].stop! rescue nil
|
89
|
-
@process.wait
|
90
|
-
end
|
84
|
+
def __create_nested_proxy(meth, *args, &block)
|
85
|
+
# Get the DRb URI from the remote.
|
86
|
+
uri = __timed(meth) { @object.public_send(meth, *args, &block) }
|
91
87
|
|
92
|
-
|
93
|
-
|
94
|
-
proc { MainLocalProxy.stop_remote_application(main_uri) }
|
88
|
+
# Create a new local proxy and return that.
|
89
|
+
LocalProxy.new(uri, @process, @opts[:methods][meth])
|
95
90
|
end
|
96
91
|
|
97
|
-
def
|
98
|
-
#
|
99
|
-
|
100
|
-
# This looks weird, but doesn't affect concurrent uses of multiple
|
101
|
-
# Watchdogs, I tested it. Trust me.
|
102
|
-
DRb.start_service
|
103
|
-
|
104
|
-
# Initialize the base class, connect to the DRb service or the main client class.
|
105
|
-
super(process.main_uri, process, opts, sigkill_only)
|
92
|
+
def __destroy
|
93
|
+
# We rescue the exception here in case the subprocess is already dead.
|
94
|
+
@object.__local_proxy_destroyed rescue nil
|
106
95
|
|
107
|
-
|
108
|
-
MainLocalProxy.register_remote_proxy(process.main_uri, @object)
|
109
|
-
ObjectSpace.define_finalizer(self, self.class.stop_remote_application_proc(process.main_uri))
|
96
|
+
LocalProxy.unregister(@process.pid, @uri)
|
110
97
|
end
|
111
98
|
end
|
112
99
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module DontStallMyProcess
|
4
|
+
module Local
|
5
|
+
class ProcessExitHandler
|
6
|
+
def self.disable_at_exit
|
7
|
+
@at_exit_disabled = true
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.at_exit_disabled?
|
11
|
+
@at_exit_disabled
|
12
|
+
end
|
13
|
+
|
14
|
+
at_exit do
|
15
|
+
# If we're in a subprocess, this handler should not run.
|
16
|
+
unless ProcessExitHandler.at_exit_disabled?
|
17
|
+
|
18
|
+
# Make sure we terminate all subprocesses when
|
19
|
+
# the main process exits.
|
20
|
+
ChildProcessPool.each do |process|
|
21
|
+
process.quit
|
22
|
+
end
|
23
|
+
|
24
|
+
# Clean remaining unix sockets from /tmp.
|
25
|
+
FileUtils.rm(Dir["/tmp/dsmp-#{Process.pid}*"])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module DontStallMyProcess
|
2
|
+
module ProxyRegistry
|
3
|
+
def setup_proxy_registry(pid, &block)
|
4
|
+
@empty_handler ||= {}
|
5
|
+
@empty_handler[pid] = block
|
6
|
+
|
7
|
+
@proxies ||= {}
|
8
|
+
@proxies[pid] = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def register(pid, key, object)
|
12
|
+
@proxies[pid][key] = object
|
13
|
+
end
|
14
|
+
|
15
|
+
def unregister(pid, key)
|
16
|
+
if @proxies[pid].key?(key) && block_given?
|
17
|
+
yield @proxies[pid]
|
18
|
+
end
|
19
|
+
@proxies[pid].delete(key)
|
20
|
+
|
21
|
+
# Check if all proxies are gone now and call the process.
|
22
|
+
@empty_handler[pid].call if @proxies[pid].empty?
|
23
|
+
end
|
24
|
+
|
25
|
+
def each_proxy(pid, &block)
|
26
|
+
@proxies[pid].dup.values.each(&block)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -4,18 +4,62 @@ module DontStallMyProcess
|
|
4
4
|
# RemoteObject is the base 'application' class for the child process.
|
5
5
|
# It starts the DRb service and goes to sleep.
|
6
6
|
class RemoteApplication
|
7
|
-
def
|
7
|
+
def self.update_process_name(klass_name = '<pool>')
|
8
|
+
if Configuration.subprocess_name
|
9
|
+
$0 = Configuration.subprocess_name % { klass: klass_name.to_s }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(pipe)
|
14
|
+
# Do not write to stdout/stderr unless requested by the client.
|
15
|
+
if Configuration.close_stdio
|
16
|
+
$stdout.reopen('/dev/null', 'w')
|
17
|
+
$stderr.reopen('/dev/null', 'w')
|
18
|
+
end
|
19
|
+
|
20
|
+
# Reset signal handlers if requested by client.
|
21
|
+
if Configuration.restore_all_traps
|
22
|
+
# Plenty of signals are not trappable, simply ignore this here.
|
23
|
+
Signal.list.keys.each { |sig| Signal.trap(sig, 'DEFAULT') rescue nil }
|
24
|
+
end
|
25
|
+
|
26
|
+
if Configuration.skip_at_exit_handlers
|
27
|
+
# Clearing the Ruby end proc list is not possible without modifying
|
28
|
+
# Ruby itself, see eval_jump.c in current Ruby sources. However,
|
29
|
+
# as these handlers get executed in reverse order, simply calling
|
30
|
+
# exit! here is enough.
|
31
|
+
at_exit { exit! }
|
32
|
+
end
|
33
|
+
|
34
|
+
# Call the after_block_handler early, before DRb setup (i.e. before anything
|
35
|
+
# can go wrong).
|
36
|
+
Configuration.after_fork_handler.call
|
37
|
+
|
38
|
+
# Initially display the process name without a class name (-> '<pool>').
|
39
|
+
RemoteApplication.update_process_name
|
40
|
+
|
41
|
+
RemoteProxy.setup_proxy_registry(Process.pid) do
|
42
|
+
# This block is called when all RemoteProxies have closed there
|
43
|
+
# DRb servers. We're now idling again, waiting for new jobs in the
|
44
|
+
# pool *or* are going to be be terminated in a second.
|
45
|
+
|
46
|
+
# Update process name again to indicate availability.
|
47
|
+
RemoteApplication.update_process_name
|
48
|
+
end
|
49
|
+
|
50
|
+
# Start our controller class, expose via DRb.
|
51
|
+
@controller = RemoteApplicationController.new(self)
|
52
|
+
|
53
|
+
# Everything went fine, set up the main process synchronization now.
|
8
54
|
@m = Mutex.new
|
9
55
|
@c = ConditionVariable.new
|
10
56
|
|
11
|
-
# Start the main DRb service.
|
12
|
-
proxy = MainRemoteProxy.new(self, klass, opts)
|
13
|
-
|
14
57
|
# Tell our parent that setup is done and the new main DRb URI.
|
15
|
-
|
58
|
+
Marshal.dump(@controller.uri, pipe)
|
16
59
|
rescue => e
|
17
60
|
# Something went wrong, also tell our parent.
|
18
|
-
|
61
|
+
Marshal.dump(e, pipe)
|
62
|
+
raise
|
19
63
|
ensure
|
20
64
|
pipe.close
|
21
65
|
end
|
@@ -25,17 +69,17 @@ module DontStallMyProcess
|
|
25
69
|
@m.synchronize do
|
26
70
|
@c.wait(@m)
|
27
71
|
end
|
72
|
+
|
73
|
+
# Remote application end.
|
74
|
+
exit 0
|
75
|
+
rescue SystemExit, Interrupt
|
76
|
+
raise
|
28
77
|
end
|
29
78
|
|
30
79
|
def stop!
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
# End main thread -> exit.
|
36
|
-
@m.synchronize do
|
37
|
-
@c.signal
|
38
|
-
end
|
80
|
+
# End main thread -> exit.
|
81
|
+
@m.synchronize do
|
82
|
+
@c.signal
|
39
83
|
end
|
40
84
|
end
|
41
85
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'drb'
|
3
|
+
|
4
|
+
module DontStallMyProcess
|
5
|
+
module Remote
|
6
|
+
class RemoteApplicationController
|
7
|
+
attr_reader :uri
|
8
|
+
|
9
|
+
def initialize(application)
|
10
|
+
@applicarion = application
|
11
|
+
|
12
|
+
@uri = "drbunix:///tmp/dsmp-#{Process.ppid}-controller-#{Process.pid}"
|
13
|
+
@server = DRb.start_service(uri, self)
|
14
|
+
end
|
15
|
+
|
16
|
+
def start_service(klass, opts)
|
17
|
+
# Instantiate the main class now to get early failures.
|
18
|
+
instance = klass.new
|
19
|
+
|
20
|
+
# Start the main DRb service.
|
21
|
+
@proxy = RemoteProxy.new(opts, instance)
|
22
|
+
|
23
|
+
# Set subprocess name if requested.
|
24
|
+
RemoteApplication.update_process_name(klass.name.to_s)
|
25
|
+
|
26
|
+
# Return the DRb URI.
|
27
|
+
@proxy.uri
|
28
|
+
end
|
29
|
+
|
30
|
+
def stop_application
|
31
|
+
# Kill remaining DRb servers, shouldn't be any at this point.
|
32
|
+
RemoteProxy.each_proxy { |proxy| __destroy.destroy }
|
33
|
+
|
34
|
+
Thread.new do
|
35
|
+
# Wait for DRb answer package to be sent.
|
36
|
+
sleep 0.2
|
37
|
+
|
38
|
+
# Kill our own DRb server.
|
39
|
+
@server.stop_service
|
40
|
+
FileUtils.rm_f(@uri)
|
41
|
+
|
42
|
+
# Let DRb resolve its pthread mutexes and stuff.
|
43
|
+
sleep 0.2
|
44
|
+
|
45
|
+
# Wake up the main application thread.
|
46
|
+
@application.stop!
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def alive?
|
51
|
+
true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'fileutils'
|
1
2
|
require 'drb'
|
2
3
|
require 'securerandom'
|
3
4
|
|
@@ -9,15 +10,24 @@ module DontStallMyProcess
|
|
9
10
|
# instance of the 'real' class. Furthermore, it takes care of creating
|
10
11
|
# nested DRb services as requested in the option hash.
|
11
12
|
class RemoteProxy
|
13
|
+
class << self
|
14
|
+
include DontStallMyProcess::ProxyRegistry
|
15
|
+
end
|
16
|
+
|
12
17
|
attr_reader :uri
|
13
18
|
|
14
|
-
def initialize(opts, instance)
|
19
|
+
def initialize(opts, instance, parent = nil)
|
15
20
|
@opts = opts
|
16
21
|
@object = instance
|
17
|
-
@children = {}
|
18
22
|
|
19
|
-
@uri = "drbunix:///tmp/dsmp-#{SecureRandom.hex(8)}"
|
20
|
-
DRb.start_service(uri, self)
|
23
|
+
@uri = "drbunix:///tmp/dsmp-#{Process.ppid}-#{SecureRandom.hex(8)}"
|
24
|
+
@server = DRb.start_service(@uri, self)
|
25
|
+
|
26
|
+
RemoteProxy.register(Process.pid, @uri, self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def __local_proxy_destroyed
|
30
|
+
__destroy
|
21
31
|
end
|
22
32
|
|
23
33
|
def respond_to?(m, ia = false)
|
@@ -39,37 +49,18 @@ module DontStallMyProcess
|
|
39
49
|
private
|
40
50
|
|
41
51
|
def __create_nested_proxy(meth, *args, &block)
|
42
|
-
|
43
|
-
unless @children[meth]
|
44
|
-
# Call the object method now, save the returned object.
|
45
|
-
instance = @object.public_send(meth, *args, &block)
|
52
|
+
instance = @object.public_send(meth, *args, &block)
|
46
53
|
|
47
|
-
|
48
|
-
|
49
|
-
end
|
50
|
-
|
51
|
-
# Return the DRb URI.
|
52
|
-
@children[meth]
|
54
|
+
# Start the proxy, convert the object into a DRb service.
|
55
|
+
RemoteProxy.new(@opts[:methods][meth], instance, self).uri
|
53
56
|
end
|
54
|
-
end
|
55
57
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
# gracefully.
|
60
|
-
class MainRemoteProxy < RemoteProxy
|
61
|
-
def initialize(mother, klass, opts)
|
62
|
-
@mother = mother
|
58
|
+
def __destroy
|
59
|
+
@server.stop_service
|
60
|
+
FileUtils.rm_f(@uri)
|
63
61
|
|
64
|
-
|
65
|
-
# the new instance, create the DRb service.
|
66
|
-
super(opts, klass.new)
|
67
|
-
end
|
68
|
-
|
69
|
-
def stop!
|
70
|
-
@mother.stop!
|
62
|
+
RemoteProxy.unregister(Process.pid, @uri)
|
71
63
|
end
|
72
64
|
end
|
73
|
-
|
74
65
|
end
|
75
66
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dont-stall-my-process
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- FlavourSys Technology GmbH
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-09-
|
11
|
+
date: 2014-09-08 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Executes code or native extensions in child processes and monitors their
|
14
14
|
execution times
|
@@ -18,9 +18,15 @@ extensions: []
|
|
18
18
|
extra_rdoc_files: []
|
19
19
|
files:
|
20
20
|
- lib/dont-stall-my-process.rb
|
21
|
+
- lib/dont-stall-my-process/configuration.rb
|
22
|
+
- lib/dont-stall-my-process/exceptions.rb
|
21
23
|
- lib/dont-stall-my-process/local/child_process.rb
|
24
|
+
- lib/dont-stall-my-process/local/child_process_pool.rb
|
22
25
|
- lib/dont-stall-my-process/local/local_proxy.rb
|
26
|
+
- lib/dont-stall-my-process/local/process_exit_handler.rb
|
27
|
+
- lib/dont-stall-my-process/proxy_registry.rb
|
23
28
|
- lib/dont-stall-my-process/remote/remote_application.rb
|
29
|
+
- lib/dont-stall-my-process/remote/remote_application_controller.rb
|
24
30
|
- lib/dont-stall-my-process/remote/remote_proxy.rb
|
25
31
|
- lib/dont-stall-my-process/version.rb
|
26
32
|
homepage: http://github.com/flavoursys/dont-stall-my-process
|