Class: HotCell::Failure
- Inherits:
-
Object
- Object
- HotCell::Failure
- Defined in:
- lib/hot_cell/failure.rb
Overview
A cell's verdict on a request that did not succeed.
The message is untrusted and it outlives the request. It comes out of a worker that has just parsed a hostile file, and Vips::Error#message routinely contains the input filename. Applications store these as durable blob metadata so they can re-decide later against a newer library, which means an unscrubbed byte sequence becomes a permanently poisoned row, and an invalid UTF-8 sequence makes a downstream regex raise ArgumentError instead of answering false.
So the message is capped and scrubbed here, and that is not only hygiene: a cell that could not serialize its own error could not answer at all. The client scrubs again on receipt, because JSON.parse is not a filter — a \uD800 escape parses into an invalid UTF-8 String without complaint.
Constant Summary collapse
- MAX_MESSAGE_BYTES =
512
Instance Attribute Summary collapse
-
#cause ⇒ Object
readonly
Returns the value of attribute cause.
-
#code ⇒ Object
readonly
Returns the value of attribute code.
-
#error_class ⇒ Object
readonly
Returns the value of attribute error_class.
-
#message ⇒ Object
readonly
Returns the value of attribute message.
-
#signal ⇒ Object
readonly
Returns the value of attribute signal.
-
#stderr ⇒ Object
readonly
Returns the value of attribute stderr.
Class Method Summary collapse
-
.for(code, detail, cause: nil) ⇒ Object
Builds from either a message String or an Exception.
-
.from_wire(wire) ⇒ Object
A code this client has never heard of is not permanent.
-
.one_line(text) ⇒ Object
For a message that is about to be written as one line of a log.
-
.sanitize(message, keep: :head) ⇒ Object
keep: :tailis for a captured stream rather than a message, and it is load-bearing onfrom_wire: a cell this client does not trust can fill that field to the response limit, and head-truncating there hands the caller the noise a decoder printed first instead of the fatal that ended it.
Instance Method Summary collapse
-
#initialize(code:, permanent: nil, message: nil, error_class: nil, cause: nil, signal: nil, stderr: nil) ⇒ Failure
constructor
Every field is sanitized, not only the message.
- #permanent? ⇒ Boolean
-
#to_h ⇒ Object
compact rather than four guards: the constructor puts every one of these through
&.to_sorsanitize, so each is a String or nil and there is no falsey-but-meaningful value to protect. -
#to_s ⇒ Object
one_linerather than raw interpolation:to_sbecomes the exception message an application logs, and a transcript ends in a newline — so a peer that put newlines in one writes extra lines into that log.
Constructor Details
#initialize(code:, permanent: nil, message: nil, error_class: nil, cause: nil, signal: nil, stderr: nil) ⇒ Failure
Every field is sanitized, not only the message. All of them arrive from the wire on the client side,
so all of them carry whatever the peer put there — and they travel further than the message does, into
to_s, into the perform.hot_cell event, and into whatever a subscriber writes down. code in
particular is the field applications store. Scrubbing one and not the rest left the same poisoned row
the scrub exists to prevent, reachable through a different key.
25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/hot_cell/failure.rb', line 25 def initialize(code:, permanent: nil, message: nil, error_class: nil, cause: nil, signal: nil, stderr: nil) @code = self.class.sanitize(code).to_s @cause = self.class.sanitize(cause) @signal = self.class.sanitize(signal) @error_class = self.class.sanitize(error_class) @message = self.class.sanitize() # The tail, because this is a transcript and its last line is the one that ended the request. Every # other field is one message, where the head is what matters. @stderr = self.class.sanitize(stderr, keep: :tail) @permanent = permanent.nil? ? Codes.permanent?(@code, cause: @cause) : permanent end |
Instance Attribute Details
#cause ⇒ Object (readonly)
Returns the value of attribute cause.
18 19 20 |
# File 'lib/hot_cell/failure.rb', line 18 def cause @cause end |
#code ⇒ Object (readonly)
Returns the value of attribute code.
18 19 20 |
# File 'lib/hot_cell/failure.rb', line 18 def code @code end |
#error_class ⇒ Object (readonly)
Returns the value of attribute error_class.
18 19 20 |
# File 'lib/hot_cell/failure.rb', line 18 def error_class @error_class end |
#message ⇒ Object (readonly)
Returns the value of attribute message.
18 19 20 |
# File 'lib/hot_cell/failure.rb', line 18 def @message end |
#signal ⇒ Object (readonly)
Returns the value of attribute signal.
18 19 20 |
# File 'lib/hot_cell/failure.rb', line 18 def signal @signal end |
#stderr ⇒ Object (readonly)
Returns the value of attribute stderr.
18 19 20 |
# File 'lib/hot_cell/failure.rb', line 18 def stderr @stderr end |
Class Method Details
.for(code, detail, cause: nil) ⇒ Object
Builds from either a message String or an Exception. An Exception has to become two wire fields, and that rule was written out at three call sites across two gems — the worker, the supervisor's control answer, and the client's transport.
63 64 65 66 67 68 69 |
# File 'lib/hot_cell/failure.rb', line 63 def for(code, detail, cause: nil) if detail.is_a?(Exception) new code: code, cause: cause, error_class: detail.class.name, message: detail. else new code: code, cause: cause, message: detail end end |
.from_wire(wire) ⇒ Object
A code this client has never heard of is not permanent. An old client will meet a code added
later, and the harm of the two mistakes is not symmetrical: retrying something permanent costs
some work, while writing down a verdict that was temporary is irreversible.
A permanent that is present but not a boolean is derived rather than believed. Truthiness would make
any non-nil value permanent, and permanent is the answer that cannot be taken back — so a garbled
field must not be able to say it.
77 78 79 80 81 82 83 84 85 86 |
# File 'lib/hot_cell/failure.rb', line 77 def from_wire(wire) permanent = if [ true, false ].include?(wire[:permanent]) wire[:permanent] else Codes.known?(wire[:code]) && Codes.permanent?(wire[:code], cause: wire[:cause]) end new code: wire[:code], permanent: permanent, cause: wire[:cause], signal: wire[:signal], error_class: wire[:class], message: wire[:message], stderr: wire[:stderr] end |
.one_line(text) ⇒ Object
For a message that is about to be written as one line of a log. sanitize leaves CR and LF alone,
which is right for a message an application stores or re-raises, but a peer that puts a newline in
one writes a second log line of its own — formatted and indented like the real ones. Escape them,
and the rest of the control characters with them.
92 93 94 |
# File 'lib/hot_cell/failure.rb', line 92 def one_line(text) sanitize(text)&.gsub(/[[:cntrl:]]/) { |character| character.dump[1..-2] } end |
.sanitize(message, keep: :head) ⇒ Object
keep: :tail is for a captured stream rather than a message, and it is load-bearing on from_wire:
a cell this client does not trust can fill that field to the response limit, and head-truncating
there hands the caller the noise a decoder printed first instead of the fatal that ended it.
99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/hot_cell/failure.rb', line 99 def sanitize(, keep: :head) return nil if .nil? text = String().dup.force_encoding(Encoding::UTF_8).scrub("") text = if keep == :tail && text.bytesize > MAX_MESSAGE_BYTES text.byteslice(text.bytesize - MAX_MESSAGE_BYTES, MAX_MESSAGE_BYTES) else text.byteslice(0, MAX_MESSAGE_BYTES) end text.scrub("") end |
Instance Method Details
#permanent? ⇒ Boolean
39 40 41 |
# File 'lib/hot_cell/failure.rb', line 39 def permanent? @permanent end |
#to_h ⇒ Object
compact rather than four guards: the constructor puts every one of these through &.to_s or
sanitize, so each is a String or nil and there is no falsey-but-meaningful value to protect.
permanent is the exception and survives, because compact drops only nil.
46 47 48 49 |
# File 'lib/hot_cell/failure.rb', line 46 def to_h { code: code, permanent: permanent? } .merge(cause: cause, signal: signal, class: error_class, message: , stderr: stderr).compact end |
#to_s ⇒ Object
one_line rather than raw interpolation: to_s becomes the exception message an application logs, and
a transcript ends in a newline — so a peer that put newlines in one writes extra lines into that log.
The attribute keeps the raw text.
54 55 56 57 |
# File 'lib/hot_cell/failure.rb', line 54 def to_s text = [ code, cause, error_class, ].compact.join(": ") stderr ? "#{text} (#{self.class.one_line(stderr)})" : text end |