Class: Moderate::ClassifyJob

Inherits:
ActiveJob::Base
  • Object
show all
Defined in:
lib/moderate/jobs/classify_job.rb

Overview

The background worker for ASYNCHRONOUS filter adapters (the :openai adapter, a host’s hosted classifier, the default :image adapter) running in :flag mode.

── Why a job exists at all ──────────────────────────────────────────────────An async adapter does blocking I/O (a moderation API call), which must never run inside the request that saved the content — and CANNOT run inside a validator or an after_commit callback without stalling the write. So the :flag path works in two halves:

1. The model's filter concern (`moderates :field`) lets the write succeed, then
   in an after_commit hook enqueues THIS job for any field whose policy uses an
   async adapter. (Synchronous adapters like :wordlist skip the job — the
   concern classifies inline and files the Flag directly.)
2. This job re-reads the saved value, classifies it through the adapter, and —
   if the content is flagged — files (or updates) a Moderate::Flag for the
   moderation queue.

── Re-reading the value (deliberate) ────────────────────────────────────────The job is handed the RECORD (GlobalID-serialized by ActiveJob) and the FIELD NAME, not the raw text/image — so it always classifies the CURRENT persisted value. If the record was edited or deleted between enqueue and run, we classify what’s actually there now (or skip a vanished record), never a stale snapshot.

── Idempotency ──────────────────────────────────────────────────────────────Flag creation goes through ‘Moderate::Flag.flag!`, the single builder shared by the synchronous and asynchronous paths. It’s an upsert-by-(flaggable, field, source) so a retried job (ActiveJob retries on transient failures) doesn’t pile up duplicate queue entries for the same content.

── Base class ───────────────────────────────────────────────────────────────We subclass ‘ActiveJob::Base` directly (not the host’s ApplicationJob) so the gem doesn’t depend on a constant that lives in the host app — the same reason the rest of the gem stays host-agnostic. The host configures the queue adapter, retries, and queue name globally as usual.

Instance Method Summary collapse

Instance Method Details

#perform(record, field, adapter: nil) ⇒ Object

Parameters:

  • record (ActiveRecord::Base)

    the flaggable record (any Moderate::Reportable).

  • field (String, Symbol)

    the field whose value to classify.

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

    optional explicit adapter name; when nil, the field’s resolved FilterPolicy (or the global default) decides.



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/moderate/jobs/classify_job.rb', line 47

def perform(record, field, adapter: nil)
  # The record may have been destroyed between enqueue and execution — nothing
  # to classify, nothing to flag. (ActiveJob raises DeserializationError for a
  # GlobalID that no longer resolves; that's rescued at the framework level, but
  # we also guard a nil here defensively.)
  return if record.nil?

  field = field.to_s
  policy = resolve_policy(record, field, adapter)

  # If the field's policy is :off (e.g. it was reconfigured to off after the job
  # was enqueued), there's nothing to do.
  return if policy.respond_to?(:off?) && policy.off?

  value = field_value(record, field)
  return if blank?(value)

  result = Moderate.classify(value, policy: policy)
  return unless result.flagged?

  file_flag(record, field, policy, result, value)
end