{"id":15683488,"url":"https://github.com/square/async_task","last_synced_at":"2025-09-01T18:41:07.960Z","repository":{"id":62553669,"uuid":"113245960","full_name":"square/async_task","owner":"square","description":"Lightweight, asynchronous, and database-backed execution of singleton methods","archived":false,"fork":false,"pushed_at":"2023-03-19T08:42:19.000Z","size":31,"stargazers_count":6,"open_issues_count":1,"forks_count":2,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-08-16T13:51:59.082Z","etag":null,"topics":["async","asynchronous","asynctask","database-transactions","mixins","rails","rubygems","square"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/square.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGES.md","contributing":null,"funding":null,"license":"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":"2017-12-05T23:50:55.000Z","updated_at":"2023-08-14T20:15:42.000Z","dependencies_parsed_at":"2024-10-23T01:02:58.193Z","dependency_job_id":null,"html_url":"https://github.com/square/async_task","commit_stats":{"total_commits":5,"total_committers":1,"mean_commits":5.0,"dds":0.0,"last_synced_commit":"b0bd54e224c320e0f6b4738a9746a7c5eef1aa60"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/square/async_task","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/square%2Fasync_task","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/square%2Fasync_task/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/square%2Fasync_task/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/square%2Fasync_task/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/square","download_url":"https://codeload.github.com/square/async_task/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/square%2Fasync_task/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273174499,"owners_count":25058468,"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","status":"online","status_checked_at":"2025-09-01T02:00:09.058Z","response_time":120,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["async","asynchronous","asynctask","database-transactions","mixins","rails","rubygems","square"],"created_at":"2024-10-03T17:06:12.795Z","updated_at":"2025-09-01T18:41:07.930Z","avatar_url":"https://github.com/square.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# AsyncTask\n\n[![Gem Version](https://badge.fury.io/rb/async_task.svg)](http://badge.fury.io/rb/async_task)\n[![License](https://img.shields.io/badge/license-Apache-green.svg?style=flat)](https://github.com/square/async_task/blob/master/LICENSE)\n\nLightweight, asynchronous, and database-backed execution of singleton methods.\n\nThis gem provides generators and mixins to queue up tasks in database transactions to be performed\nlater. Doing so prevents (1) tasks from being run twice if performed within a transaction and (2)\ntasks from synchronously blocking.\n\n```ruby\ntransaction do\n  order = Order.create!(number: 7355608)\n\n  # RPC Call\n  OrderClient.fulfill(customer_token: customer_token, order_number: order.number)\n\n  raise\nend\n```\n\nDespite database transaction rolling back the creation of the `Order` record, the RPC call executes.\nThis problem becomes more difficult in nested transactions. To avoid doing something regrettable, we\ncreate an `AsyncTask::Attempt` record inside the database. These records are then performed at a\nlater time using a job:\n\n```ruby\ntransaction do\n  order = Order.create!(number: 1)\n\n  # To be performed by a job later\n  AsyncTask::Attempt.create!(\n    target: OrderClient,\n    method_name: :fulfill,\n    method_args: { customer_token: customer_token, order_number: order.number },\n  )\n\n  raise\nend\n```\n\nThe above pattern ensures we will not act when there is a rollback later in the transaction.\n\nThe gem provides the following:\n\n* Models\n  * Generators for the `AsyncTask::Attempt` migration, model, factory, and specs.\n  * Choice between using async tasks with encrypted or unencrypted method arguments.\n  * Tracking completion using `completed_at`.\n  * Fields for `target`, `method_name`, and `method_args`.\n  * `AsyncTask::BaseAttempt` mixin to provide model methods.\n  * A `num_attempts` field gives you flexibility to handle retries and other failure scenarios.\n  * `status` and `completed_at` are fields that track state.\n  * `idempotence_token` field for rolling your own presence, uniqueness, or idempotence checks.\n\n* Jobs\n  * Generators for `AsyncTask::AttemptJob`, `AsyncTask::AttemptBatchJob`, and their specs.\n  * `AsyncTask::BaseAttemptJob` and `AsyncTask::BaseAttemptBatchJob` mixins.\n\n## Getting Started\n\n1. Add the gem to your application's Gemfile and execute `bundle install` to install it:\n\n```ruby\ngem 'async_task'\n```\n\n2. Generate migrations, base models, jobs, and specs. Feel free to add any additional columns you\nneed to the generated migration file:\n\n`$ rails g async_task:install`\n\n3. Rename the model and migrations as you see fit. Make sure your model contains\n`include AsyncTask::BaseAttempt`. Use `self.table_name=` if necessary.\n\n```ruby\nclass AsyncTask::Attempt \u003c ApplicationRecord\n  include AsyncTask::BaseAttempt\nend\n```\n\n4. Implement the `handle_perform_error` in your `AsyncTask::Attempt` model. This methods is used by\n`AsyncTask::BaseAttempt` when exceptions are thrown performing the task.\n\n5. This gem provides no encryptor by default. Implement an encryptor (see below) if you need\nencrypted params.\n\n6. Create `AsyncTask::Attempt`s to be sent later by a job (generated) that includes a\n`AsyncTask::BaseAttemptJob`:\n\n```ruby\nclass AsyncTask::AttemptJob \u003c ActiveJob::Base\n  include AsyncTask::BaseAttemptJob\nend\n```\n\n```ruby\nAsyncTask::Attempt.create!(\n  target: OrderClient,\n  method_name: :fulfill,\n  method_args: { customer_token: customer_token, order_number: order.number },\n)\n```\n\n7. **Make sure to schedule the `AsyncTask::AttemptJob` to run frequently using something like [`Clockwork`](https://github.com/adamwiggins/clockwork).**\n\n## Cautionary Situations When Using This Gem\n\n### Idempotence\n\nThe `target`, `method_name`, and `method_args` should be idempotent because the\n`AsyncTask::AttemptBatchJob` could schedule multiple `AsyncTask::AttemptJob`s if the job queue is\nbacked up.\n\n### Nested Transactions\n\nTask execution occurs inside of a `with_lock` block, which executes the body inside of a database\ntransaction. Keep in mind that transactions inside the `#{target}.#{method_name}` will be nested.\nYou may have to consider implementing `transaction(require: new)` or creating transactions in\nseparate threads.\n\n## Cookbook\n\n### Custom Encryptors\n\nImplement the interface present in `AsyncTask::NullEncryptor` to provide your own encryptor.\n\n```ruby\nmodule AesEncryptor\n  extend self\n\n  def decrypt(content)\n    AesClient.decrypt(content)\n  end\n\n  def encrypt(content)\n    AesClient.encrypt(content)\n  end\nend\n```\n\n### Delayed Execution\n\nSetting the `scheduled_at` field allows delayed execution to be possible. A task that has an\n`scheduled_at` before `Time.current` will be executed by `AsyncTask::BaseAttemptBatchJob`.\n\n### Handling AsyncTask::BaseAttempt Errors\n\n```ruby\nclass AsyncTask::Attempt \u003c ActiveRecord::Base\n  include AsyncTask::BaseAttempt\n\n  def handle_perform_error(error)\n    Raven.capture_exception(error)\n  end\nend\n```\n\nLastly, the `num_attempts` field in `AsyncTask::Attempt` allows you to track the number of attempts\nthe task has undergone. Use this to implement retries and permanent failure thresholds for your\ntasks.\n\n### Proper Usage of `expire!` / `fail!`\n\n`expire!` should be used for tasks that should no longer be run.\n\n`fail!` should be used to mark permanent failure for a task.\n\n## Design Motivations\n\nWe're relying heavily on generators and mixins. Including the `AsyncTask::BaseAttempt` module allows\nus to generate a model that can inherit from both `ActiveRecord::Base` (Rails 4) and\n`ApplicationRecord` (Rails 5). The `BaseAttempt` module's methods can easily be overridden, giving\ncallers flexibility to handle errors, extend functionality, and inherit (STI). Lastly, the generated\nmigrations provide fields used by the `BaseAttempt` module, but the developer is free to add their\nown fields and extend the module's methods while calling `super`.\n\n## Development\n\n* Install dependencies with `bin/setup`.\n* Run tests/lints with `rake`\n* For an interactive prompt that will allow you to experiment, run `bin/console`.\n\n## Acknowledgments\n\n* [RickCSong](https://github.com/RickCSong)\n\n## License\n\n```\nCopyright 2017 Square, Inc.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsquare%2Fasync_task","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsquare%2Fasync_task","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsquare%2Fasync_task/lists"}