hotcell-core 0.2.0 → 0.3.1
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 +3 -3
- data/lib/hot_cell/core/version.rb +1 -1
- data/lib/hot_cell/descriptors.rb +24 -2
- data/lib/hot_cell/failure.rb +30 -12
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 214b910102e639e8d5e5d7f5f145046c53d7fe45146a661054097731303d12db
|
|
4
|
+
data.tar.gz: 02db6536062e350ae67eefae22b50d83d8aa629b24487fdb01339e97308c045e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e4be2dd15734d9b3958ee98408866b9302f0a854a6ad16d652954e42f7f986a53286034b2af38eef9f09dc2ba2cc348d20b9707cf07c0e473a74a5742c2f9076
|
|
7
|
+
data.tar.gz: 6251a4224a51c24ce0b8bc245335973e2ccc33670594e41bb374242e8d70a46d0226247225415d61945ec1c2220db0ce6bd837779c9477e22b5529582a43f112
|
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
|
data/lib/hot_cell/descriptors.rb
CHANGED
|
@@ -39,8 +39,30 @@ module HotCell
|
|
|
39
39
|
# readable at any size, where staging it would be a write and RLIMIT_FSIZE bounds writes. Reopening
|
|
40
40
|
# `/dev/fd/N` gives a fresh file description at offset zero, so a read here does not disturb the
|
|
41
41
|
# descriptor the supervisor still holds.
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
#
|
|
43
|
+
# Darwin cannot keep that promise, so it keeps the behavior instead. `/dev/fd` has no procfs behind it
|
|
44
|
+
# there, and opening the path is dup(2): one shared offset, so what one reader consumes the next open
|
|
45
|
+
# skips. libvips sniffs a format by opening the path once per candidate loader, so the second loader
|
|
46
|
+
# reads past the magic bytes and a readable file is called unreadable.
|
|
47
|
+
#
|
|
48
|
+
# Darwin answers with the filename behind the descriptor, which a by-name open does start at zero. That
|
|
49
|
+
# hands a tool a path the cold side chose, which the rest of this class exists to prevent — a corner cut
|
|
50
|
+
# on purpose. Darwin is a development platform and nothing else: no container, no isolation, the cell
|
|
51
|
+
# running as the caller on the caller's own filesystem, where hiding a path it could already reach
|
|
52
|
+
# protected nothing. What has to match across platforms is what an operation sees — a path that reads
|
|
53
|
+
# from the first byte however many times it is opened — not how that is arrived at.
|
|
54
|
+
if RUBY_PLATFORM.include?("darwin")
|
|
55
|
+
F_GETPATH = 50 # sys/fcntl.h; Ruby's Fcntl module does not carry it
|
|
56
|
+
|
|
57
|
+
def fd_path
|
|
58
|
+
buffer = "\0" * 1024 # F_GETPATH writes the path into a buffer the caller supplies, MAXPATHLEN wide
|
|
59
|
+
io.fcntl F_GETPATH, buffer
|
|
60
|
+
buffer[/\A[^\0]+/]
|
|
61
|
+
end
|
|
62
|
+
else
|
|
63
|
+
def fd_path
|
|
64
|
+
"/dev/fd/#{io.fileno}"
|
|
65
|
+
end
|
|
44
66
|
end
|
|
45
67
|
|
|
46
68
|
def close
|
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,7 +82,7 @@ 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]
|
|
77
86
|
end
|
|
78
87
|
|
|
79
88
|
# For a message that is about to be written as one line of a log. `sanitize` leaves CR and LF alone,
|
|
@@ -84,11 +93,20 @@ module HotCell
|
|
|
84
93
|
sanitize(text)&.gsub(/[[:cntrl:]]/) { |character| character.dump[1..-2] }
|
|
85
94
|
end
|
|
86
95
|
|
|
87
|
-
|
|
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)
|
|
88
100
|
return nil if message.nil?
|
|
89
101
|
|
|
90
|
-
String(message).dup.force_encoding(Encoding::UTF_8)
|
|
91
|
-
|
|
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("")
|
|
92
110
|
end
|
|
93
111
|
end
|
|
94
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.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Dalessio
|
|
@@ -43,8 +43,8 @@ licenses:
|
|
|
43
43
|
- MIT
|
|
44
44
|
metadata:
|
|
45
45
|
homepage_uri: https://github.com/basecamp/hotcell
|
|
46
|
-
source_code_uri: https://github.com/basecamp/hotcell/tree/v0.
|
|
47
|
-
changelog_uri: https://github.com/basecamp/hotcell/blob/v0.
|
|
46
|
+
source_code_uri: https://github.com/basecamp/hotcell/tree/v0.3.1/hotcell-core
|
|
47
|
+
changelog_uri: https://github.com/basecamp/hotcell/blob/v0.3.1/CHANGELOG.md
|
|
48
48
|
bug_tracker_uri: https://github.com/basecamp/hotcell/issues
|
|
49
49
|
rubygems_mfa_required: 'true'
|
|
50
50
|
rdoc_options: []
|