forked 0.1.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/Gemfile +3 -0
- data/Gemfile.lock +41 -0
- data/LICENSE.txt +22 -0
- data/README.md +47 -0
- data/Rakefile +6 -0
- data/lib/forked.rb +9 -0
- data/lib/forked/process_manager.rb +117 -0
- data/lib/forked/retry_strategies/always.rb +19 -0
- data/lib/forked/retry_strategies/exponential_backoff.rb +28 -0
- data/lib/forked/version.rb +3 -0
- data/lib/forked/with_graceful_shutdown.rb +72 -0
- data/lib/forked/worker.rb +4 -0
- metadata +111 -0
checksums.yaml
ADDED
@@ -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
data/Gemfile.lock
ADDED
@@ -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
|
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
data/lib/forked.rb
ADDED
@@ -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,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
|
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: []
|