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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e60aebb6474f67f6a35f01d9fe1e3a686c422a073e11ebf785af0816ade80e79
4
- data.tar.gz: eb13e25de57e23ff35d57fe100926cd64c11ecad18af96104dd3864d2246411b
3
+ metadata.gz: 214b910102e639e8d5e5d7f5f145046c53d7fe45146a661054097731303d12db
4
+ data.tar.gz: 02db6536062e350ae67eefae22b50d83d8aa629b24487fdb01339e97308c045e
5
5
  SHA512:
6
- metadata.gz: 0c0373bb9b2831d65e5fce1b02c7cb0ede24eed5a3d64144d905abbd93d72a7dcbf3d46250acb24caf195e1e1f9e0a530fa07ef2845c1ccba2118aed4f4e1aa2
7
- data.tar.gz: a087aedd5855c87b413c53085e23d9d579a59176bc9453d1e07e4712592d1c1405f8939d42c6123ae3fa5bcf9b50ad3d0112353d3adba359a7814d55a0e0b10f
6
+ metadata.gz: e4be2dd15734d9b3958ee98408866b9302f0a854a6ad16d652954e42f7f986a53286034b2af38eef9f09dc2ba2cc348d20b9707cf07c0e473a74a5742c2f9076
7
+ data.tar.gz: 6251a4224a51c24ce0b8bc245335973e2ccc33670594e41bb374242e8d70a46d0226247225415d61945ec1c2220db0ce6bd837779c9477e22b5529582a43f112
@@ -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 shows up in the
51
- # `unsupported` rate and in the client's boot-time warning. Recording a deploy window as permanent
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HotCell
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.1"
5
5
  end
@@ -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
- def fd_path
43
- "/dev/fd/#{io.fileno}"
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
@@ -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 five arrive from the wire on the client side, so
21
- # all five carry whatever the peer put there — and they travel further than the message does, into
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 other four left the same
24
- # poisoned row 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)
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
- def sanitize(message)
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
- .scrub("").byteslice(0, MAX_MESSAGE_BYTES).scrub("")
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.2.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.2.0/hotcell-core
47
- changelog_uri: https://github.com/basecamp/hotcell/blob/v0.2.0/CHANGELOG.md
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: []