{"id":13877608,"url":"https://github.com/toptal/active-job-style-guide","last_synced_at":"2025-03-12T00:34:12.787Z","repository":{"id":41463375,"uuid":"199739552","full_name":"toptal/active-job-style-guide","owner":"toptal","description":"This Background Jobs style guide is a list of best practices working with Ruby background jobs.","archived":false,"fork":false,"pushed_at":"2023-09-16T12:12:26.000Z","size":20,"stargazers_count":472,"open_issues_count":2,"forks_count":20,"subscribers_count":120,"default_branch":"master","last_synced_at":"2025-01-18T10:30:11.619Z","etag":null,"topics":["activejob","sidekiq","style-guide"],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/toptal.png","metadata":{"files":{"readme":"README.adoc","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2019-07-30T22:59:48.000Z","updated_at":"2024-12-29T03:02:58.000Z","dependencies_parsed_at":"2024-01-08T07:59:58.268Z","dependency_job_id":null,"html_url":"https://github.com/toptal/active-job-style-guide","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/toptal%2Factive-job-style-guide","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/toptal%2Factive-job-style-guide/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/toptal%2Factive-job-style-guide/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/toptal%2Factive-job-style-guide/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/toptal","download_url":"https://codeload.github.com/toptal/active-job-style-guide/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243136272,"owners_count":20241988,"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":["activejob","sidekiq","style-guide"],"created_at":"2024-08-06T08:01:13.645Z","updated_at":"2025-03-12T00:34:12.751Z","avatar_url":"https://github.com/toptal.png","language":null,"funding_links":[],"categories":["Others"],"sub_categories":[],"readme":"= Active Job Style Guide\n:idprefix:\n:idseparator: -\n:sectanchors:\n:sectlinks:\n:toc: preamble\n:toclevels: 1\nifndef::backend-pdf[]\n:toc-title: pass:[\u003ch2\u003eTable of Contents\u003c/h2\u003e]\nendif::[]\n:source-highlighter: rouge\n\nThis style guide is a list of best practices working with Ruby background jobs using Active Job with Sidekiq backend.\n\nDespite the common belief, they work quite well together if you follow the guidelines.\n\nSidekiq may be used without Active Job, but the latter adds transparency and a useful serialization layer.\n\nThis style guide didn't appear out of thin air - it is based on the professional experience of the editors, official documentation, and suggestions from members of the Ruby community.\n\nThose guidelines help to avoid numerous pitfalls.\nDepending on the usage of background jobs, some guidelines might apply, and some not.\n\nifdef::env-github[]\nYou can generate a PDF copy of this guide using https://asciidoctor.org/docs/asciidoctor-pdf/[AsciiDoctor PDF], and an HTML copy https://asciidoctor.org/docs/convert-documents/#converting-a-document-to-html[with] https://asciidoctor.org/#installation[AsciiDoctor] using the following commands:\n\n[source,shell]\n----\n# Generates README.pdf\nasciidoctor-pdf -a allow-uri-read README.adoc\n\n# Generates README.html\nasciidoctor\n----\n\n[TIP]\n====\nInstall the `rouge` gem to get nice syntax highlighting in the generated document.\n\n[source,shell]\n----\ngem install rouge\n----\n====\nendif::[]\n\n[#general]\n== General Recommendations\n\n[#active-record-models-as-arguments]\n=== Active Record Models as Arguments\n\nPass Active Record models as arguments; do not pass by id.\nActive Job automatically serializes and deserializes Active Record models using https://edgeguides.rubyonrails.org/active_job_basics.html#globalid[GlobalID], and manual deserialization of the models is not necessary.\n\nGlobalID handles model class mismatches properly.\n\nDeserialization errors are reported to error tracking.\n\n[source,ruby]\n----\n# bad - passing by id\n# Deserialization error is reported, the job *is* scheduled for retry.\nclass SomeJob \u003c ApplicationJob\n  def perform(model_id)\n    model = Model.find(model_id)\n    do_something_with(model)\n  end\nend\n\n# bad - model mismatch\nclass SomeJob \u003c ApplicationJob\n  def perform(model_id)\n    Model.find(model_id)\n    # ...\n  end\nend\n\n# Will try to fetch a Model using another model class, e.g. User's id.\nSomeJob.perform_later(user.id)\n\n# acceptable - passing by id\n# Deserialization error is reported, the job is *not* scheduled for retry.\nclass SomeJob \u003c ApplicationJob\n  def perform(model_id)\n    model = Model.find(model_id)\n    do_something_with(model)\n  rescue ActiveRecord::RecordNotFound\n    Rollbar.warning('Not found')\n  end\nend\n\n# good - passing with GlobalID\n# Deserialization error is reported, the job is *not* scheduled for retry.\nclass SomeJob \u003c ApplicationJob\n  def perform(model)\n    do_something_with(model)\n  end\nend\n----\n\nWARNING: Do not replace one style with another, use a transitional period to let all jobs scheduled with ids to be processed.\nUse a helper to temporarily support both numeric and GlobalID arguments.\n\n[source,ruby]\n----\nclass SomeJob \u003c ApplicationJob\n  include TransitionHelper\n\n  def perform(model)\n    # TODO: remove this when all jobs with numeric id arguments are processed\n    model = fetch(model, Model)\n    do_something_with(model)\n  end\nend\n\nmodule TransitionHelper\n  def fetch(id_or_object, model_class)\n    case id_or_object\n    when Numeric\n      model_class.find(id_or_object)\n    when model_class\n      id_or_object\n    else\n      fail \"Object type mismatch #{model_class}, #{id_or_object}\"\n    end\n  end\nend\n----\n\n[#queue-assignments]\n=== Queue Assignments\n\nExplicitly specify a queue to be used in job classes.\nMake sure the queue is on the https://github.com/mperham/sidekiq/wiki/Advanced-Options#queues[list of processed queues].\n\nPutting all jobs into one basket comes with a risk of more urgent jobs being executed with a significant delay.\nDo not put slow and fast jobs together in one queue.\nDo not put urgent and non-urgent jobs together in one queue.\n\n[source,ruby]\n----\n# bad - no queue specified\nclass SomeJob \u003c ApplicationJob\n  def perform\n    # ...\n  end\nend\n\n# bad - the wrong queue specified\nclass SomeJob \u003c ApplicationJob\n  queue_as :hgh_prioriti # nonexistent queue specified\n\n  def perform\n    # ...\n  end\nend\n\n# good\nclass SomeJob \u003c ApplicationJob\n  queue_as :high_priority\n\n  def perform\n    # ...\n  end\nend\n----\n\n[#idempotency]\n=== Idempotency\n\nIdeally, jobs should be idempotent, meaning there should be no bad side effects of them running more than once.\nSidekiq only guarantees that the jobs will run https://github.com/mperham/sidekiq/wiki/Best-Practices#2-make-your-job-idempotent-and-transactional[at least once], but not necessarily exactly once.\n\nEven jobs that do not fail due to errors https://github.com/mperham/sidekiq/wiki/FAQ#what-happens-to-long-running-jobs-when-sidekiq-restarts[might be interrupted] during https://github.com/mperham/sidekiq/wiki/Deployment#overview[non-rolling-release deployments].\n\n[source,ruby]\n----\nclass UserNotificationJob \u003c ApplicationJob\n  def perform(user)\n    send_email_to(user) unless already_notified?(user)\n  end\nend\n----\n\n[#atomicity]\n=== Atomicity\n\nDuring deployment, a job is given 25 seconds to complete by default.\nAfter that, the worker is terminated and the job is sent back to the queue.\nThis might result in part of the work being executed twice.\n\nMake the jobs atomic, i.e., all or nothing.\n\n[#threads]\n=== Threads\n\nDo not use threads in your jobs.\nSpawn jobs instead.\nSpinning up a thread in a job leads to opening a new database connection, and the connections are easily exhausted, up to the point when the webserver is down.\n\n[source,ruby]\n----\n# bad - consumes all available connections\nclass SomeJob \u003c ApplicationJob\n  def perform\n    User.find_each |user|\n      Thread.new do\n        ExternalService.update(user)\n      end\n    end\n  end\nend\n\n# good\nclass SomeJob \u003c ApplicationJob\n  def perform(user)\n    ExternalService.update(user)\n  end\nend\n\nUser.find_each |user|\n  SomeJob.perform_later(user)\nend\n----\n\n[#retries]\n=== Retries\n\nAvoid using https://edgeguides.rubyonrails.org/active_job_basics.html#exceptions[ActiveJob's built-in `retry_on`] or `ActiveJob::Retry` (`activejob-retry` gem).\nUse Sidekiq retries, which are also available from within Active Job with Sidekiq 6+.\n\nDo not hide or extract job retry mechanisms.\nKeep retries directives visible in the jobs.\n\n[source,ruby]\n----\n# bad - makes three attempts without submitting to Rollbar,\n# fails and relies on Sidekiq's retry that would also make several\n# retry attempts, submitting each of the failures to Rollbar.\nclass SomeJob \u003c ApplicationJob\n  retry_on ThirdParty::Api::Errors::SomeError, wait: 1.minute, attempts: 3\n\n  def perform(user)\n    # ...\n  end\nend\n\n# bad - it's not clear upfront if the job will be retried or not\nclass SomeJob \u003c ApplicationJob\n  include ReliableJob\n\n  def perform(user)\n    # ...\n  end\nend\n\n# good - Sidekiq deals with retries\nclass SomeJob \u003c ApplicationJob\n  sidekiq_options retry: 3\n\n  def perform(user)\n    # ...\n  end\nend\n----\n\n==== Batches\n\nAlways use retries for jobs that are executed in batches, otherwise, the batch will never succeed.\n\n[#use-retries]\n=== Use Retries\n\nUse the retry mechanism.\nDo not let jobs end up in Dead Jobs.\nLet Sidekiq retry the jobs, and don't spend time re-running the jobs manually.\n\n[#mind-transactions]\n=== Mind Transactions\n\nBackground processing of a scheduled job may happen sooner than you expect.\nMake sure to https://github.com/mperham/sidekiq/wiki/Problems-and-Troubleshooting#cannot-find-modelname-with-id12345[only schedule jobs when the transaction has been committed].\n\n[source,ruby]\n----\n# bad - job may perform earlier than the transaction is committed\nUser.transaction do\n  users_params.each do |user_params|\n    user = User.create!(user_params)\n    NotifyUserJob.perform_later(user)\n  end\nend\n\n# good\nusers = User.transaction do\n          users_params.map do |user_params|\n            User.create!(user_params)\n          end\n        end\nusers.each { |user| NotifyUserJob.perform_later(user) }\n----\n\n[#local-performance-testing]\n=== Local Performance Testing\n\nDue to Rails auto-reloading, Sidekiq jobs are executed one-by-one, with no parallelism.\nThat may be confusing.\n\nRun Sidekiq in an environment that has `eager_load` set to `true`, or with the following flags to circumvent this behavior:\n\n[source,sh]\n----\nEAGER_LOAD=true ALLOW_CONCURRENCY=true bundle exec sidekiq\n----\n\n[#critical-jobs]\n=== Critical Jobs\n\nBackground job processing may be down for a prolonged period (minutes), e.g. during a failed deployment or a burst of other jobs.\n\nConsider running time-critical and mission-critical jobs in-process.\n\n[#business-logic-in-jobs]\n=== Business Logic in Jobs\n\nDo not put business logic to jobs; extract it.\n\n[source, ruby]\n----\n# bad\nclass SendUserAgreementJob \u003c ApplicationJob\n  # Convenient method to check if preconditions are satisfied to avoid\n  # scheduling unnecessary jobs.\n  def self.perform_later_if_applies(user)\n    job = new(user)\n    return unless job.satisfy_preconditions?\n\n    job.enqueue\n  end\n\n  def perform(user)\n    @user = user\n    return unless satisfy_preconditions?\n\n    agreement = agreement_for(user: user)\n    AgreementMailer.deliver_now(agreement)\n  end\n\n  def satisfy_preconditions?\n    legal_agreement_signed? \u0026\u0026\n      !user.removed? \u0026\u0026\n      !user.referral? \u0026\u0026\n      !(user.active? || user.pending?) \u0026\u0026\n      !user.has_flag?(:on_hold)\n  end\n\n  private\n\n  attr_reader :user\n\n  # business logic\nend\n\n# good - business logic is not coupled to the job\nclass SendUserAgreementJob \u003c ApplicationJob\n  def perform(user)\n    agreement = agreement_for(user: user)\n    AgreementMailer.deliver_now(agreement)\n  end\nend\n\nSendUserAgreementJob.perform_later(user) if satisfy_preconditions?\n----\n\n[#scheduling-a-job-from-a-job]\n=== Scheduling a Job from a Job\n\nWeigh the pros and cons in each case, whether to schedule jobs from jobs or to execute them in-process.\nFactors to consider:\nIs it a retriable job?\nCan inner jobs fail?\nAre they idempotent?\nIs there anything in the host job that may fail?\n\n[source,ruby]\n----\n# good - error kernel pattern\n# bad - additional jobs are spawned\nclass SomeJob \u003c ApplicationJob\n  def perform\n    SomeMailer.some_notification.deliver_later\n    OtherJob.perform_later\n  end\nend\n\n# good - no additional jobs\n# bad - if `OtherJob` fails, `SomeMailer` will be re-executed on retry as well\nclass SomeJob \u003c ApplicationJob\n  def perform\n    SomeMailer.some_notification.deliver_now\n    OtherJob.perform_now\n  end\nend\n----\n\n==== Numerous Jobs\n\nWhen a lot of jobs should be performed, it's acceptable to schedule them.\n\nConsider using batches for improved traceability.\n\nAlso, specify the same queue for the host job and sub-jobs.\n\n[source,ruby]\n----\n# acceptable\ndef perform\n  batch = Sidekiq::Batch.new\n  batch.description = 'Send weekly reminders'\n  batch.jobs do\n    User.find_each do |user|\n      WeeklyReminderJob.perform_later(user)\n    end\n  end\nend\n----\n\n[#job-renaming]\n=== Job Renaming\n\nCarefully rename job classes to avoid situations with jobs are scheduled, but there's no class to process it.\n\nNOTE: This also relates to mailers used with `deliver_later`.\n\n[source,ruby]\n----\n# good - keep the old class\n# TODO: Delete this alias in a few weeks when old jobs are safely gone\nOldJob = NewJob\n----\n\n[#sleep]\n=== `sleep`\n\nDo not use `Kernel.sleep` in jobs.\n`sleep` blocks the worker thread, and it's not able to process other jobs.\nRe-schedule the job for a later time, or use limiters with a custom exception.\n\n[source,ruby]\n----\n# bad\nclass SomeJob \u003c ApplicationJob\n  def perform(user)\n    attempts_number = 3\n    ThirdParty::Api::User.renew(user.external_id)\n  rescue ThirdParty::Api::Errors::TooManyRequestsError =\u003e error\n    sleep(error.retry_after)\n    attempts_number -= 1\n    retry unless attempts_number.zero?\n    raise\n  end\nend\n\n# good - retry job in a while, a limited number of times\nclass SomeJob \u003c ApplicationJob\n  sidekiq_options retry: 3\n  sidekiq_retry_in do |count, exception|\n    case exception\n    when ThirdParty::Api::Errors::TooManyRequestsError\n      count + 1 # i.e. 1s, 2s, 3s\n    end\n  end\n\n  def perform(user)\n    ThirdParty::Api::User.renew(user.external_id)\n  end\nend\n\n# good - fine-grained control of API usage in jobs\nclass SomeJob \u003c ApplicationJob\n  def perform(user)\n    LIMITER.within_limit do\n      ThirdParty::Api::User.renew(user.external_id)\n    end\n  end\nend\n\n# config/initializers/sidekiq.rb\nSidekiq::Limiter.configure do |config|\n  config.errors \u003c\u003c ThirdParty::Api::Errors::TooManyRequestsError\nend\n----\n\n[#infrastructure]\n== Infrastructure\n\n[#one-process-per-core]\n=== One Process per Core\n\nOn multi-core machines, run as many Sidekiq processes as needed to fully utilize cores.\nSidekiq process only uses one CPU core.\nA rule of thumb is to run as many processes as there are cores available.\n\n[#redis-memory-constraints]\n=== Redis Memory Constraints\n\nRedis's database size is limited by server memory.\nSome prefer to explicitly set `maxmemory`, and in combination with a `noeviction` policy, this may result in errors on job scheduling.\n\n==== Dead Jobs\n\nDo not keep jobs in Dead Jobs.\nWith extended backtrace enabled for Dead Jobs, a single dead job can occupy as much as 20KB in the database.\n\nRe-run the jobs once the root cause is fixed, or delete them.\n\n==== Excessive Arguments\n\nDo not pass an excessive number of arguments to a job.\n\n[source,ruby]\n----\n# bad\nSomeJob.perform_later(user_name, user_status, user_url, user_info: huge_json)\n\n# good\nSomeJob.perform_later(user, user_url)\n----\n\n==== Hordes\n\nDo not schedule hundreds of thousands jobs at once.\nA single job with no parameters takes 0.5KB.\nMeasure the exact footprint for each job with its arguments.\n\n[#monitoring]\n=== Monitoring\n\nMonitor the server and store historical metrics.\nProperly configured metrics will provide answers to improve the throughput of job processing.\n\n[#commercial-features]\n== Commercial Features\n\nAt some scale, https://github.com/mperham/sidekiq/wiki/Build-vs-Buy[it pays out to use commercial features].\n\nSome commercial features are available as third-party add-ons.\nHowever, their reliability is in most cases questionable.\n\n[#use-batches]\n=== Use Batches\n\nGroup jobs related to one task using https://github.com/mperham/sidekiq/wiki/Batches[Sidekiq Batches].\nBatch's `jobs` method is atomic, i.e., all the jobs are scheduled together, in an all-or-nothing fashion.\n\n[source,ruby]\n----\n# bad\nclass BackfillMissingDataJob \u003c ApplicationJob\n  def self.run_batch\n    Model.where(attribute: nil).find_each do |model|\n      perform_later(model)\n    end\n  end\n\n  def perform(model)\n    # do the job\n  end\nend\n\n# good\nclass BackfillMissingDataJob \u003c ApplicationJob\n  def self.run_batch\n    batch = Sidekiq::Batch.new\n    batch.description = 'Backfill missing data'\n    batch.on(:success, BackfillComplete, to: SysAdmin.email)\n    batch.jobs do\n      Model.where(attribute: nil).find_each do |model|\n        perform_later(model)\n      end\n    end\n  end\n\n  def perform(model)\n    # do the job\n  end\nend\n----\n\n[#self-scheduling-jobs]\n=== Self-scheduling Jobs\n\nAvoid using self-scheduling jobs for long-running jobs.\nPrefer using Sidekiq Batches to split the workload.\n\n[source,ruby]\n----\n# bad\nclass BackfillMissingDataJob \u003c ApplicationJob\n  SIZE = 20\n  def perform(offset = 0)\n    models = Model.where(attribute: nil)\n      .order(:id).offset(offset).limit(SIZE)\n    return if models.empty?\n\n    models.each do |model|\n      model.update!(attribute: for(model))\n    end\n    self.class.perform_later(offset + SIZE)\n  end\nend\n\n# good\nclass BackfillMissingDataJob \u003c ApplicationJob\n  def self.run_batch\n    Sidekiq::Batch.new.jobs do\n      Model.where(attribute: nil)\n        .find_in_batches(20) do |models|\n        BackfillMissingDataJob.perform_later(models)\n      end\n    end\n  end\n\n  def perform(models)\n    models.each do |model|\n      model.update!(attribute: for(model))\n    end\n  end\nend\n----\n\n[#api-rate-limited-operations]\n=== API Rate-limited Operations\n\nMost third-party APIs have usage limits and will fail if there are too many calls in a period.\nUse rate limiting in jobs that make such external calls.\n\nNever rely on the number of jobs to be executed.\nEven if you schedule jobs to be executed at a specific moment, they might be executed all at once, due to, e.g., a traffic jam in job processing.\nUse https://github.com/mperham/sidekiq/wiki/Ent-Rate-Limiting[Enterprise Rate Limiting].\nUse the strategy (Concurrent, Bucket, Window) that is most suitable to the specific API rate limiting.\n\n[source,ruby]\n----\n# bad\nclass UpdateExternalDataJob \u003c ApplicationJob\n  def perform(user)\n    new_attribute = ThirdParty::Api.get_attribute(user.external_id)\n    user.update!(attribute: new_attribute)\n  end\nend\n\nUser.where.not(external_id: nil)\n  .find_in_batches.with_index do |group_number, users|\n  users.each do |user|\n    UpdateExternalDataJob\n      .set(wait: group_number.minutes)\n      .perform_later(users)\n    end\nend\n\n# good\nclass UpdateExternalDataJob \u003c ApplicationJob\n  LIMITER = Sidekiq::Limiter.window('third-party-attribute-update', 20, :minute, wait_timeout: 0)\n\n  def perform(user)\n    LIMITER.within_limit do\n      new_attribute = ThirdParty::Api.get_attribute(user.external_id)\n      user.update!(attribute: new_attribute)\n    end\n  end\nend\n\n# Application code\nUser.where.not(external_id: nil).find_each do |user|\n  UpdateExternalDataJob.perform_later(user)\nend\n\n# config/initializers/sidekiq.rb\nSidekiq::Limiter.configure do |config|\n  config.errors \u003c\u003c ThirdParty::Api::Errors::TooManyRequestsError\nend\n----\n\n[#default-limiter-backoff]\n=== Default Limiter Backoff\n\nDo not rely on Sidekiq's limiter backoff default.\nIt will reschedule the job in five minutes in the future.\n\n[source,ruby]\n----\nDEFAULT_BACKOFF = -\u003e(limiter, job) do\n  (300 * job['overrated']) + rand(300) + 1\nend\n----\n\nIt doesn't fit the cases when limits are released quickly or are kept for hours.\nConfigure it on a limiter basis.\n\n[source,ruby]\n----\nSidekiq::Limiter.configure do |config|\n  config.backoff = -\u003e(limiter, job) do\n    case limiter.name\n    when 'daily-third-party-api-limit'\n      12.hours\n    else\n      (300 * job['overrated']) + rand(300) + 1 # fallback to default\n    end\n  end\nend\n----\n\nKeep in mind how limiter comparison works.\nCompare limiters by the name, not by the object.\n\n[source,ruby]\n----\n Sidekiq::Limiter.bucket('custom-limiter', 1, :day) == Sidekiq::Limiter.bucket('custom-limiter', 1, :day) # =\u003e false\n----\n\n[#reuse-limiters]\n=== Reuse Limiters\n\nCreate https://github.com/mperham/sidekiq/wiki/Ent-Rate-Limiting[limiters] once during startup and reuse them.\nLimiters are thread-safe and designed to be shared.\n\nEach limiter occupies 114 bytes in Redis, and the default TTL is 3 months.\n1 million jobs a month using non-shared limiters will be constantly consuming 300MB in Redis.\n\n[source,ruby]\n----\n# bad - limiter is re-created on each job call\nclass SomeJob \u003c ApplicationJob\n  def perform(...)\n    limiter = Sidekiq::Limiter.concurrent('erp', 50, wait_timeout: 0, lock_timeout: 30)\n    limiter.within_limit do\n      # call ERP\n    end\n  end\nend\n\n# good\nclass SomeJob \u003c ApplicationJob\n  ERP_LIMIT = Sidekiq::Limiter.concurrent('erp', 50, wait_timeout: 0, lock_timeout: 30)\n\n  def perform(...)\n    ERP_LIMIT.within_limit do\n      # call ERP\n    end\n  end\nend\n\n# acceptable - an exception is when the limiter is specific to something, and that is used as a distinction key in limiter name.\nclass SomeJob \u003c ApplicationJob\n  def perform(user)\n    # Rate limiting is per user account\n    user_throttle = Sidekiq::Limiter.bucket(\"stripe-#{user.id}\", 30, :second, wait_timeout: 0)\n    user_throttle.within_limit do\n      # call stripe with user's account creds\n    end\n  end\nend\n----\n\n[#limiter-options]\n=== Limiter Options\n\nThe usage of incorrect limiter options may break its behavior.\n\n==== `wait_timeout`\n\nSet `wait_timeout` to zero or some reasonably low value.\nDoing otherwise will result in idle workers, while there might be jobs waiting in the queue.\n\nKeep in mind the backoff configuration, and carefully pick the timing when the job is retried.\n\n==== `lock_timeout` for Concurrent Limiter\n\nSet `lock_timeout` to a longer than the job executes.\nOtherwise, the lock will be released too early and more concurrent jobs will be executed than expected.\n\n[#global-limiting-middleware]\n=== Global Limiting Middleware\n\nThe `Sidekiq::Limiter::OverLimit` exception might be rescued by jobs to discard themselves from locally defined limiters.\nTo avoid interference between global throttle limiter middleware and local job limiters, wrap `Sidekiq::Limiter::OverLimit` exception in middleware.\n\n[source,ruby]\n----\n# Middleware\nclass SaturationLimiter\n  SaturationOverLimit = Class.new(StandardError)\n\n  def self.wrapper(job, block)\n    LIMITER.within_limit { block.call }\n  rescue Sidekiq::Limiter::OverLimit =\u003e e\n    limiter_name = e.limiter.name\n    # Re-raise if an over the limit exception is coming from a limiter\n    # defined on the job level.\n    raise unless limiter_name == LIMITER.name\n\n    # Use a custom exception that Sidekiq::Limiter is using to re-schedule\n    # the job to a later time, but in a way that doesn't overlap with the\n    # limiters defined on the job level.\n    raise SaturationOverLimit, limiter_name\n  end\nend\n\n# config/initializers/active_job.rb\nActiveJob::Base.around_perform(\u0026SidekiqLimiter.method(:wrapper))\n----\n\n[#ignore-overlimit]\n=== Ignore `OverLimit` Exceptions on Third-party Services\n\n`Sidekiq::Limiter::OverLimit` is an internal mechanism, and it doesn't make sense to report when it triggers.\n\n[source,ruby]\n----\n# config/initializers/rollbar.rb\nRollbar.configure do |config|\n  config.exception_level_filters.merge!('Sidekiq::Limiter::OverLimit' =\u003e 'ignore')\nend\n----\n\n[source,yaml]\n----\n# config/newrelic.yml\nproduction:\n  error_collector:\n    enabled: true\n    ignore_errors: \"Sidekiq::Limiter::OverLimit\"\n----\n\n[#rolling-restarts]\n=== Rolling Restarts\n\nUse https://github.com/mperham/sidekiq/wiki/Ent-Rolling-Restarts[Enterprise Rolling Restarts].\nWith Rolling Restarts, deployments do not suffer from downtime.\nAlso, it prevents non-atomic and non-idempotent jobs from being interrupted and executed more than once on deployments.\n\nWARNING: For Capistrano-style deployments make sure to use https://github.com/stripe/einhorn#re-exec[`--reexec-as`] and https://github.com/stripe/einhorn#options[`--drop-env-var BUNDLE_GEMFILE`] einhorn options to avoid stalled code and dependencies.\n\n[#testing]\n== Testing\n\n[#perform]\n=== `perform`\n\nDon't use `job.perform` or `job_class.new.perform`, it bypasses the Active Job serialization/deserialization stage.\nUse `job_class.perform_now`.\nWith the implicitly subject, and recommends against using `.perform` (that as you correctly mention is exclusively available on a job instance, not class):\n\n[source,ruby]\n----\n# bad - `perform` method is called directly on an implicitly defined subject\nRSpec.describe SomeJob do\n  # implicitly defined `subject` is `SomeJob.new`\n  it 'updates user status' do\n    expect { subject.perform(user) }.to change { user.status }.to(:updated) }\n  end\nend\n\n# bad - `perform` method is called directly on a job instance\nRSpec.describe SomeJob do\n  it 'updates user status' do\n    expect { SomeJob.new.perform(user) }.to change { user.status }.to(:updated) }\n  end\nend\n\n# good\nRSpec.describe SomeJob do\n  it 'updates user status' do\n    expect { SomeJob.perform_now(user) }.to change { user.status }.to(:updated) }\n  end\nend\n----\n\n[#perform_later]\n=== `perform_later`\n\nPrefer `perform_now` to `perform_later` when testing jobs.\nIt doesn't involve Redis.\n\n[source,ruby]\n----\n# bad - unnecessary roundtrip to Redis\nRSpec.describe SomeJob do\n  it 'updates user status' do\n    expect do\n      SomeJob.perform_later(user)\n      perform_scheduled_jobs\n    end.to change { user.status }.to(:updated) }\n  end\nend\n\n# good\nRSpec.describe SomeJob do\n  it 'updates user status' do\n    expect { SomeJob.perform_now(user) }.to change { user.status }.to(:updated) }\n  end\nend\n----\n\n== History\n\nThis guide came to life as an internal company list of the best practices of working with ActiveJob and Sidekiq.\nIt is compiled from remarks collected from numerous code reviews, and during the migration from another background job processing tool to Sidekiq.\nInitially created by https://github.com/pirj[Phil Pirozhkov]) with the help of colleagues, and sponsored by https://www.toptal.com[Toptal].\n\n== Contributing\n\nThe guide is a work in progress.\nImproving such guidelines is a great (and simple way) to help the Ruby community!\n\nNothing written in this guide is set in stone.\nWe desire to work together with everyone interested in gathering the best practices of working with background jobs.\nThe goal is to create a resource that will be beneficial to the entire Ruby community.\n\nFeel free to open tickets or send pull requests with improvements.\nThanks in advance for your help!\n\n=== How to Contribute\n\nIt's easy, just follow the contribution guidelines below:\n\n* https://help.github.com/articles/fork-a-repo[Fork] on GitHub\n* Make your feature addition or bug fix in a feature branch.\n* Include a http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[good description] of your changes\n* Push your feature branch to GitHub\n* Send a https://help.github.com/articles/using-pull-requests[Pull Request]\n\n== License\n\nimage:https://i.creativecommons.org/l/by/3.0/88x31.png[Creative Commons License] This work is licensed under a http://creativecommons.org/licenses/by/3.0/deed.en_US[Creative Commons Attribution 3.0 Unported License]\n\n== Spread the Word\n\nA community-driven style guide is of little use to a community that doesn't know about its existence.\nTweet about the guide and share it with your friends and colleagues.\nEvery comment, suggestion, or opinion we get makes the guide just a little bit better.\nAnd we want to have the best possible guide, don't we?\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftoptal%2Factive-job-style-guide","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftoptal%2Factive-job-style-guide","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftoptal%2Factive-job-style-guide/lists"}