Class: HotCell::Supervisor::Child

Inherits:
Struct
  • Object
show all
Includes:
Timed
Defined in:
lib/hot_cell/supervisor.rb

Overview

Owns "is this worker busy" and the two transitions that change the answer, because the supervisor asking busy? and the supervisor assigning the four fields busy? is computed from are the same fact. Spread across the caller, a new field is one the next transition forgets to clear.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Timed

#expires_at, #overdue?

Instance Attribute Details

#bufferObject

Returns the value of attribute buffer

Returns:

  • (Object)

    the current value of buffer



42
43
44
# File 'lib/hot_cell/supervisor.rb', line 42

def buffer
  @buffer
end

#capturedObject

Returns the value of attribute captured

Returns:

  • (Object)

    the current value of captured



42
43
44
# File 'lib/hot_cell/supervisor.rb', line 42

def captured
  @captured
end

#connectionObject

Returns the value of attribute connection

Returns:

  • (Object)

    the current value of connection



42
43
44
# File 'lib/hot_cell/supervisor.rb', line 42

def connection
  @connection
end

#controlObject

Returns the value of attribute control

Returns:

  • (Object)

    the current value of control



42
43
44
# File 'lib/hot_cell/supervisor.rb', line 42

def control
  @control
end

#deadlineObject

Returns the value of attribute deadline

Returns:

  • (Object)

    the current value of deadline



42
43
44
# File 'lib/hot_cell/supervisor.rb', line 42

def deadline
  @deadline
end

#dispatched_atObject

Returns the value of attribute dispatched_at

Returns:

  • (Object)

    the current value of dispatched_at



42
43
44
# File 'lib/hot_cell/supervisor.rb', line 42

def dispatched_at
  @dispatched_at
end

#killed_forObject

Returns the value of attribute killed_for

Returns:

  • (Object)

    the current value of killed_for



42
43
44
# File 'lib/hot_cell/supervisor.rb', line 42

def killed_for
  @killed_for
end

#opObject

Returns the value of attribute op

Returns:

  • (Object)

    the current value of op



42
43
44
# File 'lib/hot_cell/supervisor.rb', line 42

def op
  @op
end

#pidObject

Returns the value of attribute pid

Returns:

  • (Object)

    the current value of pid



42
43
44
# File 'lib/hot_cell/supervisor.rb', line 42

def pid
  @pid
end

#retired_atObject

Returns the value of attribute retired_at

Returns:

  • (Object)

    the current value of retired_at



42
43
44
# File 'lib/hot_cell/supervisor.rb', line 42

def retired_at
  @retired_at
end

#servedObject

Returns the value of attribute served

Returns:

  • (Object)

    the current value of served



42
43
44
# File 'lib/hot_cell/supervisor.rb', line 42

def served
  @served
end

#slotObject

Returns the value of attribute slot

Returns:

  • (Object)

    the current value of slot



42
43
44
# File 'lib/hot_cell/supervisor.rb', line 42

def slot
  @slot
end

#stderrObject

Returns the value of attribute stderr

Returns:

  • (Object)

    the current value of stderr



42
43
44
# File 'lib/hot_cell/supervisor.rb', line 42

def stderr
  @stderr
end

Class Method Details

.build(slot:, pid:, control:, deadline:, stderr: nil) ⇒ Object



46
47
48
49
# File 'lib/hot_cell/supervisor.rb', line 46

def self.build(slot:, pid:, control:, deadline:, stderr: nil)
  new slot: slot, pid: pid, control: control, deadline: deadline, served: 0, buffer: "".b,
      stderr: stderr, captured: "".b
end

Instance Method Details

#available?Boolean

Returns:

  • (Boolean)


95
96
97
# File 'lib/hot_cell/supervisor.rb', line 95

def available?
  !busy? && retired_at.nil?
end

#busy?Boolean

Returns:

  • (Boolean)


91
92
93
# File 'lib/hot_cell/supervisor.rb', line 91

def busy?
  !connection.nil?
end

#capture_stderr(chunk) ⇒ Object

Keeps the last MAX_MESSAGE_BYTES rather than the first, because the line that ended the request is the last one written. Trimmed on every read, so a worker that never stops printing costs no more than that.



54
55
56
57
58
# File 'lib/hot_cell/supervisor.rb', line 54

def capture_stderr(chunk)
  captured << chunk
  excess = captured.bytesize - Failure::MAX_MESSAGE_BYTES
  captured.slice! 0, excess if excess.positive?
end

#dispatched(connection, deadline, at:) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
# File 'lib/hot_cell/supervisor.rb', line 66

def dispatched(connection, deadline, at:)
  # An earlier request's warning is not this request's death. Not a boundary, though: a descendant
  # holding fd 2 writes whenever it likes, and a sibling can open /proc/<pid>/fd/2 and write there too.
  captured.clear
  self.connection = connection
  self.dispatched_at = at
  self.deadline = deadline
  self.killed_for = nil
  self.op = nil
  self.served += 1
end

#finishedObject

Not a reset of deadline: the supervisor re-establishes it on the next dispatch, and leaving the last one readable is what lets a log line after the fact say what this worker was being held to.



80
81
82
83
# File 'lib/hot_cell/supervisor.rb', line 80

def finished
  connection&.close
  released
end

#lingering?(now, grace) ⇒ Boolean

The retirement analogue of overdue?, and the only timer that can reach a worker whose idle report was early: finish clears what overdue? reads, so a worker still running what it reported finished answers to this and nothing else. A retired worker has nothing left to do but exit — its closed control socket ends its await_dispatch — so the wait for it is bounded where an available worker's is not. Busy is excluded because a busy retired child is a worker that already died mid-request, which the reap answers for. killed_for is the same one-kill latch overdue? reads.

Returns:

  • (Boolean)


121
122
123
# File 'lib/hot_cell/supervisor.rb', line 121

def lingering?(now, grace)
  !busy? && killed_for.nil? && !retired_at.nil? && now - retired_at >= grace
end

#lingers_until(grace) ⇒ Object



125
126
127
# File 'lib/hot_cell/supervisor.rb', line 125

def lingers_until(grace)
  retired_at + grace if !busy? && killed_for.nil? && !retired_at.nil?
end

#releasedObject

Stop being busy while leaving the connection open, for the one caller that still has to answer on it.



86
87
88
89
# File 'lib/hot_cell/supervisor.rb', line 86

def released
  self.connection = nil
  self.dispatched_at = nil
end

#started_atObject



110
111
112
# File 'lib/hot_cell/supervisor.rb', line 110

def started_at
  dispatched_at
end

#stderr_fieldObject

A silent worker gets no field at all: Log#document does not compact the hotcell namespace, so a nil would put "stderr":null on every death a cell reports.



62
63
64
# File 'lib/hot_cell/supervisor.rb', line 62

def stderr_field
  captured.empty? ? {} : { stderr: Failure.sanitize(captured, keep: :tail) }
end

#timed?Boolean

A child already killed for its deadline stops being a timer, and that guard is load-bearing rather than tidy. Killing does not clear busy? — the supervisor still holds the connection it has to answer on — so without it expires_at stays in the past, wait_for returns 0, IO.select returns at once, and the loop re-kills and re-logs on every pass until the reap. Measured at a 0.2s deadline: 72 SIGKILLs and 72 synchronous stdout writes for one breach. The window is longest exactly when the host is already struggling — a worker in uninterruptible sleep, or one tearing down gigabytes of mappings — and the loop it starves is the one enforcing every other request's deadline.

Returns:

  • (Boolean)


106
107
108
# File 'lib/hot_cell/supervisor.rb', line 106

def timed?
  busy? && killed_for.nil?
end