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 +4 -4
- data/lib/hot_cell/control.rb +5 -1
- data/lib/hot_cell/health_operations.rb +42 -0
- data/lib/hot_cell/server/version.rb +1 -1
- data/lib/hot_cell/supervisor.rb +37 -4
- data/lib/hot_cell/test_cell.rb +3 -0
- data/lib/hot_cell/test_operations.rb +10 -0
- metadata +7 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 14fae075c3d4fef73a330453eff2c295ac7e67895d50e2b9ad73d800b50ff2f8
|
|
4
|
+
data.tar.gz: d3730524324b2283332bdf2f04c90923a6dc3b0ed4a8b6821b10dc46a7e1fd3a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 063e11ec9aac319a707f3ca11d2d92b02d4aeeeffc35fdf2ec7e169ab8e14b82f4949b0d6f03255a4285186d43620ccb102584feaa616c02677ce6cefbee39b4
|
|
7
|
+
data.tar.gz: f7ca005517c6bc32c6e3b803b3c577b4e5e103bcc34b603e64534526430189b8f5b39cc87ee3235832ec4d2c078088091251fa1c66e409111ef20020d6d3e5ff
|
data/lib/hot_cell/control.rb
CHANGED
|
@@ -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,
|
|
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
|
data/lib/hot_cell/supervisor.rb
CHANGED
|
@@ -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
|
|
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"
|
data/lib/hot_cell/test_cell.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
67
|
-
changelog_uri: https://github.com/basecamp/hotcell/blob/v0.
|
|
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.
|
|
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: []
|