Module: Familia::Horreum::AuditMethods

Included in:
ManagementMethods
Defined in:
lib/familia/horreum/management/audit.rb

Overview

AuditMethods provides proactive consistency detection for Horreum models.

Included in ManagementMethods so every Horreum subclass gets these as class methods (e.g. Customer.audit_instances, Customer.health_check).

Instance Method Summary collapse

Instance Method Details

#audit_instances(batch_size: 100) {|Hash| ... } ⇒ Hash

Compares the instances timeline against actual DB keys via SCAN.

Detects:

  • Phantoms: identifiers in timeline but no corresponding hash key
  • Missing: hash keys in DB but not in timeline

Parameters:

  • batch_size (Integer) (defaults to: 100)

    SCAN cursor count hint (default: 100)

Yields:

  • (Hash)

    Progress: current:, total:

Returns:

  • (Hash)

    [], missing: [], count_timeline: N, count_scan: N



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/familia/horreum/management/audit.rb', line 25

def audit_instances(batch_size: 100, &progress)
  # Phase 1: Collect identifiers from timeline
  timeline_ids = Set.new(instances.members)
  progress&.call(phase: :timeline_collected, current: timeline_ids.size, total: nil)

  # Phase 2: SCAN keys and extract identifiers (source of truth)
  scan_ids = scan_identifiers(batch_size: batch_size, &progress)

  # Phase 3: Set differences
  phantoms = (timeline_ids - scan_ids).to_a
  missing = (scan_ids - timeline_ids).to_a

  {
    phantoms: phantoms,
    missing: missing,
    count_timeline: timeline_ids.size,
    count_scan: scan_ids.size,
  }
end

#audit_multi_indexesArray<Hash>

Audits all multi indexes.

For each multi index:

  • SCANs for per-value set keys
  • Checks that each member exists and field value matches
  • Detects orphaned set keys (sets for values no object has)

Returns:

  • (Array<Hash>)

    [stale_members: [], orphaned_keys: []]



71
72
73
74
75
76
77
# File 'lib/familia/horreum/management/audit.rb', line 71

def audit_multi_indexes
  return [] unless respond_to?(:indexing_relationships)

  indexing_relationships.select { |r|
    r.cardinality == :multi
  }.map { |rel| audit_single_multi_index(rel) }
end

#audit_participations(sample_size: nil) ⇒ Array<Hash>

Audits participation collections for stale members.

For each participation relationship defined on this class:

  • Class-level: checks the single class collection directly
  • Instance-level: SCANs for collection keys on the target class
  • Enumerates raw members of each collection
  • Verifies each referenced participant object still exists

Parameters:

  • sample_size (Integer, nil) (defaults to: nil)

    Limit members to check per collection (nil = all)

Returns:

  • (Array<Hash>)

    [stale_members: [{identifier:, collection_key:, reason:]}]



90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/familia/horreum/management/audit.rb', line 90

def audit_participations(sample_size: nil)
  return [] unless respond_to?(:participation_relationships)

  participation_relationships.flat_map { |rel|
    if rel.target_class == self
      # Class-level participation (class_participates_in)
      [audit_class_participation(rel, sample_size: sample_size)]
    else
      # Instance-level participation (participates_in TargetClass, :collection)
      audit_instance_participations(rel, sample_size: sample_size)
    end
  }
end

#audit_unique_indexesArray<Hash>

Audits all unique indexes (class-level only, where within is nil).

For each unique index:

  • Reads all entries from the index HashKey
  • Checks that each indexed object exists and its field value matches
  • Checks for objects that should be indexed but aren't

Returns:

  • (Array<Hash>)

    [stale: [...], missing: [...]]



54
55
56
57
58
59
60
# File 'lib/familia/horreum/management/audit.rb', line 54

def audit_unique_indexes
  return [] unless respond_to?(:indexing_relationships)

  indexing_relationships.select { |r|
    r.cardinality == :unique && r.within.nil?
  }.map { |rel| audit_single_unique_index(rel) }
end

#health_check(batch_size: 100, sample_size: nil) {|Hash| ... } ⇒ AuditReport

Runs all four audits and wraps results in an AuditReport.

Parameters:

  • batch_size (Integer) (defaults to: 100)

    SCAN batch size for instances audit

  • sample_size (Integer, nil) (defaults to: nil)

    Sample size for participation audit

Yields:

  • (Hash)

    Progress from audit_instances

Returns:



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/familia/horreum/management/audit.rb', line 111

def health_check(batch_size: 100, sample_size: nil, &progress)
  start_time = Familia.now

  inst = audit_instances(batch_size: batch_size, &progress)
  uniq = audit_unique_indexes
  multi = audit_multi_indexes
  parts = audit_participations(sample_size: sample_size)

  duration = Familia.now - start_time

  AuditReport.new(
    model_class: name,
    audited_at: start_time,
    instances: inst,
    unique_indexes: uniq,
    multi_indexes: multi,
    participations: parts,
    duration: duration
  )
end