hotcell-core 0.1.0 → 0.3.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/codes.rb +15 -3
- data/lib/hot_cell/core/version.rb +1 -1
- data/lib/hot_cell/failure.rb +38 -12
- metadata +6 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8891b08ef8f6bea20b5d0046b865792da18374918fd310d1c521eda6c20b5cac
|
|
4
|
+
data.tar.gz: fe246492b615e044c9762b629de4082f4ac88167a1cedc1b07e13cf235611aae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0d8e8e358a731b951f94716eec4793aca4f7595213c12b7ebbff89079eef01a205812fe87a4ddac00ed428e8a99412ee81c2bc4812efffbaeab0854da43573e3
|
|
7
|
+
data.tar.gz: 7b349aff2ee4ccf6e48a3d6545923285337c8b59c00016dd1416ea55f18f5a3b5b7e9786197febc5865bdc07702cf45574461af0717449984074d77e6a2dc368
|
data/lib/hot_cell/codes.rb
CHANGED
|
@@ -47,9 +47,9 @@ module HotCell
|
|
|
47
47
|
# updated by a deploy, so an application that ships a client for a new operation before anybody reboots
|
|
48
48
|
# the cell gets `unsupported` at one hundred percent for as long as that takes.
|
|
49
49
|
#
|
|
50
|
-
# The two mistakes are not symmetrical. Retrying a caller's typo costs some work and
|
|
51
|
-
# `
|
|
52
|
-
# condemns every blob uploaded during it, and needs a hand-written backfill to undo.
|
|
50
|
+
# The two mistakes are not symmetrical. Retrying a caller's typo costs some work, and it says which
|
|
51
|
+
# operation in the refusal `worker.rb` writes and in the `unsupported` rate. Recording a deploy window
|
|
52
|
+
# as permanent condemns every blob uploaded during it, and needs a hand-written backfill to undo.
|
|
53
53
|
|
|
54
54
|
# `killed` splits on what the worker hit, because a caller cannot otherwise tell a decompression
|
|
55
55
|
# bomb from a slow afternoon. Size and memory are properties of the input, so the same bytes will
|
|
@@ -79,6 +79,18 @@ module HotCell
|
|
|
79
79
|
# from somewhere it cannot see: a cgroup OOM kill chosen on aggregate pressure across concurrent workers,
|
|
80
80
|
# or one worker signalling another — they share a uid, and nothing stops that. Attributing either to the
|
|
81
81
|
# input this worker happened to be holding condemns a file for something it did not do.
|
|
82
|
+
#
|
|
83
|
+
# So the supervisor never infers either of them from a signal. They are decided in the worker, which is
|
|
84
|
+
# the process that holds the request: `memory` when it catches NoMemoryError itself, and `fsize` when a
|
|
85
|
+
# write of its own returns EFBIG. See Worker#disarm_file_size_signal for why the file-size verdict has
|
|
86
|
+
# to be earned that way rather than read off a wait status.
|
|
87
|
+
#
|
|
88
|
+
# That is a narrower guarantee than "a permanent verdict cannot be forged", and the difference matters.
|
|
89
|
+
# It removes the supervisor as an instrument: a sibling's signal no longer travels through a wait status
|
|
90
|
+
# into someone else's blob. It does nothing about a cell that has been compromised outright and answers
|
|
91
|
+
# a connection itself — `from_wire` believes a `permanent` boolean off the wire, and a worker that stole
|
|
92
|
+
# `work.sock` writes whatever it likes. That is the socket-theft residual `docs/DESIGN.md` records, and
|
|
93
|
+
# it is not closed here.
|
|
82
94
|
PERMANENT_BY_CAUSE = {
|
|
83
95
|
FSIZE => true,
|
|
84
96
|
MEMORY => true,
|
data/lib/hot_cell/failure.rb
CHANGED
|
@@ -15,19 +15,24 @@ module HotCell
|
|
|
15
15
|
class Failure
|
|
16
16
|
MAX_MESSAGE_BYTES = 512
|
|
17
17
|
|
|
18
|
-
attr_reader :code, :message, :error_class, :cause, :signal
|
|
18
|
+
attr_reader :code, :message, :error_class, :cause, :signal, :stderr
|
|
19
19
|
|
|
20
|
-
# Every field is sanitized, not only the message. All
|
|
21
|
-
# all
|
|
20
|
+
# Every field is sanitized, not only the message. All of them arrive from the wire on the client side,
|
|
21
|
+
# so all of them carry whatever the peer put there — and they travel further than the message does, into
|
|
22
22
|
# `to_s`, into the `perform.hot_cell` event, and into whatever a subscriber writes down. `code` in
|
|
23
|
-
# particular is the field applications store. Scrubbing one and not the
|
|
24
|
-
#
|
|
25
|
-
def initialize(code:, permanent: nil, message: nil, error_class: nil, cause: nil, signal: nil
|
|
23
|
+
# particular is the field applications store. Scrubbing one and not the rest left the same poisoned row
|
|
24
|
+
# the scrub exists to prevent, reachable through a different key.
|
|
25
|
+
def initialize(code:, permanent: nil, message: nil, error_class: nil, cause: nil, signal: nil,
|
|
26
|
+
stderr: nil)
|
|
26
27
|
@code = self.class.sanitize(code).to_s
|
|
27
28
|
@cause = self.class.sanitize(cause)
|
|
28
29
|
@signal = self.class.sanitize(signal)
|
|
29
30
|
@error_class = self.class.sanitize(error_class)
|
|
30
31
|
@message = self.class.sanitize(message)
|
|
32
|
+
|
|
33
|
+
# The tail, because this is a transcript and its last line is the one that ended the request. Every
|
|
34
|
+
# other field is one message, where the head is what matters.
|
|
35
|
+
@stderr = self.class.sanitize(stderr, keep: :tail)
|
|
31
36
|
@permanent = permanent.nil? ? Codes.permanent?(@code, cause: @cause) : permanent
|
|
32
37
|
end
|
|
33
38
|
|
|
@@ -40,11 +45,15 @@ module HotCell
|
|
|
40
45
|
# `permanent` is the exception and survives, because compact drops only nil.
|
|
41
46
|
def to_h
|
|
42
47
|
{ code: code, permanent: permanent? }
|
|
43
|
-
.merge(cause: cause, signal: signal, class: error_class, message: message).compact
|
|
48
|
+
.merge(cause: cause, signal: signal, class: error_class, message: message, stderr: stderr).compact
|
|
44
49
|
end
|
|
45
50
|
|
|
51
|
+
# `one_line` rather than raw interpolation: `to_s` becomes the exception message an application logs, and
|
|
52
|
+
# a transcript ends in a newline — so a peer that put newlines in one writes extra lines into that log.
|
|
53
|
+
# The attribute keeps the raw text.
|
|
46
54
|
def to_s
|
|
47
|
-
[ code, cause, error_class, message ].compact.join(": ")
|
|
55
|
+
text = [ code, cause, error_class, message ].compact.join(": ")
|
|
56
|
+
stderr ? "#{text} (#{self.class.one_line(stderr)})" : text
|
|
48
57
|
end
|
|
49
58
|
|
|
50
59
|
class << self
|
|
@@ -73,14 +82,31 @@ module HotCell
|
|
|
73
82
|
end
|
|
74
83
|
|
|
75
84
|
new code: wire[:code], permanent: permanent, cause: wire[:cause], signal: wire[:signal],
|
|
76
|
-
error_class: wire[:class], message: wire[:message]
|
|
85
|
+
error_class: wire[:class], message: wire[:message], stderr: wire[:stderr]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# For a message that is about to be written as one line of a log. `sanitize` leaves CR and LF alone,
|
|
89
|
+
# which is right for a message an application stores or re-raises, but a peer that puts a newline in
|
|
90
|
+
# one writes a second log line of its own — formatted and indented like the real ones. Escape them,
|
|
91
|
+
# and the rest of the control characters with them.
|
|
92
|
+
def one_line(text)
|
|
93
|
+
sanitize(text)&.gsub(/[[:cntrl:]]/) { |character| character.dump[1..-2] }
|
|
77
94
|
end
|
|
78
95
|
|
|
79
|
-
|
|
96
|
+
# `keep: :tail` is for a captured stream rather than a message, and it is load-bearing on `from_wire`:
|
|
97
|
+
# a cell this client does not trust can fill that field to the response limit, and head-truncating
|
|
98
|
+
# there hands the caller the noise a decoder printed first instead of the fatal that ended it.
|
|
99
|
+
def sanitize(message, keep: :head)
|
|
80
100
|
return nil if message.nil?
|
|
81
101
|
|
|
82
|
-
String(message).dup.force_encoding(Encoding::UTF_8)
|
|
83
|
-
|
|
102
|
+
text = String(message).dup.force_encoding(Encoding::UTF_8).scrub("")
|
|
103
|
+
text = if keep == :tail && text.bytesize > MAX_MESSAGE_BYTES
|
|
104
|
+
text.byteslice(text.bytesize - MAX_MESSAGE_BYTES, MAX_MESSAGE_BYTES)
|
|
105
|
+
else
|
|
106
|
+
text.byteslice(0, MAX_MESSAGE_BYTES)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
text.scrub("")
|
|
84
110
|
end
|
|
85
111
|
end
|
|
86
112
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hotcell-core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Dalessio
|
|
@@ -10,12 +10,9 @@ cert_chain: []
|
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: |
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
This gem holds what both sides must agree on: the request and response format, SCM_RIGHTS
|
|
17
|
-
marshalling, descriptor access-mode verification, payload validation, and the error taxonomy.
|
|
18
|
-
It performs no I/O of its own and loads no media library.
|
|
13
|
+
The wire protocol between an application and a HotCell container: the request and response format,
|
|
14
|
+
file descriptor passing over SCM_RIGHTS, payload validation, and the error taxonomy. Both
|
|
15
|
+
hotcell-client and hotcell-server depend on it. Install one of those rather than this.
|
|
19
16
|
email:
|
|
20
17
|
- mike@37signals.com
|
|
21
18
|
executables: []
|
|
@@ -46,8 +43,8 @@ licenses:
|
|
|
46
43
|
- MIT
|
|
47
44
|
metadata:
|
|
48
45
|
homepage_uri: https://github.com/basecamp/hotcell
|
|
49
|
-
source_code_uri: https://github.com/basecamp/hotcell/tree/v0.
|
|
50
|
-
changelog_uri: https://github.com/basecamp/hotcell/blob/v0.
|
|
46
|
+
source_code_uri: https://github.com/basecamp/hotcell/tree/v0.3.0/hotcell-core
|
|
47
|
+
changelog_uri: https://github.com/basecamp/hotcell/blob/v0.3.0/CHANGELOG.md
|
|
51
48
|
bug_tracker_uri: https://github.com/basecamp/hotcell/issues
|
|
52
49
|
rubygems_mfa_required: 'true'
|
|
53
50
|
rdoc_options: []
|