{"id":15464862,"url":"https://github.com/blocknotes/active_job_store","last_synced_at":"2025-07-10T13:34:07.695Z","repository":{"id":62965870,"uuid":"564043386","full_name":"blocknotes/active_job_store","owner":"blocknotes","description":"Store jobs' state and custom data on DB","archived":false,"fork":false,"pushed_at":"2023-10-21T07:05:38.000Z","size":98,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-24T22:13:12.500Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/blocknotes.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":["blocknotes"],"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"lfx_crowdfunding":null,"custom":null}},"created_at":"2022-11-09T21:46:51.000Z","updated_at":"2023-02-24T09:15:35.000Z","dependencies_parsed_at":"2023-01-23T16:30:57.252Z","dependency_job_id":"5206f21c-8763-4712-a846-68db65b5475f","html_url":"https://github.com/blocknotes/active_job_store","commit_stats":{"total_commits":30,"total_committers":1,"mean_commits":30.0,"dds":0.0,"last_synced_commit":"b96d9f8aa7b987d507fdf494e5454b6d98127b33"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/blocknotes/active_job_store","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blocknotes%2Factive_job_store","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blocknotes%2Factive_job_store/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blocknotes%2Factive_job_store/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blocknotes%2Factive_job_store/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/blocknotes","download_url":"https://codeload.github.com/blocknotes/active_job_store/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blocknotes%2Factive_job_store/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264585371,"owners_count":23632647,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-02T00:43:11.942Z","updated_at":"2025-07-10T13:34:07.642Z","avatar_url":"https://github.com/blocknotes.png","language":"Ruby","funding_links":["https://github.com/sponsors/blocknotes"],"categories":[],"sub_categories":[],"readme":"# ActiveJob Store\n\n[![Gem Version](https://badge.fury.io/rb/active_job_store.svg)](https://badge.fury.io/rb/active_job_store)\n[![Specs Rails 6.1](https://github.com/blocknotes/active_job_store/actions/workflows/specs_61.yml/badge.svg)](https://github.com/blocknotes/active_job_store/actions/workflows/specs_61.yml)\n[![Specs Rails 7.0](https://github.com/blocknotes/active_job_store/actions/workflows/specs_70.yml/badge.svg)](https://github.com/blocknotes/active_job_store/actions/workflows/specs_70.yml)\n[![Specs Rails 7.1](https://github.com/blocknotes/active_job_store/actions/workflows/specs_71.yml/badge.svg)](https://github.com/blocknotes/active_job_store/actions/workflows/specs_71.yml)\n[![Linters](https://github.com/blocknotes/active_job_store/actions/workflows/linters.yml/badge.svg)](https://github.com/blocknotes/active_job_store/actions/workflows/linters.yml)\n\nPersist job execution information on a support model `ActiveJobStore::Record`.\n\nIt can be useful to:\n- store the job's state / set progress value / add custom data to the jobs;\n- query historical data about job executions / extract job's statistical data;\n- improve jobs' instrumentation / logging capabilities.\n\nBy default gem's internal errors are sent to stderr without compromising the job's execution.\n\nPlease ⭐ if you like it.\n\n## Installation\n\n- Add to your Gemfile `gem 'active_job_store'` (and execute: `bundle`)\n- Install the gem's migrations: `bundle exec rails active_job_store:install:migrations`\n- Apply the migrations: `bundle exec rails db:migrate`\n- Add to your job `include ActiveJobStore` (or to your `ApplicationJob` class if you prefer)\n- Access to the job executions data using the class method `job_executions` on your job (ex. `YourJob.job_executions`)\n\n## API\n\nattr_accessor on the jobs:\n- `active_job_store_custom_data`: to set / manipulate job's custom data\n\nInstance methods on the jobs:\n- `active_job_store_format_result(result) =\u003e result2`: to format / manipulate / serialize the job result\n- `active_job_store_internal_error(context, exception)`: handler for internal errors\n- `active_job_store_record =\u003e store record`: returns the store's record\n- `save_job_custom_data(custom_data = nil)`: to persist custom data while the job is performing\n\nClass methods on the jobs:\n- `job_executions =\u003e relation`: query the list of job executions for the specific job class (returns an ActiveRecord Relation)\n\n## Usage examples\n\n```rb\nSomeJob.perform_now(123)\nSomeJob.perform_later(456)\nSomeJob.set(wait: 1.minute).perform_later(789)\n\nSomeJob.job_executions.first\n# =\u003e #\u003cActiveJobStore::Record:0x00000001120f6320\n#  id: 1,\n#  job_id: \"58daef7c-6b78-4d90-8043-39116eb9fe77\",\n#  job_class: \"SomeJob\",\n#  state: \"completed\",\n#  arguments: [123],\n#  custom_data: nil,\n#  details: {\"queue_name\"=\u003e\"default\", \"priority\"=\u003enil, \"executions\"=\u003e1, \"exception_executions\"=\u003e{}, \"timezone\"=\u003e\"UTC\"},\n#  result: \"some_result\",\n#  exception: nil,\n#  enqueued_at: nil,\n#  started_at: Wed, 09 Nov 2022 21:09:50.611355000 UTC +00:00,\n#  completed_at: Wed, 09 Nov 2022 21:09:50.622797000 UTC +00:00,\n#  created_at: Wed, 09 Nov 2022 21:09:50.611900000 UTC +00:00\u003e\n```\n\nQuery jobs in a specific range of time:\n\n```rb\nSomeJob.job_executions.where(started_at: 16.minutes.ago...).pluck(:job_id, :result, :started_at)\n# =\u003e [[\"02beb3d6-a4eb-442c-8d78-29103ab894dc\", \"some_result\", Wed, 09 Nov 2022 21:20:57.576018000 UTC +00:00],\n#  [\"267e087e-cfa7-4c88-8d3b-9d40f912733f\", \"some_result\", Wed, 09 Nov 2022 21:13:18.011484000 UTC +00:00]]\n```\n\nSome statistics on completed jobs:\n\n```rb\nSomeJob.job_executions.completed.map { |job| { id: job.id, execution_time: job.completed_at - job.started_at, started_at: job.started_at } }\n# =\u003e [{:id=\u003e6, :execution_time=\u003e1.005239, :started_at=\u003eWed, 09 Nov 2022 21:20:57.576018000 UTC +00:00},\n#  {:id=\u003e4, :execution_time=\u003e1.004485, :started_at=\u003eWed, 09 Nov 2022 21:13:18.011484000 UTC +00:00},\n#  {:id=\u003e1, :execution_time=\u003e0.011442, :started_at=\u003eWed, 09 Nov 2022 21:09:50.611355000 UTC +00:00}]\n```\n\nExtract some logs:\n\n```rb\nputs ::ActiveJobStore::Record.order(id: :desc).pluck(:created_at, :job_class, :arguments, :state, :completed_at).map { _1.join(', ') }\n# 2022-11-09 21:20:57 UTC, SomeJob, 123, completed, 2022-11-09 21:20:58 UTC\n# 2022-11-09 21:18:26 UTC, AnotherJob, another test 2, completed, 2022-11-09 21:18:26 UTC\n# 2022-11-09 21:13:18 UTC, SomeJob, Some test 3, completed, 2022-11-09 21:13:19 UTC\n# 2022-11-09 21:12:18 UTC, SomeJob, Some test 2, error,\n# 2022-11-09 21:10:13 UTC, AnotherJob, another test, completed, 2022-11-09 21:10:13 UTC\n# 2022-11-09 21:09:50 UTC, SomeJob, Some test, completed, 2022-11-09 21:09:50 UTC\n```\n\nQuery information from a job (even while performing):\n\n```rb\njob = SomeJob.perform_later 123\njob.active_job_store_record.slice(:job_id, :job_class, :arguments)\n# =\u003e {\"job_id\"=\u003e\"b009f7c7-a264-4fb5-a1f8-68a8141f323b\", \"job_class\"=\u003e\"SomeJob\", \"arguments\"=\u003e[123]}\n\njob = AnotherJob.perform_later 456\njob.active_job_store_record.custom_data\n# =\u003e {\"progress\"=\u003e0.5}\n### After a while:\njob.active_job_store_record.reload.custom_data\n# =\u003e {\"progress\"=\u003e1.0}\n```\n\n## Setup examples\n\nStore some custom data during the perform (ex. a progress value):\n\n```rb\nclass AnotherJob \u003c ApplicationJob\n  include ActiveJobStore\n\n  def perform\n    # do something...\n    save_job_custom_data(progress: 0.5)\n    # do something else...\n    save_job_custom_data(progress: 1.0)\n\n    'some_result'\n  end\nend\n\n# Usage example:\nAnotherJob.perform_later(456)\nAnotherJob.job_executions.last.custom_data['progress'] # 1.0 (after the job's execution)\n```\n\nPrepare the custom data but it gets stored only at the end of the job's execution:\n\n```rb\nclass AnotherJob \u003c ApplicationJob\n  include ActiveJobStore\n\n  def perform(some_id)\n    self.active_job_store_custom_data = []\n\n    active_job_store_custom_data \u003c\u003c { time: Time.current, message: 'SomeJob step 1' }\n    sleep 1\n    active_job_store_custom_data \u003c\u003c { time: Time.current, message: 'SomeJob step 2' }\n\n    'some_result'\n  end\nend\n\n# Usage example:\nAnotherJob.perform_now(123)\nAnotherJob.job_executions.last.custom_data\n# =\u003e [{\"time\"=\u003e\"2022-11-09T21:20:57.580Z\", \"message\"=\u003e\"SomeJob step 1\"}, {\"time\"=\u003e\"2022-11-09T21:20:58.581Z\", \"message\"=\u003e\"SomeJob step 2\"}]\n```\n\nProcess the job's result before storing it (ex. for serialization):\n\n```rb\nclass AnotherJob \u003c ApplicationJob\n  include ActiveJobStore\n\n  def perform(some_id)\n    21\n  end\n\n  def active_job_store_format_result(result)\n    result * 2\n  end\nend\n\n# Usage example:\nAnotherJob.perform_now(123)\nAnotherJob.job_executions.last.result\n# =\u003e 42\n```\n\nTo raise an exception also when there is a gem's internal error:\n\n```rb\nclass AnotherJob \u003c ApplicationJob\n  include ActiveJobStore\n\n  # ...\n\n  def active_job_store_internal_error(context, exception)\n    # Handle the exception (for example using services like Sentry/Honeybadger) and / or raise it again:\n    raise exception\n  end\nend\n```\n\n## Do you like it? Star it!\n\nIf you use this component just star it. A developer is more motivated to improve a project when there is some interest.\n\nOr consider offering me a coffee, it's a small thing but it is greatly appreciated: [about me](https://www.blocknot.es/about-me).\n\n## Contributors\n\n- [Mattia Roccoberton](https://blocknot.es/): author\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fblocknotes%2Factive_job_store","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fblocknotes%2Factive_job_store","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fblocknotes%2Factive_job_store/lists"}