overman 0.88.2 → 0.88.3
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 +4 -4
- data/README.md +14 -0
- data/lib/foreman/engine.rb +55 -9
- data/lib/foreman/version.rb +1 -1
- data/man/overman.1 +1 -1
- data/spec/foreman/engine_spec.rb +66 -0
- data/spec/foreman/shutdown_spec.rb +117 -0
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a59c47fdea5d85e4feed1826151bc2960e46a3dee0b4a51864f21a31fcb7b737
|
|
4
|
+
data.tar.gz: 1181655ed2d379993e36adcac427cc66626c1abbedd513059d8e8d81aa4bbb18
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2c503d7c15d4254006a46b92858f271dd3dc679668459de9f9fad2a5a78337af7e37bc297a9135003eab56ad999a79b4478b93642fd58490935cb34d2d38564b
|
|
7
|
+
data.tar.gz: 2f5badad607d6a17591459cff32354d9f8d06f37aede7d05b40cf7a63f2a9a31ea1bb8c6ff32508e6ea2f007ba7e089990d21c7132499a69401905580422e46e
|
data/README.md
CHANGED
|
@@ -14,6 +14,20 @@ Ruby users should take care _not_ to install foreman in their project's `Gemfile
|
|
|
14
14
|
|
|
15
15
|
- http://blog.daviddollar.org/2011/05/06/introducing-foreman.html
|
|
16
16
|
|
|
17
|
+
## Process lifecycle
|
|
18
|
+
|
|
19
|
+
`overman start` runs each command in a separate process group. On Unix, shutdown
|
|
20
|
+
sends SIGTERM to those groups and waits for them to exit, including descendants
|
|
21
|
+
that remain in the group after their immediate parent exits. Groups still running
|
|
22
|
+
after the shutdown timeout receive SIGKILL. Descendants that leave the group,
|
|
23
|
+
for example by calling `setsid`, are not tracked. On Windows, shutdown sends
|
|
24
|
+
SIGKILL immediately.
|
|
25
|
+
|
|
26
|
+
Groups that cannot be signaled due to permission errors are skipped with a warning.
|
|
27
|
+
On systems where zombie-only groups still appear alive, an ancestor that does not
|
|
28
|
+
reap orphaned processes can cause shutdown to wait the full timeout; use a reaping
|
|
29
|
+
init process when running in a container, such as Docker's `--init` option.
|
|
30
|
+
|
|
17
31
|
## Supported Ruby versions
|
|
18
32
|
|
|
19
33
|
See [ci.yml](.github/workflows/ci.yml) for a list of Ruby versions against which Foreman is tested.
|
data/lib/foreman/engine.rb
CHANGED
|
@@ -35,7 +35,9 @@ class Foreman::Engine
|
|
|
35
35
|
@names = {}
|
|
36
36
|
@processes = []
|
|
37
37
|
@running = {}
|
|
38
|
+
@process_groups = {}
|
|
38
39
|
@readers = {}
|
|
40
|
+
@reader_names = {}
|
|
39
41
|
@shutdown = false
|
|
40
42
|
|
|
41
43
|
# Self-pipe for deferred signal-handling (ala djb: http://cr.yp.to/docs/selfpipe.html)
|
|
@@ -196,10 +198,15 @@ class Foreman::Engine
|
|
|
196
198
|
end
|
|
197
199
|
end
|
|
198
200
|
else
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
@process_groups.each_key do |pid|
|
|
202
|
+
begin
|
|
203
|
+
Process.kill("-#{signal}", pid)
|
|
204
|
+
rescue Errno::ESRCH
|
|
205
|
+
@process_groups.delete(pid)
|
|
206
|
+
rescue Errno::EPERM
|
|
207
|
+
warn_unsignalable_group(pid)
|
|
208
|
+
@process_groups.delete(pid)
|
|
209
|
+
end
|
|
203
210
|
end
|
|
204
211
|
end
|
|
205
212
|
end
|
|
@@ -312,7 +319,7 @@ private
|
|
|
312
319
|
end
|
|
313
320
|
|
|
314
321
|
def name_for(pid)
|
|
315
|
-
process, index = @running[pid]
|
|
322
|
+
process, index = @running[pid] || @process_groups[pid]
|
|
316
323
|
name_for_index(process, index)
|
|
317
324
|
end
|
|
318
325
|
|
|
@@ -353,7 +360,7 @@ private
|
|
|
353
360
|
def flush_reader(reader)
|
|
354
361
|
until reader.eof?
|
|
355
362
|
data = reader.gets
|
|
356
|
-
output_with_mutex
|
|
363
|
+
output_with_mutex @reader_names[reader], data
|
|
357
364
|
end
|
|
358
365
|
end
|
|
359
366
|
|
|
@@ -373,7 +380,9 @@ private
|
|
|
373
380
|
writer.puts "unknown command: #{process.command}"
|
|
374
381
|
end
|
|
375
382
|
@running[pid] = [process, n]
|
|
383
|
+
@process_groups[pid] = [process, n] if pid && !Foreman.windows?
|
|
376
384
|
@readers[pid] = reader
|
|
385
|
+
@reader_names[reader] = name_for_index(process, n)
|
|
377
386
|
end
|
|
378
387
|
end
|
|
379
388
|
end
|
|
@@ -396,9 +405,10 @@ private
|
|
|
396
405
|
|
|
397
406
|
if reader.eof?
|
|
398
407
|
@readers.delete_if { |key, value| value == reader }
|
|
408
|
+
@reader_names.delete(reader)
|
|
399
409
|
else
|
|
400
410
|
data = reader.gets
|
|
401
|
-
output_with_mutex
|
|
411
|
+
output_with_mutex @reader_names[reader], data
|
|
402
412
|
end
|
|
403
413
|
end
|
|
404
414
|
end
|
|
@@ -464,6 +474,10 @@ private
|
|
|
464
474
|
def terminate_gracefully
|
|
465
475
|
restore_default_signal_handlers
|
|
466
476
|
|
|
477
|
+
reap_children
|
|
478
|
+
prune_process_groups
|
|
479
|
+
return unless processes_running?
|
|
480
|
+
|
|
467
481
|
# Tell all children to stop gracefully
|
|
468
482
|
if Foreman.windows?
|
|
469
483
|
system "sending SIGKILL to all processes"
|
|
@@ -476,8 +490,9 @@ private
|
|
|
476
490
|
# Wait for all children to stop or until the time comes to kill them all
|
|
477
491
|
start_time = Time.now
|
|
478
492
|
while Time.now - start_time <= options[:timeout]
|
|
479
|
-
|
|
480
|
-
|
|
493
|
+
reap_children
|
|
494
|
+
prune_process_groups
|
|
495
|
+
return unless processes_running?
|
|
481
496
|
|
|
482
497
|
# Sleep for a moment and do not blow up if more signals are coming our way
|
|
483
498
|
begin
|
|
@@ -487,8 +502,39 @@ private
|
|
|
487
502
|
end
|
|
488
503
|
end
|
|
489
504
|
|
|
505
|
+
reap_children
|
|
506
|
+
prune_process_groups
|
|
507
|
+
return unless processes_running?
|
|
508
|
+
|
|
490
509
|
# Ok, we have no other option than to kill all of our children
|
|
491
510
|
system "sending SIGKILL to all processes"
|
|
492
511
|
kill_children "SIGKILL"
|
|
493
512
|
end
|
|
513
|
+
|
|
514
|
+
def processes_running?
|
|
515
|
+
!@running.empty? || !@process_groups.empty?
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def reap_children
|
|
519
|
+
loop { break unless check_for_termination }
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def prune_process_groups
|
|
523
|
+
# A group can outlive its immediate child when descendants ignore SIGTERM.
|
|
524
|
+
@process_groups.delete_if do |pid, _|
|
|
525
|
+
begin
|
|
526
|
+
Process.kill(0, -pid)
|
|
527
|
+
false
|
|
528
|
+
rescue Errno::ESRCH
|
|
529
|
+
true
|
|
530
|
+
rescue Errno::EPERM
|
|
531
|
+
warn_unsignalable_group(pid)
|
|
532
|
+
true
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def warn_unsignalable_group(pid)
|
|
538
|
+
system "WARNING: permission denied signaling process group #{pid}; skipping group"
|
|
539
|
+
end
|
|
494
540
|
end
|
data/lib/foreman/version.rb
CHANGED
data/man/overman.1
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
.\" generated with Ronn-NG/v0.10.1
|
|
2
2
|
.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
|
|
3
|
-
.TH "FOREMAN" "1" "September 2026" "Foreman 0.88.
|
|
3
|
+
.TH "FOREMAN" "1" "September 2026" "Foreman 0.88.3" "Foreman Manual"
|
|
4
4
|
.SH "NAME"
|
|
5
5
|
\fBforeman\fR \- manage Procfile\-based applications
|
|
6
6
|
.SH "SYNOPSIS"
|
data/spec/foreman/engine_spec.rb
CHANGED
|
@@ -60,6 +60,72 @@ describe "Foreman::Engine", :fakefs do
|
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
describe "shutdown", :unless => Foreman.windows? do
|
|
64
|
+
before do
|
|
65
|
+
subject.options[:formation] = "alpha=1"
|
|
66
|
+
allow(subject.process("alpha")).to receive(:run) do |options|
|
|
67
|
+
@process_writer = options[:output]
|
|
68
|
+
1234
|
|
69
|
+
end
|
|
70
|
+
subject.startup
|
|
71
|
+
subject.send(:spawn_processes)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
let(:status) { instance_double(Process::Status, :exitstatus => 0, :exited? => true) }
|
|
75
|
+
|
|
76
|
+
it "warns and stops tracking a group it cannot signal" do
|
|
77
|
+
reaped = false
|
|
78
|
+
allow(Process).to receive(:wait2) do
|
|
79
|
+
raise Errno::ECHILD if reaped
|
|
80
|
+
reaped = true
|
|
81
|
+
[1234, status]
|
|
82
|
+
end
|
|
83
|
+
allow(Process).to receive(:kill).and_raise(Errno::EPERM)
|
|
84
|
+
expect(subject).not_to receive(:sleep)
|
|
85
|
+
|
|
86
|
+
subject.send(:terminate_gracefully)
|
|
87
|
+
|
|
88
|
+
expect(subject.buffer).to include("WARNING: permission denied signaling process group 1234")
|
|
89
|
+
expect(subject.buffer).not_to include("sending SIGKILL")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "reaps and rechecks groups at the timeout before escalating" do
|
|
93
|
+
subject.options[:timeout] = 0.2
|
|
94
|
+
allow(Time).to receive(:now).and_return(Time.at(0), Time.at(1))
|
|
95
|
+
polls = 0
|
|
96
|
+
allow(Process).to receive(:wait2) do
|
|
97
|
+
polls += 1
|
|
98
|
+
polls == 2 ? [1234, status] : [nil, nil]
|
|
99
|
+
end
|
|
100
|
+
allow(Process).to receive(:kill).with(0, -1234) do
|
|
101
|
+
raise Errno::ESRCH if polls >= 2
|
|
102
|
+
1
|
|
103
|
+
end
|
|
104
|
+
expect(Process).to receive(:kill).with("-SIGTERM", 1234).and_return(1)
|
|
105
|
+
expect(Process).not_to receive(:kill).with("-SIGKILL", 1234)
|
|
106
|
+
|
|
107
|
+
subject.send(:terminate_gracefully)
|
|
108
|
+
|
|
109
|
+
expect(subject.buffer).to include("exited with code 0")
|
|
110
|
+
expect(subject.buffer).not_to include("sending SIGKILL")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it "keeps the process name while draining output after it exits" do
|
|
114
|
+
reader = subject.instance_variable_get(:@readers).fetch(1234)
|
|
115
|
+
subject.send(:handle_io, [reader])
|
|
116
|
+
|
|
117
|
+
allow(Process).to receive(:wait2).and_return([1234, status])
|
|
118
|
+
subject.send(:check_for_termination)
|
|
119
|
+
allow(Process).to receive(:kill).with(0, -1234).and_raise(Errno::ESRCH)
|
|
120
|
+
subject.send(:prune_process_groups)
|
|
121
|
+
|
|
122
|
+
@process_writer.puts "buffered output"
|
|
123
|
+
subject.send(:handle_io, [reader])
|
|
124
|
+
|
|
125
|
+
expect(subject.buffer).to include("alpha.1: buffered output")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
63
129
|
describe "environment" do
|
|
64
130
|
it "should read env files" do
|
|
65
131
|
write_file("/tmp/env") { |f| f.puts("FOO=baz") }
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "foreman"
|
|
3
|
+
require "foreman/engine"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
require "timeout"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
|
|
8
|
+
describe "Process-group shutdown", :unless => Foreman.windows? do
|
|
9
|
+
def wait_until
|
|
10
|
+
Timeout.timeout(10) { sleep 0.02 until yield }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def stop_supervisor(pid)
|
|
14
|
+
return if Process.waitpid(pid, Process::WNOHANG)
|
|
15
|
+
|
|
16
|
+
Process.kill("INT", pid)
|
|
17
|
+
begin
|
|
18
|
+
Timeout.timeout(2) { Process.wait(pid) }
|
|
19
|
+
rescue Timeout::Error
|
|
20
|
+
Process.kill("KILL", pid)
|
|
21
|
+
Process.wait(pid)
|
|
22
|
+
end
|
|
23
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
|
24
|
+
# The supervisor has already exited or been reaped by the example.
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def with_supervisor(dir)
|
|
28
|
+
command = File.expand_path("../../bin/overman", __dir__)
|
|
29
|
+
output = "#{dir}/output"
|
|
30
|
+
supervisor = Process.spawn(RbConfig.ruby, command, "start", "-t", "0.2",
|
|
31
|
+
:chdir => dir, [:out, :err] => output)
|
|
32
|
+
begin
|
|
33
|
+
yield supervisor
|
|
34
|
+
rescue Exception
|
|
35
|
+
warn "Supervisor output:\n#{File.read(output)}"
|
|
36
|
+
raise
|
|
37
|
+
ensure
|
|
38
|
+
stop_supervisor(supervisor)
|
|
39
|
+
Dir["#{dir}/*.group"].each do |file|
|
|
40
|
+
group = File.read(file).to_i
|
|
41
|
+
Process.kill("KILL", -group) rescue nil if group > 0
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "kills surviving descendants after their immediate parent exits" do
|
|
47
|
+
Dir.mktmpdir("overman-shutdown") do |dir|
|
|
48
|
+
File.write("#{dir}/service.rb", <<~RUBY)
|
|
49
|
+
File.write("service.group", Process.getpgrp)
|
|
50
|
+
fork do
|
|
51
|
+
trap("TERM", "IGNORE")
|
|
52
|
+
File.open("descendant.lock", "w") do |lock|
|
|
53
|
+
lock.flock(File::LOCK_EX)
|
|
54
|
+
File.write("ready", "ready")
|
|
55
|
+
sleep
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
sleep
|
|
59
|
+
RUBY
|
|
60
|
+
File.write("#{dir}/Procfile", "service: #{RbConfig.ruby} service.rb\n")
|
|
61
|
+
|
|
62
|
+
with_supervisor(dir) do |supervisor|
|
|
63
|
+
wait_until { File.size?("#{dir}/ready") }
|
|
64
|
+
File.open("#{dir}/descendant.lock", "r") do |lock|
|
|
65
|
+
expect(lock.flock(File::LOCK_EX | File::LOCK_NB)).to eq(false)
|
|
66
|
+
Process.kill("INT", supervisor)
|
|
67
|
+
Timeout.timeout(10) { Process.wait(supervisor) }
|
|
68
|
+
|
|
69
|
+
wait_until { lock.flock(File::LOCK_EX | File::LOCK_NB) }
|
|
70
|
+
end
|
|
71
|
+
expect(File.read("#{dir}/output")).to match(/terminated by SIGTERM.*sending SIGKILL/m)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "signals later groups when an earlier group has disappeared" do
|
|
77
|
+
Dir.mktmpdir("overman-signals") do |dir|
|
|
78
|
+
File.write("#{dir}/service.rb", <<~RUBY)
|
|
79
|
+
name = ARGV.fetch(0)
|
|
80
|
+
File.write("\#{name}.group", Process.getpgrp)
|
|
81
|
+
exit if name == "gone"
|
|
82
|
+
trap("TERM") do
|
|
83
|
+
File.write("terminated", "TERM")
|
|
84
|
+
exit
|
|
85
|
+
end
|
|
86
|
+
File.write("ready", "ready")
|
|
87
|
+
sleep
|
|
88
|
+
RUBY
|
|
89
|
+
File.write("#{dir}/Procfile", <<~PROCFILE)
|
|
90
|
+
gone: #{RbConfig.ruby} service.rb gone
|
|
91
|
+
remaining: #{RbConfig.ruby} service.rb remaining
|
|
92
|
+
PROCFILE
|
|
93
|
+
engine = Foreman::Engine.new.load_procfile("#{dir}/Procfile")
|
|
94
|
+
begin
|
|
95
|
+
engine.send(:spawn_processes)
|
|
96
|
+
wait_until { File.size?("#{dir}/gone.group") && File.size?("#{dir}/ready") }
|
|
97
|
+
gone = File.read("#{dir}/gone.group").to_i
|
|
98
|
+
remaining = File.read("#{dir}/remaining.group").to_i
|
|
99
|
+
Timeout.timeout(10) { Process.wait(gone) }
|
|
100
|
+
expect { Process.kill(0, -gone) }.to raise_error(Errno::ESRCH)
|
|
101
|
+
|
|
102
|
+
engine.kill_children
|
|
103
|
+
|
|
104
|
+
Timeout.timeout(10) { Process.wait(remaining) }
|
|
105
|
+
expect(File.read("#{dir}/terminated")).to eq("TERM")
|
|
106
|
+
ensure
|
|
107
|
+
Dir["#{dir}/*.group"].each do |file|
|
|
108
|
+
group = File.read(file).to_i
|
|
109
|
+
if group > 0
|
|
110
|
+
Process.kill("KILL", -group) rescue nil
|
|
111
|
+
Process.wait(group) rescue nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
metadata
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: overman
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.88.
|
|
4
|
+
version: 0.88.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Dollar
|
|
8
|
+
- Patrik Ragnarsson
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
11
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
12
|
dependencies: []
|
|
12
13
|
description: Process manager for applications with multiple components, fork of ddollar/foreman
|
|
13
|
-
email:
|
|
14
|
+
email:
|
|
15
|
+
- ddollar@gmail.com
|
|
16
|
+
- patrik@starkast.net
|
|
14
17
|
executables:
|
|
15
18
|
- overman
|
|
16
19
|
extensions: []
|
|
@@ -108,6 +111,7 @@ files:
|
|
|
108
111
|
- spec/foreman/helpers_spec.rb
|
|
109
112
|
- spec/foreman/process_spec.rb
|
|
110
113
|
- spec/foreman/procfile_spec.rb
|
|
114
|
+
- spec/foreman/shutdown_spec.rb
|
|
111
115
|
- spec/foreman_spec.rb
|
|
112
116
|
- spec/helper_spec.rb
|
|
113
117
|
- spec/resources/Procfile
|
|
@@ -169,7 +173,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
169
173
|
- !ruby/object:Gem::Version
|
|
170
174
|
version: '0'
|
|
171
175
|
requirements: []
|
|
172
|
-
rubygems_version: 4.0.
|
|
176
|
+
rubygems_version: 4.0.20
|
|
173
177
|
specification_version: 4
|
|
174
178
|
summary: Process manager for applications with multiple components, fork of ddollar/foreman
|
|
175
179
|
test_files: []
|