upkeep-rails 0.1.6
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.
Potentially problematic release.
This version of upkeep-rails might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +614 -0
- data/docs/architecture/ambient-inputs-roadmap.md +308 -0
- data/docs/architecture/herb-roadmap.md +324 -0
- data/docs/architecture/identity-and-sharing.md +306 -0
- data/docs/architecture/query-dependencies.md +230 -0
- data/docs/architecture/subscription-store-contract.md +66 -0
- data/docs/cost-model-roadmap.md +704 -0
- data/docs/guides/getting-started.md +462 -0
- data/docs/handoff-2026-05-15.md +230 -0
- data/docs/production_roadmap.md +372 -0
- data/docs/shared-warm-scale-roadmap.md +214 -0
- data/docs/single-subscriber-cold-roadmap.md +192 -0
- data/docs/stress-test-findings.md +310 -0
- data/docs/testing.md +143 -0
- data/lib/generators/upkeep/install/install_generator.rb +127 -0
- data/lib/generators/upkeep/install/templates/create_upkeep_subscriptions.rb.erb +49 -0
- data/lib/generators/upkeep/install/templates/subscription.js +99 -0
- data/lib/generators/upkeep/install/templates/upkeep.rb +63 -0
- data/lib/upkeep/active_record_query.rb +294 -0
- data/lib/upkeep/capture/request.rb +150 -0
- data/lib/upkeep/dag/subscription_shape.rb +244 -0
- data/lib/upkeep/dag.rb +370 -0
- data/lib/upkeep/delivery/action_cable_adapter.rb +43 -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 +302 -0
- data/lib/upkeep/delivery.rb +7 -0
- data/lib/upkeep/dependencies.rb +518 -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 +514 -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 +360 -0
- data/lib/upkeep/invalidation.rb +7 -0
- data/lib/upkeep/rails/action_view_capture.rb +821 -0
- data/lib/upkeep/rails/activation_token.rb +55 -0
- data/lib/upkeep/rails/cable/channel.rb +143 -0
- data/lib/upkeep/rails/cable/subscriber_identity.rb +341 -0
- data/lib/upkeep/rails/cable.rb +4 -0
- data/lib/upkeep/rails/client_subscription.rb +45 -0
- data/lib/upkeep/rails/configuration.rb +245 -0
- data/lib/upkeep/rails/controller_runtime.rb +137 -0
- data/lib/upkeep/rails/delivery_job.rb +29 -0
- data/lib/upkeep/rails/install.rb +28 -0
- data/lib/upkeep/rails/railtie.rb +50 -0
- data/lib/upkeep/rails/replay.rb +176 -0
- data/lib/upkeep/rails/testing.rb +97 -0
- data/lib/upkeep/rails.rb +349 -0
- data/lib/upkeep/replay.rb +408 -0
- data/lib/upkeep/runtime.rb +1100 -0
- data/lib/upkeep/shared_streams.rb +72 -0
- data/lib/upkeep/subscriptions/active_record_store.rb +383 -0
- data/lib/upkeep/subscriptions/active_record_subscription_persistence.rb +407 -0
- data/lib/upkeep/subscriptions/active_registry.rb +87 -0
- data/lib/upkeep/subscriptions/async_durable_writer.rb +131 -0
- data/lib/upkeep/subscriptions/json_snapshot.rb +98 -0
- data/lib/upkeep/subscriptions/layered_reverse_index.rb +129 -0
- data/lib/upkeep/subscriptions/persistent_reverse_index.rb +223 -0
- data/lib/upkeep/subscriptions/registrar.rb +36 -0
- data/lib/upkeep/subscriptions/reverse_index.rb +298 -0
- data/lib/upkeep/subscriptions/shape.rb +116 -0
- data/lib/upkeep/subscriptions/store.rb +171 -0
- data/lib/upkeep/subscriptions.rb +7 -0
- data/lib/upkeep/targeting.rb +135 -0
- data/lib/upkeep/version.rb +5 -0
- data/lib/upkeep-rails.rb +3 -0
- data/lib/upkeep.rb +14 -0
- data/upkeep-rails.gemspec +54 -0
- metadata +320 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Upkeep
|
|
4
|
+
module Rails
|
|
5
|
+
# Test helpers for asserting the public Upkeep subscription lifecycle from
|
|
6
|
+
# Rails request, integration, and system tests.
|
|
7
|
+
module Testing
|
|
8
|
+
class << self
|
|
9
|
+
# Drains the async delivery dispatcher when a test needs deterministic
|
|
10
|
+
# broadcast assertions.
|
|
11
|
+
#
|
|
12
|
+
# Production code should not call this; normal app delivery runs
|
|
13
|
+
# through the configured adapter.
|
|
14
|
+
#
|
|
15
|
+
# @return [void]
|
|
16
|
+
def drain_delivery!
|
|
17
|
+
Upkeep::Rails.send(:drain_delivery_dispatcher!)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Asserts that the last successful HTML response injected an Upkeep
|
|
22
|
+
# subscription marker and registered a subscription in the configured
|
|
23
|
+
# store.
|
|
24
|
+
#
|
|
25
|
+
# @param message [String, nil] optional assertion failure message.
|
|
26
|
+
# @return [void]
|
|
27
|
+
def assert_upkeep_subscription_registered(message = nil)
|
|
28
|
+
assert_select "upkeep-subscription-source[data-upkeep-subscription]"
|
|
29
|
+
assert Upkeep::Rails.subscriptions.subscriptions.any?,
|
|
30
|
+
message || "expected Upkeep to register at least one subscription"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns the most recently registered Upkeep subscription.
|
|
34
|
+
#
|
|
35
|
+
# @return [Upkeep::Subscriptions::Subscription, nil]
|
|
36
|
+
def upkeep_subscription
|
|
37
|
+
Upkeep::Rails.subscriptions.subscriptions.last
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns every ActionCable stream name that can receive broadcasts for a
|
|
41
|
+
# subscription, including shared streams.
|
|
42
|
+
#
|
|
43
|
+
# @param subscription [Upkeep::Subscriptions::Subscription]
|
|
44
|
+
# @return [Array<String>]
|
|
45
|
+
# @raise [ArgumentError] when no subscription is registered.
|
|
46
|
+
def upkeep_stream_names(subscription = upkeep_subscription)
|
|
47
|
+
raise ArgumentError, "no Upkeep subscription is registered" unless subscription
|
|
48
|
+
|
|
49
|
+
([subscription.metadata.fetch(:stream_name)] + subscription.metadata.fetch(:shared_stream_names, [])).uniq
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Activates the registered subscription so delivery lookup can find it.
|
|
53
|
+
#
|
|
54
|
+
# @param subscription [Upkeep::Subscriptions::Subscription]
|
|
55
|
+
# @return [Upkeep::Subscriptions::Subscription]
|
|
56
|
+
# @raise [ArgumentError] when no subscription is registered.
|
|
57
|
+
# @raise [Upkeep::Subscriptions::NotFound] when activation fails.
|
|
58
|
+
def activate_upkeep_subscription!(subscription = upkeep_subscription)
|
|
59
|
+
raise ArgumentError, "no Upkeep subscription is registered" unless subscription
|
|
60
|
+
|
|
61
|
+
activated = Upkeep::Rails.subscriptions.activate(subscription.id)
|
|
62
|
+
raise Upkeep::Subscriptions::NotFound, subscription.id unless activated
|
|
63
|
+
|
|
64
|
+
subscription
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Captures ActionCable broadcasts for every stream associated with a
|
|
68
|
+
# subscription while the block runs.
|
|
69
|
+
#
|
|
70
|
+
# Include ActionCable::TestHelper before calling this helper.
|
|
71
|
+
#
|
|
72
|
+
# @param subscription [Upkeep::Subscriptions::Subscription]
|
|
73
|
+
# @return [Array<String>]
|
|
74
|
+
# @raise [ArgumentError] when called without a block or subscription.
|
|
75
|
+
# @raise [NoMethodError] when ActionCable::TestHelper is not included.
|
|
76
|
+
def capture_upkeep_broadcasts(subscription = upkeep_subscription, &block)
|
|
77
|
+
raise ArgumentError, "capture_upkeep_broadcasts requires a block" unless block
|
|
78
|
+
raise NoMethodError, "include ActionCable::TestHelper before calling capture_upkeep_broadcasts" unless respond_to?(:capture_broadcasts)
|
|
79
|
+
|
|
80
|
+
captures = {}
|
|
81
|
+
nested = upkeep_stream_names(subscription).reverse_each.reduce(block) do |inner, stream_name|
|
|
82
|
+
proc { captures[stream_name] = capture_broadcasts(stream_name, &inner) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
nested.call
|
|
86
|
+
captures.values.flatten
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Drains async Upkeep delivery for deterministic test assertions.
|
|
90
|
+
#
|
|
91
|
+
# @return [void]
|
|
92
|
+
def drain_upkeep_delivery!
|
|
93
|
+
Upkeep::Rails::Testing.drain_delivery!
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
data/lib/upkeep/rails.rb
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
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/activation_token"
|
|
8
|
+
require_relative "rails/delivery_job"
|
|
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/install"
|
|
15
|
+
require_relative "rails/testing"
|
|
16
|
+
require_relative "rails/railtie" if defined?(::Rails::Railtie)
|
|
17
|
+
|
|
18
|
+
module Upkeep
|
|
19
|
+
module Rails
|
|
20
|
+
SUBSCRIPTION_IDENTITY = "upkeep.subscription_identity"
|
|
21
|
+
REQUEST_CAPTURE = "request_capture.upkeep"
|
|
22
|
+
DELIVERY_ENQUEUE = "delivery_enqueue.upkeep"
|
|
23
|
+
DELIVERY_ENQUEUE_ERROR = "delivery_enqueue_error.upkeep"
|
|
24
|
+
INTERNAL_DELIVERY_TABLES = %w[
|
|
25
|
+
upkeep_subscriptions
|
|
26
|
+
upkeep_subscription_index_entries
|
|
27
|
+
upkeep_subscription_shape_index_entries
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
def configuration
|
|
32
|
+
@configuration ||= Configuration.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def configure
|
|
36
|
+
yield configuration
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def subscriptions
|
|
40
|
+
discard_subscription_store! if @subscriptions && subscription_store_config_changed?
|
|
41
|
+
@subscriptions ||= build_subscription_store
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def transport
|
|
45
|
+
@transport ||= Delivery::BroadcastTransport.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def reset_runtime!
|
|
49
|
+
@delivery_dispatcher&.shutdown
|
|
50
|
+
@delivery_dispatcher = nil
|
|
51
|
+
@subscription_shape_cache&.reset
|
|
52
|
+
@subscription_registrar = nil
|
|
53
|
+
discard_subscription_store! if @subscriptions
|
|
54
|
+
@subscriptions = build_subscription_store
|
|
55
|
+
@subscriptions.reset
|
|
56
|
+
@transport = Delivery::BroadcastTransport.new
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def register_controller_subscription(controller, capture)
|
|
60
|
+
recorder = capture.recorder
|
|
61
|
+
return unless subscription_response?(controller, capture)
|
|
62
|
+
|
|
63
|
+
decision = Cable::SubscriberIdentity.decision_for(controller.request, recorder: recorder)
|
|
64
|
+
unless recorder.reactive?
|
|
65
|
+
instrument_subscription_identity(
|
|
66
|
+
decision,
|
|
67
|
+
registered: false,
|
|
68
|
+
deopt_reason: "refused_boundary",
|
|
69
|
+
refused_boundaries: recorder.refused_boundaries.map(&:reason)
|
|
70
|
+
)
|
|
71
|
+
return
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
identity = Cable::SubscriberIdentity.derive_from_request(
|
|
75
|
+
controller.request,
|
|
76
|
+
recorder: recorder,
|
|
77
|
+
decision: decision
|
|
78
|
+
)
|
|
79
|
+
registration = subscription_registrar.register(
|
|
80
|
+
identity: identity,
|
|
81
|
+
decision: decision,
|
|
82
|
+
recorder: recorder,
|
|
83
|
+
signature: capture.signature,
|
|
84
|
+
metadata: identity_metadata(decision).merge(
|
|
85
|
+
path: controller.request.fullpath,
|
|
86
|
+
stream_name: identity.stream_name
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
instrument_subscription_identity(decision, registered: true, subscription: registration.subscription)
|
|
90
|
+
|
|
91
|
+
registration
|
|
92
|
+
rescue Cable::UnidentifiedSubscriber => error
|
|
93
|
+
instrument_subscription_identity(
|
|
94
|
+
decision || Cable::SubscriberIdentity.decision_for(controller.request, recorder: recorder),
|
|
95
|
+
registered: false,
|
|
96
|
+
deopt_reason: "unidentified_identity",
|
|
97
|
+
error: error.message
|
|
98
|
+
)
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Dispatches committed application changes through the configured delivery
|
|
103
|
+
# adapter.
|
|
104
|
+
#
|
|
105
|
+
# ControllerRuntime calls this automatically after non-GET actions. Apps
|
|
106
|
+
# usually should not call it from controllers or models.
|
|
107
|
+
#
|
|
108
|
+
# @param changes [Array<#to_h>] change events to deliver. Defaults to the
|
|
109
|
+
# current runtime change log.
|
|
110
|
+
# @return [Upkeep::Delivery::Transport::DispatchReport]
|
|
111
|
+
def deliver_changes!(changes = Runtime::ChangeLog.drain)
|
|
112
|
+
changes = deliverable_changes(changes)
|
|
113
|
+
return Delivery::Transport::DispatchReport.new([]) if changes.empty?
|
|
114
|
+
|
|
115
|
+
dispatch_changes(changes)
|
|
116
|
+
rescue StandardError => error
|
|
117
|
+
instrument_delivery_enqueue_error(changes, error)
|
|
118
|
+
Delivery::Transport::DispatchReport.new([])
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Delivers committed application changes immediately in the current
|
|
122
|
+
# process.
|
|
123
|
+
#
|
|
124
|
+
# This is used by the inline delivery adapter and Active Job worker. Tests
|
|
125
|
+
# that need deterministic async delivery should use
|
|
126
|
+
# Upkeep::Rails::Testing instead of calling this directly.
|
|
127
|
+
#
|
|
128
|
+
# @param changes [Array<#to_h>] change events to deliver. Defaults to the
|
|
129
|
+
# current runtime change log.
|
|
130
|
+
# @return [Upkeep::Delivery::TurboStreams::Batch, Upkeep::Delivery::Transport::DispatchReport]
|
|
131
|
+
def deliver_changes_now!(changes = Runtime::ChangeLog.drain)
|
|
132
|
+
changes = deliverable_changes(changes)
|
|
133
|
+
return Delivery::Transport::DispatchReport.new([]) if changes.empty?
|
|
134
|
+
|
|
135
|
+
batch = delivery_batch_for([changes])
|
|
136
|
+
transport.deliver(batch)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def validate_configuration!(environment: rails_environment)
|
|
140
|
+
return true unless configuration.enabled
|
|
141
|
+
|
|
142
|
+
validate_subscription_store!(environment: environment)
|
|
143
|
+
true
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
def delivery_dispatcher
|
|
149
|
+
@delivery_dispatcher ||= Delivery::AsyncDispatcher.new(batch_window: configuration.delivery_batch_window) do |change_sets|
|
|
150
|
+
batch = delivery_batch_for(change_sets)
|
|
151
|
+
transport.deliver(batch)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def drain_delivery_dispatcher!
|
|
156
|
+
@delivery_dispatcher&.drain
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def dispatch_changes(changes)
|
|
160
|
+
payload = {
|
|
161
|
+
adapter: configuration.delivery_adapter,
|
|
162
|
+
queue: configuration.delivery_queue,
|
|
163
|
+
change_count: changes.size
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
ActiveSupport::Notifications.instrument(DELIVERY_ENQUEUE, payload) do
|
|
167
|
+
case configuration.delivery_adapter
|
|
168
|
+
when :active_job
|
|
169
|
+
DeliveryJob.perform_later(changes)
|
|
170
|
+
Delivery::Transport::DispatchReport.new([])
|
|
171
|
+
when :async
|
|
172
|
+
delivery_dispatcher.enqueue(changes)
|
|
173
|
+
when :inline
|
|
174
|
+
deliver_changes_now!(changes)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def instrument_delivery_enqueue_error(changes, error)
|
|
180
|
+
ActiveSupport::Notifications.instrument(
|
|
181
|
+
DELIVERY_ENQUEUE_ERROR,
|
|
182
|
+
adapter: configuration.delivery_adapter,
|
|
183
|
+
queue: configuration.delivery_queue,
|
|
184
|
+
change_count: changes.size,
|
|
185
|
+
error_class: error.class.name,
|
|
186
|
+
error_message: error.message
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def subscription_registrar
|
|
191
|
+
@subscription_registrar ||= Subscriptions::Registrar.new(
|
|
192
|
+
store: subscriptions,
|
|
193
|
+
shape_cache: subscription_shape_cache
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def subscription_shape_cache
|
|
198
|
+
@subscription_shape_cache ||= Subscriptions::ShapeCache.new
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def delivery_batch_for(change_sets)
|
|
202
|
+
change_sets = compact_change_sets(change_sets)
|
|
203
|
+
return Delivery::TurboStreams::Batch.new([]) if change_sets.empty?
|
|
204
|
+
|
|
205
|
+
planner = Invalidation::Planner.new(store: subscriptions)
|
|
206
|
+
plans = change_sets.map { |changes| planner.plan(changes) }
|
|
207
|
+
plans = plans.reject { |plan| plan.targets.empty? }
|
|
208
|
+
return Delivery::TurboStreams::Batch.new([]) if plans.empty?
|
|
209
|
+
|
|
210
|
+
Delivery::TurboStreams.new.build_many(plans)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def compact_change_sets(change_sets)
|
|
214
|
+
change_sets
|
|
215
|
+
.map { |changes| deliverable_changes(changes) }
|
|
216
|
+
.reject(&:empty?)
|
|
217
|
+
.uniq { |changes| change_set_key(changes) }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def deliverable_changes(changes)
|
|
221
|
+
Array(changes).reject { |change| internal_delivery_change?(change) }
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def internal_delivery_change?(change)
|
|
225
|
+
INTERNAL_DELIVERY_TABLES.include?(change_value(change, :table).to_s)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def change_set_key(changes)
|
|
229
|
+
changes.map { |change| change_key(change) }.sort
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def change_key(change)
|
|
233
|
+
[
|
|
234
|
+
change_value(change, :type).to_s,
|
|
235
|
+
change_value(change, :table).to_s,
|
|
236
|
+
change_value(change, :model).to_s,
|
|
237
|
+
change_value(change, :id).to_s,
|
|
238
|
+
Array(change_value(change, :changed_attributes)).map(&:to_s).sort,
|
|
239
|
+
change_value(change, :old_values).inspect,
|
|
240
|
+
change_value(change, :new_values).inspect
|
|
241
|
+
]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def change_value(change, key)
|
|
245
|
+
return unless change.respond_to?(:[])
|
|
246
|
+
|
|
247
|
+
change[key] || change[key.to_s]
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def discard_subscription_store!
|
|
251
|
+
@subscriptions&.shutdown
|
|
252
|
+
@subscriptions = nil
|
|
253
|
+
@subscription_registrar = nil
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def identity_metadata(decision)
|
|
257
|
+
{
|
|
258
|
+
identity_mode: decision.mode,
|
|
259
|
+
anonymous: decision.anonymous,
|
|
260
|
+
anonymous_deopt_reason: decision.deopt_reason,
|
|
261
|
+
identity_sources: decision.identity_sources,
|
|
262
|
+
identity_names: decision.identity_names
|
|
263
|
+
}.compact
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def instrument_subscription_identity(decision, registered:, subscription: nil, deopt_reason: nil, **extra)
|
|
267
|
+
ActiveSupport::Notifications.instrument(
|
|
268
|
+
SUBSCRIPTION_IDENTITY,
|
|
269
|
+
{
|
|
270
|
+
registered: registered,
|
|
271
|
+
subscription_id: subscription&.id,
|
|
272
|
+
subscriber_id: subscription&.subscriber_id,
|
|
273
|
+
identity_mode: decision&.mode,
|
|
274
|
+
anonymous: decision&.anonymous,
|
|
275
|
+
anonymous_deopt_reason: deopt_reason || decision&.deopt_reason,
|
|
276
|
+
identity_sources: decision&.identity_sources,
|
|
277
|
+
identity_names: decision&.identity_names
|
|
278
|
+
}.merge(extra)
|
|
279
|
+
)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def subscription_response?(controller, capture)
|
|
283
|
+
controller.request.get? &&
|
|
284
|
+
capture.successful? &&
|
|
285
|
+
capture.html_response? &&
|
|
286
|
+
capture.html.include?("</") &&
|
|
287
|
+
capture.recorder.graph.frame_nodes.any?
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def build_subscription_store
|
|
291
|
+
case configuration.subscription_store
|
|
292
|
+
when :active_record
|
|
293
|
+
schema_errors = Subscriptions::ActiveRecordStore.schema_errors(connect: true)
|
|
294
|
+
unless schema_errors.empty?
|
|
295
|
+
raise ConfigurationError,
|
|
296
|
+
active_record_subscription_store_error(schema_errors)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
Subscriptions::ActiveRecordStore.new
|
|
300
|
+
when :memory
|
|
301
|
+
Subscriptions::Store.new
|
|
302
|
+
end.tap do
|
|
303
|
+
@subscription_store_name = configuration.subscription_store
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def validate_subscription_store!(environment:)
|
|
308
|
+
if production_environment?(environment) && configuration.subscription_store == :memory
|
|
309
|
+
raise ConfigurationError,
|
|
310
|
+
"Upkeep subscription_store=:memory is only for development/test; production requires :active_record."
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
return true unless production_environment?(environment)
|
|
314
|
+
return true unless configuration.subscription_store == :active_record
|
|
315
|
+
schema_errors = Subscriptions::ActiveRecordStore.schema_errors(connect: true)
|
|
316
|
+
return true if schema_errors.empty?
|
|
317
|
+
|
|
318
|
+
raise ConfigurationError, active_record_subscription_store_error(schema_errors, production: true)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def active_record_subscription_store_error(schema_errors, production: false)
|
|
322
|
+
prefix = if production
|
|
323
|
+
"Upkeep production boot requires compatible upkeep_subscriptions, " \
|
|
324
|
+
"upkeep_subscription_index_entries, and upkeep_subscription_shape_index_entries tables " \
|
|
325
|
+
"for subscription_store=:active_record."
|
|
326
|
+
else
|
|
327
|
+
"Upkeep subscription_store=:active_record requires compatible upkeep_subscriptions, " \
|
|
328
|
+
"upkeep_subscription_index_entries, and upkeep_subscription_shape_index_entries tables."
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
"#{prefix} Schema errors: #{schema_errors.join("; ")}. Run bin/rails generate upkeep:install " \
|
|
332
|
+
"and bin/rails db:migrate, rebuild stale development/test databases, or set " \
|
|
333
|
+
"config.upkeep.subscription_store = :memory in development/test."
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def production_environment?(environment)
|
|
337
|
+
environment.to_s == "production"
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def rails_environment
|
|
341
|
+
::Rails.env if defined?(::Rails) && ::Rails.respond_to?(:env)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def subscription_store_config_changed?
|
|
345
|
+
@subscription_store_name != configuration.subscription_store
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|