Class: HotCell::Cell

Inherits:
Object
  • Object
show all
Defined in:
lib/hot_cell/cell.rb

Overview

One registered cell: where its sockets are, how long this application will wait, and which of its own exception classes to raise for each side of the permanent split.

Both socket paths are derived from one directory, so the volume mounts are mechanical rather than something to remember and the two sockets cannot end up apart.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, dir: nil, timeout: 30, control_timeout: 5, permanent: PermanentFailure, transient: TransientFailure, on_contract_skew: nil, transport: Transport::Socket.new) ⇒ Cell

timeout covers work, so it is sized to clear the cell's answer_within and a saturated cell reports its own verdict rather than a transport failure. control_timeout covers describe and metrics, which the supervisor answers inline with no fork or queue — so it is short on purpose. Sharing one number would give the call whose job is to say "this cell is down" the patience of a video transcode.

Both bound the answer rather than the whole call: connecting is not covered. Transport::Socket says why that is left alone.



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/hot_cell/cell.rb', line 19

def initialize(name, dir: nil, timeout: 30, control_timeout: 5,
               permanent: PermanentFailure, transient: TransientFailure,
               on_contract_skew: nil, transport: Transport::Socket.new)
  @name = name.to_s
  @dir = dir
  @timeout = timeout
  @control_timeout = control_timeout
  @permanent = permanent
  @transient = transient
  @on_contract_skew = on_contract_skew
  @transport = transport

  verify_bounds!
  verify_classification!
end

Instance Attribute Details

#control_timeoutObject (readonly)

Returns the value of attribute control_timeout.



10
11
12
# File 'lib/hot_cell/cell.rb', line 10

def control_timeout
  @control_timeout
end

#nameObject (readonly)

Returns the value of attribute name.



10
11
12
# File 'lib/hot_cell/cell.rb', line 10

def name
  @name
end

#permanentObject (readonly)

Returns the value of attribute permanent.



10
11
12
# File 'lib/hot_cell/cell.rb', line 10

def permanent
  @permanent
end

#timeoutObject (readonly)

Returns the value of attribute timeout.



10
11
12
# File 'lib/hot_cell/cell.rb', line 10

def timeout
  @timeout
end

#transientObject (readonly)

Returns the value of attribute transient.



10
11
12
# File 'lib/hot_cell/cell.rb', line 10

def transient
  @transient
end

#transportObject (readonly)

Returns the value of attribute transport.



10
11
12
# File 'lib/hot_cell/cell.rb', line 10

def transport
  @transport
end

Instance Method Details

#control_socketObject



53
54
55
# File 'lib/hot_cell/cell.rb', line 53

def control_socket
  File.join directory, "control.sock"
end

#describeObject

Static, and called once at boot. The cheapest way to catch a client pointed at a cell that does not carry the operation it wants, which is otherwise an unsupported on the first real request.

Boot must not fail when a cell does not answer. A cell that is down at app boot is a degraded deployment rather than a broken one, and an application that refuses to start because its thumbnail cell is restarting is worse than one that serves placeholders. So this warns and carries on.

Nor when a cell answers something this client cannot read. The three warnings below reach into the description without checking types, so a cell that sends the wrong ones raises here — and the README calls describe_cells from after_initialize, where that is not a failed check but an application that does not boot. The process that wrote the description is the one that runs untrusted content. So rescue: returning nil is what an unreachable cell already returns, and every caller handles it.



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/hot_cell/cell.rb', line 81

def describe
  return nil unless enabled?

  response = control(DESCRIBE)
  return warn_unreachable(response.failure) unless response.ok?

  response.result.tap do |described|
    warn_about_timeout described
    warn_about_missing_operations described
    warn_about_group_skew described
  end
rescue StandardError => error
  HotCell.logger.warn "hotcell #{name}: this cell's description could not be read and is being " \
                      "ignored (#{error.class}: #{Failure.one_line error.message}). Boot continues; " \
                      "nothing it carries is assumed."
  nil
end

#directoryObject

Resolved on every call rather than at registration, which is what makes turning a path on a configuration change instead of a release. A directory consulted once in HotCell.register would make every flip a deploy, and reverting one too.



38
39
40
41
42
# File 'lib/hot_cell/cell.rb', line 38

def directory
  return @dir.call if @dir.respond_to?(:call)

  @dir || (HotCell.root && File.join(HotCell.root, name))
end

#enabled?Boolean

Unset means this path is off, and the caller runs in process exactly as it did before.

Returns:

  • (Boolean)


45
46
47
# File 'lib/hot_cell/cell.rb', line 45

def enabled?
  !directory.nil?
end

#exception_for(failure) ⇒ Object



57
58
59
# File 'lib/hot_cell/cell.rb', line 57

def exception_for(failure)
  failure.permanent? ? permanent : transient
end

#metricsObject



99
100
101
# File 'lib/hot_cell/cell.rb', line 99

def metrics
  enabled? ? control(METRICS) : nil
end

#report_contract_skew(error) ⇒ Object

Contract skew needs its own reporting hook because applications rescue broadly around representations, so "raise" is indistinguishable from "placeholder" and the skew is otherwise invisible. An application running several clients against several independently-booted cells needs to know which one skewed.



65
66
67
# File 'lib/hot_cell/cell.rb', line 65

def report_contract_skew(error)
  @on_contract_skew&.call error, self
end

#work_socketObject



49
50
51
# File 'lib/hot_cell/cell.rb', line 49

def work_socket
  File.join directory, "work.sock"
end