ActiveJob Store
Persist job execution information on a support model ActiveJobStore::Record.
It can be useful to:
- store the job's state / set progress value / add custom data to the jobs;
- query historical data about job executions / extract job's statistical data;
- improve jobs' instrumentation / logging capabilities.
By default gem's internal errors are sent to stderr without compromising the job's execution.
Please ⭐ if you like it.
Installation
- Add to your Gemfile gem 'active_job_store'(and execute:bundle)
- Install the gem's migrations: bundle exec rails active_job_store:install:migrations
- Apply the migrations: bundle exec rails db:migrate
- Add to your job include ActiveJobStore(or to yourApplicationJobclass if you prefer)
- Access to the job executions data using the class method job_executionson your job (ex.YourJob.job_executions)
API
attr_accessor on the jobs:
- 
active_job_store_custom_data: to set / manipulate job's custom data
Instance methods on the jobs:
- 
active_job_store_format_result(result) => result2: to format / manipulate / serialize the job result
- 
active_job_store_internal_error(context, exception): handler for internal errors
- 
active_job_store_record => store record: returns the store's record
- 
save_job_custom_data(custom_data = nil): to persist custom data while the job is performing
Class methods on the jobs:
- 
job_executions => relation: query the list of job executions for the specific job class (returns an ActiveRecord Relation)
Usage examples
SomeJob.perform_now(123)
SomeJob.perform_later(456)
SomeJob.set(wait: 1.minute).perform_later(789)
SomeJob.job_executions.first
# => #<ActiveJobStore::Record:0x00000001120f6320
#  id: 1,
#  job_id: "58daef7c-6b78-4d90-8043-39116eb9fe77",
#  job_class: "SomeJob",
#  state: "completed",
#  arguments: [123],
#  custom_data: nil,
#  details: {"queue_name"=>"default", "priority"=>nil, "executions"=>1, "exception_executions"=>{}, "timezone"=>"UTC"},
#  result: "some_result",
#  exception: nil,
#  enqueued_at: nil,
#  started_at: Wed, 09 Nov 2022 21:09:50.611355000 UTC +00:00,
#  completed_at: Wed, 09 Nov 2022 21:09:50.622797000 UTC +00:00,
#  created_at: Wed, 09 Nov 2022 21:09:50.611900000 UTC +00:00>Query jobs in a specific range of time:
SomeJob.job_executions.where(started_at: 16.minutes.ago...).pluck(:job_id, :result, :started_at)
# => [["02beb3d6-a4eb-442c-8d78-29103ab894dc", "some_result", Wed, 09 Nov 2022 21:20:57.576018000 UTC +00:00],
#  ["267e087e-cfa7-4c88-8d3b-9d40f912733f", "some_result", Wed, 09 Nov 2022 21:13:18.011484000 UTC +00:00]]Some statistics on completed jobs:
SomeJob.job_executions.completed.map { |job| { id: job.id, execution_time: job.completed_at - job.started_at, started_at: job.started_at } }
# => [{:id=>6, :execution_time=>1.005239, :started_at=>Wed, 09 Nov 2022 21:20:57.576018000 UTC +00:00},
#  {:id=>4, :execution_time=>1.004485, :started_at=>Wed, 09 Nov 2022 21:13:18.011484000 UTC +00:00},
#  {:id=>1, :execution_time=>0.011442, :started_at=>Wed, 09 Nov 2022 21:09:50.611355000 UTC +00:00}]Extract some logs:
puts ::ActiveJobStore::Record.order(id: :desc).pluck(:created_at, :job_class, :arguments, :state, :completed_at).map { _1.join(', ') }
# 2022-11-09 21:20:57 UTC, SomeJob, 123, completed, 2022-11-09 21:20:58 UTC
# 2022-11-09 21:18:26 UTC, AnotherJob, another test 2, completed, 2022-11-09 21:18:26 UTC
# 2022-11-09 21:13:18 UTC, SomeJob, Some test 3, completed, 2022-11-09 21:13:19 UTC
# 2022-11-09 21:12:18 UTC, SomeJob, Some test 2, error,
# 2022-11-09 21:10:13 UTC, AnotherJob, another test, completed, 2022-11-09 21:10:13 UTC
# 2022-11-09 21:09:50 UTC, SomeJob, Some test, completed, 2022-11-09 21:09:50 UTCQuery information from a job (even while performing):
job = SomeJob.perform_later 123
job.active_job_store_record.slice(:job_id, :job_class, :arguments)
# => {"job_id"=>"b009f7c7-a264-4fb5-a1f8-68a8141f323b", "job_class"=>"SomeJob", "arguments"=>[123]}
job = AnotherJob.perform_later 456
job.active_job_store_record.custom_data
# => {"progress"=>0.5}
### After a while:
job.active_job_store_record.reload.custom_data
# => {"progress"=>1.0}Setup examples
Store some custom data during the perform (ex. a progress value):
class AnotherJob < ApplicationJob
  include ActiveJobStore
  def perform
    # do something...
    save_job_custom_data(progress: 0.5)
    # do something else...
    save_job_custom_data(progress: 1.0)
    'some_result'
  end
end
# Usage example:
AnotherJob.perform_later(456)
AnotherJob.job_executions.last.custom_data['progress'] # 1.0 (after the job's execution)Prepare the custom data but it gets stored only at the end of the job's execution:
class AnotherJob < ApplicationJob
  include ActiveJobStore
  def perform(some_id)
    self.active_job_store_custom_data = []
    active_job_store_custom_data << { time: Time.current, message: 'SomeJob step 1' }
    sleep 1
    active_job_store_custom_data << { time: Time.current, message: 'SomeJob step 2' }
    'some_result'
  end
end
# Usage example:
AnotherJob.perform_now(123)
AnotherJob.job_executions.last.custom_data
# => [{"time"=>"2022-11-09T21:20:57.580Z", "message"=>"SomeJob step 1"}, {"time"=>"2022-11-09T21:20:58.581Z", "message"=>"SomeJob step 2"}]Process the job's result before storing it (ex. for serialization):
class AnotherJob < ApplicationJob
  include ActiveJobStore
  def perform(some_id)
    21
  end
  def active_job_store_format_result(result)
    result * 2
  end
end
# Usage example:
AnotherJob.perform_now(123)
AnotherJob.job_executions.last.result
# => 42To raise an exception also when there is a gem's internal error:
class AnotherJob < ApplicationJob
  include ActiveJobStore
  # ...
  def active_job_store_internal_error(context, exception)
    # Handle the exception (for example using services like Sentry/Honeybadger) and / or raise it again:
    raise exception
  end
endDo you like it? Star it!
If you use this component just star it. A developer is more motivated to improve a project when there is some interest.
Or consider offering me a coffee, it's a small thing but it is greatly appreciated: about me.
Contributors
- Mattia Roccoberton: author
License
The gem is available as open source under the terms of the MIT License.