hotcell-server 0.5.0 → 0.6.0

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: b69f25f352406df26617692f42c1ce78c7191df4d7b617360c62c2371e488bed
4
- data.tar.gz: 34ca8c1eea5a9de3cf739ce548151ac5c9487e185739f684d179844c13257b1d
3
+ metadata.gz: 14fae075c3d4fef73a330453eff2c295ac7e67895d50e2b9ad73d800b50ff2f8
4
+ data.tar.gz: d3730524324b2283332bdf2f04c90923a6dc3b0ed4a8b6821b10dc46a7e1fd3a
5
5
  SHA512:
6
- metadata.gz: 5667ac9be32cd60ab131df56fb4be0a0354f007a19d31990c6210076dcd6e72c019a903b8e4f4f89ed5bf4ee444269de2b30d4776aa6373750f02ab21fa2869b
7
- data.tar.gz: 9567d005d357239081a4c804b081939714c1ead9afb2588fcbd95d6795c638770348959d00549d49d9b76ec7cdc7bb0e3b2f7dd226a1a001d0c532fb524a05d2
6
+ metadata.gz: 063e11ec9aac319a707f3ca11d2d92b02d4aeeeffc35fdf2ec7e169ab8e14b82f4949b0d6f03255a4285186d43620ccb102584feaa616c02677ce6cefbee39b4
7
+ data.tar.gz: f7ca005517c6bc32c6e3b803b3c577b4e5e103bcc34b603e64534526430189b8f5b39cc87ee3235832ec4d2c078088091251fa1c66e409111ef20020d6d3e5ff
@@ -40,8 +40,12 @@ module HotCell
40
40
 
41
41
  # Static, and called once per registered cell at app boot. It is the cheapest way to catch a client
42
42
  # whose own timeout is below what this cell may take, and it is what `bin/hotcell describe` reads.
43
+ #
44
+ # `v` is the wire format and changes only when the format does; `server_version` says which release of
45
+ # hotcell-server is answering.
43
46
  def describe
44
- { v: PROTOCOL_VERSION, operations: Registry.names, groups: groups, **@configuration.to_h }
47
+ { v: PROTOCOL_VERSION, server_version: Server::VERSION, operations: Registry.names, groups: groups,
48
+ **@configuration.to_h }
45
49
  end
46
50
 
47
51
  private
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hot_cell/server"
4
+
5
+ # The two round trips an application runs against its own cell for a health check. Shipped in the
6
+ # gem because every deployment was copying them from examples/, and copies drift.
7
+ #
8
+ # A cell serves them only if it requires this file.
9
+ module HotCell
10
+ module Health
11
+ # The message goes in through the caller's input descriptor and out through the caller's output
12
+ # descriptor, with no copy onto scratch, so one round trip proves descriptor passing end to end.
13
+ class Echo < HotCell::Operation
14
+ operation "health.echo"
15
+
16
+ def perform(inputs, outputs)
17
+ bytes = outputs.first.to_io.write(inputs.first.to_io.read)
18
+
19
+ { bytes: bytes, staged: inputs.first.staged? }
20
+ end
21
+ end
22
+
23
+ # `fd_path` is a fresh open by name — `/dev/fd/N` on Linux, the file's own path on macOS — checked
24
+ # against this process's uid and the file's mode, so this succeeds only when the cell can open the
25
+ # caller's files by name — which echo never checks.
26
+ # It reads the input and writes the output, because a tool may need either permission.
27
+ class Reopen < HotCell::Operation
28
+ operation "health.reopen"
29
+
30
+ def perform(inputs, outputs)
31
+ source, = inputs
32
+ destination, = outputs
33
+
34
+ bytes = File.open(source.fd_path, "rb") do |input|
35
+ File.open(destination.fd_path, "wb") { |output| output.write(input.read) }
36
+ end
37
+
38
+ { bytes: bytes, staged: source.staged? || destination.staged? }
39
+ end
40
+ end
41
+ end
42
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module HotCell
4
4
  module Server
5
- VERSION = "0.5.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
@@ -191,6 +191,7 @@ module HotCell
191
191
  verify_scratches!
192
192
  prepare_directories
193
193
  preload
194
+ warm_up
194
195
  @work = listen "work.sock"
195
196
  @control = listen "control.sock"
196
197
  @control_handler = Control.new(configuration: configuration, counters: counters)
@@ -202,6 +203,8 @@ module HotCell
202
203
  end
203
204
 
204
205
  def run
206
+ prefork
207
+
205
208
  until stopped?
206
209
  readable, = IO.select(sources, nil, nil, wait_for)
207
210
  Array(readable).each { |source| handle source }
@@ -463,8 +466,20 @@ module HotCell
463
466
  @children.each_value.find(&:available?) || spawn
464
467
  end
465
468
 
469
+ # Forks a worker into each free slot, so that a request does not wait for `fork`. If a fork fails,
470
+ # `spawn` logs it and this stops. The next request forks its own worker.
471
+ def prefork
472
+ return if @stopping
473
+
474
+ while free_slot
475
+ break unless spawn
476
+ end
477
+ end
478
+
466
479
  def spawn
467
- number = free_slot or return nil
480
+ number = free_slot
481
+ return nil if number.nil?
482
+
468
483
  slot = Slot.build(workspace, number)
469
484
  supervisor_side, worker_side = UNIXSocket.pair(:STREAM)
470
485
  stderr_reader, stderr_writer = IO.pipe
@@ -857,18 +872,23 @@ module HotCell
857
872
  # the sweep must not signal it. An empty group is the common case, so ESRCH is expected. It is safe to
858
873
  # kill the group by the leader's pid even though the leader is reaped, because the supervisor is single
859
874
  # threaded and mints group leaders only in `spawn`, which cannot run between the `wait2` above and here.
875
+ #
876
+ # macOS answers EPERM rather than ESRCH for a group whose only members are zombies, and a tool killed
877
+ # with its worker stays one until launchd reaps it. Raised, it unwound `run` and ended the cell before
878
+ # this reap answered the caller. EPERM means no member received the signal, the same outcome as ESRCH.
860
879
  def sweep_group(child)
861
880
  Process.kill :KILL, -child.pid
862
- rescue Errno::ESRCH
881
+ rescue Errno::ESRCH, Errno::EPERM
863
882
  nil
864
883
  end
865
884
 
866
885
  # The whole process group, which is the worker and everything it started. Negative pid is the group.
867
886
  # Falls back to the worker alone if the group is already gone, so a worker that died between the check
868
- # and the signal is not an error.
887
+ # and the signal is not an error. A group answering EPERM falls back as well, for the reason
888
+ # `sweep_group` gives; the bare pid does not, because macOS signals a zombie pid without complaint.
869
889
  def kill_group(child)
870
890
  Process.kill :KILL, -child.pid
871
- rescue Errno::ESRCH
891
+ rescue Errno::ESRCH, Errno::EPERM
872
892
  begin
873
893
  Process.kill :KILL, child.pid
874
894
  rescue Errno::ESRCH
@@ -936,6 +956,12 @@ module HotCell
936
956
 
937
957
  log.write "worker.reaped", pid: pid, slot: child.slot.number, served: child.served,
938
958
  signal: signal_name(status), exit_code: status.exitstatus
959
+
960
+ # A worker that exited without serving a request is not replaced. This guards against a worker
961
+ # that crashes during its startup, for example after a bad deploy. A replacement would crash too,
962
+ # and the supervisor would fork again, with no requests arriving. Instead, the next request forks
963
+ # its own worker.
964
+ prefork if child.served.positive?
939
965
  end
940
966
  rescue Errno::ECHILD
941
967
  nil
@@ -1237,6 +1263,13 @@ module HotCell
1237
1263
  Registry.operations.each { |operation| operation.before_fork.each(&:call) }
1238
1264
  end
1239
1265
 
1266
+ # Runs after `preload`, so that the objects the operations loaded are compacted too. Runs before the
1267
+ # first fork, so that each worker inherits the compacted heap. The compacted objects use fewer pages,
1268
+ # so a worker copies fewer pages when it writes to them.
1269
+ def warm_up
1270
+ Process.warmup
1271
+ end
1272
+
1240
1273
 
1241
1274
  def shutdown
1242
1275
  refuse_queue "the cell is stopping"
@@ -26,6 +26,9 @@ module HotCell
26
26
  # which is the only place such a library may be loaded.
27
27
  #
28
28
  # HotCell::TestCell.boot(operations: -> { require "active_storage/hot_cell/server" })
29
+ #
30
+ # The cell's sockets go beneath `Dir.tmpdir`, and a Unix socket path holds 104 bytes on macOS, whose per-user
31
+ # `TMPDIR` spends about half of that. A suite on macOS should set a short `TMPDIR`, such as `/tmp`.
29
32
  class TestCell
30
33
  READY = "up"
31
34
 
@@ -70,6 +70,16 @@ module HotCell
70
70
  end
71
71
  end
72
72
 
73
+ # A worker inherits the supervisor's GC counters at the fork. So this reports what the supervisor did to
74
+ # its heap before the fork.
75
+ class HeapState < HotCell::Operation
76
+ operation "test.heap_state"
77
+
78
+ def perform(_inputs, _outputs)
79
+ { compactions: GC.stat(:compact_count) }
80
+ end
81
+ end
82
+
73
83
  # Leaves its $HOME in a state the worker cannot remove, by taking write permission off a subdirectory
74
84
  # that still has a file in it. A tool running as this user can do the same to a sibling's directory,
75
85
  # which is why a removal that fails has to be reported rather than swallowed.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hotcell-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Dalessio
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.5.0
18
+ version: 0.6.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.5.0
25
+ version: 0.6.0
26
26
  description: |
27
27
  Runs a HotCell container. A supervisor listens on two Unix sockets, forks a worker for each request,
28
28
  and enforces a wall clock deadline and resource limits on it. Write the work as a subclass of
@@ -43,6 +43,7 @@ files:
43
43
  - lib/hot_cell/control.rb
44
44
  - lib/hot_cell/counters.rb
45
45
  - lib/hot_cell/filesystem.rb
46
+ - lib/hot_cell/health_operations.rb
46
47
  - lib/hot_cell/limits.rb
47
48
  - lib/hot_cell/log.rb
48
49
  - lib/hot_cell/operation.rb
@@ -63,8 +64,8 @@ licenses:
63
64
  - MIT
64
65
  metadata:
65
66
  homepage_uri: https://github.com/basecamp/hotcell
66
- source_code_uri: https://github.com/basecamp/hotcell/tree/v0.5.0/hotcell-server
67
- changelog_uri: https://github.com/basecamp/hotcell/blob/v0.5.0/CHANGELOG.md
67
+ source_code_uri: https://github.com/basecamp/hotcell/tree/v0.6.0/hotcell-server
68
+ changelog_uri: https://github.com/basecamp/hotcell/blob/v0.6.0/CHANGELOG.md
68
69
  bug_tracker_uri: https://github.com/basecamp/hotcell/issues
69
70
  rubygems_mfa_required: 'true'
70
71
  rdoc_options: []
@@ -81,7 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
81
82
  - !ruby/object:Gem::Version
82
83
  version: '0'
83
84
  requirements: []
84
- rubygems_version: 4.0.16
85
+ rubygems_version: 4.0.6
85
86
  specification_version: 4
86
87
  summary: 'Run a HotCell: the supervisor, the worker, and the operation API.'
87
88
  test_files: []