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.
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.
- .sanitize(message) ⇒ Object
Instance Method Summary collapse
-
#initialize(code:, permanent: nil, message: nil, error_class: nil, cause: nil, signal: 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
Constructor Details
#initialize(code:, permanent: nil, message: nil, error_class: nil, cause: nil, signal: nil) ⇒ Failure
Every field is sanitized, not only the message. All five arrive from the wire on the client side, so
all five 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 other four left the same
poisoned row the scrub exists to prevent, reachable through a different key.
25 26 27 28 29 30 31 32 |
# File 'lib/hot_cell/failure.rb', line 25 def initialize(code:, permanent: nil, message: nil, error_class: nil, cause: nil, signal: 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() @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 |
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.
54 55 56 57 58 59 60 |
# File 'lib/hot_cell/failure.rb', line 54 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.
68 69 70 71 72 73 74 75 76 77 |
# File 'lib/hot_cell/failure.rb', line 68 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] end |
.sanitize(message) ⇒ Object
79 80 81 82 83 84 |
# File 'lib/hot_cell/failure.rb', line 79 def sanitize() return nil if .nil? String().dup.force_encoding(Encoding::UTF_8) .scrub("").byteslice(0, MAX_MESSAGE_BYTES).scrub("") end |
Instance Method Details
#permanent? ⇒ Boolean
34 35 36 |
# File 'lib/hot_cell/failure.rb', line 34 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.
41 42 43 44 |
# File 'lib/hot_cell/failure.rb', line 41 def to_h { code: code, permanent: permanent? } .merge(cause: cause, signal: signal, class: error_class, message: ).compact end |
#to_s ⇒ Object
46 47 48 |
# File 'lib/hot_cell/failure.rb', line 46 def to_s [ code, cause, error_class, ].compact.join(": ") end |