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 +4 -4
- data/lib/hot_cell/cell.rb +50 -13
- data/lib/hot_cell/client/version.rb +1 -1
- data/lib/hot_cell/install.rb +22 -3
- metadata +8 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 62be0a0a7b86a0273cfdfbe7ed7cd04460e536aeb3af1a03b08b944930c18082
|
|
4
|
+
data.tar.gz: cf6ffc9e65616d8cb89219f57e8273ecd8a7f8d47b5fc85db74f6014ff1e61bd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
128
|
-
"
|
|
129
|
-
"
|
|
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
|
data/lib/hot_cell/install.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
42
|
-
|
|
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.
|
|
75
|
-
changelog_uri: https://github.com/basecamp/hotcell/blob/v0.
|
|
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: []
|