Module: Familia::Features::Housekeeping::ModelClassMethods

Defined in:
lib/familia/features/housekeeping.rb

Overview

Housekeeping::ModelClassMethods

Instance Method Summary collapse

Instance Method Details

#chore(name, callable = nil) {|obj| ... } ⇒ #call

Register a chore by name. Takes either a block or any callable (a class or object responding to +call+) -- both receive the instance. Callables allow shipping reusable chores as classes, e.g. Housekeeping::EnforceCollectionCaps.

Parameters:

  • name (Symbol, String)

    chore identifier

  • callable (#call, nil) (defaults to: nil)

    callable invoked with the instance

Yields:

  • (obj)

    block invoked with the instance during do_chore!/do_chores!

Returns:

  • (#call)

    the registered handler

Raises:

  • (ArgumentError)

    if name is blank, neither or both of callable/block are given, or the callable does not respond to call



75
76
77
78
79
80
81
82
83
84
# File 'lib/familia/features/housekeeping.rb', line 75

def chore(name, callable = nil, &block)
  raise ArgumentError, 'chore name required' if name.nil? || name.to_s.empty?
  raise ArgumentError, "chore #{name.inspect} takes a callable or a block, not both" if callable && block

  handler = callable || block
  raise ArgumentError, "chore #{name.inspect} requires a callable or block" unless handler
  raise ArgumentError, "chore #{name.inspect} callable must respond to #call" unless handler.respond_to?(:call)

  chores[name.to_sym] = handler
end

#choresHash{Symbol => #call}

Registered chores in registration order. Subclasses inherit a copy of their parent's chores on first access, so registering a new chore on a subclass does not mutate the parent.

Returns:

  • (Hash{Symbol => #call})


91
92
93
94
95
96
97
# File 'lib/familia/features/housekeeping.rb', line 91

def chores
  @chores ||= if superclass.respond_to?(:chores)
    superclass.chores.dup
  else
    {}
  end
end

#run_chores!(chore_name: nil, limit: nil, batch_size: DEFAULT_BATCH_SIZE) ⇒ Hash

Run registered chores against every record reachable via the instances class-level sorted set. Records are loaded in pipelined batches (load_multi) and each chore is executed per record with error isolation -- one failing chore call does not abort the run.

Stats shape:

{ model: 'User', scanned: 4200, chores: { downcase_email: { modified: 37, errors: 0 }, default_timezone: { modified: 102, errors: 1 }, }, }

The modified counter increments when the chore block returns a truthy value (the conventional "modified" signal). The errors counter increments when the chore block raises; the exception is logged via Familia.warn and iteration continues.

Requires the class to expose an instances collection (Horreum's default class-level sorted set) and load_multi (Horreum default).

Parameters:

  • chore_name (Symbol, String, nil) (defaults to: nil)

    specific chore to run; nil runs every registered chore against each record

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

    cap on records scanned; nil iterates all

  • batch_size (Integer) (defaults to: DEFAULT_BATCH_SIZE)

    pipeline batch size for load_multi

Returns:

  • (Hash)

    stats hash (see above)

Raises:

  • (ArgumentError)

    if the class has no chores, the named chore is not registered, or instances/load_multi are unavailable



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/familia/features/housekeeping.rb', line 130

def run_chores!(chore_name: nil, limit: nil, batch_size: DEFAULT_BATCH_SIZE)
  unless respond_to?(:instances) && respond_to?(:load_multi)
    raise ArgumentError, "#{name} cannot run_chores! without instances and load_multi"
  end

  chore_keys = resolve_chore_keys(chore_name)
  stats      = chore_keys.to_h { |key| [key, { modified: 0, errors: 0 }] }
  scanned    = 0

  instances.to_a.each_slice(batch_size) do |batch_ids|
    break if limit && scanned >= limit

    batch_ids = batch_ids.take(limit - scanned) if limit
    records   = load_multi(batch_ids).compact
    records.each do |record|
      scanned += 1
      chore_keys.each do |key|
        begin
          stats[key][:modified] += 1 if record.do_chore!(key)
        rescue StandardError => e
          stats[key][:errors] += 1
          Familia.warn "[run_chores!] #{name}##{record.identifier} chore=#{key} failed: #{e.message}"
        end
      end
    end
  end

  { model: name, scanned: scanned, chores: stats }
end