hotcell-client 0.1.0 → 0.2.0

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: d6808ab8f7c6d12e3917dca07053e1c1ca88b686f43626b895f96a84e2625611
4
- data.tar.gz: ee7b60efeaf1b1baed200abe2bc2756d80b75507259b98171c55e78b652ca1d8
3
+ metadata.gz: 62be0a0a7b86a0273cfdfbe7ed7cd04460e536aeb3af1a03b08b944930c18082
4
+ data.tar.gz: cf6ffc9e65616d8cb89219f57e8273ecd8a7f8d47b5fc85db74f6014ff1e61bd
5
5
  SHA512:
6
- metadata.gz: 0135bebaa226693ef7c11f5e34ab1562bf6c58694ffbfb0af7aa3666acdeca05930a814696ed0b7097126deb1b8b3683033e3dead4134cecb12edfce65b20d3f
7
- data.tar.gz: 1998e3e48ded09c5a5975574ce4f961652c8f0e6f635ab875dafc8cc2eb0553301c9dc0271386657f6348cc22d80358e29f0961d046b417a1cfbcae0931b86da
6
+ metadata.gz: 93e74a6462bf95bed04585d87588df0916eaadb4eaab24b37966c8c7266f0051a5a9c8de6c825a542c54a0481ae885799bf56b892f7d184eaf99ee83895ff6ee
7
+ data.tar.gz: 99571523cc72ad1845a95dcab86dab7f9de917673543832d7abf591e182ec927dd77c542640fe9b94777eb39fcdbf8e7746e147f6fd41571b3f00251870d0646
data/lib/hot_cell/cell.rb CHANGED
@@ -28,6 +28,7 @@ module HotCell
28
28
  @on_contract_skew = on_contract_skew
29
29
  @transport = transport
30
30
 
31
+ verify_bounds!
31
32
  verify_classification!
32
33
  end
33
34
 
@@ -71,19 +72,28 @@ module HotCell
71
72
  # Boot must not fail when a cell does not answer. A cell that is down at app boot is a degraded
72
73
  # deployment rather than a broken one, and an application that refuses to start because its thumbnail
73
74
  # cell is restarting is worse than one that serves placeholders. So this warns and carries on.
75
+ #
76
+ # Nor when a cell answers something this client cannot read. The three warnings below reach into the
77
+ # description without checking types, so a cell that sends the wrong ones raises here — and the README
78
+ # calls `describe_cells` from `after_initialize`, where that is not a failed check but an application
79
+ # that does not boot. The process that wrote the description is the one that runs untrusted content.
80
+ # So rescue: returning nil is what an unreachable cell already returns, and every caller handles it.
74
81
  def describe
75
82
  return nil unless enabled?
76
83
 
77
84
  response = control(DESCRIBE)
78
- unless response.ok?
79
- HotCell.logger.warn "hotcell #{name}: #{unreachable_because response.failure}"
80
- return nil
81
- end
85
+ return warn_unreachable(response.failure) unless response.ok?
82
86
 
83
- warn_about_timeout response.result
84
- warn_about_missing_operations response.result
85
- warn_about_group_skew response.result
86
- response.result
87
+ response.result.tap do |described|
88
+ warn_about_timeout described
89
+ warn_about_missing_operations described
90
+ warn_about_group_skew described
91
+ end
92
+ rescue StandardError => error
93
+ HotCell.logger.warn "hotcell #{name}: this cell's description could not be read and is being " \
94
+ "ignored (#{error.class}: #{Failure.one_line error.message}). Boot continues; " \
95
+ "nothing it carries is assumed."
96
+ nil
87
97
  end
88
98
 
89
99
  def metrics
@@ -95,13 +105,18 @@ module HotCell
95
105
  # that admits a caller. EACCES therefore means one thing, and it is worth saying rather than leaving an
96
106
  # operator to read "could not describe the cell" as "the cell is down". Every other failure reads that
97
107
  # way correctly, because a restarting accessory is the common one.
108
+ def warn_unreachable(failure)
109
+ HotCell.logger.warn "hotcell #{name}: #{unreachable_because failure}"
110
+ nil
111
+ end
112
+
98
113
  def unreachable_because(failure)
99
114
  if failure.error_class == "Errno::EACCES"
100
115
  "this process may not open the cell's socket. Both sides share a group, and this one is in " \
101
116
  "#{Process.groups.sort.inspect}. Add the cell's gid to this container (Kamal: `group-add` under " \
102
117
  "the role's `options:`)."
103
118
  else
104
- "could not describe the cell (#{failure})"
119
+ "could not describe the cell (#{Failure.one_line failure})"
105
120
  end
106
121
  end
107
122
 
@@ -124,10 +139,9 @@ module HotCell
124
139
  return if needed.nil? || timeout.nil? || timeout >= needed
125
140
 
126
141
  HotCell.logger.warn "hotcell #{name}: this client waits #{seconds timeout} and the cell says it may " \
127
- "take #{seconds needed} to answer (queue_wait #{seconds described[:queue_wait]} + " \
128
- "deadline #{seconds described[:deadline]} + the time to kill and reply), so a " \
129
- "saturated cell will arrive here as a transport failure rather than as its own " \
130
- "verdict. Deliberate on a synchronous path; a mistake for a background job."
142
+ "take #{seconds needed} to answer, so a saturated cell will arrive here as a " \
143
+ "transport failure rather than as its own verdict. Deliberate on a synchronous " \
144
+ "path; a mistake for a background job."
131
145
  end
132
146
 
133
147
  # The cell reports seconds as floats, and "41.0s" is a worse sentence than "41s".
@@ -160,6 +174,29 @@ module HotCell
160
174
  end
161
175
  end
162
176
 
177
+ # A `nil` timeout reaches `Transport::Socket#receive` as `deadline: nil`, and reading with no deadline
178
+ # blocks: a cell that accepts the connection and then never answers holds this caller for good. At
179
+ # boot that is an application that never finishes starting, with no exception and nothing to rescue.
180
+ #
181
+ # Refused at registration rather than defaulted, so that a `timeout:` read from an unset environment
182
+ # variable says so instead of quietly becoming 30 seconds. Same reason as `verify_classification!`.
183
+ def verify_bounds!
184
+ @timeout = bounded!(:timeout, timeout)
185
+ @control_timeout = bounded!(:control_timeout, control_timeout)
186
+ end
187
+
188
+ # Stored as a Float, because the only thing done with the number is `Clock.now + timeout`. That is
189
+ # also why `finite?` is part of the test: `10**400` is a positive Integer, and adding it to a clock
190
+ # gives `Infinity` — a deadline that never passes, which is the wait this refuses.
191
+ def bounded!(name, value)
192
+ seconds = Float(value, exception: false)
193
+ return seconds if seconds&.finite? && seconds.positive?
194
+
195
+ raise ConfigurationError,
196
+ "#{name}: #{value.inspect[0, 60]} does not bound anything, so a cell that never answers " \
197
+ "would hold this call for as long as it liked. It must be a positive number of seconds."
198
+ end
199
+
163
200
  def verify_classification!
164
201
  return if permanent.nil? || transient.nil?
165
202
  return unless transient <= permanent
@@ -3,6 +3,6 @@
3
3
  module HotCell
4
4
  # A class and not a module: this file loads before hot_cell/client.rb opens the same name.
5
5
  class Client
6
- VERSION = "0.1.0"
6
+ VERSION = "0.2.0"
7
7
  end
8
8
  end
@@ -18,8 +18,12 @@ module HotCell
18
18
  def call(root, out: $stdout)
19
19
  templates.each do |template|
20
20
  relative = template.delete_prefix("#{TEMPLATES}/").delete_suffix(".tt")
21
- install template, File.join(root, "hotcell", relative), "hotcell/#{relative}", out
21
+ write File.join(root, "hotcell", relative), "hotcell/#{relative}", out do
22
+ render template
23
+ end
22
24
  end
25
+
26
+ keep_operations root, out
23
27
  end
24
28
 
25
29
  private
@@ -27,12 +31,27 @@ module HotCell
27
31
  Dir.glob("#{TEMPLATES}/**/*.tt", File::FNM_DOTMATCH).sort
28
32
  end
29
33
 
30
- def install(template, destination, label, out)
34
+ # **The directory the generated Dockerfile copies, made here rather than shipped.**
35
+ #
36
+ # It used to be a template, `install/operations/.keep.tt`, and a dotfile is exactly what the
37
+ # gemspec's `Dir["lib/**/*"]` does not match — so the published 0.1.0 carried the directory's only
38
+ # file nowhere, `COPY operations/` had nothing to copy, and the advertised scaffold could not build.
39
+ # The checkout's own install test could not see it, because in a checkout the file is simply there.
40
+ #
41
+ # An empty file rather than an empty directory, because the application's git is what has to keep
42
+ # this after a fresh clone, and git tracks no empty directories.
43
+ def keep_operations(root, out)
44
+ write File.join(root, "hotcell", "operations", ".keep"), "hotcell/operations/.keep", out do
45
+ ""
46
+ end
47
+ end
48
+
49
+ def write(destination, label, out)
31
50
  if File.exist?(destination)
32
51
  out.puts " skip #{label} (already exists)"
33
52
  else
34
53
  FileUtils.mkdir_p File.dirname(destination)
35
- File.write destination, render(template)
54
+ File.write destination, yield
36
55
  out.puts " create #{label}"
37
56
  end
38
57
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hotcell-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Dalessio
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.1.0
18
+ version: 0.2.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.1.0
25
+ version: 0.2.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: activesupport
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -38,13 +38,9 @@ dependencies:
38
38
  - !ruby/object:Gem::Version
39
39
  version: '7.1'
40
40
  description: |
41
- The application side of HotCell. Register the cells a deployment runs, subclass HotCell::Client to
42
- name one, and call it with descriptors and a payload.
43
-
44
- The client owns everything a caller needs to respond correctly to a cell that is saturated,
45
- restarting, or absent: the classification of every error into permanent and transient, the
46
- exception classes an application injects for each, and instrumentation through
47
- ActiveSupport::Notifications.
41
+ Call operations that run in a HotCell container. Register a cell, subclass HotCell::Client to name
42
+ an operation, and call it with open file descriptors and a payload. Failures arrive classified as
43
+ permanent or transient, as exception classes your application supplies.
48
44
  email:
49
45
  - mike@37signals.com
50
46
  executables: []
@@ -71,8 +67,8 @@ licenses:
71
67
  - MIT
72
68
  metadata:
73
69
  homepage_uri: https://github.com/basecamp/hotcell
74
- source_code_uri: https://github.com/basecamp/hotcell/tree/v0.1.0/hotcell-client
75
- changelog_uri: https://github.com/basecamp/hotcell/blob/v0.1.0/CHANGELOG.md
70
+ source_code_uri: https://github.com/basecamp/hotcell/tree/v0.2.0/hotcell-client
71
+ changelog_uri: https://github.com/basecamp/hotcell/blob/v0.2.0/CHANGELOG.md
76
72
  bug_tracker_uri: https://github.com/basecamp/hotcell/issues
77
73
  rubygems_mfa_required: 'true'
78
74
  rdoc_options: []