From 8b72394b9343986b90e0fa994c93f8bfdac2b835 Mon Sep 17 00:00:00 2001 From: harriet oughton Date: Wed, 6 Nov 2024 18:33:27 +0000 Subject: [PATCH 01/19] Rewrite and simply Section 1 --- guides/source/threading_and_code_execution.md | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md index 2337a2611b668..880dfd0b0a409 100644 --- a/guides/source/threading_and_code_execution.md +++ b/guides/source/threading_and_code_execution.md @@ -5,34 +5,34 @@ Threading and Code Execution in Rails After reading this guide, you will know: -* What code Rails will automatically execute concurrently -* How to integrate manual concurrency with Rails internals +* Where to find automatically concurrent code execution in Rails +* How to integrate manual concurrency within Rails * How to wrap all application code * How to affect application reloading -------------------------------------------------------------------------------- -Automatic Concurrency ---------------------- +Concurrency in Rails +-------------------- -Rails automatically allows various operations to be performed at the same time. +Rails automatically allows various operations to be performed at the same time (concurrently). In this section, we will explore some of the ways this happens. -When using a threaded web server, such as the default Puma, multiple HTTP -requests will be served simultaneously, with each request provided its own +When using a threaded web server, such as Rails' default server, Puma, multiple HTTP +requests will be served simultaneously. Rails provides each request with its own controller instance. -Threaded Active Job adapters, including the built-in Async, will likewise +Threaded Active Job adapters, including the built-in Async adapter, will likewise execute several jobs at the same time. Action Cable channels are managed this way too. These mechanisms all involve multiple threads, each managing work for a unique instance of some object (controller, job, channel), while sharing the global process space (such as classes and their configurations, and global variables). -As long as your code doesn't modify any of those shared things, it can mostly -ignore that other threads exist. +As long as your code doesn't modify any of those shared things, it is mostly irrelevant to it +that the other threads exist. -The rest of this guide describes the mechanisms Rails uses to make it "mostly -ignorable", and how extensions and applications with special needs can use them. +The rest of this guide describes the mechanisms Rails uses to make threads independent, +and how extensions and applications with special needs can use them. Executor -------- From 91aacc9c40f79f5135c397d03f7c599e78b48e0e Mon Sep 17 00:00:00 2001 From: harriet oughton Date: Wed, 6 Nov 2024 18:51:38 +0000 Subject: [PATCH 02/19] Simplify the Rails Executor section --- guides/source/threading_and_code_execution.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md index 880dfd0b0a409..6c012ceb497f9 100644 --- a/guides/source/threading_and_code_execution.md +++ b/guides/source/threading_and_code_execution.md @@ -34,15 +34,13 @@ that the other threads exist. The rest of this guide describes the mechanisms Rails uses to make threads independent, and how extensions and applications with special needs can use them. -Executor --------- +The Rails Executor +------------------ -The Rails Executor separates application code from framework code: any time the -framework invokes code you've written in your application, it will be wrapped by -the Executor. +The [Rails Executor](https://api.rubyonrails.org/classes/ActiveSupport/ExecutionWrapper.html) separates application code from framework code. Any time the framework invokes code you've written in your application, it will be wrapped by the executor. -The Executor consists of two callbacks: `to_run` and `to_complete`. The Run -callback is called before the application code, and the Complete callback is +The executor consists of two callbacks: `to_run` and `to_complete`. The `to_run` +callback is called before the application code, and the `to_complete` callback is called after. ### Default Callbacks From 2b3facd2ed0bae595a028430ed3432d33799afa0 Mon Sep 17 00:00:00 2001 From: harriet oughton Date: Thu, 7 Nov 2024 18:58:48 +0000 Subject: [PATCH 03/19] Add further rewrites to the Rails Executor section --- guides/source/threading_and_code_execution.md | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md index 6c012ceb497f9..5f82ddf42942e 100644 --- a/guides/source/threading_and_code_execution.md +++ b/guides/source/threading_and_code_execution.md @@ -28,11 +28,10 @@ way too. These mechanisms all involve multiple threads, each managing work for a unique instance of some object (controller, job, channel), while sharing the global process space (such as classes and their configurations, and global variables). -As long as your code doesn't modify any of those shared things, it is mostly irrelevant to it -that the other threads exist. +As long as your code doesn't modify any of those shared things, the other threads are mostly irrelevant to it. -The rest of this guide describes the mechanisms Rails uses to make threads independent, -and how extensions and applications with special needs can use them. +The rest of this guide goes into more detail about threading in Rails, +and how extensions and applications with particular requirements can use it. The Rails Executor ------------------ @@ -43,21 +42,15 @@ The executor consists of two callbacks: `to_run` and `to_complete`. The `to_run` callback is called before the application code, and the `to_complete` callback is called after. -### Default Callbacks +### Callbacks -In a default Rails application, the Executor callbacks are used to: +In a default Rails application, the Rails Executor callbacks are used to: * track which threads are in safe positions for autoloading and reloading * enable and disable the Active Record query cache * return acquired Active Record connections to the pool * constrain internal cache lifetimes -Prior to Rails 5.0, some of these were handled by separate Rack middleware -classes (such as `ActiveRecord::ConnectionAdapters::ConnectionManagement`), or -directly wrapping code with methods like -`ActiveRecord::Base.connection_pool.with_connection`. The Executor replaces -these with a single more abstract interface. - ### Wrapping Application Code If you're writing a library or component that will invoke application code, you @@ -74,7 +67,7 @@ may want to wrap using the [Reloader](#reloader) instead. Each thread should be wrapped before it runs application code, so if your application manually delegates work to other threads, such as via `Thread.new` -or Concurrent Ruby features that use thread pools, you should immediately wrap +or uses features from the [Concurrent Ruby](https://github.com/ruby-concurrency/concurrent-ruby) gem that use thread pools, you should immediately wrap the block: ```ruby @@ -86,10 +79,9 @@ end ``` NOTE: Concurrent Ruby uses a `ThreadPoolExecutor`, which it sometimes configures -with an `executor` option. Despite the name, it is unrelated. +with an `executor` option. Despite the name, it is unrelated to the Rails Executor. -The Executor is safely re-entrant; if it is already active on the current -thread, `wrap` is a no-op. +The Rails Executor is safely re-entrant; it can be called again if it is already running. In this case, the `wrap` method would have no effect. If it's impractical to wrap the application code in a block (for example, the Rack API makes this problematic), you can also use the `run!` / From 099379975116ead94b1ff6eae4aaa044cf3a2e3d Mon Sep 17 00:00:00 2001 From: harriet oughton Date: Mon, 11 Nov 2024 19:01:19 +0000 Subject: [PATCH 04/19] Rewrite and clarify Section 2 and 3 (Reloader) --- guides/source/threading_and_code_execution.md | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md index 5f82ddf42942e..018d7913ca14a 100644 --- a/guides/source/threading_and_code_execution.md +++ b/guides/source/threading_and_code_execution.md @@ -7,7 +7,7 @@ After reading this guide, you will know: * Where to find automatically concurrent code execution in Rails * How to integrate manual concurrency within Rails -* How to wrap all application code +* How to wrap all application code using the Rails Executor * How to affect application reloading -------------------------------------------------------------------------------- @@ -15,7 +15,7 @@ After reading this guide, you will know: Concurrency in Rails -------------------- -Rails automatically allows various operations to be performed at the same time (concurrently). In this section, we will explore some of the ways this happens. +Rails automatically allows various operations to be performed at the same time (concurrently) in order for an application to run more efficiently. In this section, we will explore some of the ways this happens. When using a threaded web server, such as Rails' default server, Puma, multiple HTTP requests will be served simultaneously. Rails provides each request with its own @@ -36,9 +36,9 @@ and how extensions and applications with particular requirements can use it. The Rails Executor ------------------ -The [Rails Executor](https://api.rubyonrails.org/classes/ActiveSupport/ExecutionWrapper.html) separates application code from framework code. Any time the framework invokes code you've written in your application, it will be wrapped by the executor. +The [Rails Executor](https://api.rubyonrails.org/classes/ActiveSupport/ExecutionWrapper.html) separates application code from framework code. Any time the Rails framework invokes code you've written in your application, it will be wrapped by the Executor. -The executor consists of two callbacks: `to_run` and `to_complete`. The `to_run` +The Executor consists of two callbacks: `to_run` and `to_complete`. The `to_run` callback is called before the application code, and the `to_complete` callback is called after. @@ -54,7 +54,7 @@ In a default Rails application, the Rails Executor callbacks are used to: ### Wrapping Application Code If you're writing a library or component that will invoke application code, you -should wrap it with a call to the executor: +should wrap it with a call to the Executor: ```ruby Rails.application.executor.wrap do @@ -66,7 +66,7 @@ TIP: If you repeatedly invoke application code from a long-running process, you may want to wrap using the [Reloader](#reloader) instead. Each thread should be wrapped before it runs application code, so if your -application manually delegates work to other threads, such as via `Thread.new` +application manually delegates work to other threads, such as via `Thread.new`, or uses features from the [Concurrent Ruby](https://github.com/ruby-concurrency/concurrent-ruby) gem that use thread pools, you should immediately wrap the block: @@ -78,11 +78,9 @@ Thread.new do end ``` -NOTE: Concurrent Ruby uses a `ThreadPoolExecutor`, which it sometimes configures +NOTE: The Concurrent Ruby gem uses a `ThreadPoolExecutor`, which it sometimes configures with an `executor` option. Despite the name, it is unrelated to the Rails Executor. -The Rails Executor is safely re-entrant; it can be called again if it is already running. In this case, the `wrap` method would have no effect. - If it's impractical to wrap the application code in a block (for example, the Rack API makes this problematic), you can also use the `run!` / `complete!` pair: @@ -96,7 +94,7 @@ ensure end ``` -### Concurrency +NOTE: The Rails Executor is safely re-entrant; it can be called again if it is already running. In this case, the `wrap` method would have no effect. The Executor will put the current thread into `running` mode in the [Reloading Interlock](#reloading-interlock). This operation will block temporarily if another @@ -105,10 +103,10 @@ thread is currently unloading/reloading the application. Reloader -------- -Like the Executor, the Reloader also wraps application code. If the Executor is +Like the Executor, the [Reloader](https://api.rubyonrails.org/classes/ActiveSupport/Reloader.html) also wraps application code. If the Executor is not already active on the current thread, the Reloader will invoke it for you, so you only need to call one. This also guarantees that everything the Reloader -does, including all its callback invocations, occurs wrapped inside the +does, including all its callback executions, occurs wrapped inside the Executor. ```ruby @@ -117,7 +115,7 @@ Rails.application.reloader.wrap do end ``` -The Reloader is only suitable where a long-running framework-level process +NOTE: The Reloader is only suitable where a long-running framework-level process repeatedly calls into application code, such as for a web server or job queue. Rails automatically wraps web requests and Active Job workers, so you'll rarely need to invoke the Reloader for yourself. Always consider whether the Executor @@ -139,9 +137,9 @@ necessary, the Reloader will invoke the wrapped block with no other callbacks. ### Class Unload -The most significant part of the reloading process is the Class Unload, where +The most significant part of the reloading process is the 'class unload', where all autoloaded classes are removed, ready to be loaded again. This will occur -immediately before either the Run or Complete callback, depending on the +immediately before either the `to_run` or `to_complete` callback, depending on the `reload_classes_only_on_change` setting. Often, additional reloading actions need to be performed either just before or From 6bafe45df46b5410d0d37168a35d5513c268a472 Mon Sep 17 00:00:00 2001 From: harriet oughton Date: Wed, 13 Nov 2024 19:14:42 +0000 Subject: [PATCH 05/19] Rewrite and clarify Section 4 and 5 --- guides/source/threading_and_code_execution.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md index 018d7913ca14a..35ad6cf64b743 100644 --- a/guides/source/threading_and_code_execution.md +++ b/guides/source/threading_and_code_execution.md @@ -25,6 +25,8 @@ Threaded Active Job adapters, including the built-in Async adapter, will likewis execute several jobs at the same time. Action Cable channels are managed this way too. +You can read more about these processes, and how to configure them, in the [Framework Behavior](#framework-behavior) section. + These mechanisms all involve multiple threads, each managing work for a unique instance of some object (controller, job, channel), while sharing the global process space (such as classes and their configurations, and global variables). @@ -140,11 +142,11 @@ necessary, the Reloader will invoke the wrapped block with no other callbacks. The most significant part of the reloading process is the 'class unload', where all autoloaded classes are removed, ready to be loaded again. This will occur immediately before either the `to_run` or `to_complete` callback, depending on the -`reload_classes_only_on_change` setting. +[`reload_classes_only_on_change`](configuring.html#config-reload-classes-only-on-change) setting. Often, additional reloading actions need to be performed either just before or -just after the Class Unload, so the Reloader also provides `before_class_unload` -and `after_class_unload` callbacks. +just after the Class Unload, so the Reloader also provides [`before_class_unload`](https://api.rubyonrails.org/classes/ActiveSupport/Reloader.html#method-c-before_class_unload) +and [`after_class_unload`](https://api.rubyonrails.org/classes/ActiveSupport/Reloader.html#method-c-after_class_unload) callbacks. ### Concurrency @@ -176,7 +178,7 @@ Action Cable uses the Executor instead: because a Cable connection is linked to a specific instance of a class, it's not possible to reload for every arriving WebSocket message. Only the message handler is wrapped, though; a long-running Cable connection does not prevent a reload that's triggered by a new incoming -request or job. Instead, Action Cable uses the Reloader's `before_class_unload` +request or job. Instead, Action Cable also uses the Reloader's `before_class_unload` callback to disconnect all its connections. When the client automatically reconnects, it will be speaking to the new version of the code. @@ -187,8 +189,8 @@ additional threads. ### Configuration -The Reloader only checks for file changes when `config.enable_reloading` is -`true` and so is `config.reload_classes_only_on_change`. These are the defaults in the +The Reloader only checks for file changes when [`config.enable_reloading`](configuring.html#config-enable-reloading) is +`true` and so is [`config.reload_classes_only_on_change`](configuring.html#config-reload-classes-only-on-change). These are the defaults in the `development` environment. When `config.enable_reloading` is `false` (in `production`, by default), the From cecb12a8445be921302b2f0f828b7252ec22cf45 Mon Sep 17 00:00:00 2001 From: harriet oughton Date: Thu, 14 Nov 2024 19:14:31 +0000 Subject: [PATCH 06/19] Add suggestions for missing information --- guides/source/threading_and_code_execution.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md index 35ad6cf64b743..9189140d5f0e2 100644 --- a/guides/source/threading_and_code_execution.md +++ b/guides/source/threading_and_code_execution.md @@ -15,7 +15,7 @@ After reading this guide, you will know: Concurrency in Rails -------------------- -Rails automatically allows various operations to be performed at the same time (concurrently) in order for an application to run more efficiently. In this section, we will explore some of the ways this happens. +Rails automatically allows various operations to be performed at the same time (concurrently) in order for an application to run more efficiently. In this section, we will explore some of the ways this happens behind the scenes. When using a threaded web server, such as Rails' default server, Puma, multiple HTTP requests will be served simultaneously. Rails provides each request with its own @@ -25,9 +25,7 @@ Threaded Active Job adapters, including the built-in Async adapter, will likewis execute several jobs at the same time. Action Cable channels are managed this way too. -You can read more about these processes, and how to configure them, in the [Framework Behavior](#framework-behavior) section. - -These mechanisms all involve multiple threads, each managing work for a unique +The above mechanisms all involve multiple threads, each managing work for a unique instance of some object (controller, job, channel), while sharing the global process space (such as classes and their configurations, and global variables). As long as your code doesn't modify any of those shared things, the other threads are mostly irrelevant to it. @@ -35,10 +33,12 @@ As long as your code doesn't modify any of those shared things, the other thread The rest of this guide goes into more detail about threading in Rails, and how extensions and applications with particular requirements can use it. +NOTE: You can read more about Rails' in-built concurrency, and how to configure it, in the [Framework Behavior](#framework-behavior) section. + The Rails Executor ------------------ -The [Rails Executor](https://api.rubyonrails.org/classes/ActiveSupport/ExecutionWrapper.html) separates application code from framework code. Any time the Rails framework invokes code you've written in your application, it will be wrapped by the Executor. +The Rails Executor inherits from the [`ActiveSupport::ExecutionWrapper`](https://api.rubyonrails.org/classes/ActiveSupport/ExecutionWrapper.html). The Executor separates application code from framework code by wrapping code that you've written. The Executor consists of two callbacks: `to_run` and `to_complete`. The `to_run` callback is called before the application code, and the `to_complete` callback is @@ -53,7 +53,7 @@ In a default Rails application, the Rails Executor callbacks are used to: * return acquired Active Record connections to the pool * constrain internal cache lifetimes -### Wrapping Application Code +### Wrapping Code Execution If you're writing a library or component that will invoke application code, you should wrap it with a call to the Executor: From d64368a5c9a7295a051ea180ab73ea2d9daaa225 Mon Sep 17 00:00:00 2001 From: harriet oughton Date: Fri, 15 Nov 2024 18:16:07 +0000 Subject: [PATCH 07/19] Add Information about Async Active Record and rework Section structure and introduction --- guides/source/threading_and_code_execution.md | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md index 9189140d5f0e2..82fd40cc1d429 100644 --- a/guides/source/threading_and_code_execution.md +++ b/guides/source/threading_and_code_execution.md @@ -12,39 +12,43 @@ After reading this guide, you will know: -------------------------------------------------------------------------------- -Concurrency in Rails --------------------- +In-Built Concurrency in Rails +----------------------------- Rails automatically allows various operations to be performed at the same time (concurrently) in order for an application to run more efficiently. In this section, we will explore some of the ways this happens behind the scenes. -When using a threaded web server, such as Rails' default server, Puma, multiple HTTP -requests will be served simultaneously. Rails provides each request with its own -controller instance. +When using a threaded web server (such as Rails' default server, Puma) multiple HTTP +requests will be served simultaneously as each request is given its own controller instance. Threaded Active Job adapters, including the built-in Async adapter, will likewise execute several jobs at the same time. Action Cable channels are managed this way too. -The above mechanisms all involve multiple threads, each managing work for a unique +Asynchronous Active Record queries are also performed in the background, allowing other processes to run on the main thread. + +The above mechanisms all involve multiple threads, often managing work for a unique instance of some object (controller, job, channel), while sharing the global process space (such as classes and their configurations, and global variables). -As long as your code doesn't modify any of those shared things, the other threads are mostly irrelevant to it. +As long as the code on each thread doesn't modify any of those shared things, the other threads are mostly irrelevant to it. -The rest of this guide goes into more detail about threading in Rails, -and how extensions and applications with particular requirements can use it. +Rails' in-built concurrency will cover the day-to-day needs of many application developers, and ensure applications remain generally performant. -NOTE: You can read more about Rails' in-built concurrency, and how to configure it, in the [Framework Behavior](#framework-behavior) section. +NOTE: You can read more about how to configure Rails' in-built concurrency in the [Framework Behavior](#framework-behavior) section. -The Rails Executor ------------------- +The next section of this guide details advanced ways of directly wrapping code within the Rails framework, and how extensions and applications with particular concurrency requirements, such as library maintainers, should do this. + +Wrapping Application Code +------------------------- -The Rails Executor inherits from the [`ActiveSupport::ExecutionWrapper`](https://api.rubyonrails.org/classes/ActiveSupport/ExecutionWrapper.html). The Executor separates application code from framework code by wrapping code that you've written. +### The Rails Executor + +The Rails Executor inherits from the [`ActiveSupport::ExecutionWrapper`](https://api.rubyonrails.org/classes/ActiveSupport/ExecutionWrapper.html). The Executor separates application code from framework code by wrapping code that you've written and is necessary when using threads. The Executor consists of two callbacks: `to_run` and `to_complete`. The `to_run` callback is called before the application code, and the `to_complete` callback is called after. -### Callbacks +#### Callbacks In a default Rails application, the Rails Executor callbacks are used to: @@ -53,7 +57,7 @@ In a default Rails application, the Rails Executor callbacks are used to: * return acquired Active Record connections to the pool * constrain internal cache lifetimes -### Wrapping Code Execution +#### Code Execution If you're writing a library or component that will invoke application code, you should wrap it with a call to the Executor: @@ -65,7 +69,7 @@ end ``` TIP: If you repeatedly invoke application code from a long-running process, you -may want to wrap using the [Reloader](#reloader) instead. +may want to wrap using the [Reloader](#the-reloader) instead. Each thread should be wrapped before it runs application code, so if your application manually delegates work to other threads, such as via `Thread.new`, @@ -102,8 +106,7 @@ The Executor will put the current thread into `running` mode in the [Reloading Interlock](#reloading-interlock). This operation will block temporarily if another thread is currently unloading/reloading the application. -Reloader --------- +### The Reloader Like the Executor, the [Reloader](https://api.rubyonrails.org/classes/ActiveSupport/Reloader.html) also wraps application code. If the Executor is not already active on the current thread, the Reloader will invoke it for you, @@ -123,7 +126,7 @@ Rails automatically wraps web requests and Active Job workers, so you'll rarely need to invoke the Reloader for yourself. Always consider whether the Executor is a better fit for your use case. -### Callbacks +#### Callbacks Before entering the wrapped block, the Reloader will check whether the running application needs to be reloaded -- for example, because a model's source file has @@ -137,7 +140,7 @@ invoked at the same points as those of the Executor, but only when the current execution has initiated an application reload. When no reload is deemed necessary, the Reloader will invoke the wrapped block with no other callbacks. -### Class Unload +#### Class Unload The most significant part of the reloading process is the 'class unload', where all autoloaded classes are removed, ready to be loaded again. This will occur @@ -148,7 +151,7 @@ Often, additional reloading actions need to be performed either just before or just after the Class Unload, so the Reloader also provides [`before_class_unload`](https://api.rubyonrails.org/classes/ActiveSupport/Reloader.html#method-c-before_class_unload) and [`after_class_unload`](https://api.rubyonrails.org/classes/ActiveSupport/Reloader.html#method-c-after_class_unload) callbacks. -### Concurrency +#### Concurrency Only long-running "top level" processes should invoke the Reloader, because if it determines a reload is needed, it will block until all other threads have @@ -162,7 +165,7 @@ thread is mid-execution. Child threads should use the Executor instead. Framework Behavior ------------------ -The Rails framework components use these tools to manage their own concurrency +The Rails framework components use the Executor and the Reloader to manage their own concurrency needs too. `ActionDispatch::Executor` and `ActionDispatch::Reloader` are Rack middlewares @@ -184,9 +187,11 @@ reconnects, it will be speaking to the new version of the code. The above are the entry points to the framework, so they are responsible for ensuring their respective threads are protected, and deciding whether a reload -is necessary. Other components only need to use the Executor when they spawn +is necessary. Most other components only need to use the Executor when they spawn additional threads. + + ### Configuration The Reloader only checks for file changes when [`config.enable_reloading`](configuring.html#config-enable-reloading) is From 876cae7becb8fe5886c1d54830accea2b0bd7caf Mon Sep 17 00:00:00 2001 From: harriet oughton Date: Sun, 8 Dec 2024 11:53:47 +0000 Subject: [PATCH 08/19] Remove 'work in progress' from threading guide --- guides/source/documents.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index ae5a1d3df120a..bf29eb4015040 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -292,7 +292,6 @@ name: Threading and Code Execution in Rails url: threading_and_code_execution.html description: This guide describes the considerations needed and tools available when working directly with concurrency in a Rails application. - work_in_progress: true - name: Contributing documents: From d376e7f85623ffc1dfaa578c5334c0073edd97aa Mon Sep 17 00:00:00 2001 From: harriet oughton Date: Sun, 8 Dec 2024 11:54:05 +0000 Subject: [PATCH 09/19] Add Examples of Wrapped Application Code section --- guides/source/threading_and_code_execution.md | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md index 82fd40cc1d429..a74616e74d3a8 100644 --- a/guides/source/threading_and_code_execution.md +++ b/guides/source/threading_and_code_execution.md @@ -106,6 +106,42 @@ The Executor will put the current thread into `running` mode in the [Reloading Interlock](#reloading-interlock). This operation will block temporarily if another thread is currently unloading/reloading the application. +#### Examples of Wrapped Application Code + +Any time your library or component needs to invoke code that will need to run in the application, this code should be wrapped to ensure thread safety and a consistent and clean runtime state. + +For example, you may be setting a `Current` user (using [`ActiveSupport::CurrentAttributes`](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html)). + +```ruby +def log_with_user_context(message) + Rails.application.executor.wrap do + Current.user = User.find_by(id: 1) + end +end +``` + +You may be triggering an ActiveRecord callback or lifecycle hook in an application: + +```ruby +def perform_task_with_record(record) + Rails.application.executor.wrap do + record.save! # Executes before_save, after_save, etc. + end +end +``` + +Or enqueuing or performing a background job within the application: + +```ruby +def enqueue_background_job(job_class, *args) + Rails.application.executor.wrap do + job_class.perform_later(*args) + end +end +``` + +These are just a few of many possible other use cases, including rendering views or templates, broadcasting via [`Action Cable`](action_cable_overview.html) or using [`Rails.cache`](caching_with_rails.html). + ### The Reloader Like the Executor, the [Reloader](https://api.rubyonrails.org/classes/ActiveSupport/Reloader.html) also wraps application code. If the Executor is @@ -190,8 +226,6 @@ ensuring their respective threads are protected, and deciding whether a reload is necessary. Most other components only need to use the Executor when they spawn additional threads. - - ### Configuration The Reloader only checks for file changes when [`config.enable_reloading`](configuring.html#config-enable-reloading) is From 9374683f464208848e84f2551e2acaf51c2243f8 Mon Sep 17 00:00:00 2001 From: harriet oughton Date: Sun, 8 Dec 2024 18:42:33 +0000 Subject: [PATCH 10/19] Add information on Isolated Execution State --- guides/source/threading_and_code_execution.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md index a74616e74d3a8..59c3905b14d46 100644 --- a/guides/source/threading_and_code_execution.md +++ b/guides/source/threading_and_code_execution.md @@ -228,12 +228,13 @@ additional threads. ### Configuration +#### Reloader and Executor Configuration + The Reloader only checks for file changes when [`config.enable_reloading`](configuring.html#config-enable-reloading) is `true` and so is [`config.reload_classes_only_on_change`](configuring.html#config-reload-classes-only-on-change). These are the defaults in the `development` environment. -When `config.enable_reloading` is `false` (in `production`, by default), the -Reloader is only a pass-through to the Executor. +When `config.enable_reloading` is `false` (in `production`, by default), the Reloader is only a pass-through to the Executor. The Executor always has important work to do, like database connection management. When `config.enable_reloading` is `false` and `config.eager_load` is @@ -241,8 +242,12 @@ management. When `config.enable_reloading` is `false` and `config.eager_load` is Reloading Interlock. With the default settings in the `development` environment, the Executor will use the Reloading Interlock to ensure code reloading is performed safely. -Reloading Interlock -------------------- +#### Isolated Execution State + +The `active_support.isolation_level` value in your `configuration.rb` file defines where Rails internal state should be stored while tasks are run. If you use a fiber-based server or job processor (e.g. [`falcon`](https://github.com/socketry/falcon)), you should set this value to `:fiber`, otherwise it is best to set it to `:thread`. + +Load Interlock +-------------- The Reloading Interlock ensures that code reloading can be performed safely in a multi-threaded runtime environment. From 49a55c6aaab1f4a57b5e77cbb5718ee39f28f0d0 Mon Sep 17 00:00:00 2001 From: harriet oughton Date: Sun, 8 Dec 2024 19:46:08 +0000 Subject: [PATCH 11/19] Add CurrentAttributes section --- guides/source/threading_and_code_execution.md | 56 +++++++++++++++++-- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md index 59c3905b14d46..e692ee9b23860 100644 --- a/guides/source/threading_and_code_execution.md +++ b/guides/source/threading_and_code_execution.md @@ -35,6 +35,58 @@ Rails' in-built concurrency will cover the day-to-day needs of many application NOTE: You can read more about how to configure Rails' in-built concurrency in the [Framework Behavior](#framework-behavior) section. +### `CurrentAttributes` and Threading + +The [`ActiveSupport::CurrentAttributes`](https://edgeapi.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html) class is a special class in Rails that helps you manage temporary data for each request in your app, and helps make sure this data is available to the whole system. It keeps this data separate for every request (even if there are multiple threads running) and makes sure the data is cleaned up automatically when the request is done. + +You can think of this class as a place to store data that you need to access anywhere in your app without having to pass it around in your code. + +To use the `Current` class to store data, first you need to create a file as below, with `attribute` values for the attributes and models whose values you would like to access throughout your application. You can also define a method (e.g the `user` method below) which, when called, will return set values: + +```ruby +# app/models/current.rb +class Current < ActiveSupport::CurrentAttributes + attribute :account, :user + + resets { Time.zone = nil } + + def user=(user) + super + self.account = user.account + Time.zone = user.time_zone + end +end +``` + +In the example above, you will now have access to `Current.user` elsewhere in your application. For example, in authenticating a user: + +```ruby +# app/controllers/concerns/authentication.rb +module Authentication + extend ActiveSupport::Concern + + included do + before_action :authenticate + end + + private + def authenticate + if authenticated_user = User.find_by(id: cookies.encrypted[:user_id]) + Current.user = authenticated_user + else + redirect_to new_session_url + end + end +end +``` +WARNING: It’s easy to put too many attributes in the `Current` class and tangle your model as a result. Current should only be used for a few, top-level globals, like account, user, and request details. + +### Isolated Execution State + +The `active_support.isolation_level` value in your `configuration.rb` file provides you the option to define where Rails internal state should be stored while tasks are run. If you use a fiber-based server or job processor (e.g. [`falcon`](https://github.com/socketry/falcon)), you should set this value to `:fiber`, otherwise it is best to set it to `:thread`. + +### Going Futher + The next section of this guide details advanced ways of directly wrapping code within the Rails framework, and how extensions and applications with particular concurrency requirements, such as library maintainers, should do this. Wrapping Application Code @@ -242,10 +294,6 @@ management. When `config.enable_reloading` is `false` and `config.eager_load` is Reloading Interlock. With the default settings in the `development` environment, the Executor will use the Reloading Interlock to ensure code reloading is performed safely. -#### Isolated Execution State - -The `active_support.isolation_level` value in your `configuration.rb` file defines where Rails internal state should be stored while tasks are run. If you use a fiber-based server or job processor (e.g. [`falcon`](https://github.com/socketry/falcon)), you should set this value to `:fiber`, otherwise it is best to set it to `:thread`. - Load Interlock -------------- From 4aafb326fdcc2381fd69f335e1c97652994e6998 Mon Sep 17 00:00:00 2001 From: harriet oughton Date: Sun, 8 Dec 2024 20:22:50 +0000 Subject: [PATCH 12/19] Apply changes from proof-read --- guides/source/threading_and_code_execution.md | 290 +++++++++++++----- 1 file changed, 215 insertions(+), 75 deletions(-) diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md index e692ee9b23860..5a8edb5dae9be 100644 --- a/guides/source/threading_and_code_execution.md +++ b/guides/source/threading_and_code_execution.md @@ -15,33 +15,50 @@ After reading this guide, you will know: In-Built Concurrency in Rails ----------------------------- -Rails automatically allows various operations to be performed at the same time (concurrently) in order for an application to run more efficiently. In this section, we will explore some of the ways this happens behind the scenes. +Rails automatically allows various operations to be performed at the same time +(concurrently) in order for an application to run more efficiently. In this +section, we will explore some of the ways this happens behind the scenes. -When using a threaded web server (such as Rails' default server, Puma) multiple HTTP -requests will be served simultaneously as each request is given its own controller instance. +When using a threaded web server (such as Rails' default server, Puma) multiple +HTTP requests will be served simultaneously as each request is given its own +controller instance. -Threaded Active Job adapters, including the built-in Async adapter, will likewise -execute several jobs at the same time. Action Cable channels are managed this -way too. +Threaded Active Job adapters, including the built-in Async adapter, will +likewise execute several jobs at the same time. Action Cable channels are +managed this way too. -Asynchronous Active Record queries are also performed in the background, allowing other processes to run on the main thread. +Asynchronous Active Record queries are also performed in the background, +allowing other processes to run on the main thread. -The above mechanisms all involve multiple threads, often managing work for a unique -instance of some object (controller, job, channel), while sharing the global -process space (such as classes and their configurations, and global variables). -As long as the code on each thread doesn't modify any of those shared things, the other threads are mostly irrelevant to it. +The above mechanisms all involve multiple threads, often managing work for a +unique instance of some object (controller, job, channel), while sharing the +global process space (such as classes and their configurations, and global +variables). As long as the code on each thread doesn't modify any of those +shared things, the other threads are mostly irrelevant to it. -Rails' in-built concurrency will cover the day-to-day needs of many application developers, and ensure applications remain generally performant. +Rails' in-built concurrency will cover the day-to-day needs of many application +developers, and ensure applications remain generally performant. -NOTE: You can read more about how to configure Rails' in-built concurrency in the [Framework Behavior](#framework-behavior) section. +NOTE: You can read more about how to configure Rails' in-built concurrency in +the [Framework Behavior](#framework-behavior) section. ### `CurrentAttributes` and Threading -The [`ActiveSupport::CurrentAttributes`](https://edgeapi.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html) class is a special class in Rails that helps you manage temporary data for each request in your app, and helps make sure this data is available to the whole system. It keeps this data separate for every request (even if there are multiple threads running) and makes sure the data is cleaned up automatically when the request is done. +The +[`ActiveSupport::CurrentAttributes`](https://edgeapi.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html) +class is a special class in Rails that helps you manage temporary data for each +request in your app, and helps make sure this data is available to the whole +system. It keeps this data separate for every request (even if there are +multiple threads running) and makes sure the data is cleaned up automatically +when the request is done. -You can think of this class as a place to store data that you need to access anywhere in your app without having to pass it around in your code. +You can think of this class as a place to store data that you need to access +anywhere in your app without having to pass it around in your code. -To use the `Current` class to store data, first you need to create a file as below, with `attribute` values for the attributes and models whose values you would like to access throughout your application. You can also define a method (e.g the `user` method below) which, when called, will return set values: +To use the `Current` class to store data, first you need to create a file as +below, with `attribute` values for the attributes and models whose values you +would like to access throughout your application. You can also define a method +(e.g the `user` method below) which, when called, will contain set values: ```ruby # app/models/current.rb @@ -58,7 +75,8 @@ class Current < ActiveSupport::CurrentAttributes end ``` -In the example above, you will now have access to `Current.user` elsewhere in your application. For example, in authenticating a user: +You will now have access to `Current.user` elsewhere in your application. For +example, when authenticating a user: ```ruby # app/controllers/concerns/authentication.rb @@ -79,26 +97,38 @@ module Authentication end end ``` -WARNING: It’s easy to put too many attributes in the `Current` class and tangle your model as a result. Current should only be used for a few, top-level globals, like account, user, and request details. + +WARNING: It’s easy to put too many attributes in the `Current` class and tangle +your model as a result. Current should only be used for a few, top-level +globals, like account, user, and request details. ### Isolated Execution State -The `active_support.isolation_level` value in your `configuration.rb` file provides you the option to define where Rails internal state should be stored while tasks are run. If you use a fiber-based server or job processor (e.g. [`falcon`](https://github.com/socketry/falcon)), you should set this value to `:fiber`, otherwise it is best to set it to `:thread`. +The `active_support.isolation_level` value in your `configuration.rb` file +provides you the option to define where Rails internal state should be stored +while tasks are run. If you use a fiber-based server or job processor (e.g. +[`falcon`](https://github.com/socketry/falcon)), you should set this value to +`:fiber`, otherwise it is best to set it to `:thread`. ### Going Futher -The next section of this guide details advanced ways of directly wrapping code within the Rails framework, and how extensions and applications with particular concurrency requirements, such as library maintainers, should do this. +The next section of this guide details advanced ways of wrapping code to ensure +thread safety, and how extensions and applications with particular concurrency +requirements, such as library maintainers, should do this. Wrapping Application Code ------------------------- ### The Rails Executor -The Rails Executor inherits from the [`ActiveSupport::ExecutionWrapper`](https://api.rubyonrails.org/classes/ActiveSupport/ExecutionWrapper.html). The Executor separates application code from framework code by wrapping code that you've written and is necessary when using threads. +The Rails Executor inherits from the +[`ActiveSupport::ExecutionWrapper`](https://api.rubyonrails.org/classes/ActiveSupport/ExecutionWrapper.html). +The Executor separates application code from framework code by wrapping code +that you've written and is necessary when threads are being used. The Executor consists of two callbacks: `to_run` and `to_complete`. The `to_run` -callback is called before the application code, and the `to_complete` callback is -called after. +callback is called before the application code, and the `to_complete` callback +is called after. #### Callbacks @@ -123,10 +153,11 @@ end TIP: If you repeatedly invoke application code from a long-running process, you may want to wrap using the [Reloader](#the-reloader) instead. -Each thread should be wrapped before it runs application code, so if your -application manually delegates work to other threads, such as via `Thread.new`, -or uses features from the [Concurrent Ruby](https://github.com/ruby-concurrency/concurrent-ruby) gem that use thread pools, you should immediately wrap -the block: +If your application manually delegates work to other threads, such as via +`Thread.new`, or uses features from the [Concurrent +Ruby](https://github.com/ruby-concurrency/concurrent-ruby) gem that use thread +pools, you should immediately wrap the block, before any application code is +run: ```ruby Thread.new do @@ -136,12 +167,13 @@ Thread.new do end ``` -NOTE: The Concurrent Ruby gem uses a `ThreadPoolExecutor`, which it sometimes configures -with an `executor` option. Despite the name, it is unrelated to the Rails Executor. +NOTE: The Concurrent Ruby gem uses a `ThreadPoolExecutor`, which it sometimes +configures with an `executor` option. Despite the name, it is unrelated to the +Rails Executor. -If it's impractical to wrap the application code in a block (for -example, the Rack API makes this problematic), you can also use the `run!` / -`complete!` pair: +If it's impractical to wrap the application code in a block (for example, the +Rack API makes this problematic), you can also use the `run!` / `complete!` +pair: ```ruby Thread.new do @@ -152,17 +184,25 @@ ensure end ``` -NOTE: The Rails Executor is safely re-entrant; it can be called again if it is already running. In this case, the `wrap` method would have no effect. +NOTE: The Rails Executor is safely re-entrant; it can be called again if it is +already running. In this case, the `wrap` method has no effect. + +#### Running + +When called, the Rails Executor will put the current thread into `running` mode +in the [Load Interlock](#load-interlock). -The Executor will put the current thread into `running` mode in the [Reloading -Interlock](#reloading-interlock). This operation will block temporarily if another -thread is currently unloading/reloading the application. +This operation will block temporarily if another thread is currently either +autoloading a constant or unloading/reloading the application. #### Examples of Wrapped Application Code -Any time your library or component needs to invoke code that will need to run in the application, this code should be wrapped to ensure thread safety and a consistent and clean runtime state. +Any time your library or component needs to invoke code that will need to run in +the application, this code should be wrapped to ensure thread safety and a +consistent and clean runtime state. -For example, you may be setting a `Current` user (using [`ActiveSupport::CurrentAttributes`](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html)). +For example, you may be setting a `Current` user (using +[`ActiveSupport::CurrentAttributes`](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html)). ```ruby def log_with_user_context(message) @@ -172,7 +212,8 @@ def log_with_user_context(message) end ``` -You may be triggering an ActiveRecord callback or lifecycle hook in an application: +You may be triggering an ActiveRecord callback or lifecycle hook in an +application: ```ruby def perform_task_with_record(record) @@ -192,15 +233,18 @@ def enqueue_background_job(job_class, *args) end ``` -These are just a few of many possible other use cases, including rendering views or templates, broadcasting via [`Action Cable`](action_cable_overview.html) or using [`Rails.cache`](caching_with_rails.html). +These are just a few of many possible other use cases, including rendering views +or templates, broadcasting via [`Action Cable`](action_cable_overview.html) or +using [`Rails.cache`](caching_with_rails.html). ### The Reloader -Like the Executor, the [Reloader](https://api.rubyonrails.org/classes/ActiveSupport/Reloader.html) also wraps application code. If the Executor is -not already active on the current thread, the Reloader will invoke it for you, -so you only need to call one. This also guarantees that everything the Reloader -does, including all its callback executions, occurs wrapped inside the -Executor. +Like the Executor, the +[Reloader](https://api.rubyonrails.org/classes/ActiveSupport/Reloader.html) also +wraps application code. If the Executor is not already active on the current +thread, the Reloader will invoke it for you, so you only need to call one. This +also guarantees that everything the Reloader does, including all its callback +executions, occurs wrapped inside the Executor. ```ruby Rails.application.reloader.wrap do @@ -217,10 +261,10 @@ is a better fit for your use case. #### Callbacks Before entering the wrapped block, the Reloader will check whether the running -application needs to be reloaded -- for example, because a model's source file has -been modified. If it determines a reload is required, it will wait until it's -safe, and then do so, before continuing. When the application is configured to -always reload regardless of whether any changes are detected, the reload is +application needs to be reloaded -- for example, because a model's source file +has been modified. If it determines a reload is required, it will wait until +it's safe, and then do so, before continuing. When the application is configured +to always reload regardless of whether any changes are detected, the reload is instead performed at the end of the block. The Reloader also provides `to_run` and `to_complete` callbacks; they are @@ -232,12 +276,17 @@ necessary, the Reloader will invoke the wrapped block with no other callbacks. The most significant part of the reloading process is the 'class unload', where all autoloaded classes are removed, ready to be loaded again. This will occur -immediately before either the `to_run` or `to_complete` callback, depending on the -[`reload_classes_only_on_change`](configuring.html#config-reload-classes-only-on-change) setting. +immediately before either the `to_run` or `to_complete` callback, depending on +the +[`reload_classes_only_on_change`](configuring.html#config-reload-classes-only-on-change) +setting. Often, additional reloading actions need to be performed either just before or -just after the Class Unload, so the Reloader also provides [`before_class_unload`](https://api.rubyonrails.org/classes/ActiveSupport/Reloader.html#method-c-before_class_unload) -and [`after_class_unload`](https://api.rubyonrails.org/classes/ActiveSupport/Reloader.html#method-c-after_class_unload) callbacks. +just after the Class Unload, so the Reloader also provides +[`before_class_unload`](https://api.rubyonrails.org/classes/ActiveSupport/Reloader.html#method-c-before_class_unload) +and +[`after_class_unload`](https://api.rubyonrails.org/classes/ActiveSupport/Reloader.html#method-c-after_class_unload) +callbacks. #### Concurrency @@ -253,46 +302,54 @@ thread is mid-execution. Child threads should use the Executor instead. Framework Behavior ------------------ -The Rails framework components use the Executor and the Reloader to manage their own concurrency -needs too. +The Rails framework components use the Executor and the Reloader to manage their +own concurrency needs too. -`ActionDispatch::Executor` and `ActionDispatch::Reloader` are Rack middlewares -that wrap requests with a supplied Executor or Reloader, respectively. They -are automatically included in the default application stack. The Reloader will -ensure any arriving HTTP request is served with a freshly-loaded copy of the -application if any code changes have occurred. +[`ActionDispatch::Executor`](https://api.rubyonrails.org/classes/ActionDispatch/Executor.html) +and +[`ActionDispatch::Reloader`](https://api.rubyonrails.org/classes/ActionDispatch/Reloader.html) +are Rack middlewares that wrap requests with a supplied Executor or Reloader, +respectively. They are automatically included in the default application stack. +The Reloader will ensure any arriving HTTP request is served with a +freshly-loaded copy of the application if any code changes have occurred. Active Job also wraps its job executions with the Reloader, loading the latest code to execute each job as it comes off the queue. -Action Cable uses the Executor instead: because a Cable connection is linked to -a specific instance of a class, it's not possible to reload for every arriving -WebSocket message. Only the message handler is wrapped, though; a long-running -Cable connection does not prevent a reload that's triggered by a new incoming -request or job. Instead, Action Cable also uses the Reloader's `before_class_unload` -callback to disconnect all its connections. When the client automatically -reconnects, it will be speaking to the new version of the code. +Action Cable uses the Executor instead. A Cable connection is linked to a +specific instance of a class, which means it's not possible to reload for every +arriving WebSocket message. Only the message handler is wrapped, though; a +long-running Cable connection does not prevent a reload that's triggered by a +new incoming request or job. Instead, Action Cable also uses the Reloader's +`before_class_unload` callback to disconnect all its connections. When the +client automatically reconnects, it will be interacting with the new version of +the code. The above are the entry points to the framework, so they are responsible for ensuring their respective threads are protected, and deciding whether a reload -is necessary. Most other components only need to use the Executor when they spawn -additional threads. +is necessary. Most other components only need to use the Executor when they +spawn additional threads. ### Configuration #### Reloader and Executor Configuration -The Reloader only checks for file changes when [`config.enable_reloading`](configuring.html#config-enable-reloading) is -`true` and so is [`config.reload_classes_only_on_change`](configuring.html#config-reload-classes-only-on-change). These are the defaults in the -`development` environment. +The Reloader only checks for file changes when +[`config.enable_reloading`](configuring.html#config-enable-reloading) and +[`config.reload_classes_only_on_change`](configuring.html#config-reload-classes-only-on-change) +are both `true`. These are the defaults in the `development` environment. -When `config.enable_reloading` is `false` (in `production`, by default), the Reloader is only a pass-through to the Executor. +When `config.enable_reloading` is `false` (in `production`, by default), the +Reloader is only a pass-through to the Executor. The Executor always has important work to do, like database connection management. When `config.enable_reloading` is `false` and `config.eager_load` is `true` (`production` defaults), no reloading will occur, so it does not need the -Reloading Interlock. With the default settings in the `development` environment, the -Executor will use the Reloading Interlock to ensure code reloading is performed safely. +[Load Interlock](#load-interlock). With the default settings in the +`development` environment, the Executor will use the Load Interlock to ensure +constants are only loaded when it is safe. + +You can read more about the Load Interlock in the following section. Load Interlock -------------- @@ -310,3 +367,86 @@ threads are currently running application code, and ensuring that reloading waits until no other threads are executing application code. +### `permit_concurrent_loads` + +The Executor automatically acquires a `running` lock for the duration of its +block, and autoload knows when to upgrade to a `load` lock, and switch back to +`running` again afterwards. + +Other blocking operations performed inside the Executor block (which includes +all application code), however, can needlessly retain the `running` lock. If +another thread encounters a constant it must autoload, this can cause a +deadlock. + +For example, assuming `User` is not yet loaded, the following will deadlock: + +```ruby +Rails.application.executor.wrap do + th = Thread.new do + Rails.application.executor.wrap do + User # inner thread waits here; it cannot load + # User while another thread is running + end + end + + th.join # outer thread waits here, holding 'running' lock +end +``` + +To prevent this deadlock, the outer thread can `permit_concurrent_loads`. By +calling this method, the thread guarantees it will not dereference any +possibly-autoloaded constant inside the supplied block. The safest way to meet +that promise is to put it as close as possible to the blocking call: + +```ruby +Rails.application.executor.wrap do + th = Thread.new do + Rails.application.executor.wrap do + User # inner thread can acquire the 'load' lock, + # load User, and continue + end + end + + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + th.join # outer thread waits here, but has no lock + end +end +``` + +Another example, using Concurrent Ruby: + +```ruby +Rails.application.executor.wrap do + futures = 3.times.collect do |i| + Concurrent::Promises.future do + Rails.application.executor.wrap do + # do work here + end + end + end + + values = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + futures.collect(&:value) + end +end +``` + +### ActionDispatch::DebugLocks + +If your application is deadlocking and you think the Load Interlock may be +involved, you can temporarily add the ActionDispatch::DebugLocks middleware to +`config/application.rb`: + +```ruby +config.middleware.insert_before Rack::Sendfile, + ActionDispatch::DebugLocks +``` + +If you then restart the application and re-trigger the deadlock condition, +`/rails/locks` will show a summary of all threads currently known to the +interlock, which lock level they are holding or awaiting, and their current +backtrace. + +Generally a deadlock will be caused by the interlock conflicting with some other +external lock or blocking input/output call. Once you find it, you can wrap it +with `permit_concurrent_loads`. From b2da77691f017d115c81d7ece318eab7c7262688 Mon Sep 17 00:00:00 2001 From: Harriet Oughton Date: Sun, 8 Jun 2025 12:16:03 +0100 Subject: [PATCH 13/19] Apply docs team PR review changes --- guides/source/threading_and_code_execution.md | 103 +++++++++--------- 1 file changed, 52 insertions(+), 51 deletions(-) diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md index 5a8edb5dae9be..6d5ea78b8333b 100644 --- a/guides/source/threading_and_code_execution.md +++ b/guides/source/threading_and_code_execution.md @@ -5,9 +5,9 @@ Threading and Code Execution in Rails After reading this guide, you will know: -* Where to find automatically concurrent code execution in Rails +* Where to find concurrent code execution in Rails * How to integrate manual concurrency within Rails -* How to wrap all application code using the Rails Executor +* How to wrap application code using the Rails Executor * How to affect application reloading -------------------------------------------------------------------------------- @@ -19,9 +19,9 @@ Rails automatically allows various operations to be performed at the same time (concurrently) in order for an application to run more efficiently. In this section, we will explore some of the ways this happens behind the scenes. -When using a threaded web server (such as Rails' default server, Puma) multiple -HTTP requests will be served simultaneously as each request is given its own -controller instance. +When using a threaded web server (such as Rails' default server, Puma) requests +will be served simultaneously as each request is given its own controller +instance. Threaded Active Job adapters, including the built-in Async adapter, will likewise execute several jobs at the same time. Action Cable channels are @@ -30,35 +30,36 @@ managed this way too. Asynchronous Active Record queries are also performed in the background, allowing other processes to run on the main thread. -The above mechanisms all involve multiple threads, often managing work for a -unique instance of some object (controller, job, channel), while sharing the +The above mechanisms all involve multiple threads, each managing work for a +unique instance of an object (controller, job, channel), while sharing the global process space (such as classes and their configurations, and global -variables). As long as the code on each thread doesn't modify any of those -shared things, the other threads are mostly irrelevant to it. +variables). As long as the code on each thread doesn't modify anything shared, +multiple threads can safely run concurrently. -Rails' in-built concurrency will cover the day-to-day needs of many application +Rails' in-built concurrency will cover the day-to-day needs of most application developers, and ensure applications remain generally performant. -NOTE: You can read more about how to configure Rails' in-built concurrency in -the [Framework Behavior](#framework-behavior) section. +NOTE: You can read more about how to configure Rails' concurrency in the +[Framework Behavior](#framework-behavior) section. ### `CurrentAttributes` and Threading The [`ActiveSupport::CurrentAttributes`](https://edgeapi.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html) -class is a special class in Rails that helps you manage temporary data for each -request in your app, and helps make sure this data is available to the whole -system. It keeps this data separate for every request (even if there are -multiple threads running) and makes sure the data is cleaned up automatically -when the request is done. +class is a special class in Rails that helps you manage temporary, +thread-specific data for each request in your app, and helps make sure this data +is available to the whole system. It keeps this data separate for every request +(even if there are multiple threads running) and makes sure the data is cleaned +up automatically when the request is done. You can think of this class as a place to store data that you need to access anywhere in your app without having to pass it around in your code. To use the `Current` class to store data, first you need to create a file as -below, with `attribute` values for the attributes and models whose values you -would like to access throughout your application. You can also define a method -(e.g the `user` method below) which, when called, will contain set values: +shown in the example below, with `attribute` values for the attributes and +models whose values you would like to access throughout your application. You +can also define a method (e.g the `user` method below) which, when called, will +contain set values: ```ruby # app/models/current.rb @@ -99,7 +100,7 @@ end ``` WARNING: It’s easy to put too many attributes in the `Current` class and tangle -your model as a result. Current should only be used for a few, top-level +your model as a result. `Current` should only be used for a few, top-level globals, like account, user, and request details. ### Isolated Execution State @@ -110,8 +111,6 @@ while tasks are run. If you use a fiber-based server or job processor (e.g. [`falcon`](https://github.com/socketry/falcon)), you should set this value to `:fiber`, otherwise it is best to set it to `:thread`. -### Going Futher - The next section of this guide details advanced ways of wrapping code to ensure thread safety, and how extensions and applications with particular concurrency requirements, such as library maintainers, should do this. @@ -126,18 +125,18 @@ The Rails Executor inherits from the The Executor separates application code from framework code by wrapping code that you've written and is necessary when threads are being used. +#### Callbacks + The Executor consists of two callbacks: `to_run` and `to_complete`. The `to_run` callback is called before the application code, and the `to_complete` callback is called after. -#### Callbacks - In a default Rails application, the Rails Executor callbacks are used to: -* track which threads are in safe positions for autoloading and reloading -* enable and disable the Active Record query cache -* return acquired Active Record connections to the pool -* constrain internal cache lifetimes +* Track which threads are in safe positions for autoloading and reloading. +* Enable and disable the Active Record query cache. +* Return acquired Active Record connections to the pool. +* Constrain internal cache lifetimes. #### Code Execution @@ -168,8 +167,8 @@ end ``` NOTE: The Concurrent Ruby gem uses a `ThreadPoolExecutor`, which it sometimes -configures with an `executor` option. Despite the name, it is unrelated to the -Rails Executor. +configures with an `executor` option. Despite the name, it is _not_ related to +the Rails Executor. If it's impractical to wrap the application code in a block (for example, the Rack API makes this problematic), you can also use the `run!` / `complete!` @@ -197,9 +196,9 @@ autoloading a constant or unloading/reloading the application. #### Examples of Wrapped Application Code -Any time your library or component needs to invoke code that will need to run in -the application, this code should be wrapped to ensure thread safety and a -consistent and clean runtime state. +Any time your library or component needs to invoke code that will run in the +application, the code should be wrapped to ensure thread safety and a consistent +and clean runtime state. For example, you may be setting a `Current` user (using [`ActiveSupport::CurrentAttributes`](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html)). @@ -233,18 +232,26 @@ def enqueue_background_job(job_class, *args) end ``` -These are just a few of many possible other use cases, including rendering views -or templates, broadcasting via [`Action Cable`](action_cable_overview.html) or +These are just a few of many possible use cases, including rendering views or +templates, broadcasting via [`Action Cable`](action_cable_overview.html) or using [`Rails.cache`](caching_with_rails.html). ### The Reloader Like the Executor, the [Reloader](https://api.rubyonrails.org/classes/ActiveSupport/Reloader.html) also -wraps application code. If the Executor is not already active on the current -thread, the Reloader will invoke it for you, so you only need to call one. This -also guarantees that everything the Reloader does, including all its callback -executions, occurs wrapped inside the Executor. +wraps application code. The Reloader is only suitable where a long-running +framework-level process repeatedly calls into application code, such as for a +web server or job queue. + +NOTE: Rails automatically wraps web requests and Active Job workers, so you'll +rarely need to invoke the Reloader for yourself. Always consider whether the +Executor is a better fit for your use case. + +If the Executor is not already active on the current thread, the Reloader will +invoke it for you, so you only need to call one. This also guarantees that +everything the Reloader does, including all its callback executions, occurs +wrapped inside the Executor. ```ruby Rails.application.reloader.wrap do @@ -252,20 +259,14 @@ Rails.application.reloader.wrap do end ``` -NOTE: The Reloader is only suitable where a long-running framework-level process -repeatedly calls into application code, such as for a web server or job queue. -Rails automatically wraps web requests and Active Job workers, so you'll rarely -need to invoke the Reloader for yourself. Always consider whether the Executor -is a better fit for your use case. - #### Callbacks Before entering the wrapped block, the Reloader will check whether the running -application needs to be reloaded -- for example, because a model's source file -has been modified. If it determines a reload is required, it will wait until -it's safe, and then do so, before continuing. When the application is configured -to always reload regardless of whether any changes are detected, the reload is -instead performed at the end of the block. +application needs to be reloaded (because a model's source file has been +modified, for example). If it determines a reload is required, it will wait +until it's safe, and then do so, before continuing. When the application is +configured to always reload regardless of whether any changes are detected, the +reload is instead performed at the end of the block. The Reloader also provides `to_run` and `to_complete` callbacks; they are invoked at the same points as those of the Executor, but only when the current From ff140217709867dba69ca1a164a7ce5f7e245d60 Mon Sep 17 00:00:00 2001 From: Harriet Oughton Date: Tue, 10 Jun 2025 17:21:28 +0100 Subject: [PATCH 14/19] Apply further docs team review suggestions --- guides/source/threading_and_code_execution.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md index 6d5ea78b8333b..055463dbb1555 100644 --- a/guides/source/threading_and_code_execution.md +++ b/guides/source/threading_and_code_execution.md @@ -12,7 +12,7 @@ After reading this guide, you will know: -------------------------------------------------------------------------------- -In-Built Concurrency in Rails +Built-in Concurrency in Rails ----------------------------- Rails automatically allows various operations to be performed at the same time @@ -36,7 +36,7 @@ global process space (such as classes and their configurations, and global variables). As long as the code on each thread doesn't modify anything shared, multiple threads can safely run concurrently. -Rails' in-built concurrency will cover the day-to-day needs of most application +Rails' built-in concurrency will cover the day-to-day needs of most application developers, and ensure applications remain generally performant. NOTE: You can read more about how to configure Rails' concurrency in the @@ -186,7 +186,7 @@ end NOTE: The Rails Executor is safely re-entrant; it can be called again if it is already running. In this case, the `wrap` method has no effect. -#### Running +#### Running Mode When called, the Rails Executor will put the current thread into `running` mode in the [Load Interlock](#load-interlock). @@ -211,7 +211,7 @@ def log_with_user_context(message) end ``` -You may be triggering an ActiveRecord callback or lifecycle hook in an +You may be triggering an Active Record callback or lifecycle hook in an application: ```ruby @@ -376,7 +376,7 @@ block, and autoload knows when to upgrade to a `load` lock, and switch back to Other blocking operations performed inside the Executor block (which includes all application code), however, can needlessly retain the `running` lock. If -another thread encounters a constant it must autoload, this can cause a +another thread encounters a constant it must autoload, which can cause a deadlock. For example, assuming `User` is not yet loaded, the following will deadlock: From 49cb0dbda68308d70bd50add772ada9a3bacb837 Mon Sep 17 00:00:00 2001 From: Harriet Oughton Date: Fri, 27 Jun 2025 19:05:56 +0100 Subject: [PATCH 15/19] Apply final review changes --- guides/source/threading_and_code_execution.md | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md index 055463dbb1555..e0b8a14e7ae40 100644 --- a/guides/source/threading_and_code_execution.md +++ b/guides/source/threading_and_code_execution.md @@ -42,7 +42,21 @@ developers, and ensure applications remain generally performant. NOTE: You can read more about how to configure Rails' concurrency in the [Framework Behavior](#framework-behavior) section. -### `CurrentAttributes` and Threading + +### Isolated Execution State + +The `active_support.isolation_level` value in your `configuration.rb` file +provides you the option to define where Rails' internal state should be stored +while tasks are run. If you use a fiber-based server or job processor (e.g. +[`falcon`](https://github.com/socketry/falcon)), you should set this value to +`:fiber`, otherwise it is best to set it to `:thread`. + +Later sections of this guide detail advanced ways of wrapping code to ensure +thread safety, and how extensions and applications with particular concurrency +requirements, such as library maintainers, should do this. + +Storing Thread-specific Data +---------------------------- The [`ActiveSupport::CurrentAttributes`](https://edgeapi.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html) @@ -55,7 +69,7 @@ up automatically when the request is done. You can think of this class as a place to store data that you need to access anywhere in your app without having to pass it around in your code. -To use the `Current` class to store data, first you need to create a file as +To use a `Current` class to store data, first you need to create a file as shown in the example below, with `attribute` values for the attributes and models whose values you would like to access throughout your application. You can also define a method (e.g the `user` method below) which, when called, will @@ -103,18 +117,6 @@ WARNING: It’s easy to put too many attributes in the `Current` class and tangl your model as a result. `Current` should only be used for a few, top-level globals, like account, user, and request details. -### Isolated Execution State - -The `active_support.isolation_level` value in your `configuration.rb` file -provides you the option to define where Rails internal state should be stored -while tasks are run. If you use a fiber-based server or job processor (e.g. -[`falcon`](https://github.com/socketry/falcon)), you should set this value to -`:fiber`, otherwise it is best to set it to `:thread`. - -The next section of this guide details advanced ways of wrapping code to ensure -thread safety, and how extensions and applications with particular concurrency -requirements, such as library maintainers, should do this. - Wrapping Application Code ------------------------- @@ -160,6 +162,7 @@ run: ```ruby Thread.new do + # no code here Rails.application.executor.wrap do # your code here end @@ -201,7 +204,7 @@ application, the code should be wrapped to ensure thread safety and a consistent and clean runtime state. For example, you may be setting a `Current` user (using -[`ActiveSupport::CurrentAttributes`](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html)). +[`ActiveSupport::CurrentAttributes`](#storing-thread-specific-data)). ```ruby def log_with_user_context(message) @@ -273,9 +276,15 @@ invoked at the same points as those of the Executor, but only when the current execution has initiated an application reload. When no reload is deemed necessary, the Reloader will invoke the wrapped block with no other callbacks. +```ruby +Rails.application.reloader.to_run do + # call reloading code here +end +``` + #### Class Unload -The most significant part of the reloading process is the 'class unload', where +The most significant part of the reloading process is the "class unload", where all autoloaded classes are removed, ready to be loaded again. This will occur immediately before either the `to_run` or `to_complete` callback, depending on the @@ -283,12 +292,18 @@ the setting. Often, additional reloading actions need to be performed either just before or -just after the Class Unload, so the Reloader also provides +just after the "class unload", so the Reloader also provides [`before_class_unload`](https://api.rubyonrails.org/classes/ActiveSupport/Reloader.html#method-c-before_class_unload) and [`after_class_unload`](https://api.rubyonrails.org/classes/ActiveSupport/Reloader.html#method-c-after_class_unload) callbacks. +```ruby +Rails.application.reloader.before_class_unload do + # call class unloading code here +end +``` + #### Concurrency Only long-running "top level" processes should invoke the Reloader, because if From 119974df6a2c12b46b713ea9371b10ed693620e3 Mon Sep 17 00:00:00 2001 From: Harriet Oughton Date: Sun, 5 Oct 2025 10:01:13 +0100 Subject: [PATCH 16/19] Apply first section changes following Rails Core review --- guides/source/threading_and_code_execution.md | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md index e0b8a14e7ae40..66381c85ecf14 100644 --- a/guides/source/threading_and_code_execution.md +++ b/guides/source/threading_and_code_execution.md @@ -12,16 +12,14 @@ After reading this guide, you will know: -------------------------------------------------------------------------------- -Built-in Concurrency in Rails ------------------------------ +Automatic Concurrency in Rails +------------------------------ Rails automatically allows various operations to be performed at the same time (concurrently) in order for an application to run more efficiently. In this section, we will explore some of the ways this happens behind the scenes. -When using a threaded web server (such as Rails' default server, Puma) requests -will be served simultaneously as each request is given its own controller -instance. +When using a threaded web server, such as the default Puma, multiple HTTP requests will be served simultaneously, with a separate controller instance for each request. Threaded Active Job adapters, including the built-in Async adapter, will likewise execute several jobs at the same time. Action Cable channels are @@ -30,14 +28,9 @@ managed this way too. Asynchronous Active Record queries are also performed in the background, allowing other processes to run on the main thread. -The above mechanisms all involve multiple threads, each managing work for a -unique instance of an object (controller, job, channel), while sharing the -global process space (such as classes and their configurations, and global -variables). As long as the code on each thread doesn't modify anything shared, -multiple threads can safely run concurrently. +These mechanisms all involve multiple threads, each managing work for a unique instance of some object (controller, job, channel), while sharing the global process space (such as classes and their configurations, and global variables). As long as your code doesn't modify any of those shared resources, it can mostly ignore that other threads exist. -Rails' built-in concurrency will cover the day-to-day needs of most application -developers, and ensure applications remain generally performant. +The rest of this guide describes the mechanisms Rails uses to make other threads "mostly ignorable", and how extensions and applications with special requirements can use these mechanisms. NOTE: You can read more about how to configure Rails' concurrency in the [Framework Behavior](#framework-behavior) section. From 357d4ff2f5b03c478be76c5ab360e5054efaa67d Mon Sep 17 00:00:00 2001 From: Harriet Oughton Date: Fri, 17 Oct 2025 18:07:08 +0100 Subject: [PATCH 17/19] Apply changes to the rest of guide following core review --- guides/source/threading_and_code_execution.md | 171 ++---------------- 1 file changed, 19 insertions(+), 152 deletions(-) diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md index 66381c85ecf14..5866abf240f9b 100644 --- a/guides/source/threading_and_code_execution.md +++ b/guides/source/threading_and_code_execution.md @@ -36,89 +36,20 @@ NOTE: You can read more about how to configure Rails' concurrency in the [Framework Behavior](#framework-behavior) section. -### Isolated Execution State +### Threads vs Fibres -The `active_support.isolation_level` value in your `configuration.rb` file +The `active_support.isolation_level` value in your `config/application.rb` file provides you the option to define where Rails' internal state should be stored while tasks are run. If you use a fiber-based server or job processor (e.g. -[`falcon`](https://github.com/socketry/falcon)), you should set this value to +`falcon`), you should set this value to `:fiber`, otherwise it is best to set it to `:thread`. -Later sections of this guide detail advanced ways of wrapping code to ensure -thread safety, and how extensions and applications with particular concurrency -requirements, such as library maintainers, should do this. - -Storing Thread-specific Data ----------------------------- - -The -[`ActiveSupport::CurrentAttributes`](https://edgeapi.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html) -class is a special class in Rails that helps you manage temporary, -thread-specific data for each request in your app, and helps make sure this data -is available to the whole system. It keeps this data separate for every request -(even if there are multiple threads running) and makes sure the data is cleaned -up automatically when the request is done. - -You can think of this class as a place to store data that you need to access -anywhere in your app without having to pass it around in your code. - -To use a `Current` class to store data, first you need to create a file as -shown in the example below, with `attribute` values for the attributes and -models whose values you would like to access throughout your application. You -can also define a method (e.g the `user` method below) which, when called, will -contain set values: - -```ruby -# app/models/current.rb -class Current < ActiveSupport::CurrentAttributes - attribute :account, :user - - resets { Time.zone = nil } - - def user=(user) - super - self.account = user.account - Time.zone = user.time_zone - end -end -``` - -You will now have access to `Current.user` elsewhere in your application. For -example, when authenticating a user: - -```ruby -# app/controllers/concerns/authentication.rb -module Authentication - extend ActiveSupport::Concern - - included do - before_action :authenticate - end - - private - def authenticate - if authenticated_user = User.find_by(id: cookies.encrypted[:user_id]) - Current.user = authenticated_user - else - redirect_to new_session_url - end - end -end -``` - -WARNING: It’s easy to put too many attributes in the `Current` class and tangle -your model as a result. `Current` should only be used for a few, top-level -globals, like account, user, and request details. - Wrapping Application Code ------------------------- ### The Rails Executor -The Rails Executor inherits from the -[`ActiveSupport::ExecutionWrapper`](https://api.rubyonrails.org/classes/ActiveSupport/ExecutionWrapper.html). -The Executor separates application code from framework code by wrapping code -that you've written and is necessary when threads are being used. +The Rails Executor separates application code from framework code by wrapping code that you've written, which is necessary when threads are being used. #### Callbacks @@ -147,11 +78,9 @@ end TIP: If you repeatedly invoke application code from a long-running process, you may want to wrap using the [Reloader](#the-reloader) instead. -If your application manually delegates work to other threads, such as via -`Thread.new`, or uses features from the [Concurrent -Ruby](https://github.com/ruby-concurrency/concurrent-ruby) gem that use thread -pools, you should immediately wrap the block, before any application code is -run: + +Each thread should be wrapped before it runs application code, so if your +application manually delegates work to other threads, such as via `Thread.new` or [Concurrent Ruby](https://github.com/ruby-concurrency/concurrent-ruby) features that use thread pools, you should immediately wrap the block: ```ruby Thread.new do @@ -162,7 +91,7 @@ Thread.new do end ``` -NOTE: The Concurrent Ruby gem uses a `ThreadPoolExecutor`, which it sometimes +NOTE: Concurrent Ruby uses a `ThreadPoolExecutor`, which it sometimes configures with an `executor` option. Despite the name, it is _not_ related to the Rails Executor. @@ -179,58 +108,12 @@ ensure end ``` -NOTE: The Rails Executor is safely re-entrant; it can be called again if it is -already running. In this case, the `wrap` method has no effect. - #### Running Mode -When called, the Rails Executor will put the current thread into `running` mode -in the [Load Interlock](#load-interlock). - -This operation will block temporarily if another thread is currently either -autoloading a constant or unloading/reloading the application. - -#### Examples of Wrapped Application Code - -Any time your library or component needs to invoke code that will run in the -application, the code should be wrapped to ensure thread safety and a consistent -and clean runtime state. - -For example, you may be setting a `Current` user (using -[`ActiveSupport::CurrentAttributes`](#storing-thread-specific-data)). - -```ruby -def log_with_user_context(message) - Rails.application.executor.wrap do - Current.user = User.find_by(id: 1) - end -end -``` - -You may be triggering an Active Record callback or lifecycle hook in an -application: - -```ruby -def perform_task_with_record(record) - Rails.application.executor.wrap do - record.save! # Executes before_save, after_save, etc. - end -end -``` - -Or enqueuing or performing a background job within the application: - -```ruby -def enqueue_background_job(job_class, *args) - Rails.application.executor.wrap do - job_class.perform_later(*args) - end -end -``` - -These are just a few of many possible use cases, including rendering views or -templates, broadcasting via [`Action Cable`](action_cable_overview.html) or -using [`Rails.cache`](caching_with_rails.html). +The Executor will put the current thread into `running` mode in the [Load +Interlock](#load-interlock). This operation will block temporarily if another +thread is currently either autoloading a constant or unloading/reloading +the application. ### The Reloader @@ -246,7 +129,7 @@ Executor is a better fit for your use case. If the Executor is not already active on the current thread, the Reloader will invoke it for you, so you only need to call one. This also guarantees that -everything the Reloader does, including all its callback executions, occurs +everything the Reloader does, including its callbacks, occurs wrapped inside the Executor. ```ruby @@ -269,12 +152,6 @@ invoked at the same points as those of the Executor, but only when the current execution has initiated an application reload. When no reload is deemed necessary, the Reloader will invoke the wrapped block with no other callbacks. -```ruby -Rails.application.reloader.to_run do - # call reloading code here -end -``` - #### Class Unload The most significant part of the reloading process is the "class unload", where @@ -291,12 +168,6 @@ and [`after_class_unload`](https://api.rubyonrails.org/classes/ActiveSupport/Reloader.html#method-c-after_class_unload) callbacks. -```ruby -Rails.application.reloader.before_class_unload do - # call class unloading code here -end -``` - #### Concurrency Only long-running "top level" processes should invoke the Reloader, because if @@ -325,18 +196,16 @@ freshly-loaded copy of the application if any code changes have occurred. Active Job also wraps its job executions with the Reloader, loading the latest code to execute each job as it comes off the queue. -Action Cable uses the Executor instead. A Cable connection is linked to a -specific instance of a class, which means it's not possible to reload for every -arriving WebSocket message. Only the message handler is wrapped, though; a -long-running Cable connection does not prevent a reload that's triggered by a -new incoming request or job. Instead, Action Cable also uses the Reloader's +Action Cable uses the Executor instead: because a Cable connection is linked to a specific instance of a class, it's not possible to reload for every arriving +WebSocket message. Only the message handler is wrapped, though; a long-running Cable connection does not prevent a reload that's triggered by a +new incoming request or job. Instead, Action Cable uses the Reloader's `before_class_unload` callback to disconnect all its connections. When the client automatically reconnects, it will be interacting with the new version of the code. The above are the entry points to the framework, so they are responsible for ensuring their respective threads are protected, and deciding whether a reload -is necessary. Most other components only need to use the Executor when they +is necessary. Other components only need to use the Executor when they spawn additional threads. ### Configuration @@ -358,8 +227,6 @@ management. When `config.enable_reloading` is `false` and `config.eager_load` is `development` environment, the Executor will use the Load Interlock to ensure constants are only loaded when it is safe. -You can read more about the Load Interlock in the following section. - Load Interlock -------------- @@ -384,7 +251,7 @@ block, and autoload knows when to upgrade to a `load` lock, and switch back to Other blocking operations performed inside the Executor block (which includes all application code), however, can needlessly retain the `running` lock. If -another thread encounters a constant it must autoload, which can cause a +another thread encounters a constant that it must autoload, this can cause a deadlock. For example, assuming `User` is not yet loaded, the following will deadlock: @@ -457,5 +324,5 @@ interlock, which lock level they are holding or awaiting, and their current backtrace. Generally a deadlock will be caused by the interlock conflicting with some other -external lock or blocking input/output call. Once you find it, you can wrap it +external lock or blocking I/O call. Once you find it, you can wrap it with `permit_concurrent_loads`. From 3fb5369b5510aadb967e10110180700a494d33b5 Mon Sep 17 00:00:00 2001 From: Harriet Oughton Date: Wed, 19 Nov 2025 21:01:38 +0000 Subject: [PATCH 18/19] Add column wrap and address typo --- guides/source/threading_and_code_execution.md | 62 +++++++++++-------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md index 5866abf240f9b..298761ff2dd47 100644 --- a/guides/source/threading_and_code_execution.md +++ b/guides/source/threading_and_code_execution.md @@ -19,7 +19,9 @@ Rails automatically allows various operations to be performed at the same time (concurrently) in order for an application to run more efficiently. In this section, we will explore some of the ways this happens behind the scenes. -When using a threaded web server, such as the default Puma, multiple HTTP requests will be served simultaneously, with a separate controller instance for each request. +When using a threaded web server, such as the default Puma, multiple HTTP +requests will be served simultaneously, with a separate controller instance for +each request. Threaded Active Job adapters, including the built-in Async adapter, will likewise execute several jobs at the same time. Action Cable channels are @@ -28,28 +30,35 @@ managed this way too. Asynchronous Active Record queries are also performed in the background, allowing other processes to run on the main thread. -These mechanisms all involve multiple threads, each managing work for a unique instance of some object (controller, job, channel), while sharing the global process space (such as classes and their configurations, and global variables). As long as your code doesn't modify any of those shared resources, it can mostly ignore that other threads exist. +These mechanisms all involve multiple threads, each managing work for a unique +instance of some object (controller, job, channel), while sharing the global +process space (such as classes and their configurations, and global variables). +As long as your code doesn't modify any of those shared resources, it can mostly +ignore that other threads exist. -The rest of this guide describes the mechanisms Rails uses to make other threads "mostly ignorable", and how extensions and applications with special requirements can use these mechanisms. +The rest of this guide describes the mechanisms Rails uses to make other threads +"mostly ignorable", and how extensions and applications with special +requirements can use these mechanisms. NOTE: You can read more about how to configure Rails' concurrency in the [Framework Behavior](#framework-behavior) section. -### Threads vs Fibres +### Threads vs Fibers The `active_support.isolation_level` value in your `config/application.rb` file provides you the option to define where Rails' internal state should be stored while tasks are run. If you use a fiber-based server or job processor (e.g. -`falcon`), you should set this value to -`:fiber`, otherwise it is best to set it to `:thread`. +`falcon`), you should set this value to `:fiber`, otherwise it is best to set it +to `:thread`. Wrapping Application Code ------------------------- ### The Rails Executor -The Rails Executor separates application code from framework code by wrapping code that you've written, which is necessary when threads are being used. +The Rails Executor separates application code from framework code by wrapping +code that you've written, which is necessary when threads are being used. #### Callbacks @@ -80,7 +89,9 @@ may want to wrap using the [Reloader](#the-reloader) instead. Each thread should be wrapped before it runs application code, so if your -application manually delegates work to other threads, such as via `Thread.new` or [Concurrent Ruby](https://github.com/ruby-concurrency/concurrent-ruby) features that use thread pools, you should immediately wrap the block: +application manually delegates work to other threads, such as via `Thread.new` +or [Concurrent Ruby](https://github.com/ruby-concurrency/concurrent-ruby) +features that use thread pools, you should immediately wrap the block: ```ruby Thread.new do @@ -91,9 +102,9 @@ Thread.new do end ``` -NOTE: Concurrent Ruby uses a `ThreadPoolExecutor`, which it sometimes -configures with an `executor` option. Despite the name, it is _not_ related to -the Rails Executor. +NOTE: Concurrent Ruby uses a `ThreadPoolExecutor`, which it sometimes configures +with an `executor` option. Despite the name, it is _not_ related to the Rails +Executor. If it's impractical to wrap the application code in a block (for example, the Rack API makes this problematic), you can also use the `run!` / `complete!` @@ -112,8 +123,8 @@ end The Executor will put the current thread into `running` mode in the [Load Interlock](#load-interlock). This operation will block temporarily if another -thread is currently either autoloading a constant or unloading/reloading -the application. +thread is currently either autoloading a constant or unloading/reloading the +application. ### The Reloader @@ -129,8 +140,8 @@ Executor is a better fit for your use case. If the Executor is not already active on the current thread, the Reloader will invoke it for you, so you only need to call one. This also guarantees that -everything the Reloader does, including its callbacks, occurs -wrapped inside the Executor. +everything the Reloader does, including its callbacks, occurs wrapped inside the +Executor. ```ruby Rails.application.reloader.wrap do @@ -196,17 +207,18 @@ freshly-loaded copy of the application if any code changes have occurred. Active Job also wraps its job executions with the Reloader, loading the latest code to execute each job as it comes off the queue. -Action Cable uses the Executor instead: because a Cable connection is linked to a specific instance of a class, it's not possible to reload for every arriving -WebSocket message. Only the message handler is wrapped, though; a long-running Cable connection does not prevent a reload that's triggered by a -new incoming request or job. Instead, Action Cable uses the Reloader's -`before_class_unload` callback to disconnect all its connections. When the -client automatically reconnects, it will be interacting with the new version of -the code. +Action Cable uses the Executor instead: because a Cable connection is linked to +a specific instance of a class, it's not possible to reload for every arriving +WebSocket message. Only the message handler is wrapped, though; a long-running +Cable connection does not prevent a reload that's triggered by a new incoming +request or job. Instead, Action Cable uses the Reloader's `before_class_unload` +callback to disconnect all its connections. When the client automatically +reconnects, it will be interacting with the new version of the code. The above are the entry points to the framework, so they are responsible for ensuring their respective threads are protected, and deciding whether a reload -is necessary. Other components only need to use the Executor when they -spawn additional threads. +is necessary. Other components only need to use the Executor when they spawn +additional threads. ### Configuration @@ -324,5 +336,5 @@ interlock, which lock level they are holding or awaiting, and their current backtrace. Generally a deadlock will be caused by the interlock conflicting with some other -external lock or blocking I/O call. Once you find it, you can wrap it -with `permit_concurrent_loads`. +external lock or blocking I/O call. Once you find it, you can wrap it with +`permit_concurrent_loads`. From 4ae9bf96e6ce696e191ffb418866b682e7aad480 Mon Sep 17 00:00:00 2001 From: Harriet Oughton Date: Thu, 11 Dec 2025 10:54:39 +0000 Subject: [PATCH 19/19] Combine both configuration headers into one in threading guide --- guides/source/threading_and_code_execution.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/guides/source/threading_and_code_execution.md b/guides/source/threading_and_code_execution.md index 298761ff2dd47..f8d5c5d07d33d 100644 --- a/guides/source/threading_and_code_execution.md +++ b/guides/source/threading_and_code_execution.md @@ -220,9 +220,7 @@ ensuring their respective threads are protected, and deciding whether a reload is necessary. Other components only need to use the Executor when they spawn additional threads. -### Configuration - -#### Reloader and Executor Configuration +### Reloader and Executor Configuration The Reloader only checks for file changes when [`config.enable_reloading`](configuring.html#config-enable-reloading) and