async 2.27.0 → 2.27.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c9d36d758f8a197b7c00d3d2e4d83d280cfd8ddf4fb60bba1d7b9e95b64ec47
4
- data.tar.gz: c0c19dcc509563b18a9385c1ff07a982054e6e984c9ed9c1fd715ee027c74f09
3
+ metadata.gz: '09bbe8d1e72307456e130873e0b8085c3e9dfa4c6a5d683628a8b9d71296d536'
4
+ data.tar.gz: df36d6567530daaf5a0973eccd6c7731a0fc74fa16f14492a4166bb52f3e0b74
5
5
  SHA512:
6
- metadata.gz: b936e5c17d8e9e2eec7f3d9b3d2d4b00b5a16359cfb267a036f72fb254b53aeb234f8b9368ba1cd2ba41010438dd8da120621005bc1791fc554ee62647e46ed1
7
- data.tar.gz: 808b0ce51e2bfa4a28d01126d59627409e6a25b37ee8e9e1f8146c138fcef995ab693e699f9c3ece326e234100a397c19e917a55c9d1e34dd797d26531e411f5
6
+ metadata.gz: 1950f70ab897e009cdd3d662e23fa26647070a8b83c455d5265be83544b7c5e32c5f67647b9d712972a9ee165406be05e26d4aa4d4b4604be96126b7e13baa34
7
+ data.tar.gz: 7dabc3ef0f79ebdbda86736a21c2df628623cf1a707e942f3e7222fbc8aaedc4b836a5c736946002f56e00a5659531acbcf574d768382a4ffe60bf154ce2f1e8
checksums.yaml.gz.sig CHANGED
Binary file
data/agent.md CHANGED
@@ -30,6 +30,10 @@ This guide explains how to test and monitor documentation coverage in your Ruby
30
30
 
31
31
  This guide covers documentation practices and pragmas supported by the Decode gem for documenting Ruby code. These pragmas provide structured documentation that can be parsed and used to generate A...
32
32
 
33
+ #### [Setting Up RBS Types and Steep Type Checking for Ruby Gems](.context/decode/types.md)
34
+
35
+ This guide covers the process for establishing robust type checking in Ruby gems using RBS and Steep, focusing on automated generation from source documentation and proper validation.
36
+
33
37
  ### sus
34
38
 
35
39
  A fast and scalable test runner.
@@ -45,3 +49,15 @@ There are two types of mocking in sus: `receive` and `mock`. The `receive` match
45
49
  #### [Shared Test Behaviors and Fixtures](.context/sus/shared.md)
46
50
 
47
51
  Sus provides shared test contexts which can be used to define common behaviours or tests that can be reused across one or more test files.
52
+
53
+ ### sus-fixtures-agent-context
54
+
55
+ Test fixtures for running in Async.
56
+
57
+ #### [Getting Started](.context/sus-fixtures-agent-context/getting-started.md)
58
+
59
+ This guide explains how to use the `sus-fixtures-agent-context` gem to test agent contexts.
60
+
61
+ #### [GitHub Actions](.context/sus-fixtures-agent-context/github-actions.md)
62
+
63
+ This guide explains how to integrate the `sus-fixtures-agent-context` gem with GitHub Actions for testing agent contexts.
@@ -0,0 +1,188 @@
1
+ # Best Practices
2
+
3
+ This guide gives an overview of best practices for using Async.
4
+
5
+ ## Use a top-level `Sync` to denote the root of your program
6
+
7
+ `Async{}` has two uses: it creates an event loop if one doesn't exist, and it creates a task which runs asynchronously with respect to the parent scope. However, the top level `Async{}` block will be synchronous because it creates the event loop. In some programs, you do not care about executing asynchronously, but you still want your code to run in an event loop. `Sync{}` exists to do this efficiently.
8
+
9
+ ```ruby
10
+ require 'async'
11
+
12
+ class Packages
13
+ def initialize(urls)
14
+ @urls = urls
15
+ end
16
+
17
+ def fetch
18
+ # A common use case is to make functions which appear synchronous, but internally use asynchronous execution:
19
+ Sync do |task|
20
+ @urls.map do |url|
21
+ task.async do
22
+ fetch(url)
23
+ end
24
+ end.map(&:wait)
25
+ end
26
+ end
27
+ end
28
+ ```
29
+
30
+ `Sync{...}` is semantically equivalent to `Async{}.wait`, but it is more efficient. It is the preferred way to run code in an event loop at the top level of your program or to ensure some code runs in an event loop without creating a new task. The name `Sync` means "Synchronous Async", indicating that it runs synchronously with respect to the outer scope, but still allows for asynchronous execution within it.
31
+
32
+ ### Current Task
33
+
34
+ In some scenarios, it can be invalid to call a method outside of an event loop, for example a top level `Async{...}` can block forever, which might be unexpected.
35
+
36
+ ```ruby
37
+ def wait(queue)
38
+ Async do
39
+ queue.pop
40
+ end
41
+ end
42
+ ```
43
+
44
+ You can force callers of a method to only call the method within an asynchronous context by using a keyword argument `parent: Async::Task.current`. If no task is present, this will raise an exception.
45
+
46
+ ```ruby
47
+ def wait(queue, parent: Async::Task.current)
48
+ parent.async do
49
+ queue.pop
50
+ end
51
+ end
52
+ ```
53
+
54
+ This expresses the intent to the caller that this method should only be invoked from within an asynchonous task. In addition, it allows the caller to substitute other parent objects, like semaphores or barriers, which can be useful for managing concurrency.
55
+
56
+ ## Use barriers to manage unbounded concurrency
57
+
58
+ Barriers provide a way to manage an unbounded number of tasks.
59
+
60
+ ```ruby
61
+ Async do
62
+ barrier = Async::Barrier.new
63
+
64
+ items.each do |item|
65
+ barrier.async do
66
+ process(item)
67
+ end
68
+ end
69
+
70
+ # Process the tasks in order of completion:
71
+ barrier.wait do |task|
72
+ result = task.wait
73
+ # Do something with result.
74
+
75
+ # If you don't want to wait for any more tasks you can break:
76
+ break
77
+ end
78
+
79
+ # Or just wait for all tasks to finish:
80
+ barrier.wait # May raise an exception if a task failed.
81
+ ensure
82
+ # Stop all outstanding tasks in the barrier:
83
+ barrier&.stop
84
+ end
85
+ ```
86
+
87
+ ## Use a semaphore to limit the number of concurrent tasks
88
+
89
+ Semaphores allow you to limit the level of concurrency to a fixed number of tasks:
90
+
91
+ ```ruby
92
+ Async do |task|
93
+ barrier = Async::Barrier.new
94
+ semaphore = Async::Semaphore.new(4, parent: barrier)
95
+
96
+ # Since the semaphore.async may block, we need to run the work scheduling in a child task:
97
+ task.async do
98
+ items.each do |item|
99
+ semaphore.async do
100
+ process(item)
101
+ end
102
+ end
103
+ end
104
+
105
+ # Wait for all the work to complete:
106
+ barrier.wait
107
+ ensure
108
+ # Stop all outstanding tasks in the barrier:
109
+ barrier&.stop
110
+ end
111
+ ```
112
+
113
+ In general, the barrier should be the root of your task hierarchy, and the semaphore should be a child of the barrier. This allows you to manage the lifetime of all tasks created by the semaphore, and ensures that all tasks are stopped when the barrier is stopped.
114
+
115
+ ### Idler
116
+
117
+ Idlers are like semaphores but with a limit defined by current processor utilization. In other words, an idler will do work up to a specific ratio of idle/busy time in the scheduler, and try to maintain that.
118
+
119
+ ```ruby
120
+ Async do
121
+ # Create an idler that will aim for a load average of 80%:
122
+ idler = Async::Idler.new(0.8)
123
+
124
+ # Some list of work to be done:
125
+ work.each do |work|
126
+ idler.async do
127
+ # Do the work:
128
+ work.call
129
+ end
130
+ end
131
+ end
132
+ ```
133
+
134
+ The idler will try to schedule as much work such that the load of the scheduler stays at around 80% saturation.
135
+
136
+ ## Use queues to share data between tasks
137
+
138
+ Queues allow you to share data between tasks without the risk of data corruption or deadlocks.
139
+
140
+ ```ruby
141
+ Async do |task|
142
+ queue = Async::Queue.new
143
+
144
+ reader = task.async do
145
+ while chunk = socket.gets
146
+ queue.push(chunk)
147
+ end
148
+ end
149
+ # After this point, we won't be able to add items to the queue, and popping items will eventually result in nil once all items are dequeued:
150
+ queue.close
151
+ end
152
+
153
+ # Process items from the queue:
154
+ while line = queue.pop
155
+ process(line)
156
+ end
157
+ end
158
+ ```
159
+
160
+ The above program may have unbounded memory use, so it can be a good idea to use a limited queue with back-pressure:
161
+
162
+ ```ruby
163
+ Async do |task|
164
+ queue = Async::LimitedQueue.new(8)
165
+
166
+ # Everything else is the same from the queue example, except that the pushing onto the queue will block once 8 items are buffered.
167
+ end
168
+ ```
169
+
170
+ ## Use timeouts for operations that might block forever
171
+
172
+ General timeouts can be imposed by using `task.with_timeout(duration)`.
173
+
174
+ ```ruby
175
+ Async do |task|
176
+ # This will raise an Async::TimeoutError after 1 second:
177
+ task.with_timeout(1) do |timeout|
178
+ # Timeout#duration= can be used to adjust the duration of the timeout.
179
+ # Timeout#cancel can be used to cancel the timeout completely.
180
+
181
+ sleep 10
182
+ end
183
+ end
184
+ ```
185
+
186
+ It can be especially important to impose timeouts when processing user-provided data.
187
+
188
+ ##
@@ -0,0 +1,63 @@
1
+ # Debugging
2
+
3
+ This guide explains how to debug issues with programs that use Async.
4
+
5
+ ## Debugging Techniques
6
+
7
+ ### Debugging with `puts`
8
+
9
+ The simplest way to debug an Async program is to use `puts` to print messages to the console. This is useful for understanding the flow of your program and the values of variables. However, it can be difficult to use `puts` to debug programs that use asynchronous code, as the output may be interleaved. To prevent this, wrap it in `Fiber.blocking{}`:
10
+
11
+ ```ruby
12
+ require 'async'
13
+
14
+ Async do
15
+ 3.times do |i|
16
+ sleep i
17
+ Fiber.blocking{puts "Slept for #{i} seconds."}
18
+ end
19
+ end
20
+ ```
21
+
22
+ Using `Fiber.blocking{}` prevents any context switching until the block is complete, ensuring that the output is not interleaved and that flow control is strictly sequential. You should not use `Fiber.blocking{}` in production code, as it will block the reactor.
23
+
24
+ ### Debugging with IRB
25
+
26
+ You can use IRB to debug your Async program. In some cases, you will want to stop the world and inspect the state of your program. You can do this by wrapping `binding.irb` inside a `Fiber.blocking{}` block:
27
+
28
+ ```ruby
29
+ Async do
30
+ 3.times do |i|
31
+ sleep i
32
+ # The event loop will stop at this point and you can inspect the state of your program.
33
+ Fiber.blocking{binding.irb}
34
+ end
35
+ end
36
+ ```
37
+
38
+ If you don't use `Fiber.blocking{}`, the event loop will continue to run and you will end up with three instances of `binding.irb` running.
39
+
40
+ ### Debugging with `Async::Debug`
41
+
42
+ The `async-debug` gem provides a visual debugger for Async programs. It is a powerful tool that allows you to inspect the state of your program and see the hierarchy of your program:
43
+
44
+ ```ruby
45
+ require 'async'
46
+ require 'async/debug'
47
+
48
+ Sync do
49
+ debugger = Async::Debug.serve
50
+
51
+ 3.times do
52
+ Async do |task|
53
+ while true
54
+ duration = rand
55
+ task.annotate("Sleeping for #{duration} second...")
56
+ sleep(duration)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ ```
62
+
63
+ When you run this program, it will start a web server on `http://localhost:9000`. You can open this URL in your browser to see the state of your program.
@@ -0,0 +1,177 @@
1
+ # Getting Started
2
+
3
+ This guide shows how to add async to your project and run code asynchronously.
4
+
5
+ Async is a Ruby library that provides asynchronous programming capabilities using fibers and a fiber scheduler. It allows you to write non-blocking, concurrent code that's easy to understand and maintain.
6
+
7
+ ## Installation
8
+
9
+ Add the gem to your project:
10
+
11
+ ~~~ bash
12
+ $ bundle add async
13
+ ~~~
14
+
15
+ ## Core Concepts
16
+
17
+ `async` has several core concepts:
18
+
19
+ - A {ruby Async::Task} instance which captures your sequential computations.
20
+ - A {ruby Async::Reactor} instance which implements the fiber scheduler interface and event loop.
21
+ - A {ruby Fiber} is an object which executes user code with cooperative concurrency, i.e. you can transfer execution from one fiber to another and back again.
22
+
23
+ ### What is a scheduler?
24
+
25
+ A scheduler is an interface which manages the execution of fibers. It is responsible for intercepting blocking operations and redirecting them to an event loop.
26
+
27
+ ### What is an event loop?
28
+
29
+ An event loop is part of the implementation of a scheduler which is responsible for waiting for events to occur, and waking up fibers when they are ready to run.
30
+
31
+ ### What is a selector?
32
+
33
+ A selector is part of the implementation of an event loop which is responsible for interacting with the operating system and waiting for specific events to occur. This is often referred to as "select"ing ready events from a set of file descriptors, but in practice has expanded to encompass a wide range of blocking operations.
34
+
35
+ ### What is a reactor?
36
+
37
+ A reactor is a specific implementation of the scheduler interface, which includes an event loop and selector, and is responsible for managing the execution of fibers.
38
+
39
+ ## Creating an Asynchronous Task
40
+
41
+ The main entry point for creating tasks is the {ruby Kernel#Async} method. Because this method is defined on `Kernel`, it's available in all parts of your program.
42
+
43
+ ~~~ ruby
44
+ require 'async'
45
+
46
+ Async do |task|
47
+ puts "Hello World!"
48
+ end
49
+ ~~~
50
+
51
+ A {ruby Async::Task} runs using a {ruby Fiber} and blocking operations e.g. `sleep`, `read`, `write` yield control until the operation can complete. When a blocking operation yields control, it means another fiber can execute, giving the illusion of simultaneous execution.
52
+
53
+ ### When should I use `Async`?
54
+
55
+ You should use `Async` when you desire explicit concurrency in your program. That means you want to run multiple tasks at the same time, and you want to be able to wait for the results of those tasks.
56
+
57
+ - You should use `Async` when you want to perform network operations concurrently, such as HTTP requests or database queries.
58
+ - You should use `Async` when you want to process independent requests concurrently, such as a web server.
59
+ - You should use `Async` when you want to handle multiple connections concurrently, such as a chat server.
60
+
61
+ You should consider the boundary around your program and the request handling. For example, one task per operation, request or connection, is usually appropriate.
62
+
63
+ ### Waiting for Results
64
+
65
+ Similar to a promise, {ruby Async::Task} produces results. In order to wait for these results, you must invoke {ruby Async::Task#wait}:
66
+
67
+ ``` ruby
68
+ require 'async'
69
+
70
+ task = Async do
71
+ rand
72
+ end
73
+
74
+ puts "The number was: #{task.wait}"
75
+ ```
76
+
77
+ ## Creating a Fiber Scheduler
78
+
79
+ The first (top level) async block will also create an instance of {ruby Async::Reactor} which is a subclass of {ruby Async::Scheduler} to handle the event loop. You can also do this directly using {ruby Fiber.set_scheduler}:
80
+
81
+ ~~~ ruby
82
+ require 'async/scheduler'
83
+
84
+ scheduler = Async::Scheduler.new
85
+ Fiber.set_scheduler(scheduler)
86
+
87
+ Fiber.schedule do
88
+ 1.upto(3) do |i|
89
+ Fiber.schedule do
90
+ sleep 1
91
+ puts "Hello World"
92
+ end
93
+ end
94
+ end
95
+ ~~~
96
+
97
+ ## Synchronous Execution in an existing Fiber Scheduler
98
+
99
+ Unless you need fan-out, map-reduce style concurrency, you can actually use a slightly more efficient {ruby Kernel::Sync} execution model. This method will run your block in the current event loop if one exists, or create an event loop if not. You can use it for code which uses asynchronous primitives, but itself does not need to be asynchronous with respect to other tasks.
100
+
101
+ ```ruby
102
+ require 'async/http/internet'
103
+
104
+ def fetch(url)
105
+ Sync do
106
+ internet = Async::HTTP::Internet.new
107
+ return internet.get(url).read
108
+ end
109
+ end
110
+
111
+ # At the level of your program, this method will create an event loop:
112
+ fetch(...)
113
+
114
+ Sync do
115
+ # The event loop already exists, and will be reused:
116
+ fetch(...)
117
+ end
118
+ ```
119
+
120
+ In other words, `Sync{...}` is very similar in behaviour to `Async{...}.wait`, but significantly more efficient.
121
+
122
+ ## Enforcing Embedded Execution
123
+
124
+ In some methods, you may want to implement a fan-out or map-reduce. That requires a parent scheduler. There are two ways you can do this:
125
+
126
+ ```ruby
127
+ def fetch_all(urls, parent: Async::Task.current)
128
+ urls.map do |url|
129
+ parent.async do
130
+ fetch(url)
131
+ end
132
+ end.map(&:wait)
133
+ end
134
+ ```
135
+
136
+ or:
137
+
138
+ ```ruby
139
+ def fetch_all(urls)
140
+ Sync do |parent|
141
+ urls.map do |url|
142
+ parent.async do
143
+ fetch(url)
144
+ end
145
+ end.map(&:wait)
146
+ end
147
+ end
148
+ ```
149
+
150
+ The former allows you to inject the parent, which could be a barrier or semaphore, while the latter will create a new parent scheduler if one does not exist. In both cases, you guarantee that the map operation will be executed in the parent task (of some sort).
151
+
152
+ ## Compatibility
153
+
154
+ The Fiber Scheduler interface is compatible with most pure Ruby code and well-behaved C code. For example, you can use {ruby Net::HTTP} for performing concurrent HTTP requests:
155
+
156
+ ```ruby
157
+ urls = [...]
158
+
159
+ Async do
160
+ # Perform several concurrent requests:
161
+ responses = urls.map do |url|
162
+ Async do
163
+ Net::HTTP.get(url)
164
+ end
165
+ end.map(&:wait)
166
+ end
167
+ ```
168
+
169
+ Unfortunately, some libraries do not integrate well with the fiber scheduler: either they are blocking, processor bound, or use thread locals for execution state. To use these libraries, you may be able to use a background thread.
170
+
171
+ ```ruby
172
+ Async do
173
+ result = Thread.new do
174
+ # Code which is otherwise unsafe...
175
+ end.value # Wait for the result of the thread, internally non-blocking.
176
+ end
177
+ ```
@@ -0,0 +1,29 @@
1
+ # Automatically generated context index for Utopia::Project guides.
2
+ # Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
3
+ ---
4
+ description: A concurrency framework for Ruby.
5
+ metadata:
6
+ documentation_uri: https://socketry.github.io/async/
7
+ funding_uri: https://github.com/sponsors/ioquatix/
8
+ source_code_uri: https://github.com/socketry/async.git
9
+ files:
10
+ - path: getting-started.md
11
+ title: Getting Started
12
+ description: This guide shows how to add async to your project and run code asynchronously.
13
+ - path: scheduler.md
14
+ title: Scheduler
15
+ description: This guide gives an overview of how the scheduler is implemented.
16
+ - path: tasks.md
17
+ title: Tasks
18
+ description: This guide explains how asynchronous tasks work and how to use them.
19
+ - path: best-practices.md
20
+ title: Best Practices
21
+ description: This guide gives an overview of best practices for using Async.
22
+ - path: debugging.md
23
+ title: Debugging
24
+ description: This guide explains how to debug issues with programs that use Async.
25
+ - path: thread-safety.md
26
+ title: Thread safety
27
+ description: This guide explains thread safety in Ruby, focusing on fibers and threads,
28
+ common pitfalls, and best practices to avoid problems like data corruption, race
29
+ conditions, and deadlocks.
@@ -0,0 +1,109 @@
1
+ # Scheduler
2
+
3
+ This guide gives an overview of how the scheduler is implemented.
4
+
5
+ ## Overview
6
+
7
+ The {ruby Async::Scheduler} uses an event loop to execute tasks. When tasks are waiting on blocking operations like IO, the scheduler will use the operating system's native event system to wait for the operation to complete. This allows the scheduler to efficiently handle many tasks.
8
+
9
+ ### Tasks
10
+
11
+ Tasks are the building blocks of concurrent programs. They are lightweight and can be scheduled by the event loop. Tasks can be nested, and the parent task is used to determine the current reactor. Tasks behave like promises, in the sense you can wait on them to complete, and they might fail with an exception.
12
+
13
+ ~~~ ruby
14
+ require 'async'
15
+
16
+ def sleepy(duration, task: Async::Task.current)
17
+ task.async do |subtask|
18
+ subtask.annotate "I'm going to sleep #{duration}s..."
19
+ sleep duration
20
+ puts "I'm done sleeping!"
21
+ end
22
+ end
23
+
24
+ def nested_sleepy(task: Async::Task.current)
25
+ task.async do |subtask|
26
+ subtask.annotate "Invoking sleepy 5 times..."
27
+ 5.times do |index|
28
+ sleepy(index, task: subtask)
29
+ end
30
+ end
31
+ end
32
+
33
+ Async do |task|
34
+ task.annotate "Invoking nested_sleepy..."
35
+ subtask = nested_sleepy
36
+
37
+ # Print out all running tasks in a tree:
38
+ task.print_hierarchy($stderr)
39
+
40
+ # Kill the subtask
41
+ subtask.stop
42
+ end
43
+ ~~~
44
+
45
+ ### Thread Safety
46
+
47
+ Most methods of the reactor and related tasks are not thread-safe, so you'd typically have [one reactor per thread or process](https://github.com/socketry/async-container).
48
+
49
+ ### Embedding Schedulers
50
+
51
+ {ruby Async::Scheduler#run} will run until the reactor runs out of work to do. To run a single iteration of the reactor, use {ruby Async::Scheduler#run_once}.
52
+
53
+ ~~~ ruby
54
+ require 'async'
55
+
56
+ Console.logger.debug!
57
+ reactor = Async::Scheduler.new
58
+
59
+ # Run the reactor for 1 second:
60
+ reactor.async do |task|
61
+ sleep 1
62
+ puts "Finished!"
63
+ end
64
+
65
+ while reactor.run_once
66
+ # Round and round we go!
67
+ end
68
+ ~~~
69
+
70
+ You can use this approach to embed the reactor in another event loop. For some integrations, you may want to specify the maximum time to wait to {ruby Async::Scheduler#run_once}.
71
+
72
+ ### Stopping a Scheduler
73
+
74
+ {ruby Async::Scheduler#stop} will stop the current scheduler and all children tasks.
75
+
76
+ ### Fiber Scheduler Integration
77
+
78
+ In order to integrate with native Ruby blocking operations, the {ruby Async::Scheduler} uses a {ruby Fiber::Scheduler} interface.
79
+
80
+ ```ruby
81
+ require 'async'
82
+
83
+ scheduler = Async::Scheduler.new
84
+ Fiber.set_scheduler(scheduler)
85
+
86
+ Fiber.schedule do
87
+ puts "Hello World!"
88
+ end
89
+ ```
90
+
91
+ ## Design
92
+
93
+ ### Optimistic vs Pessimistic Scheduling
94
+
95
+ There are two main strategies for scheduling tasks: optimistic and pessimistic. An optimistic scheduler is usually greedy and will try to execute tasks as soon as they are scheduled using a direct transfer of control flow. A pessimistic scheduler will schedule tasks into the event loop ready list and will only execute them on the next iteration of the event loop.
96
+
97
+ ```ruby
98
+ Async do
99
+ puts "Hello "
100
+
101
+ Async do
102
+ puts "World"
103
+ end
104
+
105
+ puts "!"
106
+ end
107
+ ```
108
+
109
+ An optimstic scheduler will print "Hello World!", while a pessimistic scheduler will print "Hello !World". In practice you should not design your code to rely on the order of execution, but it's important to understand the difference. It is an unspecifed implementation detail of the scheduler.