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
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Upkeep
|
|
4
|
+
module Rails
|
|
5
|
+
# Detects boot configurations where live updates silently break across
|
|
6
|
+
# cluster workers: an in-process cable adapter or an in-memory
|
|
7
|
+
# subscription store cannot reach browsers or subscriptions held by
|
|
8
|
+
# another process.
|
|
9
|
+
class ClusterGuard
|
|
10
|
+
IN_PROCESS_CABLE_ADAPTERS = %w[async].freeze
|
|
11
|
+
|
|
12
|
+
attr_reader :cable_adapter, :worker_count, :subscription_store, :environment
|
|
13
|
+
|
|
14
|
+
def initialize(cable_adapter:, worker_count:, subscription_store:, environment:)
|
|
15
|
+
@cable_adapter = cable_adapter.to_s
|
|
16
|
+
@worker_count = worker_count.to_i
|
|
17
|
+
@subscription_store = subscription_store&.to_sym
|
|
18
|
+
@environment = environment.to_s
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def clustered?
|
|
22
|
+
worker_count.positive?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def problems
|
|
26
|
+
return [] unless clustered?
|
|
27
|
+
|
|
28
|
+
problems = []
|
|
29
|
+
if IN_PROCESS_CABLE_ADAPTERS.include?(cable_adapter)
|
|
30
|
+
problems << "the #{cable_adapter} Action Cable adapter is in-process, so broadcasts from one worker " \
|
|
31
|
+
"never reach sockets held by another; configure a cross-process cable adapter such as solid_cable " \
|
|
32
|
+
"or redis in config/cable.yml"
|
|
33
|
+
end
|
|
34
|
+
if subscription_store == :memory
|
|
35
|
+
problems << "subscription_store=:memory is per-process, so subscriptions registered in one worker are " \
|
|
36
|
+
"invisible to the others; set config.upkeep.subscription_store = :active_record"
|
|
37
|
+
end
|
|
38
|
+
problems
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def error?
|
|
42
|
+
problems.any? && environment == "production"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def warning?
|
|
46
|
+
problems.any? && !error?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def message
|
|
50
|
+
return if problems.empty?
|
|
51
|
+
|
|
52
|
+
"Upkeep detected a clustered server (#{worker_count} workers) with a configuration that cannot " \
|
|
53
|
+
"deliver live updates across processes: #{problems.join("; ")}."
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Upkeep
|
|
4
|
+
module Rails
|
|
5
|
+
class ConfigurationError < StandardError; end
|
|
6
|
+
|
|
7
|
+
class Configuration
|
|
8
|
+
SUBSCRIPTION_STORES = [:active_record, :memory].freeze
|
|
9
|
+
REFUSED_BOUNDARY_BEHAVIORS = [:raise, :warn].freeze
|
|
10
|
+
REQUEST_ACTIVATIONS = [:all, :opt_in].freeze
|
|
11
|
+
IDENTITY_SOURCES = [:current, :session, :cookie, :warden].freeze
|
|
12
|
+
|
|
13
|
+
class IdentityDefinition
|
|
14
|
+
attr_reader :name, :source, :source_key, :subscribe_block
|
|
15
|
+
|
|
16
|
+
def initialize(name:, source:, source_key:, subscribe_block:, absent_block: nil)
|
|
17
|
+
@name = name.to_sym
|
|
18
|
+
@source = source.to_sym
|
|
19
|
+
@source_key = source_key
|
|
20
|
+
@subscribe_block = subscribe_block
|
|
21
|
+
@absent_block = absent_block
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def matches_dependency?(dependency)
|
|
25
|
+
case source
|
|
26
|
+
when :current
|
|
27
|
+
dependency.source == :current_attribute &&
|
|
28
|
+
metadata_value(dependency, :owner) == source_key.fetch(:owner) &&
|
|
29
|
+
metadata_value(dependency, :name) == source_key.fetch(:name)
|
|
30
|
+
when :session
|
|
31
|
+
dependency.source == :session && metadata_value(dependency, :key) == source_key
|
|
32
|
+
when :cookie
|
|
33
|
+
dependency.source == :cookie && metadata_value(dependency, :key) == source_key
|
|
34
|
+
when :warden
|
|
35
|
+
dependency.source == :warden_user && metadata_value(dependency, :scope) == source_key
|
|
36
|
+
else
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def matches_source?(source, key)
|
|
42
|
+
source = source.to_sym
|
|
43
|
+
|
|
44
|
+
case self.source
|
|
45
|
+
when :current
|
|
46
|
+
source == :current &&
|
|
47
|
+
key.fetch(:owner).to_s == source_key.fetch(:owner) &&
|
|
48
|
+
key.fetch(:name).to_s == source_key.fetch(:name)
|
|
49
|
+
when :session, :cookie, :warden
|
|
50
|
+
source == self.source && key.to_s == source_key
|
|
51
|
+
else
|
|
52
|
+
false
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def absent?(value)
|
|
57
|
+
return value.nil? unless @absent_block
|
|
58
|
+
|
|
59
|
+
@absent_block.arity == 1 ? @absent_block.call(value) : @absent_block.call
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def absent_dependency?(dependency)
|
|
63
|
+
Upkeep::Dependencies.identity_absent_for?(dependency, name)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def source_label
|
|
67
|
+
case source
|
|
68
|
+
when :current
|
|
69
|
+
"#{source_key.fetch(:owner)}.#{source_key.fetch(:name)}"
|
|
70
|
+
when :session
|
|
71
|
+
"session[:#{source_key}]"
|
|
72
|
+
when :cookie
|
|
73
|
+
"cookies[:#{source_key}]"
|
|
74
|
+
when :warden
|
|
75
|
+
"warden.user(:#{source_key})"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def metadata_value(dependency, key)
|
|
82
|
+
value = dependency.metadata[key] || dependency.metadata[key.to_s]
|
|
83
|
+
value.to_s
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class IdentityBuilder
|
|
88
|
+
attr_reader :subscribe_block, :absent_block
|
|
89
|
+
|
|
90
|
+
def absent_if(&block)
|
|
91
|
+
@absent_block = block
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def subscribe(&block)
|
|
95
|
+
@subscribe_block = block
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
attr_accessor :enabled
|
|
100
|
+
attr_accessor :activation_token_expires_in
|
|
101
|
+
attr_accessor :delivery_batch_window
|
|
102
|
+
# Seconds a subscription may go untouched before it counts as abandoned
|
|
103
|
+
# and becomes eligible for pruning. Connected pages are kept alive by the
|
|
104
|
+
# cable channel heartbeat, which fires far more often than this TTL.
|
|
105
|
+
attr_accessor :subscription_ttl
|
|
106
|
+
# Test/console hook: deliver changes synchronously in the caller instead
|
|
107
|
+
# of on the in-process background dispatcher.
|
|
108
|
+
attr_accessor :deliver_inline
|
|
109
|
+
attr_reader :subscription_store
|
|
110
|
+
attr_reader :request_activation
|
|
111
|
+
|
|
112
|
+
def initialize
|
|
113
|
+
@enabled = true
|
|
114
|
+
@subscription_store = :active_record
|
|
115
|
+
@deliver_inline = false
|
|
116
|
+
@delivery_batch_window = 0.01
|
|
117
|
+
@refused_boundary_behavior = nil
|
|
118
|
+
@request_activation = :all
|
|
119
|
+
@activation_token_expires_in = 24 * 60 * 60
|
|
120
|
+
@subscription_ttl = 24 * 60 * 60
|
|
121
|
+
@identity_definitions = {}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def subscription_store=(value)
|
|
125
|
+
value = value.to_sym if value.respond_to?(:to_sym)
|
|
126
|
+
|
|
127
|
+
unless SUBSCRIPTION_STORES.include?(value)
|
|
128
|
+
raise ConfigurationError,
|
|
129
|
+
"Unknown Upkeep subscription_store #{value.inspect}; expected one of #{SUBSCRIPTION_STORES.join(", ")}"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
@subscription_store = value
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def refused_boundary_behavior
|
|
136
|
+
@refused_boundary_behavior || default_refused_boundary_behavior
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def refused_boundary_behavior=(value)
|
|
140
|
+
value = value.to_sym if value.respond_to?(:to_sym)
|
|
141
|
+
|
|
142
|
+
unless REFUSED_BOUNDARY_BEHAVIORS.include?(value)
|
|
143
|
+
raise ConfigurationError,
|
|
144
|
+
"Unknown Upkeep refused_boundary_behavior #{value.inspect}; expected one of #{REFUSED_BOUNDARY_BEHAVIORS.join(", ")}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
@refused_boundary_behavior = value
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def request_activation=(value)
|
|
151
|
+
value = value.to_sym if value.respond_to?(:to_sym)
|
|
152
|
+
|
|
153
|
+
unless REQUEST_ACTIVATIONS.include?(value)
|
|
154
|
+
raise ConfigurationError,
|
|
155
|
+
"Unknown Upkeep request_activation #{value.inspect}; expected one of #{REQUEST_ACTIVATIONS.join(", ")}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
@request_activation = value
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def identify(name, current: nil, session: nil, cookie: nil, warden: nil, &block)
|
|
162
|
+
source, source_key = identity_source(current: current, session: session, cookie: cookie, warden: warden)
|
|
163
|
+
builder = IdentityBuilder.new
|
|
164
|
+
if block
|
|
165
|
+
block.arity == 1 ? block.call(builder) : builder.instance_eval(&block)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
unless builder.subscribe_block
|
|
169
|
+
raise ConfigurationError, "config.identify :#{name} requires a subscribe block"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
@identity_definitions[name.to_sym] = IdentityDefinition.new(
|
|
173
|
+
name: name,
|
|
174
|
+
source: source,
|
|
175
|
+
source_key: source_key,
|
|
176
|
+
subscribe_block: builder.subscribe_block,
|
|
177
|
+
absent_block: builder.absent_block
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def identity_presence_metadata(source:, key:, value:)
|
|
182
|
+
definitions = identity_definitions.select { |definition| definition.matches_source?(source, key) }
|
|
183
|
+
absent_by_name = definitions.to_h do |definition|
|
|
184
|
+
[definition.name.to_s, definition.absent?(value)]
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
{
|
|
188
|
+
partitioning: if definitions.any?
|
|
189
|
+
absent_by_name.values.any? { |absent| !absent }
|
|
190
|
+
else
|
|
191
|
+
!value.nil?
|
|
192
|
+
end,
|
|
193
|
+
absent_by_name: absent_by_name
|
|
194
|
+
}
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def identity_definitions
|
|
198
|
+
@identity_definitions.values
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def identity_definition(name)
|
|
202
|
+
@identity_definitions.fetch(name.to_sym)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def clear_identities!
|
|
206
|
+
@identity_definitions.clear
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
private
|
|
210
|
+
|
|
211
|
+
def identity_source(current:, session:, cookie:, warden:)
|
|
212
|
+
sources = {
|
|
213
|
+
current: current,
|
|
214
|
+
session: session,
|
|
215
|
+
cookie: cookie,
|
|
216
|
+
warden: warden
|
|
217
|
+
}.compact
|
|
218
|
+
|
|
219
|
+
unless sources.size == 1
|
|
220
|
+
raise ConfigurationError,
|
|
221
|
+
"config.identify requires exactly one source: #{IDENTITY_SOURCES.join(", ")}"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
source, value = sources.first
|
|
225
|
+
[source, normalize_identity_source(source, value)]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def normalize_identity_source(source, value)
|
|
229
|
+
case source
|
|
230
|
+
when :current
|
|
231
|
+
owner, name = Array(value)
|
|
232
|
+
unless owner && name
|
|
233
|
+
raise ConfigurationError,
|
|
234
|
+
"config.identify current: expects [CurrentClass, :attribute]"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
{ owner: owner.to_s, name: name.to_s }
|
|
238
|
+
when :session, :cookie, :warden
|
|
239
|
+
value.to_s
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def default_refused_boundary_behavior
|
|
244
|
+
if defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env.to_s == "production"
|
|
245
|
+
:warn
|
|
246
|
+
else
|
|
247
|
+
:raise
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
|
|
5
|
+
module Upkeep
|
|
6
|
+
module Rails
|
|
7
|
+
module ControllerRuntime
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
SUPPRESS_KEY = :upkeep_rails_controller_runtime_suppressed
|
|
11
|
+
|
|
12
|
+
included do
|
|
13
|
+
class_attribute :upkeep_reactive_actions, instance_accessor: false, default: [].freeze
|
|
14
|
+
prepend_around_action :upkeep_capture_request
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class_methods do
|
|
18
|
+
def upkeep_reactive(only:)
|
|
19
|
+
self.upkeep_reactive_actions = Array(only).map(&:to_s).freeze
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Public override point for host controllers. Return false to skip subscription
|
|
24
|
+
# capture and registration for this request. Writes are still captured and can
|
|
25
|
+
# update subscribed pages.
|
|
26
|
+
def upkeep_reactive_request?
|
|
27
|
+
return true if Upkeep::Rails.configuration.request_activation == :all
|
|
28
|
+
|
|
29
|
+
self.class.upkeep_reactive_actions.include?(action_name) || upkeep_reactive_continuation_request?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
def install
|
|
35
|
+
return if @installed
|
|
36
|
+
return unless defined?(::ActionController::Base)
|
|
37
|
+
|
|
38
|
+
::ActionController::Base.include(self)
|
|
39
|
+
@installed = true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def installed?
|
|
43
|
+
!!@installed
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def reset!
|
|
47
|
+
@installed = false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def suppress
|
|
51
|
+
previous = Thread.current[SUPPRESS_KEY]
|
|
52
|
+
Thread.current[SUPPRESS_KEY] = true
|
|
53
|
+
yield
|
|
54
|
+
ensure
|
|
55
|
+
Thread.current[SUPPRESS_KEY] = previous
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def suppressed?
|
|
59
|
+
Thread.current[SUPPRESS_KEY]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def upkeep_capture_request(&action)
|
|
65
|
+
return action.call if ControllerRuntime.suppressed?
|
|
66
|
+
return action.call if Upkeep::Runtime::Observation.recorder
|
|
67
|
+
|
|
68
|
+
payload = {
|
|
69
|
+
controller: self.class.name,
|
|
70
|
+
action: action_name,
|
|
71
|
+
method: request.request_method,
|
|
72
|
+
path: request.fullpath,
|
|
73
|
+
subscription_request: upkeep_subscription_request?
|
|
74
|
+
}
|
|
75
|
+
ActiveSupport::Notifications.instrument(Upkeep::Rails::REQUEST_CAPTURE, payload) do
|
|
76
|
+
upkeep_capture_request_with_timing(action, payload)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def upkeep_capture_request_with_timing(action, payload)
|
|
81
|
+
measure_phase(payload, :deliver_pending_ms) { Upkeep::Rails.deliver_changes_now! }
|
|
82
|
+
|
|
83
|
+
result = nil
|
|
84
|
+
capture = nil
|
|
85
|
+
changes = []
|
|
86
|
+
measure_phase(payload, :change_capture_ms) do
|
|
87
|
+
_captured, changes = Upkeep::Runtime::ChangeLog.capture do
|
|
88
|
+
if payload.fetch(:subscription_request)
|
|
89
|
+
capture = Upkeep::Capture::Request.call(self, profile: request_capture_profile?) { action.call }
|
|
90
|
+
result = capture.action_result
|
|
91
|
+
else
|
|
92
|
+
measure_phase(payload, :action_ms) { result = action.call }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
changes = upkeep_stamp_change_request_id(changes)
|
|
97
|
+
record_capture_payload(payload, capture) if capture
|
|
98
|
+
|
|
99
|
+
measure_phase(payload, :deliver_changes_ms) do
|
|
100
|
+
if capture
|
|
101
|
+
Upkeep::Rails.deliver_changes_now!(changes)
|
|
102
|
+
else
|
|
103
|
+
Upkeep::Rails.deliver_changes!(changes)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
registration = nil
|
|
108
|
+
if capture
|
|
109
|
+
measure_phase(payload, :register_ms) do
|
|
110
|
+
registration = Upkeep::Rails.register_controller_subscription(self, capture)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
payload[:registered] = !!registration
|
|
114
|
+
if capture && registration
|
|
115
|
+
measure_phase(payload, :inject_ms) do
|
|
116
|
+
Upkeep::Rails::ClientSubscription.headers_for(
|
|
117
|
+
identity: registration.identity,
|
|
118
|
+
subscription: registration.subscription
|
|
119
|
+
).each { |name, value| response.headers[name] = value }
|
|
120
|
+
response.body = Upkeep::Rails::ClientSubscription.inject(
|
|
121
|
+
capture.html,
|
|
122
|
+
identity: registration.identity,
|
|
123
|
+
subscription: registration.subscription
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
payload[:subscription_id] = registration.subscription.id
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
result
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# GET writes can otherwise self-refresh in a loop. Mutation deliveries must
|
|
133
|
+
# still reach the originating tab when the response omits an affected region.
|
|
134
|
+
def upkeep_stamp_change_request_id(changes)
|
|
135
|
+
return changes unless request.get? || request.head?
|
|
136
|
+
|
|
137
|
+
request_id = upkeep_turbo_request_id
|
|
138
|
+
return changes unless request_id
|
|
139
|
+
|
|
140
|
+
changes.map { |change| change.respond_to?(:merge) ? change.merge(request_id: request_id) : change }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def upkeep_turbo_request_id
|
|
144
|
+
turbo_request_id = ::Turbo.current_request_id if defined?(::Turbo) && ::Turbo.respond_to?(:current_request_id)
|
|
145
|
+
turbo_request_id || request.headers["X-Turbo-Request-Id"]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def record_capture_payload(payload, capture)
|
|
149
|
+
payload[:response_status] = capture.response_status
|
|
150
|
+
payload[:response_content_type] = capture.response_content_type
|
|
151
|
+
payload[:response_media_type] = capture.response_media_type
|
|
152
|
+
payload[:html_response] = capture.html_response?
|
|
153
|
+
payload[:response_successful] = capture.successful?
|
|
154
|
+
payload[:html_bytes] = capture.html.bytesize
|
|
155
|
+
payload[:graph_frames] = capture.recorder.graph.frame_nodes.size
|
|
156
|
+
payload[:graph_dependencies] = capture.recorder.graph.dependency_nodes.size
|
|
157
|
+
capture.timings.each do |phase, ms|
|
|
158
|
+
payload[:"capture_#{phase}"] = ms
|
|
159
|
+
end
|
|
160
|
+
capture.counters.each do |counter, value|
|
|
161
|
+
payload[:"capture_#{counter}"] = value
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def measure_phase(payload, key)
|
|
166
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
167
|
+
yield
|
|
168
|
+
ensure
|
|
169
|
+
payload[key] = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0).round(3)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def upkeep_subscription_request?
|
|
173
|
+
upkeep_reactive_request? && request.format.html? && (request.get? || request.head?)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def upkeep_reactive_continuation_request?
|
|
177
|
+
request.headers["Turbo-Frame"].present? &&
|
|
178
|
+
request.headers[ClientSubscription::ID_HEADER].present? &&
|
|
179
|
+
request.headers[ClientSubscription::TOKEN_HEADER].present?
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def request_capture_profile?
|
|
183
|
+
ActiveSupport::Notifications.notifier.listening?(Upkeep::Rails::REQUEST_CAPTURE)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Upkeep
|
|
4
|
+
module Rails
|
|
5
|
+
module Install
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def call
|
|
9
|
+
return unless Upkeep::Rails.configuration.enabled
|
|
10
|
+
|
|
11
|
+
Runtime::Install.call if defined?(::ActiveRecord::Base)
|
|
12
|
+
ActionViewCapture.install if defined?(::ActionView::Template)
|
|
13
|
+
ControllerRuntime.install if defined?(::ActionController::Base)
|
|
14
|
+
JobRuntime.install if defined?(::ActiveJob::Base)
|
|
15
|
+
|
|
16
|
+
@installed = true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def installed?
|
|
20
|
+
!!@installed
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def reset!
|
|
24
|
+
@installed = false
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
|
|
5
|
+
module Upkeep
|
|
6
|
+
module Rails
|
|
7
|
+
module JobRuntime
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
around_perform :upkeep_capture_job
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def install
|
|
17
|
+
return if @installed
|
|
18
|
+
return unless defined?(::ActiveJob::Base)
|
|
19
|
+
|
|
20
|
+
::ActiveJob::Base.include(self)
|
|
21
|
+
@installed = true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def installed?
|
|
25
|
+
!!@installed
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def reset!
|
|
29
|
+
@installed = false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def upkeep_capture_job
|
|
35
|
+
return yield if Upkeep::Runtime::ChangeLog.capturing?
|
|
36
|
+
|
|
37
|
+
result, changes = Upkeep::Runtime::ChangeLog.capture { yield }
|
|
38
|
+
Upkeep::Rails.deliver_changes!(changes)
|
|
39
|
+
result
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Upkeep
|
|
4
|
+
module Rails
|
|
5
|
+
class Railtie < ::Rails::Railtie
|
|
6
|
+
config.upkeep = ActiveSupport::OrderedOptions.new
|
|
7
|
+
|
|
8
|
+
initializer "upkeep_rails.configure" do |app|
|
|
9
|
+
Upkeep::Rails.configure do |config|
|
|
10
|
+
config.enabled = app.config.upkeep.fetch(:enabled, true)
|
|
11
|
+
config.subscription_store = app.config.upkeep.fetch(:subscription_store, config.subscription_store)
|
|
12
|
+
config.deliver_inline = app.config.upkeep.fetch(:deliver_inline, config.deliver_inline)
|
|
13
|
+
config.delivery_batch_window =
|
|
14
|
+
app.config.upkeep.fetch(:delivery_batch_window, config.delivery_batch_window)
|
|
15
|
+
config.activation_token_expires_in =
|
|
16
|
+
app.config.upkeep.fetch(:activation_token_expires_in, config.activation_token_expires_in)
|
|
17
|
+
config.request_activation =
|
|
18
|
+
app.config.upkeep.fetch(:request_activation, config.request_activation)
|
|
19
|
+
config.refused_boundary_behavior =
|
|
20
|
+
app.config.upkeep.fetch(:refused_boundary_behavior, config.refused_boundary_behavior)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
initializer "upkeep_rails.install", after: "turbo.helpers" do
|
|
25
|
+
ActiveSupport.on_load(:active_record) { Upkeep::Rails::Install.call }
|
|
26
|
+
ActiveSupport.on_load(:active_job) { Upkeep::Rails::Install.call }
|
|
27
|
+
ActiveSupport.on_load(:action_controller_base) { Upkeep::Rails::Install.call }
|
|
28
|
+
ActiveSupport.on_load(:action_view) { Upkeep::Rails::Install.call }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
initializer "upkeep_rails.validate_configuration", after: "upkeep_rails.install" do |app|
|
|
32
|
+
app.config.after_initialize do
|
|
33
|
+
Upkeep::Rails.validate_configuration! unless Railtie.rake_task?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.rake_task?
|
|
38
|
+
defined?(::Rake) &&
|
|
39
|
+
::Rake.respond_to?(:application) &&
|
|
40
|
+
::Rake.application.top_level_tasks.any?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|