dont-stall-my-process 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|