Class: ActiveRecord::Materialized::PartitionState
- Inherits:
-
PartitionKeyedStore
- Object
- PartitionKeyedStore
- ActiveRecord::Materialized::PartitionState
- Defined in:
- lib/activerecord/materialized/partition_state.rb
Overview
Tracks which partitions of a cold view have been materialized ("fresh") so a read can decide whether a partition is served from the cache or read through to the source. Warm views are fully materialized and ignore this.
Class Method Summary collapse
- .cartesian(value_lists) ⇒ Object
- .key_value_lists(conditions, group_keys) ⇒ Object
-
.keys_from(view_class, args) ⇒ Object
The partition key tuples a query touches, or nil unless the conditions are an exact match on the GROUP BY columns (the only case the fast path serves).
- .single_hash(args) ⇒ Object
Instance Method Summary collapse
- #all_fresh?(key_tuples) ⇒ Boolean
-
#current_generation ⇒ Object
The current fresh-set epoch, read into Ruby.
-
#mark_fresh!(key_tuples, generation:) ⇒ Object
Stamp each partition fresh with
generation(the epoch the caller captured before its source read). - #mark_stale!(key_tuples) ⇒ Object
-
#reset! ⇒ Object
Invalidate the whole fresh set (a widen recompute whose scope is unknown).
Methods inherited from PartitionKeyedStore
Constructor Details
This class inherits a constructor from ActiveRecord::Materialized::PartitionKeyedStore
Class Method Details
.cartesian(value_lists) ⇒ Object
88 89 90 91 92 93 94 |
# File 'lib/activerecord/materialized/partition_state.rb', line 88 def self.cartesian(value_lists) return nil if value_lists.any?(&:empty?) value_lists.reduce([[]]) do |tuples, values| tuples.flat_map { |tuple| values.map { |value| tuple + [value] } } end end |
.key_value_lists(conditions, group_keys) ⇒ Object
81 82 83 84 85 86 |
# File 'lib/activerecord/materialized/partition_state.rb', line 81 def self.key_value_lists(conditions, group_keys) normalized = conditions.transform_keys(&:to_s) return nil unless normalized.keys.sort == group_keys.sort group_keys.map { |column| Array(normalized.fetch(column)).map(&:to_s) } end |
.keys_from(view_class, args) ⇒ Object
The partition key tuples a query touches, or nil unless the conditions are an exact match on the GROUP BY columns (the only case the fast path serves).
63 64 65 66 67 68 69 70 71 72 |
# File 'lib/activerecord/materialized/partition_state.rb', line 63 def self.keys_from(view_class, args) conditions = single_hash(args) return nil if conditions.nil? group_keys = view_class.maintenance_key_columns return nil if group_keys.empty? value_lists = key_value_lists(conditions, group_keys) value_lists.nil? ? nil : cartesian(value_lists) end |
.single_hash(args) ⇒ Object
74 75 76 77 78 79 |
# File 'lib/activerecord/materialized/partition_state.rb', line 74 def self.single_hash(args) return nil unless args.length == 1 conditions = args.fetch(0) conditions.is_a?(Hash) ? conditions : nil end |
Instance Method Details
#all_fresh?(key_tuples) ⇒ Boolean
9 10 11 12 13 14 15 16 17 18 19 20 |
# File 'lib/activerecord/materialized/partition_state.rb', line 9 def all_fresh?(key_tuples) return false if key_tuples.empty? ensure_table! view_class..ensure_schema! # the epoch subquery reads the metadata table serialized = key_tuples.map { |tuple| serialize(tuple) }.uniq # Only marks stamped with the current epoch count as fresh — a mark left behind by a populate # that raced a reset! (a widen invalidation) carries a superseded generation and is ignored. # The epoch is a correlated subquery so this stays ONE round-trip on the keyed cold-read fast path. fresh = scope.where(partition_key: serialized, generation: current_generation_scope) fresh.count == serialized.size end |
#current_generation ⇒ Object
The current fresh-set epoch, read into Ruby. A populate captures this BEFORE its source read and stamps its marks with it, so a widen committing during the read advances the epoch and leaves the mark un-served. (all_fresh? compares against the epoch in-SQL via current_generation_scope.)
57 58 59 |
# File 'lib/activerecord/materialized/partition_state.rb', line 57 def current_generation view_class..fresh_set_generation end |
#mark_fresh!(key_tuples, generation:) ⇒ Object
Stamp each partition fresh with generation (the epoch the caller captured before its source
read). Overwrites any existing mark's generation, so a re-populate after a reset! re-establishes
freshness rather than leaving a stale-epoch row that all_fresh? would forever ignore.
25 26 27 28 29 30 31 32 33 |
# File 'lib/activerecord/materialized/partition_state.rb', line 25 def mark_fresh!(key_tuples, generation:) return if key_tuples.empty? ensure_table! key_tuples.uniq.each do |tuple| record = record_class.create_or_find_by(view_name: view_key, partition_key: serialize(tuple)) record.update!(generation: generation) unless record.generation == generation end end |
#mark_stale!(key_tuples) ⇒ Object
35 36 37 38 39 40 |
# File 'lib/activerecord/materialized/partition_state.rb', line 35 def mark_stale!(key_tuples) return if key_tuples.empty? ensure_table! scope.where(partition_key: key_tuples.map { |tuple| serialize(tuple) }).delete_all end |
#reset! ⇒ Object
Invalidate the whole fresh set (a widen recompute whose scope is unknown). Advance the epoch FIRST, then delete the marks: the epoch bump alone invalidates everything (all_fresh? honours only current-epoch marks), so the delete is pure cleanup. Ordering it this way makes reset! crash-safe without a transaction — a crash after the bump but before the delete still leaves the fresh set invalidated (stale-epoch marks are ignored, and a racing populate that captured the pre-bump epoch is ignored too); the leftover rows are reclaimed by the next reset!.
48 49 50 51 52 |
# File 'lib/activerecord/materialized/partition_state.rb', line 48 def reset! ensure_table! view_class..bump_fresh_set_generation! scope.delete_all end |