dont-stall-my-process 0.0.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 +7 -0
- data/lib/dont-stall-my-process.rb +48 -0
- data/lib/dont-stall-my-process/local/child_process.rb +53 -0
- data/lib/dont-stall-my-process/local/local_proxy.rb +113 -0
- data/lib/dont-stall-my-process/remote/remote_application.rb +44 -0
- data/lib/dont-stall-my-process/remote/remote_proxy.rb +75 -0
- data/lib/dont-stall-my-process/version.rb +3 -0
- metadata +50 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: bba5b99c3f45cf21b973302a4b9d43711045d8e0
|
4
|
+
data.tar.gz: b5754b5ebf28641cb7968a6a117e17c6fbf6f43e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d3c8179f80acb4dbbb5a84311d18fa0625dd4a5980cfe4692d74c32cba4555b68de05d12caec8879f4b239f936185b7e6e864b49ee25d0dfc753b026b0f45e65
|
7
|
+
data.tar.gz: 5b56b6b4699377beebcd92479930b31390897976b46763f8d6f222032ece2404e05cc32cda0be096fe28ba9425155ea9ae19ac3630ac6471e4c89440a44dfb68
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'dont-stall-my-process/local/child_process'
|
2
|
+
require 'dont-stall-my-process/local/local_proxy'
|
3
|
+
require 'dont-stall-my-process/remote/remote_application'
|
4
|
+
require 'dont-stall-my-process/remote/remote_proxy'
|
5
|
+
require 'dont-stall-my-process/version'
|
6
|
+
|
7
|
+
module DontStallMyProcess
|
8
|
+
DEFAULT_TIMEOUT = 300
|
9
|
+
|
10
|
+
def self.create(klass, opts = {}, sigkill_only = false)
|
11
|
+
fail 'no klass given' unless klass && klass.is_a?(Class)
|
12
|
+
|
13
|
+
# Set default values and validate configuration.
|
14
|
+
opts = sanitize_options(opts)
|
15
|
+
|
16
|
+
# Fork the child process.
|
17
|
+
p = Local::ChildProcess.new(klass, opts)
|
18
|
+
|
19
|
+
# Return the main proxy class.
|
20
|
+
Local::MainLocalProxy.new(p, opts, sigkill_only)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.sanitize_options(opts, default_timeout = DEFAULT_TIMEOUT)
|
24
|
+
fail 'opts is not a hash' unless opts.is_a?(Hash)
|
25
|
+
|
26
|
+
opts[:timeout] ||= default_timeout
|
27
|
+
opts[:methods] ||= {}
|
28
|
+
|
29
|
+
fail 'no timeout given' unless opts[:timeout] && opts[:timeout].is_a?(Fixnum)
|
30
|
+
fail 'timeout too low' unless opts[:timeout] > 0
|
31
|
+
fail 'methods is not a hash' if opts[:methods] && !opts[:methods].is_a?(Hash)
|
32
|
+
|
33
|
+
{
|
34
|
+
timeout: opts[:timeout],
|
35
|
+
methods: Hash[
|
36
|
+
opts[:methods].map { |meth, mopts| [meth, sanitize_options(mopts, opts[:timeout])] }
|
37
|
+
]
|
38
|
+
}
|
39
|
+
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
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module DontStallMyProcess
|
2
|
+
module Local
|
3
|
+
|
4
|
+
# The ChildProcess class encapsulates the forked subprocess that
|
5
|
+
# provides the DRb service object.
|
6
|
+
class ChildProcess
|
7
|
+
attr_reader :main_uri
|
8
|
+
|
9
|
+
def initialize(klass, opts)
|
10
|
+
# Start RemoteApplication in child process, connect to it thru pipe.
|
11
|
+
r, w = IO.pipe
|
12
|
+
@pid = fork do
|
13
|
+
r.close
|
14
|
+
$stdin.close rescue nil
|
15
|
+
$stdout.reopen('/dev/null', 'w')
|
16
|
+
$stderr.reopen('/dev/null', 'w')
|
17
|
+
DontStallMyProcess::Remote::RemoteApplication.new(klass, opts, w).loop!
|
18
|
+
end
|
19
|
+
w.close
|
20
|
+
|
21
|
+
# Wait for the RemoteApplication to finish its setup.
|
22
|
+
@main_uri = Marshal.load(r.gets)
|
23
|
+
r.close
|
24
|
+
|
25
|
+
# RemoteApplication sends us the DRb URI or an Exception.
|
26
|
+
raise @main_uri if @main_uri.is_a?(Exception)
|
27
|
+
end
|
28
|
+
|
29
|
+
def term
|
30
|
+
# Exceptions in these methods almost always mean the process is already dead.
|
31
|
+
Process.kill('TERM', @pid) rescue nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def detach
|
35
|
+
Process.detach(@pid) rescue nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def kill
|
39
|
+
Process.kill('KILL', @pid) rescue nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def wait
|
43
|
+
Process.wait(@pid) rescue nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def alive?
|
47
|
+
# 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?
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'drb'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
module DontStallMyProcess
|
5
|
+
module Local
|
6
|
+
|
7
|
+
# LocalProxy connects to an instance of a class on a child process
|
8
|
+
# and watches the execution of remote procedure calls.
|
9
|
+
class LocalProxy
|
10
|
+
def initialize(uri, process, opts, sigkill_only)
|
11
|
+
@process = process
|
12
|
+
@opts = opts
|
13
|
+
@sigkill_only = sigkill_only
|
14
|
+
|
15
|
+
# Get a DRb reference to the remote class.
|
16
|
+
@object = DRbObject.new_with_uri(uri)
|
17
|
+
end
|
18
|
+
|
19
|
+
def respond_to?(m, ia = false)
|
20
|
+
@opts[:methods].keys.include?(m) || @object.respond_to?(m, ia) || super(m, ia)
|
21
|
+
end
|
22
|
+
|
23
|
+
def method_missing(meth, *args, &block)
|
24
|
+
case
|
25
|
+
when Kernel.public_methods(false).include?(meth)
|
26
|
+
fail KernelMethodCalled, "Method '#{meth}' called. This would run the method that was privately inherited " +
|
27
|
+
'from the \'Kernel\' module on the local proxy, which is most certainly not what you want. Kernel methods ' +
|
28
|
+
'are not supported at the moment. Please consider adding an alias to your function.'
|
29
|
+
when @opts[:methods].keys.include?(meth)
|
30
|
+
__create_nested_proxy(meth, *args, &block)
|
31
|
+
when @object.respond_to?(meth)
|
32
|
+
__delegate_with_timeout(meth, *args, &block)
|
33
|
+
else
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
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)
|
48
|
+
end
|
49
|
+
|
50
|
+
def __delegate_with_timeout(meth, *args, &block)
|
51
|
+
__timed(meth) do
|
52
|
+
@object.public_send(meth, *args, &block)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def __timed(meth)
|
57
|
+
Timeout.timeout(@opts[:timeout]) do
|
58
|
+
yield
|
59
|
+
end
|
60
|
+
rescue Timeout::Error
|
61
|
+
__kill_child_process!
|
62
|
+
fail TimeoutExceeded, "Method #{meth} took more than #{@opts[:timeout]} seconds to process! Child process killed."
|
63
|
+
end
|
64
|
+
|
65
|
+
def __kill_child_process!
|
66
|
+
unless @sigkill_only
|
67
|
+
Sidekiq.logger.info "TERMINATIIIING"
|
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
|
91
|
+
|
92
|
+
def self.stop_remote_application_proc(main_uri)
|
93
|
+
# See also: http://www.mikeperham.com/2010/02/24/the-trouble-with-ruby-finalizers/
|
94
|
+
proc { MainLocalProxy.stop_remote_application(main_uri) }
|
95
|
+
end
|
96
|
+
|
97
|
+
def initialize(process, opts, sigkill_only)
|
98
|
+
# Start a local DRbServer (unnamed?) for callbacks (blocks!).
|
99
|
+
# Each new Watchdog will overwrite the main master DRbServer.
|
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)
|
106
|
+
|
107
|
+
# Stop the child process on GC.
|
108
|
+
MainLocalProxy.register_remote_proxy(process.main_uri, @object)
|
109
|
+
ObjectSpace.define_finalizer(self, self.class.stop_remote_application_proc(process.main_uri))
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module DontStallMyProcess
|
2
|
+
module Remote
|
3
|
+
|
4
|
+
# RemoteObject is the base 'application' class for the child process.
|
5
|
+
# It starts the DRb service and goes to sleep.
|
6
|
+
class RemoteApplication
|
7
|
+
def initialize(klass, opts, pipe)
|
8
|
+
@m = Mutex.new
|
9
|
+
@c = ConditionVariable.new
|
10
|
+
|
11
|
+
# Start the main DRb service.
|
12
|
+
proxy = MainRemoteProxy.new(self, klass, opts)
|
13
|
+
|
14
|
+
# Tell our parent that setup is done and the new main DRb URI.
|
15
|
+
pipe.write(Marshal.dump(proxy.uri))
|
16
|
+
rescue => e
|
17
|
+
# Something went wrong, also tell our parent.
|
18
|
+
pipe.write(Marshal.dump(e))
|
19
|
+
ensure
|
20
|
+
pipe.close
|
21
|
+
end
|
22
|
+
|
23
|
+
def loop!
|
24
|
+
# Sleep to keep the DRb service running, until woken up.
|
25
|
+
@m.synchronize do
|
26
|
+
@c.wait(@m)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def stop!
|
31
|
+
Thread.new do
|
32
|
+
# Wait for DRb answer package to be sent.
|
33
|
+
sleep 0.2
|
34
|
+
|
35
|
+
# End main thread -> exit.
|
36
|
+
@m.synchronize do
|
37
|
+
@c.signal
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'drb'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
module DontStallMyProcess
|
5
|
+
module Remote
|
6
|
+
|
7
|
+
# RemoteProxy is an decorator class for any of the 'real' classes
|
8
|
+
# to be served via DRb. It delegates method calls to the encapsulated
|
9
|
+
# instance of the 'real' class. Furthermore, it takes care of creating
|
10
|
+
# nested DRb services as requested in the option hash.
|
11
|
+
class RemoteProxy
|
12
|
+
attr_reader :uri
|
13
|
+
|
14
|
+
def initialize(opts, instance)
|
15
|
+
@opts = opts
|
16
|
+
@object = instance
|
17
|
+
@children = {}
|
18
|
+
|
19
|
+
@uri = "drbunix:///tmp/dsmp-#{SecureRandom.hex(8)}"
|
20
|
+
DRb.start_service(uri, self)
|
21
|
+
end
|
22
|
+
|
23
|
+
def respond_to?(m, ia = false)
|
24
|
+
@opts[:methods].keys.include?(m) || @object.respond_to?(m, ia) || super(m, ia)
|
25
|
+
end
|
26
|
+
|
27
|
+
def method_missing(meth, *args, &block)
|
28
|
+
case
|
29
|
+
when (mopts = @opts[:methods][meth])
|
30
|
+
__create_nested_proxy(meth, *args, &block)
|
31
|
+
when @object.respond_to?(meth)
|
32
|
+
# Delegate the method call to the real object.
|
33
|
+
@object.public_send(meth, *args, &block)
|
34
|
+
else
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def __create_nested_proxy(meth, *args, &block)
|
42
|
+
# Create a new DRb proxy if needed, and save its URI.
|
43
|
+
unless @children[meth]
|
44
|
+
# Call the object method now, save the returned object.
|
45
|
+
instance = @object.public_send(meth, *args, &block)
|
46
|
+
|
47
|
+
# Start the proxy, convert the object 0into a DRb service.
|
48
|
+
@children[meth] = RemoteProxy.new(@opts[:methods][meth], instance).uri
|
49
|
+
end
|
50
|
+
|
51
|
+
# Return the DRb URI.
|
52
|
+
@children[meth]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# The MainRemoteProxy is the first DRb object to be created in
|
57
|
+
# the child process. In addition to the real class' methods it
|
58
|
+
# provides a 'stop!' method that brings down the child process
|
59
|
+
# gracefully.
|
60
|
+
class MainRemoteProxy < RemoteProxy
|
61
|
+
def initialize(mother, klass, opts)
|
62
|
+
@mother = mother
|
63
|
+
|
64
|
+
# Instantiate the main class now, initialize the base class with
|
65
|
+
# the new instance, create the DRb service.
|
66
|
+
super(opts, klass.new)
|
67
|
+
end
|
68
|
+
|
69
|
+
def stop!
|
70
|
+
@mother.stop!
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
metadata
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dont-stall-my-process
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- FlavourSys Technology GmbH
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-09-05 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Executes code or native extensions in child processes and monitors their
|
14
|
+
execution times
|
15
|
+
email: technology@flavoursys.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/dont-stall-my-process.rb
|
21
|
+
- lib/dont-stall-my-process/local/child_process.rb
|
22
|
+
- lib/dont-stall-my-process/local/local_proxy.rb
|
23
|
+
- lib/dont-stall-my-process/remote/remote_application.rb
|
24
|
+
- lib/dont-stall-my-process/remote/remote_proxy.rb
|
25
|
+
- lib/dont-stall-my-process/version.rb
|
26
|
+
homepage: http://github.com/flavoursys/dont-stall-my-process
|
27
|
+
licenses:
|
28
|
+
- MIT
|
29
|
+
metadata: {}
|
30
|
+
post_install_message:
|
31
|
+
rdoc_options: []
|
32
|
+
require_paths:
|
33
|
+
- lib
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
requirements: []
|
45
|
+
rubyforge_project:
|
46
|
+
rubygems_version: 2.2.2
|
47
|
+
signing_key:
|
48
|
+
specification_version: 4
|
49
|
+
summary: Fork/Watchdog jail your Ruby code and native extensions
|
50
|
+
test_files: []
|