upkeep-rails 0.2.5-aarch64-linux-gnu
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +244 -0
- data/docs/drafts/turbo-streams-is-cache-invalidation-you-write-by-hand.md +240 -0
- data/docs/how-it-works.md +329 -0
- data/docs/plans/sqlglot-active-record-migration.md +211 -0
- data/docs/plans/turbo-frame-subscription-composition.md +90 -0
- data/docs/spikes/SQLGLOT_VS_AREL_FINDINGS.md +136 -0
- data/docs/spikes/query-source-analysis-comparison/FINDINGS.md +169 -0
- data/docs/spikes/sqlglot-first-active-record/README.md +26 -0
- data/docs/spikes/sqlglot-query-analysis/FINDINGS.md +110 -0
- data/docs/spikes/sqlglot-query-analysis/PULSE_1802_FINDINGS.md +95 -0
- data/docs/spikes/sqlglot-semantic-bindings/README.md +29 -0
- data/lib/generators/upkeep/install/install_generator.rb +192 -0
- data/lib/generators/upkeep/install/templates/create_upkeep_subscriptions.rb.erb +49 -0
- data/lib/generators/upkeep/install/templates/subscription.js +288 -0
- data/lib/generators/upkeep/install/templates/upkeep.rb +65 -0
- data/lib/upkeep/active_record_query.rb +248 -0
- data/lib/upkeep/capture/request.rb +150 -0
- data/lib/upkeep/dag/subscription_shape.rb +244 -0
- data/lib/upkeep/dag.rb +454 -0
- data/lib/upkeep/delivery/action_cable_adapter.rb +48 -0
- data/lib/upkeep/delivery/async_dispatcher.rb +102 -0
- data/lib/upkeep/delivery/broadcast_transport.rb +89 -0
- data/lib/upkeep/delivery/transport.rb +194 -0
- data/lib/upkeep/delivery/turbo_streams.rb +339 -0
- data/lib/upkeep/delivery.rb +7 -0
- data/lib/upkeep/dependencies.rb +600 -0
- data/lib/upkeep/herb/developer_report.rb +135 -0
- data/lib/upkeep/herb/manifest_cache.rb +83 -0
- data/lib/upkeep/herb/manifest_diff.rb +183 -0
- data/lib/upkeep/herb/source_instrumenter.rb +149 -0
- data/lib/upkeep/herb/template_manifest.rb +548 -0
- data/lib/upkeep/invalidation/collection_append.rb +84 -0
- data/lib/upkeep/invalidation/collection_member_replace.rb +78 -0
- data/lib/upkeep/invalidation/collection_prepend.rb +84 -0
- data/lib/upkeep/invalidation/collection_remove.rb +57 -0
- data/lib/upkeep/invalidation/planner.rb +411 -0
- data/lib/upkeep/invalidation.rb +7 -0
- data/lib/upkeep/rails/action_view_capture.rb +1007 -0
- data/lib/upkeep/rails/activation_token.rb +55 -0
- data/lib/upkeep/rails/cable/channel.rb +165 -0
- data/lib/upkeep/rails/cable/subscriber_identity.rb +361 -0
- data/lib/upkeep/rails/cable.rb +4 -0
- data/lib/upkeep/rails/client_subscription.rb +65 -0
- data/lib/upkeep/rails/cluster_guard.rb +57 -0
- data/lib/upkeep/rails/configuration.rb +252 -0
- data/lib/upkeep/rails/controller_runtime.rb +187 -0
- data/lib/upkeep/rails/install.rb +28 -0
- data/lib/upkeep/rails/job_runtime.rb +43 -0
- data/lib/upkeep/rails/railtie.rb +44 -0
- data/lib/upkeep/rails/replay.rb +244 -0
- data/lib/upkeep/rails/testing.rb +259 -0
- data/lib/upkeep/rails.rb +466 -0
- data/lib/upkeep/replay.rb +462 -0
- data/lib/upkeep/runtime.rb +1276 -0
- data/lib/upkeep/shared_streams.rb +86 -0
- data/lib/upkeep/sql_dependency_analysis.rb +553 -0
- data/lib/upkeep/sqlglot/libsqlglot_rust.so +0 -0
- data/lib/upkeep/sqlglot/native.rb +121 -0
- data/lib/upkeep/sqlglot/native_library.rb +23 -0
- data/lib/upkeep/sqlglot.rb +367 -0
- data/lib/upkeep/subscriptions/active_record_store.rb +398 -0
- data/lib/upkeep/subscriptions/active_record_subscription_persistence.rb +411 -0
- data/lib/upkeep/subscriptions/active_registry.rb +80 -0
- data/lib/upkeep/subscriptions/base_store.rb +110 -0
- data/lib/upkeep/subscriptions/json_snapshot.rb +98 -0
- data/lib/upkeep/subscriptions/layered_reverse_index.rb +125 -0
- data/lib/upkeep/subscriptions/lookup_instrumentation.rb +32 -0
- data/lib/upkeep/subscriptions/persistent_reverse_index.rb +228 -0
- data/lib/upkeep/subscriptions/registrar.rb +36 -0
- data/lib/upkeep/subscriptions/reverse_index.rb +313 -0
- data/lib/upkeep/subscriptions/shape.rb +117 -0
- data/lib/upkeep/subscriptions/store.rb +349 -0
- data/lib/upkeep/subscriptions.rb +7 -0
- data/lib/upkeep/targeting.rb +146 -0
- data/lib/upkeep/version.rb +5 -0
- data/lib/upkeep-rails.rb +3 -0
- data/lib/upkeep.rb +15 -0
- data/upkeep-rails.gemspec +66 -0
- metadata +327 -0
data/lib/upkeep/rails.rb
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/notifications"
|
|
4
|
+
require_relative "capture/request"
|
|
5
|
+
require_relative "subscriptions/registrar"
|
|
6
|
+
require_relative "rails/configuration"
|
|
7
|
+
require_relative "rails/cluster_guard"
|
|
8
|
+
require_relative "rails/activation_token"
|
|
9
|
+
require_relative "rails/replay"
|
|
10
|
+
require_relative "rails/action_view_capture"
|
|
11
|
+
require_relative "rails/cable"
|
|
12
|
+
require_relative "rails/client_subscription"
|
|
13
|
+
require_relative "rails/controller_runtime"
|
|
14
|
+
require_relative "rails/job_runtime"
|
|
15
|
+
require_relative "rails/install"
|
|
16
|
+
require_relative "rails/testing"
|
|
17
|
+
require_relative "rails/railtie" if defined?(::Rails::Railtie)
|
|
18
|
+
|
|
19
|
+
module Upkeep
|
|
20
|
+
module Rails
|
|
21
|
+
SUBSCRIPTION_IDENTITY = "upkeep.subscription_identity"
|
|
22
|
+
REQUEST_CAPTURE = "request_capture.upkeep"
|
|
23
|
+
DELIVERY_ENQUEUE = "delivery_enqueue.upkeep"
|
|
24
|
+
DELIVERY_ENQUEUE_ERROR = "delivery_enqueue_error.upkeep"
|
|
25
|
+
INTERNAL_DELIVERY_TABLES = %w[
|
|
26
|
+
upkeep_subscriptions
|
|
27
|
+
upkeep_subscription_index_entries
|
|
28
|
+
upkeep_subscription_shape_index_entries
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
def configuration
|
|
33
|
+
@configuration ||= Configuration.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def configure
|
|
37
|
+
yield configuration
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def subscriptions
|
|
41
|
+
discard_subscription_store! if @subscriptions && subscription_store_config_changed?
|
|
42
|
+
@subscriptions ||= build_subscription_store
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def transport
|
|
46
|
+
@transport ||= Delivery::BroadcastTransport.new
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def reset_runtime!
|
|
50
|
+
@delivery_dispatcher&.shutdown
|
|
51
|
+
@delivery_dispatcher = nil
|
|
52
|
+
@subscription_shape_cache&.reset
|
|
53
|
+
@subscription_registrar = nil
|
|
54
|
+
discard_subscription_store! if @subscriptions
|
|
55
|
+
@subscriptions = build_subscription_store
|
|
56
|
+
@subscriptions.reset
|
|
57
|
+
@transport = Delivery::BroadcastTransport.new
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def register_controller_subscription(controller, capture)
|
|
61
|
+
return unless subscription_response?(controller, capture)
|
|
62
|
+
|
|
63
|
+
unless capture.recorder.reactive?
|
|
64
|
+
decision = Cable::SubscriberIdentity.decision_for(controller.request, recorder: capture.recorder)
|
|
65
|
+
request_subscription_reload(controller, "refused_boundary") if controller.request.headers["Turbo-Frame"].present?
|
|
66
|
+
instrument_subscription_identity(
|
|
67
|
+
decision,
|
|
68
|
+
registered: false,
|
|
69
|
+
deopt_reason: "refused_boundary",
|
|
70
|
+
refused_boundaries: capture.recorder.refused_boundaries.map(&:reason)
|
|
71
|
+
)
|
|
72
|
+
return
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
recorder, base_subscription = controller_subscription_recorder(controller, capture)
|
|
76
|
+
return unless recorder
|
|
77
|
+
|
|
78
|
+
decision = Cable::SubscriberIdentity.decision_for(controller.request, recorder: recorder)
|
|
79
|
+
|
|
80
|
+
identity = Cable::SubscriberIdentity.derive_from_request(
|
|
81
|
+
controller.request,
|
|
82
|
+
recorder: recorder,
|
|
83
|
+
decision: decision
|
|
84
|
+
)
|
|
85
|
+
unless replacement_identity_valid?(base_subscription, identity)
|
|
86
|
+
request_subscription_reload(controller, "subscriber_identity_changed")
|
|
87
|
+
instrument_subscription_identity(decision, registered: false, deopt_reason: "subscriber_identity_changed")
|
|
88
|
+
return
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
request_signature = base_subscription ?
|
|
92
|
+
metadata_value(base_subscription, :request_signature) :
|
|
93
|
+
capture.signature
|
|
94
|
+
registration = subscription_registrar.register(
|
|
95
|
+
identity: identity,
|
|
96
|
+
decision: decision,
|
|
97
|
+
recorder: recorder,
|
|
98
|
+
signature: request_signature,
|
|
99
|
+
metadata: identity_metadata(decision).merge(
|
|
100
|
+
path: base_subscription ? metadata_value(base_subscription, :path) : controller.request.fullpath,
|
|
101
|
+
stream_name: identity.stream_name,
|
|
102
|
+
request_signature: request_signature.respond_to?(:to_h) ? request_signature.to_h : request_signature,
|
|
103
|
+
replaces_subscription_id: base_subscription&.id
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
instrument_subscription_identity(decision, registered: true, subscription: registration.subscription)
|
|
107
|
+
|
|
108
|
+
registration
|
|
109
|
+
rescue Cable::UnidentifiedSubscriber => error
|
|
110
|
+
instrument_subscription_identity(
|
|
111
|
+
decision || Cable::SubscriberIdentity.decision_for(controller.request, recorder: recorder),
|
|
112
|
+
registered: false,
|
|
113
|
+
deopt_reason: "unidentified_identity",
|
|
114
|
+
error: error.message
|
|
115
|
+
)
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Dispatches committed application changes onto the in-process delivery
|
|
120
|
+
# dispatcher (or synchronously when config.deliver_inline is set).
|
|
121
|
+
#
|
|
122
|
+
# ControllerRuntime calls this automatically after non-GET actions. Apps
|
|
123
|
+
# usually should not call it from controllers or models.
|
|
124
|
+
#
|
|
125
|
+
# @param changes [Array<#to_h>] change events to deliver. Defaults to the
|
|
126
|
+
# current runtime change log.
|
|
127
|
+
# @return [Upkeep::Delivery::Transport::DispatchReport]
|
|
128
|
+
def deliver_changes!(changes = Runtime::ChangeLog.drain)
|
|
129
|
+
changes = deliverable_changes(changes)
|
|
130
|
+
record_testing_change_facts(changes)
|
|
131
|
+
return Delivery::Transport::DispatchReport.new([]) if changes.empty?
|
|
132
|
+
|
|
133
|
+
dispatch_changes(changes)
|
|
134
|
+
rescue StandardError => error
|
|
135
|
+
instrument_delivery_enqueue_error(changes, error)
|
|
136
|
+
Delivery::Transport::DispatchReport.new([])
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Delivers committed application changes immediately in the current
|
|
140
|
+
# process.
|
|
141
|
+
#
|
|
142
|
+
# This is used by inline delivery (config.deliver_inline). Tests that
|
|
143
|
+
# need deterministic async delivery should use Upkeep::Rails::Testing
|
|
144
|
+
# instead of calling this directly.
|
|
145
|
+
#
|
|
146
|
+
# @param changes [Array<#to_h>] change events to deliver. Defaults to the
|
|
147
|
+
# current runtime change log.
|
|
148
|
+
# @return [Upkeep::Delivery::TurboStreams::Batch, Upkeep::Delivery::Transport::DispatchReport]
|
|
149
|
+
def deliver_changes_now!(changes = Runtime::ChangeLog.drain)
|
|
150
|
+
changes = deliverable_changes(changes)
|
|
151
|
+
record_testing_change_facts(changes)
|
|
152
|
+
return Delivery::Transport::DispatchReport.new([]) if changes.empty?
|
|
153
|
+
|
|
154
|
+
batch = delivery_batch_for([changes])
|
|
155
|
+
deliver_batch(batch)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def validate_configuration!(environment: rails_environment)
|
|
159
|
+
return true unless configuration.enabled
|
|
160
|
+
|
|
161
|
+
validate_subscription_store!(environment: environment)
|
|
162
|
+
validate_cluster_safety!(environment: environment)
|
|
163
|
+
true
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
def delivery_dispatcher
|
|
169
|
+
@delivery_dispatcher ||= Delivery::AsyncDispatcher.new(batch_window: configuration.delivery_batch_window) do |change_sets|
|
|
170
|
+
batch = delivery_batch_for(change_sets)
|
|
171
|
+
deliver_batch(batch)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def drain_delivery_dispatcher!
|
|
176
|
+
@delivery_dispatcher&.drain
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def dispatch_changes(changes)
|
|
180
|
+
payload = {
|
|
181
|
+
inline: configuration.deliver_inline,
|
|
182
|
+
change_count: changes.size
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
ActiveSupport::Notifications.instrument(DELIVERY_ENQUEUE, payload) do
|
|
186
|
+
if configuration.deliver_inline
|
|
187
|
+
deliver_changes_now!(changes)
|
|
188
|
+
else
|
|
189
|
+
delivery_dispatcher.enqueue(changes)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def instrument_delivery_enqueue_error(changes, error)
|
|
195
|
+
ActiveSupport::Notifications.instrument(
|
|
196
|
+
DELIVERY_ENQUEUE_ERROR,
|
|
197
|
+
inline: configuration.deliver_inline,
|
|
198
|
+
change_count: changes.size,
|
|
199
|
+
error_class: error.class.name,
|
|
200
|
+
error_message: error.message
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def subscription_registrar
|
|
205
|
+
@subscription_registrar ||= Subscriptions::Registrar.new(
|
|
206
|
+
store: subscriptions,
|
|
207
|
+
shape_cache: subscription_shape_cache
|
|
208
|
+
)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def subscription_shape_cache
|
|
212
|
+
@subscription_shape_cache ||= Subscriptions::ShapeCache.new
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def delivery_batch_for(change_sets)
|
|
216
|
+
change_sets = compact_change_sets(change_sets)
|
|
217
|
+
return Delivery::TurboStreams::Batch.new([]) if change_sets.empty?
|
|
218
|
+
|
|
219
|
+
planner = Invalidation::Planner.new(store: subscriptions)
|
|
220
|
+
plans = change_sets.map { |changes| planner.plan(changes) }
|
|
221
|
+
plans = plans.reject { |plan| plan.targets.empty? }
|
|
222
|
+
return Delivery::TurboStreams::Batch.new([]) if plans.empty?
|
|
223
|
+
|
|
224
|
+
Delivery::TurboStreams.new.build_many(plans)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def deliver_batch(batch)
|
|
228
|
+
record_testing_delivery_batch(batch)
|
|
229
|
+
transport.deliver(batch)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def compact_change_sets(change_sets)
|
|
233
|
+
change_sets
|
|
234
|
+
.map { |changes| deliverable_changes(changes) }
|
|
235
|
+
.reject(&:empty?)
|
|
236
|
+
.uniq { |changes| change_set_key(changes) }
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def deliverable_changes(changes)
|
|
240
|
+
Array(changes).reject { |change| internal_delivery_change?(change) }
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def record_testing_change_facts(changes)
|
|
244
|
+
return unless defined?(Testing) && Testing.respond_to?(:record_change_facts)
|
|
245
|
+
return if Testing.respond_to?(:capturing_change_facts?) && !Testing.capturing_change_facts?
|
|
246
|
+
|
|
247
|
+
Testing.record_change_facts(changes)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def record_testing_delivery_batch(batch)
|
|
251
|
+
return unless defined?(Testing) && Testing.respond_to?(:record_delivery_batch)
|
|
252
|
+
return if Testing.respond_to?(:capturing_broadcasts?) && !Testing.capturing_broadcasts?
|
|
253
|
+
|
|
254
|
+
Testing.record_delivery_batch(batch)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def internal_delivery_change?(change)
|
|
258
|
+
INTERNAL_DELIVERY_TABLES.include?(change_value(change, :table).to_s)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def change_set_key(changes)
|
|
262
|
+
changes.map { |change| change_key(change) }.sort
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def change_key(change)
|
|
266
|
+
[
|
|
267
|
+
change_value(change, :type).to_s,
|
|
268
|
+
change_value(change, :table).to_s,
|
|
269
|
+
change_value(change, :model).to_s,
|
|
270
|
+
change_value(change, :id).to_s,
|
|
271
|
+
Array(change_value(change, :changed_attributes)).map(&:to_s).sort,
|
|
272
|
+
change_value(change, :old_values).inspect,
|
|
273
|
+
change_value(change, :new_values).inspect
|
|
274
|
+
]
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def change_value(change, key)
|
|
278
|
+
return unless change.respond_to?(:[])
|
|
279
|
+
|
|
280
|
+
change[key] || change[key.to_s]
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def discard_subscription_store!
|
|
284
|
+
@subscriptions&.shutdown
|
|
285
|
+
@subscriptions = nil
|
|
286
|
+
@subscription_registrar = nil
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def identity_metadata(decision)
|
|
290
|
+
{
|
|
291
|
+
identity_mode: decision.mode,
|
|
292
|
+
anonymous: decision.anonymous,
|
|
293
|
+
anonymous_deopt_reason: decision.deopt_reason,
|
|
294
|
+
identity_sources: decision.identity_sources,
|
|
295
|
+
identity_names: decision.identity_names
|
|
296
|
+
}.compact
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def instrument_subscription_identity(decision, registered:, subscription: nil, deopt_reason: nil, **extra)
|
|
300
|
+
ActiveSupport::Notifications.instrument(
|
|
301
|
+
SUBSCRIPTION_IDENTITY,
|
|
302
|
+
{
|
|
303
|
+
registered: registered,
|
|
304
|
+
subscription_id: subscription&.id,
|
|
305
|
+
subscriber_id: subscription&.subscriber_id,
|
|
306
|
+
identity_mode: decision&.mode,
|
|
307
|
+
anonymous: decision&.anonymous,
|
|
308
|
+
anonymous_deopt_reason: deopt_reason || decision&.deopt_reason,
|
|
309
|
+
identity_sources: decision&.identity_sources,
|
|
310
|
+
identity_names: decision&.identity_names
|
|
311
|
+
}.merge(extra)
|
|
312
|
+
)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def subscription_response?(controller, capture)
|
|
316
|
+
controller.request.get? &&
|
|
317
|
+
capture.successful? &&
|
|
318
|
+
capture.html_response? &&
|
|
319
|
+
capture.html.include?("</") &&
|
|
320
|
+
capture.recorder.graph.frame_nodes.any?
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def controller_subscription_recorder(controller, capture)
|
|
324
|
+
target_id = controller.request.headers["Turbo-Frame"].to_s
|
|
325
|
+
return [capture.recorder, nil] if target_id.empty?
|
|
326
|
+
|
|
327
|
+
subscription_id = controller.request.headers[ClientSubscription::ID_HEADER].to_s
|
|
328
|
+
token = controller.request.headers[ClientSubscription::TOKEN_HEADER].to_s
|
|
329
|
+
return [capture.recorder, nil] if subscription_id.empty? && token.empty?
|
|
330
|
+
|
|
331
|
+
unless !subscription_id.empty? && ActivationToken.valid_for_subscription?(token, subscription_id)
|
|
332
|
+
request_subscription_reload(controller, "invalid_base_subscription")
|
|
333
|
+
return
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
base_subscription = subscriptions.fetch_for_composition(subscription_id)
|
|
337
|
+
scope_id = "turbo_frame:#{target_id}"
|
|
338
|
+
recorder = base_subscription.recorder.replace_scope(scope_id, capture.recorder)
|
|
339
|
+
[recorder, base_subscription]
|
|
340
|
+
rescue KeyError
|
|
341
|
+
request_subscription_reload(controller, "missing_frame_scope")
|
|
342
|
+
nil
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def replacement_identity_valid?(base_subscription, identity)
|
|
346
|
+
return true unless base_subscription
|
|
347
|
+
return true if metadata_value(base_subscription, :identity_mode) == Cable::SubscriberIdentity::ANONYMOUS_PUBLIC_MODE
|
|
348
|
+
|
|
349
|
+
base_subscription.subscriber_id == identity.subscriber_id
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def request_subscription_reload(controller, reason)
|
|
353
|
+
controller.response.headers[ClientSubscription::ACTION_HEADER] = "reload"
|
|
354
|
+
controller.response.headers["X-Upkeep-Subscription-Reason"] = reason
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def metadata_value(subscription, key)
|
|
358
|
+
subscription.metadata[key] || subscription.metadata[key.to_s]
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def build_subscription_store
|
|
362
|
+
case configuration.subscription_store
|
|
363
|
+
when :active_record
|
|
364
|
+
schema_errors = Subscriptions::ActiveRecordStore.schema_errors(connect: true)
|
|
365
|
+
unless schema_errors.empty?
|
|
366
|
+
raise ConfigurationError,
|
|
367
|
+
active_record_subscription_store_error(schema_errors)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
Subscriptions::ActiveRecordStore.new
|
|
371
|
+
when :memory
|
|
372
|
+
Subscriptions::Store.new
|
|
373
|
+
end.tap do
|
|
374
|
+
@subscription_store_name = configuration.subscription_store
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def validate_subscription_store!(environment:)
|
|
379
|
+
if production_environment?(environment) && configuration.subscription_store == :memory
|
|
380
|
+
raise ConfigurationError,
|
|
381
|
+
"Upkeep subscription_store=:memory is only for development/test; production requires :active_record."
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
return true unless production_environment?(environment)
|
|
385
|
+
return true unless configuration.subscription_store == :active_record
|
|
386
|
+
schema_errors = Subscriptions::ActiveRecordStore.schema_errors(connect: true)
|
|
387
|
+
return true if schema_errors.empty?
|
|
388
|
+
|
|
389
|
+
raise ConfigurationError, active_record_subscription_store_error(schema_errors, production: true)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def active_record_subscription_store_error(schema_errors, production: false)
|
|
393
|
+
prefix = if production
|
|
394
|
+
"Upkeep production boot requires compatible upkeep_subscriptions, " \
|
|
395
|
+
"upkeep_subscription_index_entries, and upkeep_subscription_shape_index_entries tables " \
|
|
396
|
+
"for subscription_store=:active_record."
|
|
397
|
+
else
|
|
398
|
+
"Upkeep subscription_store=:active_record requires compatible upkeep_subscriptions, " \
|
|
399
|
+
"upkeep_subscription_index_entries, and upkeep_subscription_shape_index_entries tables."
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
"#{prefix} Schema errors: #{schema_errors.join("; ")}. Run bin/rails generate upkeep:install " \
|
|
403
|
+
"and bin/rails db:migrate, rebuild stale development/test databases, or set " \
|
|
404
|
+
"config.upkeep.subscription_store = :memory in development/test."
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def validate_cluster_safety!(environment:, guard: nil)
|
|
408
|
+
guard ||= ClusterGuard.new(
|
|
409
|
+
cable_adapter: resolved_action_cable_adapter,
|
|
410
|
+
worker_count: detected_worker_count,
|
|
411
|
+
subscription_store: configuration.subscription_store,
|
|
412
|
+
environment: environment
|
|
413
|
+
)
|
|
414
|
+
return true if guard.problems.empty?
|
|
415
|
+
raise ConfigurationError, guard.message if guard.error?
|
|
416
|
+
|
|
417
|
+
log_cluster_warning(guard.message)
|
|
418
|
+
true
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def log_cluster_warning(message)
|
|
422
|
+
return if @cluster_warning_logged
|
|
423
|
+
|
|
424
|
+
@cluster_warning_logged = true
|
|
425
|
+
if defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
|
|
426
|
+
::Rails.logger.warn("[upkeep] #{message}")
|
|
427
|
+
else
|
|
428
|
+
warn("[upkeep] #{message}")
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def resolved_action_cable_adapter
|
|
433
|
+
return unless defined?(::ActionCable) && ::ActionCable.respond_to?(:server)
|
|
434
|
+
|
|
435
|
+
cable = ::ActionCable.server.config.cable
|
|
436
|
+
cable && (cable["adapter"] || cable[:adapter])
|
|
437
|
+
rescue StandardError
|
|
438
|
+
nil
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def detected_worker_count
|
|
442
|
+
puma_worker_count || ENV["WEB_CONCURRENCY"].to_i
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def puma_worker_count
|
|
446
|
+
return unless defined?(::Puma) && ::Puma.respond_to?(:cli_config)
|
|
447
|
+
|
|
448
|
+
::Puma.cli_config&.options&.[](:workers)
|
|
449
|
+
rescue StandardError
|
|
450
|
+
nil
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def production_environment?(environment)
|
|
454
|
+
environment.to_s == "production"
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def rails_environment
|
|
458
|
+
::Rails.env if defined?(::Rails) && ::Rails.respond_to?(:env)
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def subscription_store_config_changed?
|
|
462
|
+
@subscription_store_name != configuration.subscription_store
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
end
|