Module: ActiveRecord::Materialized::Instrumentation

Defined in:
lib/activerecord/materialized/instrumentation.rb

Overview

Emits ActiveSupport::Notifications events at the read / refresh / maintenance lifecycle points so adopters can wire freshness and behavior SLIs to any APM / StatsD / OpenTelemetry backend without the gem taking a dependency on a specific telemetry vendor.

The event names and payload keys documented here are a stable contract — subscribe with ActiveSupport::Notifications:

ActiveSupport::Notifications.subscribe("read.active_record_materialized") do |event|
StatsD.increment("mv.read.#{event.payload[:source]}", tags: ["view:#{event.payload[:view]}"])
end

Events:

  • read.active_record_materialized — one per routed read. Payload: :view (the view class), :source (+:cache+, :read_through, :serve_stale, or :raise), :staleness (seconds since the last successful refresh, or nil when never refreshed).
  • refresh.active_record_materialized — one per +refresh!+/+rebuild!+, timed. Payload: :view, :operation (+:incremental+ or :rebuild), :mode (+:summary_delta+, :scoped_recompute, or :full), :partition_count (partitions recomputed, or nil for a full pass), :row_count, :skipped, and — on failure — the standard +:exception+/+:exception_object+ keys set by ActiveSupport::Notifications.
  • maintenance.active_record_materialized — one per dependency write that records pending maintenance. Payload: :view, :table, :operation (+:create+/+:update+/+:destroy+), :path (+:summary_delta+ or :scoped_recompute), :scope (+:scoped+, or :full when the write widened to a full recompute because its partition key could not be derived), :partition_count (distinct partitions scoped, 0 on widen).
  • reconcile.active_record_materialized — one per Reconciler run. Payload: :view, :mode (the drift-check depth), :repaired_partition_count (partitions found divergent and repaired with scoped maintenance), :deferred (+true+ when a concurrent refresh deferred the run to the next tick).

Constant Summary collapse

READ =
"read.active_record_materialized"
REFRESH =
"refresh.active_record_materialized"
MAINTENANCE =
"maintenance.active_record_materialized"
RECONCILE =
"reconcile.active_record_materialized"

Class Method Summary collapse

Class Method Details

.maintenance(view_class, change:, path:, scope:, partition_count:) ⇒ Object

Fired once per dependency write that records pending maintenance.



73
74
75
76
77
78
79
# File 'lib/activerecord/materialized/instrumentation.rb', line 73

def maintenance(view_class, change:, path:, scope:, partition_count:)
  ::ActiveSupport::Notifications.instrument(
    MAINTENANCE,
    view: view_class, table: change.table_name, operation: change.operation.to_sym,
    path: path, scope: scope, partition_count: partition_count
  )
end

.read(view_class, source:) ⇒ Object

Fired once per routed read. The staleness lookup costs a metadata read, so it is skipped entirely unless a subscriber is attached.



49
50
51
52
53
54
55
56
# File 'lib/activerecord/materialized/instrumentation.rb', line 49

def read(view_class, source:)
  return unless ::ActiveSupport::Notifications.notifier.listening?(READ)

  ::ActiveSupport::Notifications.instrument(
    READ,
    view: view_class, source: source, staleness: staleness_for(view_class)
  )
end

.reconcile(view_class, mode:, repaired_partition_count:, deferred:) ⇒ Object

Fired once per Reconciler run — after it verifies and (if needed) repairs via scoped maintenance — carrying the run's mode, repaired count, and defer flag.



83
84
85
86
87
88
89
# File 'lib/activerecord/materialized/instrumentation.rb', line 83

def reconcile(view_class, mode:, repaired_partition_count:, deferred:)
  ::ActiveSupport::Notifications.instrument(
    RECONCILE,
    view: view_class, mode: mode,
    repaired_partition_count: repaired_partition_count, deferred: deferred
  )
end

.refresh(view_class, operation:, &block) ⇒ Object

Wraps a refresh/rebuild so the event carries its wall-clock duration and any raised exception. The block runs the refresh and returns its RefreshResult; the row count and skipped flag are read back from it, while the block annotates the payload with the :mode and :partition_count it can only know mid-flight.



63
64
65
66
67
68
69
70
# File 'lib/activerecord/materialized/instrumentation.rb', line 63

def refresh(view_class, operation:, &block)
  ::ActiveSupport::Notifications.instrument(REFRESH, view: view_class, operation: operation) do |payload|
    result = yield(payload)
    payload[:row_count] = result.row_count
    payload[:skipped] = result.skipped
    result
  end
end