hotcell-client 0.5.0 → 0.6.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 +71 -2
- data/lib/hot_cell/cells.rb +12 -0
- data/lib/hot_cell/client/version.rb +1 -1
- data/lib/hot_cell/client.rb +8 -3
- data/lib/hot_cell/diagnosis.rb +26 -0
- data/lib/hot_cell/diagnostics_controller.rb +14 -0
- data/lib/hot_cell/health_controller.rb +15 -0
- data/lib/hot_cell/install/Gemfile.tt +1 -2
- data/lib/hot_cell/log_subscriber.rb +43 -0
- data/lib/hot_cell/railtie.rb +8 -2
- data/lib/hot_cell/transport.rb +20 -11
- metadata +10 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: acb581746375493247ece1d1962ef0f534cbca23e97851c1539f6b75e0eaa495
|
|
4
|
+
data.tar.gz: 621d326e537edb6bdd1125cbb229e20588a3312b7f63bbba56f039ff9c42bf45
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9b94270d6241921e1f8b9793e1fafa3dd3c3928d21a68ff14b2012c50eb2fc612007b31c072462ac1b90adb738c61bbf1ab64ee3827a3c31825a20d1718f3e4c
|
|
7
|
+
data.tar.gz: 536992cc18c2777bb5e7ae31e0b7445ebec24987b3d691c8a50d9e8b251d0b00283fdf93cfd94b58c2a6a3f224833f7c3ab4462950db9218073779c3142245d1
|
data/lib/hot_cell/cell.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
3
5
|
module HotCell
|
|
4
6
|
# One registered cell: where its sockets are, how long this application will wait, and which of its own
|
|
5
7
|
# exception classes to raise for each side of the permanent split.
|
|
@@ -15,7 +17,7 @@ module HotCell
|
|
|
15
17
|
# number would give the call whose job is to say "this cell is down" the patience of a video transcode.
|
|
16
18
|
#
|
|
17
19
|
# Both bound the answer rather than the whole call: connecting is not covered. Transport::Socket says
|
|
18
|
-
# why that
|
|
20
|
+
# why that does not hang.
|
|
19
21
|
def initialize(name, dir: nil, timeout: 30, control_timeout: 5,
|
|
20
22
|
permanent: PermanentFailure, transient: TransientFailure,
|
|
21
23
|
on_contract_skew: nil, transport: Transport::Socket.new)
|
|
@@ -72,7 +74,7 @@ module HotCell
|
|
|
72
74
|
# deployment rather than a broken one, and an application that refuses to start because its thumbnail
|
|
73
75
|
# cell is restarting is worse than one that serves placeholders. So this warns and carries on.
|
|
74
76
|
#
|
|
75
|
-
# Nor when a cell answers something this client cannot read. The
|
|
77
|
+
# Nor when a cell answers something this client cannot read. The warnings below reach into the
|
|
76
78
|
# description without checking types, so a cell that sends the wrong ones raises here — and the README
|
|
77
79
|
# calls `describe_cells` from `after_initialize`, where that is not a failed check but an application
|
|
78
80
|
# that does not boot. The process that wrote the description is the one that runs untrusted content.
|
|
@@ -86,6 +88,7 @@ module HotCell
|
|
|
86
88
|
response.result.tap do |described|
|
|
87
89
|
warn_about_timeout described
|
|
88
90
|
warn_about_group_skew described
|
|
91
|
+
warn_about_version_skew described
|
|
89
92
|
end
|
|
90
93
|
rescue StandardError => error
|
|
91
94
|
HotCell.logger.warn "hotcell #{name}: this cell's description could not be read and is being " \
|
|
@@ -98,7 +101,65 @@ module HotCell
|
|
|
98
101
|
enabled? ? control(METRICS) : nil
|
|
99
102
|
end
|
|
100
103
|
|
|
104
|
+
# The control socket takes no worker but carries no descriptor, so its checks pass on a cell whose work
|
|
105
|
+
# socket this application cannot use. Only the round trips see that, and echo alone passes a cell with
|
|
106
|
+
# the wrong group. `control` rather than `describe`, whose boot warnings would repeat on every poll.
|
|
107
|
+
def diagnose(work: false)
|
|
108
|
+
checks = { describe: check { answered control(DESCRIBE) },
|
|
109
|
+
metrics: check { answered control(METRICS) } }
|
|
110
|
+
return checks unless work
|
|
111
|
+
|
|
112
|
+
checks.merge echo: check { round_trip "health.echo" }, reopen: check { round_trip "health.reopen" }
|
|
113
|
+
end
|
|
114
|
+
|
|
101
115
|
private
|
|
116
|
+
class CheckFailed < StandardError; end
|
|
117
|
+
|
|
118
|
+
def check
|
|
119
|
+
return { ok: false, error: "no socket directory, so this cell is off" } unless enabled?
|
|
120
|
+
|
|
121
|
+
{ ok: true, result: yield }
|
|
122
|
+
rescue CheckFailed => error
|
|
123
|
+
{ ok: false, error: error.message }
|
|
124
|
+
rescue StandardError => error
|
|
125
|
+
{ ok: false, error: "#{error.class}: #{Failure.one_line error.message}" }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def answered(response)
|
|
129
|
+
raise CheckFailed, response.failure.to_s unless response.ok?
|
|
130
|
+
|
|
131
|
+
response.result
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def round_trip(operation_name, message = "hotcell")
|
|
135
|
+
client = client_for(operation_name)
|
|
136
|
+
|
|
137
|
+
Dir.mktmpdir "hotcell-diagnose" do |directory|
|
|
138
|
+
source = File.join(directory, "in")
|
|
139
|
+
destination = File.join(directory, "out")
|
|
140
|
+
File.write source, message
|
|
141
|
+
|
|
142
|
+
result = File.open(source, "rb") do |input|
|
|
143
|
+
File.open(destination, "wb") { |output| client.perform_in_hotcell input, output }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
returned = File.binread(destination)
|
|
147
|
+
raise CheckFailed, "the cell returned other bytes than it was sent" unless returned == message
|
|
148
|
+
raise CheckFailed, "the input was staged, so this did not cross a descriptor" if result[:staged]
|
|
149
|
+
|
|
150
|
+
result
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def client_for(operation_name)
|
|
155
|
+
cell = self
|
|
156
|
+
|
|
157
|
+
Class.new(Client) do
|
|
158
|
+
define_singleton_method(:cell) { cell }
|
|
159
|
+
operation operation_name
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
102
163
|
# A cell's sockets are `0660`, so the group that lets a worker re-open a descriptor is also the group
|
|
103
164
|
# that admits a caller. EACCES therefore means one thing, and it is worth saying rather than leaving an
|
|
104
165
|
# operator to read "could not describe the cell" as "the cell is down". Every other failure reads that
|
|
@@ -162,6 +223,14 @@ module HotCell
|
|
|
162
223
|
"every operation that gives a tool a filename will fail with EACCES."
|
|
163
224
|
end
|
|
164
225
|
|
|
226
|
+
def warn_about_version_skew(described)
|
|
227
|
+
reported = described[:server_version]
|
|
228
|
+
return if reported == Client::VERSION
|
|
229
|
+
|
|
230
|
+
HotCell.logger.warn "hotcell #{name}: this client is #{Client::VERSION} and the cell reports " \
|
|
231
|
+
"hotcell-server #{Failure.one_line reported.inspect}."
|
|
232
|
+
end
|
|
233
|
+
|
|
165
234
|
# A `nil` timeout reaches `Transport::Socket#receive` as `deadline: nil`, and reading with no deadline
|
|
166
235
|
# blocks: a cell that accepts the connection and then never answers holds this caller for good. At
|
|
167
236
|
# boot that is an application that never finishes starting, with no exception and nothing to rescue.
|
data/lib/hot_cell/cells.rb
CHANGED
|
@@ -79,6 +79,18 @@ module HotCell
|
|
|
79
79
|
"unset HotCell.group where both sides run as one user."
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
def diagnose(work: false)
|
|
83
|
+
Diagnosis.new(cells.values, work: work)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Read when HotCell::DiagnosticsController loads, so set it in an initializer. A development reload of
|
|
87
|
+
# the parent does not reach it.
|
|
88
|
+
attr_writer :diagnostics_controller_parent
|
|
89
|
+
|
|
90
|
+
def diagnostics_controller_parent
|
|
91
|
+
@diagnostics_controller_parent || "ActionController::Base"
|
|
92
|
+
end
|
|
93
|
+
|
|
82
94
|
# Test support. Named apart from the server gem's own reset, because both gems open this module and a
|
|
83
95
|
# shared name would mean whichever loaded last silently won.
|
|
84
96
|
def reset_registrations!
|
data/lib/hot_cell/client.rb
CHANGED
|
@@ -13,9 +13,16 @@ require "hot_cell/failures"
|
|
|
13
13
|
require "hot_cell/transport"
|
|
14
14
|
require "hot_cell/cell"
|
|
15
15
|
require "hot_cell/cells"
|
|
16
|
+
require "hot_cell/diagnosis"
|
|
16
17
|
|
|
17
18
|
require "hot_cell/railtie" if defined?(::Rails::Railtie)
|
|
18
19
|
|
|
20
|
+
module HotCell
|
|
21
|
+
# Loaded when a route first names them, so an application without Action Pack never requires it.
|
|
22
|
+
autoload :HealthController, "hot_cell/health_controller"
|
|
23
|
+
autoload :DiagnosticsController, "hot_cell/diagnostics_controller"
|
|
24
|
+
end
|
|
25
|
+
|
|
19
26
|
module HotCell
|
|
20
27
|
# The application side. A client class names the cell that serves it, and the call carries the same
|
|
21
28
|
# three things an operation receives on the other side — with the payload Hash arriving there as
|
|
@@ -96,7 +103,7 @@ module HotCell
|
|
|
96
103
|
line = request_line(inputs, outputs, payload)
|
|
97
104
|
|
|
98
105
|
response = nil
|
|
99
|
-
ActiveSupport::Notifications.instrument "perform.hot_cell" do |event|
|
|
106
|
+
ActiveSupport::Notifications.instrument "perform.hot_cell", operation: self.class.operation, cell: cell.name do |event|
|
|
100
107
|
response = verify_output(cell.transport.call(cell, line, descriptors), outputs)
|
|
101
108
|
publish event, cell, response, inputs, outputs
|
|
102
109
|
end
|
|
@@ -171,8 +178,6 @@ module HotCell
|
|
|
171
178
|
def publish(event, cell, response, inputs, outputs)
|
|
172
179
|
failure = response.failure
|
|
173
180
|
|
|
174
|
-
event[:operation] = self.class.operation
|
|
175
|
-
event[:cell] = cell.name
|
|
176
181
|
event[:code] = failure&.code
|
|
177
182
|
event[:cause] = failure&.cause
|
|
178
183
|
event[:signal] = failure&.signal
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module HotCell
|
|
7
|
+
class Diagnosis
|
|
8
|
+
attr_reader :cells
|
|
9
|
+
|
|
10
|
+
def initialize(cells, work:)
|
|
11
|
+
@at = Time.now.utc.iso8601(3)
|
|
12
|
+
@host = Socket.gethostname
|
|
13
|
+
@cells = cells.to_h { |cell| [ cell.name, cell.diagnose(work: work) ] }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# `cells.any?`, so an application that never registered a cell does not answer OK.
|
|
17
|
+
def healthy?
|
|
18
|
+
cells.any? && cells.each_value.all? { |checks| checks.each_value.all? { |check| check[:ok] } }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# `host` because sockets are host-local, and a poll through a load balancer lands wherever it lands.
|
|
22
|
+
def as_json(*)
|
|
23
|
+
{ at: @at, host: @host, healthy: healthy?, cells: cells }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_controller"
|
|
4
|
+
|
|
5
|
+
module HotCell
|
|
6
|
+
# Each request takes a worker per round trip, so put this behind authentication.
|
|
7
|
+
class DiagnosticsController < HotCell.diagnostics_controller_parent.constantize
|
|
8
|
+
def show
|
|
9
|
+
diagnosis = HotCell.diagnose(work: true)
|
|
10
|
+
|
|
11
|
+
render json: diagnosis, status: diagnosis.healthy? ? :ok : :service_unavailable
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_controller"
|
|
4
|
+
|
|
5
|
+
module HotCell
|
|
6
|
+
# Safe to leave unauthenticated: it asks the control socket only, so a poll takes no worker, and it answers
|
|
7
|
+
# nothing but OK or FAIL.
|
|
8
|
+
class HealthController < ActionController::Base
|
|
9
|
+
def show
|
|
10
|
+
healthy = HotCell.diagnose.healthy?
|
|
11
|
+
|
|
12
|
+
render plain: healthy ? "OK" : "FAIL", status: healthy ? :ok : :service_unavailable
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
source "https://rubygems.org"
|
|
7
7
|
|
|
8
|
-
#
|
|
9
|
-
# them answers `protocol` on every request.
|
|
8
|
+
# Keep this at the application's hotcell-client version; HotCell.describe_cells warns at boot otherwise.
|
|
10
9
|
gem "hotcell-server", "<%= HotCell::Client::VERSION %>"
|
|
11
10
|
|
|
12
11
|
# Gems your operations need go here. For example:
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
# All three, in this order, on Rails main: log_subscriber needs ColorizeLogging, which only the top-level file
|
|
6
|
+
# autoloads, and loading it before deprecation warns of a circular require.
|
|
7
|
+
require "active_support"
|
|
8
|
+
require "active_support/deprecation"
|
|
9
|
+
require "active_support/log_subscriber"
|
|
10
|
+
|
|
11
|
+
module HotCell
|
|
12
|
+
# One line per call, success or failure, from the `perform.hot_cell` event. The railtie attaches it; an
|
|
13
|
+
# application without Rails calls `HotCell::LogSubscriber.attach_to :hot_cell` and sets
|
|
14
|
+
# `ActiveSupport::LogSubscriber.logger`.
|
|
15
|
+
#
|
|
16
|
+
# Two clocks on purpose: `perform_ms` is what the cell measured inside the worker and `duration_ms` is what
|
|
17
|
+
# this process waited, and their difference is the queue and the socket.
|
|
18
|
+
#
|
|
19
|
+
# `stderr` is text a tool wrote while reading a hostile file. It goes into the JSON and nowhere else, so
|
|
20
|
+
# its newlines arrive escaped rather than as forged log lines. `ascii_only` because JSON leaves U+2028 and
|
|
21
|
+
# its kin raw, and a log viewer may break a line on them.
|
|
22
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
|
23
|
+
def perform(event)
|
|
24
|
+
payload = event.payload
|
|
25
|
+
duration_ms = event.duration.round(1)
|
|
26
|
+
|
|
27
|
+
info do
|
|
28
|
+
" HotCell (#{duration_ms}ms) " + JSON.generate({
|
|
29
|
+
cell: payload[:cell],
|
|
30
|
+
operation: payload[:operation],
|
|
31
|
+
code: payload[:code] || ("ok" unless payload[:exception]),
|
|
32
|
+
exception: payload[:exception]&.first,
|
|
33
|
+
cause: payload[:cause],
|
|
34
|
+
perform_ms: payload[:perform_ms],
|
|
35
|
+
duration_ms: duration_ms,
|
|
36
|
+
bytes_in: payload[:bytes_in],
|
|
37
|
+
bytes_out: payload[:bytes_out],
|
|
38
|
+
stderr: payload[:stderr],
|
|
39
|
+
}.compact, ascii_only: true)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/hot_cell/railtie.rb
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "hot_cell/log_subscriber"
|
|
4
|
+
|
|
3
5
|
module HotCell
|
|
4
|
-
# Loads the hotcell:install task into a Rails application. The gem works without
|
|
5
|
-
# is required only when Rails::Railtie is already defined.
|
|
6
|
+
# Loads the hotcell:install task into a Rails application and logs each call. The gem works without
|
|
7
|
+
# Rails, so this file is required only when Rails::Railtie is already defined.
|
|
6
8
|
class Railtie < ::Rails::Railtie
|
|
9
|
+
initializer "hot_cell.log_subscriber" do
|
|
10
|
+
HotCell::LogSubscriber.attach_to :hot_cell
|
|
11
|
+
end
|
|
12
|
+
|
|
7
13
|
rake_tasks do
|
|
8
14
|
load File.expand_path("tasks/hotcell.rake", __dir__)
|
|
9
15
|
end
|
data/lib/hot_cell/transport.rb
CHANGED
|
@@ -10,20 +10,19 @@ module HotCell
|
|
|
10
10
|
# Retry belongs in the job layer, which is the whole reason the transient class exists.
|
|
11
11
|
module Transport
|
|
12
12
|
class Socket
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
13
|
+
# `timeout` covers the answer and not the connection. On Linux a blocking `connect` to a Unix socket waits
|
|
14
|
+
# in the kernel while the listener's backlog is full, which is where a supervisor that stops calling
|
|
15
|
+
# `accept` leaves it. `UNIXSocket.new` does not wait: Ruby opens every socket non-blocking and takes the
|
|
16
|
+
# kernel's EAGAIN for a connection in progress, so it returns a socket that never connected, the send
|
|
17
|
+
# fails with ENOTCONN, and the caller gets `unavailable` at once. That is Ruby's behavior rather than
|
|
18
|
+
# this gem's, so ControlTimeoutTest holds it.
|
|
17
19
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# park it, and a recursive delete is renamed out of the loop rather than performed inside it. Bounding
|
|
22
|
-
# it means `connect_nonblock` plus `wait_writable` against the deadline `receive` already builds, which
|
|
23
|
-
# is worth doing the day this is observed and not before.
|
|
20
|
+
# If Ruby ever starts waiting, `connect_nonblock` plus `wait_writable` against a deadline would bound the
|
|
21
|
+
# call only by spinning: on Linux an unconnected Unix socket is always writable, so it retries EAGAIN until
|
|
22
|
+
# the deadline.
|
|
24
23
|
def call(cell, line, descriptors, socket: cell.work_socket, timeout: cell.timeout)
|
|
25
24
|
connection = Connection.new(UNIXSocket.new(socket))
|
|
26
|
-
connection
|
|
25
|
+
deliver connection, line, descriptors
|
|
27
26
|
receive connection, timeout
|
|
28
27
|
rescue SystemCallError, IOError => error
|
|
29
28
|
# A socket that does not exist, a cell that is restarting, an accessory not yet booted. These are
|
|
@@ -36,6 +35,16 @@ module HotCell
|
|
|
36
35
|
end
|
|
37
36
|
|
|
38
37
|
private
|
|
38
|
+
# A full cell writes its answer and closes the connection without reading the request. The send can
|
|
39
|
+
# then fail while the answer is already waiting on the socket. So this ignores the failed send, and
|
|
40
|
+
# `receive` reads the answer. If the peer closed without an answer, `receive` reports that the
|
|
41
|
+
# supervisor is gone.
|
|
42
|
+
def deliver(connection, line, descriptors)
|
|
43
|
+
connection.send_message line, descriptors: descriptors
|
|
44
|
+
rescue Errno::EPIPE, Errno::ECONNRESET
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
39
48
|
# One absolute deadline across the whole response, not a wait for the first byte. Waiting for
|
|
40
49
|
# readability and then calling a blocking read bounded nothing: a peer that sent one byte inside the
|
|
41
50
|
# timeout and then stopped held this caller until the cell's own deadline, and a peer that never
|
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.6.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.6.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.6.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: activesupport
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -53,11 +53,15 @@ files:
|
|
|
53
53
|
- lib/hot_cell/cells.rb
|
|
54
54
|
- lib/hot_cell/client.rb
|
|
55
55
|
- lib/hot_cell/client/version.rb
|
|
56
|
+
- lib/hot_cell/diagnosis.rb
|
|
57
|
+
- lib/hot_cell/diagnostics_controller.rb
|
|
56
58
|
- lib/hot_cell/failures.rb
|
|
59
|
+
- lib/hot_cell/health_controller.rb
|
|
57
60
|
- lib/hot_cell/install.rb
|
|
58
61
|
- lib/hot_cell/install/Dockerfile.tt
|
|
59
62
|
- lib/hot_cell/install/Gemfile.tt
|
|
60
63
|
- lib/hot_cell/install/config.rb.tt
|
|
64
|
+
- lib/hot_cell/log_subscriber.rb
|
|
61
65
|
- lib/hot_cell/railtie.rb
|
|
62
66
|
- lib/hot_cell/tasks/hotcell.rake
|
|
63
67
|
- lib/hot_cell/transport.rb
|
|
@@ -67,8 +71,8 @@ licenses:
|
|
|
67
71
|
- MIT
|
|
68
72
|
metadata:
|
|
69
73
|
homepage_uri: https://github.com/basecamp/hotcell
|
|
70
|
-
source_code_uri: https://github.com/basecamp/hotcell/tree/v0.
|
|
71
|
-
changelog_uri: https://github.com/basecamp/hotcell/blob/v0.
|
|
74
|
+
source_code_uri: https://github.com/basecamp/hotcell/tree/v0.6.0/hotcell-client
|
|
75
|
+
changelog_uri: https://github.com/basecamp/hotcell/blob/v0.6.0/CHANGELOG.md
|
|
72
76
|
bug_tracker_uri: https://github.com/basecamp/hotcell/issues
|
|
73
77
|
rubygems_mfa_required: 'true'
|
|
74
78
|
rdoc_options: []
|
|
@@ -85,7 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
85
89
|
- !ruby/object:Gem::Version
|
|
86
90
|
version: '0'
|
|
87
91
|
requirements: []
|
|
88
|
-
rubygems_version: 4.0.
|
|
92
|
+
rubygems_version: 4.0.6
|
|
89
93
|
specification_version: 4
|
|
90
94
|
summary: Call a HotCell from an application.
|
|
91
95
|
test_files: []
|