{"id":13878576,"url":"https://github.com/hopsoft/job_contracts","last_synced_at":"2025-09-15T15:31:03.184Z","repository":{"id":40500480,"uuid":"484595088","full_name":"hopsoft/job_contracts","owner":"hopsoft","description":"Enforceable contracts with test-like assurances for jobs","archived":false,"fork":false,"pushed_at":"2024-01-12T21:10:50.000Z","size":102,"stargazers_count":78,"open_issues_count":1,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-01-02T07:32:05.645Z","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/hopsoft.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"MIT-LICENSE","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}},"created_at":"2022-04-22T23:09:02.000Z","updated_at":"2024-10-15T15:47:22.000Z","dependencies_parsed_at":"2024-01-13T20:17:24.042Z","dependency_job_id":"6e41113d-f699-4973-8264-1623441ff811","html_url":"https://github.com/hopsoft/job_contracts","commit_stats":{"total_commits":45,"total_committers":1,"mean_commits":45.0,"dds":0.0,"last_synced_commit":"070cb424c77d798d8e36f05b45f484e82592aee3"},"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hopsoft%2Fjob_contracts","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hopsoft%2Fjob_contracts/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hopsoft%2Fjob_contracts/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hopsoft%2Fjob_contracts/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hopsoft","download_url":"https://codeload.github.com/hopsoft/job_contracts/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":233128207,"owners_count":18629046,"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-08-06T08:01:53.642Z","updated_at":"2025-01-09T02:55:06.770Z","avatar_url":"https://github.com/hopsoft.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"[![Lines of Code](http://img.shields.io/badge/lines_of_code-236-brightgreen.svg?style=flat)](http://blog.codinghorror.com/the-best-code-is-no-code-at-all/)\n[![Code Quality](https://app.codacy.com/project/badge/Grade/f604d4bc6db0474c802ef51182732488)](https://www.codacy.com/gh/hopsoft/job_contracts/dashboard?utm_source=github.com\u0026amp;utm_medium=referral\u0026amp;utm_content=hopsoft/job_contracts\u0026amp;utm_campaign=Badge_Grade)\n[![Tests](https://github.com/hopsoft/job_contracts/actions/workflows/test.yml/badge.svg)](https://github.com/orbit-love/job_contracts/actions/workflows/test.yml)\n[![Gem Version](https://badge.fury.io/rb/job_contracts.svg)](https://badge.fury.io/rb/job_contracts)\n[![Gem Downloads](https://img.shields.io/gem/dt/job_contracts)](https://rubygems.org/gems/job_contracts)\n\n# Job Contracts\n\n## Test-like assurances for jobs\n\nHave you ever wanted to prevent a background job from writing to the database or perhaps ensure that it completes within a fixed amount of time?\n\nContracts allow you to easily enforce guarantees like this.\n\n\u003c!-- Tocer[start]: Auto-generated, don't remove. --\u003e\n\n## Table of Contents\n\n  - [Why use Contracts?](#why-use-contracts)\n  - [Quick Start](#quick-start)\n  - [Contracts](#contracts)\n    - [Breach of Contract](#breach-of-contract)\n    - [Anatomy of a Contract](#anatomy-of-a-contract)\n    - [Defining a Contract](#defining-a-contract)\n    - [Using a Contract](#using-a-contract)\n  - [Worker Formation/Topology](#worker-formationtopology)\n  - [Advanced Usage](#advanced-usage)\n  - [Sidekiq](#sidekiq)\n  - [Todo](#todo)\n  - [License](#license)\n  - [Sponsors](#sponsors)\n\n\u003c!-- Tocer[finish]: Auto-generated, don't remove. --\u003e\n\n## Why use Contracts?\n\n- Organize your code for better reuse, consistency, and maintainability\n- Refine your telemetry and instrumentation efforts\n- Improve job performance via enforced *(SLAs/SLOs/SLIs)*\n- Monitor and manage job queue backpressure\n- Improve your worker formation/topology to support high throughput\n\n## Quick Start\n\nImagine you want to ensure a specific job completes within 5 seconds of being enqueued.\n\n```ruby\nclass ImportantJob \u003c ApplicationJob\n  include JobContracts::Contractable\n\n  queue_as :default\n  add_contract JobContracts::DurationContract.new(max: 5.seconds)\n\n  def perform\n    # logic...\n  end\n\n  # default callback that's invoked if the contract is breached\n  def contract_breached!(contract)\n    # handle breach...\n  end\nend\n```\n\n*How to handle a [__breach of contract__](#breach-of-contract).*\n\n## Contracts\n\nA contract is an agreement that a job should fulfill.\nFailing to satisfy the contract is considered a __breach of contract__.\n\nContracts help you track `actual` results and compare them to `expected` outcomes.\nFor example, this project has a default set of contracts that verify the following:\n\n- That a job will [execute within a set amount of time](https://github.com/hopsoft/job_contracts/blob/main/lib/job_contracts/contracts/duration_contract.rb)\n- That a job is only [performed on a specific queue](https://github.com/hopsoft/job_contracts/blob/main/lib/job_contracts/contracts/queue_name_contract.rb)\n- That a job [does not write to the database](https://github.com/hopsoft/job_contracts/blob/main/lib/job_contracts/contracts/read_only_contract.rb)\n\n### Breach of Contract\n\nA __breach of contract__ is similar to a test failure; however, the breach can be handled in many different ways.\n\n- Log and instrument the breach and continue\n- Halt processing of the job and all other contracts and raise an exception\n- Move the job to a queue where the contract will not be enforced\n- etc...\n\n*Mix and match any combination of these options to support your requirements.*\n\n### Anatomy of a Contract\n\nContracts support the following constructor arguments.\n\n- __`trigger`__ `[Symbol] (:before, *:after)` - when contract enforcement takes place, *before or after perform*\n- __`halt`__ `[Boolean] (true, *false)` - indicates whether or not to stop processing when the contract is breached\n- __`queues`__ `[Array\u003cString,Symbol\u003e]` - a list of queue names where this contract will be enforced _(defaults to the configured queue, or `*` if the queue has not beeen configured)_\n- __`expected`__ `[Hash]` - a dictionary of contract expectations\n\n### Defining a Contract\n\nHere's a contrived, but simple, example that ensures the first argument passed to perform fits within a specific range of values.\n\n```ruby\n# app/contracts/argument_contract.rb\nclass ArgumentContract \u003c JobContracts::Contract\n  def initialize(range:)\n    # enforced on all queues\n    super queues: [\"*\"], expected: {range: range}\n  end\n\n  def enforce!(contractable)\n    actual[:argument] = contractable.arguments.first\n    self.satisfied = expected[:range].cover?(actual[:argument])\n    super\n  end\nend\n```\n\n### Using a Contract\n\nHere's how to use the `ArgumentContract` in a job.\n\n```ruby\n# app/jobs/argument_example_job.rb\nclass ArgumentExampleJob \u003c ApplicationJob\n  include JobContracts::Contractable\n\n  queue_as :default\n  add_contract ArgumentContract.new(range: (1..10))\n\n  def perform(arg)\n    # logic...\n  end\n\n  # default callback that's invoked if the contract is breached\n  def contract_breached!(contract)\n    # handle breach...\n  end\nend\n```\n\nThis job will help ensure that the argument passed to perform is between 1 and 10.\n*It's up to you to determine how to handle a breach of contract.*\n\n## Worker Formation/Topology\n\nThoughtful Rails applications often use specialized worker formations.\n\nA simple formation might be to use two sets of workers.\nOne set dedicated to fast low-latency jobs with plenty of dedicated compute resources *(CPUs, processes, threads, etc...)*,\nwith another set dedicated to slower jobs that uses fewer compute resources.\n\n\u003cimg width=\"593\" alt=\"Untitled 2 2022-04-29 15-06-13\" src=\"https://user-images.githubusercontent.com/32920/166069103-e316dcc7-e601-43d0-90df-ad0eda20409b.png\"\u003e\n\nSay we determine that fast low-latency jobs should __not__ write to the database.\n\nWe can use a [`ReadOnlyContract`](https://github.com/hopsoft/job_contracts/blob/main/lib/job_contracts/contracts/read_only_contract.rb)\nto enforce this decision. If the contract is breached, we can notify our apm/monitoring service and re-enqueue the job to a slower queue *(worker set)* where database writes are permitted.\nThis will ensure that our fast low-latency queue doesn't get clogged with slow-running jobs.\n\nHere's an example job implementation that accomplishes this.\n\n```ruby\nclass FastJob \u003c ApplicationJob\n  include JobContracts::Contractable\n\n  # Configure the queue before adding contracts\n  # It will be used as the default enforcement queue for contracts\n  queue_as :critical\n\n  # Only enforces on the critical queue\n  # This allows us to halt job execution and reenqueue the job to a different queue\n  # where the contract will not be enforced\n  #\n  # NOTE: the arg `queues: [:critical]` is default behavior in this example\n  #       we're setting it explicitly here for illustration purposes\n  add_contract JobContracts::ReadOnlyContract.new(queues: [:critical])\n\n  def perform\n    # logic that shouldn't write to the database,\n    # but might accidentally due to complex or opaque internals\n  end\n\n  def contract_breached!(contract)\n    # log and notify apm/monitoring service\n\n    # re-enqueue to a different queue\n    # where the database write will be permitted\n    # i.e. where the contract will not be enforced\n    enqueue queue: :default\n  end\nend\n```\n\n*Worker formations can be designed in countless ways to handle incredibly sophisticated requirements and operational constraints.\nThe only real limitation is your creativity.*\n\n## Advanced Usage\n\nIt's possible to override the default callback method that handles contract breaches.\n\n```ruby\nclass ImportantJob \u003c ApplicationJob\n  include JobContracts::Contractable\n\n  queue_as :default\n  on_contract_breach :take_action\n  add_contract JobContracts::DurationContract.new(max: 5.seconds)\n\n  def perform\n    # logic...\n  end\n\n  def take_action(contract)\n    # handle breach...\n  end\nend\n```\n\n```ruby\nclass ImportantJob \u003c ApplicationJob\n  include JobContracts::Contractable\n\n  queue_as :default\n  on_contract_breach -\u003e (contract) { # take action... }\n\n  add_contract JobContracts::DurationContract.new(max: 5.seconds)\n\n  def perform\n    # logic...\n  end\nend\n```\n\n## Sidekiq\n\n`Sidekiq::Job`s are also supported.\n\n```ruby\nclass ImportantJob\n  include Sidekiq::Job\n  include JobContracts::SidekiqContractable\n\n  sidekiq_options queue: :default\n  add_contract JobContracts::DurationContract.new(max: 1.second)\n\n  def perform\n    # logic...\n  end\n\n  # default callback that's invoked if the contract is breached\n  def contract_breached!(contract)\n    # handle breach...\n  end\nend\n```\n\n## Todo\n\n- [ ] Sidekiq tests\n\n## License\n\nThe gem is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n\n## Sponsors\n\nThis project is sponsored by [Orbit.love](https://orbit.love/?utm_source=github\u0026utm_medium=repo\u0026utm_campaign=hopsoft\u0026utm_content=job_contracts) *(mission control for your community)*.\n\n\u003ca href=\"https://orbit.love/?utm_source=github\u0026utm_medium=repo\u0026utm_campaign=hopsoft\u0026utm_content=job_contracts\"\u003e\n  \u003cimg height=\"50\" src=\"https://user-images.githubusercontent.com/32920/166343064-55f92cdb-c81b-4f85-80a8-167bfda73c85.png\"\u003e\u003c/img\u003e\n\u003c/a\u003e\n\n---\n\nThis effort was partly inspired by a presentation at [Sin City Ruby](https://www.sincityruby.com/) from our friends on the platform team at [Gusto](https://gusto.com/).\nTheir presentation validated some of my prior solutions aimed at accomplishing similar goals and motivated me to extract that work into a GEM.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhopsoft%2Fjob_contracts","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhopsoft%2Fjob_contracts","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhopsoft%2Fjob_contracts/lists"}