{"id":17598970,"url":"https://github.com/luizkowalski/concurrent_rails","last_synced_at":"2025-11-11T18:48:12.671Z","repository":{"id":44875678,"uuid":"358954050","full_name":"luizkowalski/concurrent_rails","owner":"luizkowalski","description":"🕹 Small library to make concurrent-ruby and Rails play nice together","archived":false,"fork":false,"pushed_at":"2025-08-09T15:11:48.000Z","size":122,"stargazers_count":40,"open_issues_count":0,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-10-07T14:54:22.198Z","etag":null,"topics":["concurrency","concurrent-rails","concurrent-ruby","ruby","ruby-on-rails","thread","thread-pool"],"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/luizkowalski.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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":"2021-04-17T18:30:56.000Z","updated_at":"2025-08-24T13:28:02.000Z","dependencies_parsed_at":"2024-04-29T22:25:59.682Z","dependency_job_id":"ef434911-0e60-4d6d-81a6-a650ef9cd43a","html_url":"https://github.com/luizkowalski/concurrent_rails","commit_stats":{"total_commits":114,"total_committers":5,"mean_commits":22.8,"dds":0.0964912280701754,"last_synced_commit":"e1a746d104846b167b080adb4aaa71386204f837"},"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"purl":"pkg:github/luizkowalski/concurrent_rails","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luizkowalski%2Fconcurrent_rails","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luizkowalski%2Fconcurrent_rails/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luizkowalski%2Fconcurrent_rails/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luizkowalski%2Fconcurrent_rails/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/luizkowalski","download_url":"https://codeload.github.com/luizkowalski/concurrent_rails/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luizkowalski%2Fconcurrent_rails/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":283910074,"owners_count":26915128,"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-11-11T02:00:06.610Z","response_time":65,"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":["concurrency","concurrent-rails","concurrent-ruby","ruby","ruby-on-rails","thread","thread-pool"],"created_at":"2024-10-22T10:08:32.692Z","updated_at":"2025-11-11T18:48:12.665Z","avatar_url":"https://github.com/luizkowalski.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ConcurrentRails\n\n![status](https://github.com/luizkowalski/concurrent_rails/actions/workflows/ruby.yml/badge.svg?branch=main)\n\nMultithread is hard. [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) did an amazing job implementing the concepts of multithread in the Ruby world. The problem is that Rails doesn't play nice with it. Rails has a complex way of managing threads called Executor and concurrent-ruby (most specifically, [Future](https://github.com/ruby-concurrency/concurrent-ruby/blob/master/docs-source/future.md)) does not work seamlessly with it.\n\nThe goal of this gem is to provide a simple library that allows the developer to work with Futures without having to care about Rails's Executor and the whole pack of problems that come with it: autoload, thread pools, active record connections, etc.\n\n## Usage\n\nThis library provides three classes that will help you run tasks in parallel: `ConcurrentRails::Promises`,  `ConcurrentRails::Future` ([in the process of being deprecated by concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby#deprecated)) and `ConcurrentRails::Multi`\n\n### Promises\n\n`Promises` is the recommended way from `concurrent-ruby` to create `Future`s as `Concurrent::Future` will be deprecated at some point. All you have to do is call `#future` and pass a block to be executed asynchronously:\n\n```ruby\nirb(main):001:0\u003e future = ConcurrentRails::Promises.future(5) { |v| sleep(v); 42 }\n=\u003e #\u003cConcurrentRails::Promises:0x00007fed68db66b0 @future_instance=#\u003cConcurrent::Promises::Future\n\nirb(main):002:0\u003e future.state\n=\u003e :pending\n\n# After the process slept for 5 seconds\nirb(main):003:0\u003e future.state\n=\u003e :fulfilled\n\nirb(main):004:0\u003e future.value\n=\u003e 42\n```\n\nThe benefit of `Promises` over a pure `Future` class is that you can chain futures without blocking the main thread.\n\n```ruby\nirb(main):001:0\u003e future = ConcurrentRails::Promises.future { 42 }.then { |v| v * 2 }\n=\u003e #\u003cConcurrentRails::Promises:0x00007fe92eba3460 @future_instance=#...\nirb(main):002:0\u003e future.value\n=\u003e 84\n```\n\n### Delayed futures\n\nA delayed future is a Future that is enqueued but not run until `#touch` or any other method that requires a resolution is called.\n\n```ruby\nirb(main):002:0\u003e delay = ConcurrentRails::Promises.delay { 42 }\n=\u003e #\u003cConcurrentRails::Promises:0x00007f8b55333d48 @executor=:io, @instan...\n\nirb(main):003:0\u003e delay.state\n=\u003e :pending\n\nirb(main):004:0\u003e delay.touch\n=\u003e #\u003cConcurrent::Promises::Future:0x00007f8b553325b0 pending\u003e\n\nirb(main):005:0\u003e delay.state\n=\u003e :fulfilled\n\nirb(main):006:0\u003e delay.value\n=\u003e 42\n```\n\nThree methods will trigger a resolution: `#touch`, `#value` and `#wait`: `#touch` will simply trigger the execution but won't block the main thread, while `#wait` and `#value` will block the main thread until a resolution is given.\n\n### Callbacks\n\nDelayed and regular futures can set a callback to be executed after the resolution of the future. There are three different callbacks:\n\n* `on_resolution`: runs after the Future is resolved and yields three parameters to the callback in the following order: `true/false` for future's fulfillment, `value` as the result of the future execution, and `reason`, that will be `nil` if the future fulfilled or the error that the future triggered.\n\n* `on_fulfillment`: runs after the Future is fulfilled and yields `value` to the callback\n\n* `on_rejection`: runs after the future is rejected and yields the `error` to the callback\n\n```ruby\ndelay = ConcurrentRails::Promises.delay { complex_find_user_query }.\n        on_fulfillment { |user| user.update!(name: 'John Doe') }.\n        on_rejection { |reason| log_error(reason) }\n\ndelay.touch\n```\n\nAll of these callbacks have a bang version (e.g. `on_fulfillment!`). The bang version will execute the callback on the same thread pool that was initially set up and the version without bang will run asynchronously on a different executor.\n\n## Testing\n\nIf you are using RSpec, you will notice that it might not play well with threads. ActiveRecord opens a database connection for every thread and since RSpec tests are wrapped in a transaction, by the time your promise tries to access something on the database, for example, a user, gems like Database Cleaner probably already triggered and deleted the user, resulting in `ActiveRecord::RecordNotFound` errors. You have a couple of solutions like disabling transactional fixtures if you are using it or update the Database Cleaner strategy (that will result in much slower tests).\nSince none of these solutions were satisfactory to me, I created `ConcurrentRails::Testing` with two strategies: `immediate` and `fake`. When you wrap a Promise's `future` with `immediate`, the executor gets replaced from `:io` to `:immediate`. It still returns a promise anyway. This is not the case with `fake` strategy: it executes the task outside the `ConcurrentRails` engine and returns whatever `.value` would return:\n\n`immediate` strategy:\n\n```ruby\nirb(main):001:1* result = ConcurrentRails::Testing.immediate do\nirb(main):002:1*       ConcurrentRails::Promises.future { 42 }\nirb(main):003:0\u003e end\n=\u003e\n#\u003cConcurrentRails::Promises:0x000000013e5fc870\n...\nirb(main):004:0\u003e result.class\n=\u003e ConcurrentRails::Promises # \u003c-- Still a `ConcurrentRails::Promises` class\nirb(main):005:0\u003e result.executor\n=\u003e :immediate # \u003c-- default executor (:io) gets replaced\n```\n\n`fake` strategy:\n\n```ruby\nirb(main):001:1* result = ConcurrentRails::Testing.fake do\nirb(main):002:1*       ConcurrentRails::Promises.future { 42 }\nirb(main):003:0\u003e end\n=\u003e 42 # \u003c-- yields the task but does not return a Promise\nirb(main):004:0\u003e result.class\n=\u003e Integer\n```\n\nYou can also set the strategy globally using `ConcurrentRails::Testing.fake!` or `ConcurrentRails::Testing.immediate!`\n\n## Further reading\n\nFor more information on how Futures works and how Rails handles multithread check these links:\n\n[Future documentation](https://github.com/ruby-concurrency/concurrent-ruby/blob/master/docs-source/future.md)\n\n[Threading and code execution on rails](https://guides.rubyonrails.org/threading_and_code_execution.html)\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'concurrent_rails', '~\u003e 0.5.1'\n```\n\nAnd then execute:\n\n```bash\nbundle\n```\n\nOr install it yourself as:\n\n```bash\ngem install concurrent_rails\n```\n\n## Contributing\n\nPull requests are always welcome\n\n\n## Updating Ruby or Rails versions using Appraisal\n\nThis gem uses Appraisal for multiple Ruby and Rails versions testing. To update the Ruby or Rails versions, you can run:\n\n```bash\nbundle exec appraisal install\n```\n\nand to run the tests for all versions, you can run:\n\n```bash\nbundle exec appraisal rake test\n```\n\nCheck the [usage](https://github.com/thoughtbot/appraisal?tab=readme-ov-file#usage) section of the Appraisal gem for more information on how to use it.\n\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%2Fluizkowalski%2Fconcurrent_rails","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fluizkowalski%2Fconcurrent_rails","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fluizkowalski%2Fconcurrent_rails/lists"}