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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aa4c1fac0ddfb73ffbdabc7baefeacfd726c2d3515d12c235bc7d1866e9fa94c
4
- data.tar.gz: 7182befecdc71d50c0d243f184fe2d7c7232ff7c1e5a475d047a46adfecd71df
3
+ metadata.gz: a59c47fdea5d85e4feed1826151bc2960e46a3dee0b4a51864f21a31fcb7b737
4
+ data.tar.gz: 1181655ed2d379993e36adcac427cc66626c1abbedd513059d8e8d81aa4bbb18
5
5
  SHA512:
6
- metadata.gz: 9416439d17c702295a94ee9ca347e285e2be0af7d61ac512e9824a6329b088a82c67aa992325b13aa83b19cbb1b608616d27f775cbdf28a4e9ae55746d9fa336
7
- data.tar.gz: b7925120857061b1eb7774ca72719e1fb207a31d5693f197cb9feae90b641d3cc3587fcbb31a9e7e6e5c253dd1edf814981c66b9fa6a92edcd9dbbd966e21165
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.
@@ -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
- begin
200
- pids = @running.keys.compact
201
- Process.kill("-#{signal}", *pids) unless pids.empty?
202
- rescue Errno::ESRCH, Errno::EPERM
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 name_for(@readers.key(reader)), data
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 name_for(@readers.invert[reader]), data
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
- return if @running.empty?
480
- check_for_termination
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
@@ -1,5 +1,5 @@
1
1
  module Foreman
2
2
 
3
- VERSION = "0.88.2"
3
+ VERSION = "0.88.3"
4
4
 
5
5
  end
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.2" "Foreman Manual"
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"
@@ -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.2
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: ddollar@gmail.com
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.16
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: []