Class: SeoBatchJob
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- SeoBatchJob
- Defined in:
- app/models/seo_batch_job.rb
Overview
Tracks a single batch submission to the Gemini or Anthropic Batch API.
Lifecycle:
collecting -> submitting -> processing -> completed
-> failed
The nightly pipeline creates one SeoBatchJob per run. It progresses through:
- SeoBatchCollectorWorker builds prompts for each page (status: collecting)
- SeoBatchSubmitWorker submits to Batch API (status: submitting -> processing)
- SeoBatchPollWorker monitors until done
- SeoBatchResultsWorker saves results (status: completed)
Provider routing:
- Gemini models -> Gemini Batch API (50% discount)
- Claude models -> Anthropic Message Batches API (50% discount)
== Schema Information
Table name: seo_batch_jobs
Database name: primary
id :bigint not null, primary key
completed_at :datetime
errored_count :integer default(0)
expired_count :integer default(0)
metadata :jsonb
model :string default("claude-sonnet-4-5"), not null
status :string default("collecting"), not null
submitted_at :datetime
succeeded_count :integer default(0)
total_pages :integer default(0)
created_at :datetime not null
updated_at :datetime not null
ai_batch_id :string
Indexes
index_seo_batch_jobs_on_ai_batch_id (ai_batch_id) UNIQUE
index_seo_batch_jobs_on_status (status)
Constant Summary collapse
- STATUSES =
Statuses.
%w[collecting submitting processing completed failed].freeze
- STUCK_LOCAL_AFTER =
Self-healing TTLs for the single-flight guard (see .fail_stuck_jobs!).
Local phases (collecting/submitting) finish in minutes on a healthy run,
so 24h means abandoned. Provider-side processing can legitimately take up
to the 24h batch expiry — SeoBatchPollWorker already fails jobs at its own
24h cap, so 48h here is strictly the backstop for a dead poll chain. 24.hours
- STUCK_PROCESSING_AFTER =
48.hours
Constants included from Models::Schedulable
Models::Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
- #model ⇒ Object readonly
- #status ⇒ Object readonly
Has many collapse
Class Method Summary collapse
-
.active ⇒ ActiveRecord::Relation<SeoBatchJob>
A relation of SeoBatchJobs that are active.
-
.completed ⇒ ActiveRecord::Relation<SeoBatchJob>
A relation of SeoBatchJobs that are completed.
-
.fail_stuck_jobs! ⇒ Object
Auto-recovery for the single-flight guard: fail every stuck job so the next collection starts fresh.
-
.stuck ⇒ ActiveRecord::Relation<SeoBatchJob>
A relation of SeoBatchJobs that are stuck.
Instance Method Summary collapse
- #anthropic? ⇒ Boolean
- #collecting? ⇒ Boolean
- #completed? ⇒ Boolean
- #failed? ⇒ Boolean
- #gemini? ⇒ Boolean
- #mark_completed!(succeeded: 0, errored: 0, expired: 0) ⇒ Object
- #mark_failed!(error_message) ⇒ Object
- #mark_submitted!(provider_batch_id:, provider: nil, extra_metadata: {}) ⇒ Object
- #processing? ⇒ Boolean
- #provider_batch_id ⇒ Object
- #submitting? ⇒ Boolean
Methods inherited from ApplicationRecord
ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation
Methods included from Models::Schedulable
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Instance Attribute Details
#model ⇒ Object (readonly)
57 |
# File 'app/models/seo_batch_job.rb', line 57 validates :model, presence: true |
#status ⇒ Object (readonly)
56 |
# File 'app/models/seo_batch_job.rb', line 56 validates :status, inclusion: { in: STATUSES } |
Class Method Details
.active ⇒ ActiveRecord::Relation<SeoBatchJob>
A relation of SeoBatchJobs that are active. Active Record Scope
59 |
# File 'app/models/seo_batch_job.rb', line 59 scope :active, -> { where(status: %w[collecting submitting processing]) } |
.completed ⇒ ActiveRecord::Relation<SeoBatchJob>
A relation of SeoBatchJobs that are completed. Active Record Scope
60 |
# File 'app/models/seo_batch_job.rb', line 60 scope :completed, -> { where(status: 'completed') } |
.fail_stuck_jobs! ⇒ Object
Auto-recovery for the single-flight guard: fail every stuck job so the
next collection starts fresh. Called by SeoBatchCollectorWorker before it
checks .active. Returns the number of jobs actually failed.
74 75 76 |
# File 'app/models/seo_batch_job.rb', line 74 def self.fail_stuck_jobs! stuck.find_each.count { |job| fail_if_still_stuck(job) } end |
.stuck ⇒ ActiveRecord::Relation<SeoBatchJob>
A relation of SeoBatchJobs that are stuck. Active Record Scope
66 67 68 69 |
# File 'app/models/seo_batch_job.rb', line 66 scope :stuck, -> { where(status: %w[collecting submitting], updated_at: ..STUCK_LOCAL_AFTER.ago) .or(where(status: 'processing', updated_at: ..STUCK_PROCESSING_AFTER.ago)) } |
Instance Method Details
#anthropic? ⇒ Boolean
98 |
# File 'app/models/seo_batch_job.rb', line 98 def anthropic? = model.to_s.start_with?('claude-') |
#collecting? ⇒ Boolean
91 |
# File 'app/models/seo_batch_job.rb', line 91 def collecting? = status == 'collecting' |
#completed? ⇒ Boolean
94 |
# File 'app/models/seo_batch_job.rb', line 94 def completed? = status == 'completed' |
#failed? ⇒ Boolean
95 |
# File 'app/models/seo_batch_job.rb', line 95 def failed? = status == 'failed' |
#gemini? ⇒ Boolean
97 |
# File 'app/models/seo_batch_job.rb', line 97 def gemini? = model.to_s.start_with?('gemini-') |
#items ⇒ ActiveRecord::Relation<SeoBatchItem>
54 |
# File 'app/models/seo_batch_job.rb', line 54 has_many :items, class_name: 'SeoBatchItem', dependent: :destroy |
#mark_completed!(succeeded: 0, errored: 0, expired: 0) ⇒ Object
118 119 120 121 122 123 124 125 126 |
# File 'app/models/seo_batch_job.rb', line 118 def mark_completed!(succeeded: 0, errored: 0, expired: 0) update!( status: 'completed', succeeded_count: succeeded, errored_count: errored, expired_count: expired, completed_at: Time.current ) end |
#mark_failed!(error_message) ⇒ Object
128 129 130 131 132 133 134 |
# File 'app/models/seo_batch_job.rb', line 128 def mark_failed!() update!( status: 'failed', completed_at: Time.current, metadata: .merge('error' => ) ) end |
#mark_submitted!(provider_batch_id:, provider: nil, extra_metadata: {}) ⇒ Object
104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'app/models/seo_batch_job.rb', line 104 def mark_submitted!(provider_batch_id:, provider: nil, extra_metadata: {}) merged = .merge('provider' => provider || inferred_provider) .merge() update!( status: 'processing', ai_batch_id: provider_batch_id, submitted_at: Time.current, total_pages: items.count, metadata: merged ) end |
#processing? ⇒ Boolean
93 |
# File 'app/models/seo_batch_job.rb', line 93 def processing? = status == 'processing' |
#provider_batch_id ⇒ Object
100 101 102 |
# File 'app/models/seo_batch_job.rb', line 100 def provider_batch_id ai_batch_id end |
#submitting? ⇒ Boolean
92 |
# File 'app/models/seo_batch_job.rb', line 92 def submitting? = status == 'submitting' |