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 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
@@ -0,0 +1,3 @@
1
+ module DontStallMyProcess
2
+ VERSION = '0.0.1'
3
+ 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: []