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 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: []