forked 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1f591d8bc0b287d508ca0663dc5da9eaff43aaf9e52dd493650a415e2e8046a6
4
+ data.tar.gz: aad8dbad91f2ad51f369e05966ba99ae743c36f4e60d596df65f2db468c79e13
5
+ SHA512:
6
+ metadata.gz: bd13eeacb29d220bf95083e5339797ff4af2e930719461faa8271e68fffed1f58928db4d43d57e3d8dd6a767e96b328c926072363e797b078aa59b66b23d03fc
7
+ data.tar.gz: 03fcc1b66c0d23c224e00f84315ba789679fd07bd7ce9a1b61704a3bd1b58d8f8b70abc1ca8099c6cfc1cea1471f6ebfd3d4e22cc361bfd1d0cf9c5858f6ba97
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
@@ -0,0 +1,41 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ forked (0.1.1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ coderay (1.1.2)
10
+ diff-lcs (1.3)
11
+ method_source (0.9.0)
12
+ pry (0.11.3)
13
+ coderay (~> 1.1.0)
14
+ method_source (~> 0.9.0)
15
+ rake (10.5.0)
16
+ rspec (3.7.0)
17
+ rspec-core (~> 3.7.0)
18
+ rspec-expectations (~> 3.7.0)
19
+ rspec-mocks (~> 3.7.0)
20
+ rspec-core (3.7.0)
21
+ rspec-support (~> 3.7.0)
22
+ rspec-expectations (3.7.0)
23
+ diff-lcs (>= 1.2.0, < 2.0)
24
+ rspec-support (~> 3.7.0)
25
+ rspec-mocks (3.7.0)
26
+ diff-lcs (>= 1.2.0, < 2.0)
27
+ rspec-support (~> 3.7.0)
28
+ rspec-support (3.7.0)
29
+
30
+ PLATFORMS
31
+ ruby
32
+
33
+ DEPENDENCIES
34
+ bundler (~> 1.15)
35
+ forked!
36
+ pry
37
+ rake (~> 10.0)
38
+ rspec (~> 3.0)
39
+
40
+ BUNDLED WITH
41
+ 1.16.1
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2018 Envato
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,47 @@
1
+ # Forked
2
+
3
+ Forked manages long running worker processes.
4
+
5
+ Processes that crash are restarted, whereas processes that exit successfully
6
+ aren't. Errors that occur within forked processes are retried according to the
7
+ configured retry strategy.
8
+
9
+ Once `wait_for_shutdown` is called, the current process watches for shutdown
10
+ signals or crashed processes. On shutdown, each worker is sent a TERM signal,
11
+ indicating that it should finish any in progress work and shutdown. After a set
12
+ timeout period workers are sent a KILL signal.
13
+
14
+ ## Usage
15
+
16
+ ```ruby
17
+ require 'forked'
18
+
19
+ process_manager = Forked::ProcessManager.new(logger: Logger.new(STDOUT), process_timeout: 5)
20
+
21
+ process_manager.fork('monitor', on_error: ->(e) { puts e.inspect }) do
22
+ loop do
23
+ puts "hi"
24
+ sleep 1
25
+ end
26
+ end
27
+
28
+ process_manager.fork('processor_1', retry_strategy: Forked::RetryStrategies::ExponentialBackoff) do |ready_to_stop|
29
+ loop do
30
+ ready_to_stop.call # triggers a shutdown if a TERM/INT signal has been received
31
+ # do something
32
+ end
33
+ end
34
+
35
+ # blocks the current process, restarts any crashed processes and waits for shutdown signals (TERM/INT).
36
+ process_manager.wait_for_shutdown
37
+ ```
38
+
39
+ ## Development
40
+
41
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
42
+
43
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
44
+
45
+ ## Contributing
46
+
47
+ Bug reports and pull requests are welcome on GitHub at https://github.com/envato/forked.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,9 @@
1
+ require 'forked/version'
2
+ require 'forked/worker'
3
+ require 'forked/retry_strategies/always'
4
+ require 'forked/retry_strategies/exponential_backoff'
5
+ require 'forked/process_manager'
6
+ require 'forked/with_graceful_shutdown'
7
+
8
+ module Forked
9
+ end
@@ -0,0 +1,117 @@
1
+ require 'logger'
2
+ require 'timeout'
3
+
4
+ module Forked
5
+ class ProcessManager
6
+ def initialize(process_timeout: 5, logger: Logger.new(STDOUT))
7
+ @process_timeout = process_timeout
8
+ @workers = {}
9
+ @logger = logger
10
+ end
11
+
12
+ def fork(name = nil, retry_strategy: ::Forked::RetryStrategies::ExponentialBackoff, on_error: -> (e) {}, &block)
13
+ worker = Worker.new(name, retry_strategy, on_error, block)
14
+ fork_worker(worker)
15
+ end
16
+
17
+ def wait_for_shutdown
18
+ trap_shutdown_signals
19
+ handle_child_processes
20
+ shutdown
21
+ end
22
+
23
+ def shutdown
24
+ @logger.info "Master shutting down"
25
+ send_signal_to_workers(:TERM)
26
+ wait_for_workers_until_timeout
27
+ send_signal_to_workers(:KILL)
28
+ @logger.info "Master shutdown complete"
29
+ end
30
+
31
+ def worker_pids
32
+ @workers.keys
33
+ end
34
+
35
+ private
36
+
37
+ def fork_worker(worker)
38
+ retry_strategy = worker.retry_strategy.new(logger: @logger, on_error: worker.on_error)
39
+ pid = Kernel.fork do
40
+ WithGracefulShutdown.run(logger: @logger) do |ready_to_stop|
41
+ retry_strategy.run(ready_to_stop) do
42
+ if worker.block.arity > 0
43
+ worker.block.call(ready_to_stop)
44
+ else
45
+ worker.block.call
46
+ end
47
+ end
48
+ end
49
+ end
50
+ @workers[pid] = worker
51
+ end
52
+
53
+ def handle_child_processes
54
+ until @shutdown_requested
55
+ # Returns nil immediately if no child process exists
56
+ pid, status = Process.wait2(-1, Process::WNOHANG)
57
+ if pid
58
+ handle_child_exit(pid, status)
59
+ end
60
+ sleep(0.5)
61
+ end
62
+ end
63
+
64
+ def handle_child_exit(pid, status)
65
+ worker = @workers.delete(pid)
66
+ if status.exited?
67
+ @logger.info "#{worker.name || pid} exited with status #{status.exitstatus.inspect}"
68
+ else
69
+ @logger.info "#{worker.name || pid} terminated"
70
+ end
71
+ if status.exitstatus.nil? || status.exitstatus.nonzero?
72
+ @logger.error "Restarting #{worker.name || pid}"
73
+ fork_worker(worker)
74
+ end
75
+ end
76
+
77
+ def trap_shutdown_signals
78
+ %i(TERM INT).each do |signal|
79
+ Signal.trap(signal) do
80
+ start_shutdown
81
+ end
82
+ end
83
+ end
84
+
85
+ def start_shutdown
86
+ @shutdown_requested = true
87
+ end
88
+
89
+ def wait_for_workers_until_timeout
90
+ @waiting_since = Time.now
91
+ until @workers.empty? || timed_out?(@waiting_since)
92
+ # Returns nil immediately if no child process exists
93
+ pid, status = Process.wait2(-1, Process::WNOHANG)
94
+ @workers.delete(pid) if pid
95
+ end
96
+ end
97
+
98
+ def send_signal_to_workers(signal)
99
+ if !@workers.empty?
100
+ @logger.info "Sending #{signal} to #{@workers.keys}"
101
+ @workers.each_key do |pid|
102
+ begin
103
+ Process.kill(signal, pid)
104
+ rescue Errno::ESRCH => e
105
+ # Errno::ESRCH: No such process
106
+ # Move along if the process is already dead
107
+ @workers.delete(pid)
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ def timed_out?(waiting_since)
114
+ Time.now > (waiting_since + @process_timeout)
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,19 @@
1
+ module Forked
2
+ module RetryStrategies
3
+ # Relies on the master restarting the worker process
4
+ class Always
5
+ def initialize(logger:, on_error:)
6
+ @logger = logger
7
+ @on_error = on_error
8
+ end
9
+
10
+ def run(ready_to_stop, &block)
11
+ block.call
12
+ rescue => e
13
+ @logger.error("#{e.class} #{e.message}")
14
+ @on_error.call(e, 1)
15
+ raise
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,28 @@
1
+ module Forked
2
+ module RetryStrategies
3
+ class ExponentialBackoff
4
+ def initialize(logger:, on_error:, backoff_factor: 2)
5
+ @logger = logger
6
+ @on_error = on_error
7
+ @backoff_factor = backoff_factor
8
+ end
9
+
10
+ def run(ready_to_stop, &block)
11
+ tries = 0
12
+ begin
13
+ block.call
14
+ rescue => e
15
+ tries += 1
16
+ sleep_seconds = @backoff_factor**tries
17
+ @logger.error("#{e.class} #{e.message}")
18
+ @on_error.call(e, tries)
19
+ sleep_seconds.times do
20
+ ready_to_stop.call
21
+ sleep 1
22
+ end
23
+ retry
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module Forked
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,72 @@
1
+ # Traps SIGTERM and SIGINT and shuts down only when the block
2
+ # is called, allowing for graceful shutdown.
3
+ #
4
+ # @example
5
+ # WithGracefulShutdown.run do |ready_to_stop|
6
+ # loop do
7
+ # # do some work
8
+ # ready_to_stop.call
9
+ # end
10
+ # end
11
+ #
12
+ # @example
13
+ # WithGracefulShutdown.loop(sleep_seconds: 2) do |ready_to_stop|
14
+ # print '.'
15
+ # end
16
+ module Forked
17
+ class WithGracefulShutdown
18
+ def self.loop(sleep_seconds: 1, &block)
19
+ run do |ready_to_stop|
20
+ Kernel.loop do
21
+ ready_to_stop.call
22
+ yield ready_to_stop
23
+ ready_to_stop.call
24
+ sleep_seconds.times do
25
+ ready_to_stop.call
26
+ sleep(1)
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def self.run(logger: Logger.new(STDOUT), &block)
33
+ new(logger: logger).run(&block)
34
+ end
35
+
36
+ def initialize(logger:)
37
+ @logger = logger
38
+ @shutdown = false
39
+ end
40
+
41
+ def run
42
+ trap_shutdown_signals do
43
+ catch(:stop) do
44
+ ready_to_stop = -> { stop_if_necessary }
45
+ yield ready_to_stop
46
+ end
47
+ end
48
+ end
49
+
50
+ def stop_if_necessary
51
+ if @shutdown
52
+ @logger.info("Shutting down")
53
+ throw :stop
54
+ end
55
+ end
56
+
57
+ def shutdown
58
+ @shutdown = true
59
+ end
60
+
61
+ private
62
+
63
+ def trap_shutdown_signals
64
+ orig_int_handler = Signal.trap(:INT) { shutdown }
65
+ orig_term_handler = Signal.trap(:TERM) { shutdown }
66
+ yield
67
+ ensure
68
+ Signal.trap(:INT, orig_int_handler)
69
+ Signal.trap(:TERM, orig_term_handler)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,4 @@
1
+ module Forked
2
+ class Worker < Struct.new(:name, :retry_strategy, :on_error, :block)
3
+ end
4
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: forked
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Steve Hodgkiss
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-08-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.15'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.15'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: ''
70
+ email:
71
+ - steve@hodgkiss.me
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - Gemfile
77
+ - Gemfile.lock
78
+ - LICENSE.txt
79
+ - README.md
80
+ - Rakefile
81
+ - lib/forked.rb
82
+ - lib/forked/process_manager.rb
83
+ - lib/forked/retry_strategies/always.rb
84
+ - lib/forked/retry_strategies/exponential_backoff.rb
85
+ - lib/forked/version.rb
86
+ - lib/forked/with_graceful_shutdown.rb
87
+ - lib/forked/worker.rb
88
+ homepage: https://github.com/envato/forked
89
+ licenses: []
90
+ metadata: {}
91
+ post_install_message:
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubyforge_project:
107
+ rubygems_version: 2.7.3
108
+ signing_key:
109
+ specification_version: 4
110
+ summary: Manage long running forked processes
111
+ test_files: []