No release in over a year
ActiveJob Store permits to store jobs state and custom data on a database
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Runtime

 Project Readme

ActiveJob Store

Gem Version Specs Rails 6.1 Specs Rails 7.0 Specs Rails 7.1 Linters

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 your ApplicationJob class if you prefer)
  • Access to the job executions data using the class method job_executions on 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 UTC

Query 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
# => 42

To 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
end

Do 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

License

The gem is available as open source under the terms of the MIT License.