async 2.39.0 → 2.44.1

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: 1d8427e86e8ab22c81e41dd6e7df7bd79087e3fa72ce786f07ae7b65d9d94545
4
- data.tar.gz: 30e9e7e88b211aa21afe844e3f9510b5af39d2b4af1d97710ce533f581b5c57b
3
+ metadata.gz: 0a98ba175586bacb13a3f00ccb770083208e4c8381cbaf17ce4f59fdfc56a85a
4
+ data.tar.gz: 6a8614f33cbfa4230e66e12750308a1a339d9a2d6fe8a624b015498c5bc60928
5
5
  SHA512:
6
- metadata.gz: 337694b65547afc0d4c1568a02c0c08d03d0d70a8b2319b9b9a77660ad9f37c82ff3ab49a3856ebee477657df6b254d62e59ca6b31e9312484c02e696deab007
7
- data.tar.gz: e9bc3a1a27f9ba9ea6004dee40df9b385d7959c30e7059521805e91d1f64e9ed16bbc0920b4eb3b1dae8a727ab52ed8258f95bb2605e219f311920bf3fe2b031
6
+ metadata.gz: 0d3c4cd7e58f0eb601e37ffbf8d415c6b66349f268f2b9bec37ed4a945744409ecdb21f13e101cacba2794ea1dd3e6dd3be7a5cf7ca6cc5e67cd557bd65cd11e
7
+ data.tar.gz: dc2c1d73dc53df633260e279a20f8cbcb4c40c94b362fc383495e020f750807624c2de5bff446732585b5e2b4121b0ec382de65983db3af23301e937b96b4074
checksums.yaml.gz.sig CHANGED
Binary file
@@ -74,6 +74,42 @@ end
74
74
  puts "The number was: #{task.wait}"
75
75
  ```
76
76
 
77
+ ### Comparing `Async` and `Sync`
78
+
79
+ If you are familiar with JavaScript's `async`/`await`, `Async{...}` is similar to calling an asynchronous function: it starts the work and returns a promise-like {ruby Async::Task} that you can wait on later.
80
+
81
+ ```ruby
82
+ task = Async do
83
+ bar
84
+ end
85
+
86
+ task.wait # Returns the value of bar.
87
+ ```
88
+
89
+ This is similar to JavaScript code that keeps the promise:
90
+
91
+ ```javascript
92
+ const promise = bar();
93
+
94
+ await promise; // Returns the value of bar.
95
+ ```
96
+
97
+ `Sync{...}` is similar to immediately awaiting that work: it runs the block in an event loop and returns the block's value directly.
98
+
99
+ ```ruby
100
+ result = Sync do
101
+ bar
102
+ end
103
+ ```
104
+
105
+ This is similar to:
106
+
107
+ ```javascript
108
+ const result = await bar();
109
+ ```
110
+
111
+ The main difference is that JavaScript starts with an event loop already available, while Ruby code needs one to be provided by a fiber scheduler. `Sync{...}` is the usual way to create or reuse that event loop when the caller wants a direct return value instead of a task.
112
+
77
113
  ## Creating a Fiber Scheduler
78
114
 
79
115
  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}:
data/context/index.yaml CHANGED
@@ -3,6 +3,8 @@
3
3
  ---
4
4
  description: A concurrency framework for Ruby.
5
5
  metadata:
6
+ bug_tracker_uri: https://github.com/socketry/async/issues
7
+ changelog_uri: https://github.com/socketry/async/blob/main/releases.md
6
8
  documentation_uri: https://socketry.github.io/async/
7
9
  funding_uri: https://github.com/sponsors/ioquatix/
8
10
  source_code_uri: https://github.com/socketry/async.git
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2017-2025, by Samuel Williams.
4
+ # Copyright, 2017-2026, by Samuel Williams.
5
5
  # Copyright, 2017, by Kent Gruber.
6
+ # Copyright, 2026, by Robert Mosolgo.
6
7
 
7
8
  require "fiber"
8
9
  require_relative "list"
@@ -32,6 +33,11 @@ module Async
32
33
  !self.empty?
33
34
  end
34
35
 
36
+ # @returns [Integer] Number of fibers waiting on this condition.
37
+ def waiting_count
38
+ @ready.num_waiting
39
+ end
40
+
35
41
  # Signal to a given task that it should resume operations.
36
42
  # @parameter value [Object | Nil] The value to return to the waiting fibers.
37
43
  def signal(value = nil)
data/lib/async/node.rb CHANGED
@@ -5,6 +5,7 @@
5
5
  # Copyright, 2017, by Kent Gruber.
6
6
  # Copyright, 2022, by Shannon Skipper.
7
7
  # Copyright, 2025-2026, by Shopify Inc.
8
+ # Copyright, 2026, by Kyle Tate.
8
9
 
9
10
  require "fiber/annotation"
10
11
 
@@ -283,9 +284,10 @@ module Async
283
284
  # Attempt to cancel the current node immediately, including all non-transient children. Invokes {#stop_children} to cancel all children.
284
285
  #
285
286
  # @parameter later [Boolean] Whether to defer cancelling until some point in the future.
286
- def cancel(later = false)
287
+ # @parameter cause [Exception | Nil] The cause of the cancel operation.
288
+ def cancel(later = false, cause: $!)
287
289
  # The implementation of this method may defer calling `stop_children`.
288
- stop_children(later)
290
+ stop_children(later, cause: cause)
289
291
  end
290
292
 
291
293
  # Backward compatibility alias for {#cancel}.
@@ -295,9 +297,9 @@ module Async
295
297
  end
296
298
 
297
299
  # Attempt to stop all non-transient children.
298
- private def stop_children(later = false)
300
+ private def stop_children(later = false, cause:)
299
301
  @children&.each do |child|
300
- child.cancel(later) unless child.transient?
302
+ child.cancel(later, cause: cause) unless child.transient?
301
303
  end
302
304
  end
303
305
 
data/lib/async/promise.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2025, by Shopify Inc.
5
5
  # Copyright, 2025-2026, by Samuel Williams.
6
+ # Copyright, 2026, by Kyle Tate.
6
7
 
7
8
  require_relative "error"
8
9
  require_relative "deadline"
@@ -98,7 +99,9 @@ module Async
98
99
  end
99
100
 
100
101
  # Wait with deadline tracking:
101
- until @resolved
102
+ loop do
103
+ break if @resolved
104
+
102
105
  # Get remaining time for this wait iteration:
103
106
  remaining = deadline.remaining
104
107
 
@@ -5,6 +5,7 @@
5
5
  # Copyright, 2020, by Jun Jiang.
6
6
  # Copyright, 2021, by Julien Portalier.
7
7
  # Copyright, 2025-2026, by Shopify Inc.
8
+ # Copyright, 2026, by Kyle Tate.
8
9
 
9
10
  require_relative "clock"
10
11
  require_relative "task"
@@ -514,23 +515,40 @@ module Async
514
515
  # Cancel all children, including transient children.
515
516
  #
516
517
  # @public Since *Async v1*.
517
- def cancel
518
+ def cancel(later = false, cause: $!)
519
+ if @children and !cause
520
+ cause = Cancel::Cause.for("Cancelling task!")
521
+ end
522
+
518
523
  @children&.each do |child|
519
- child.cancel
524
+ child.cancel(later, cause: cause)
520
525
  end
521
526
  end
522
527
 
523
528
  # Backward compatibility alias for cancel.
524
- def stop
525
- cancel
529
+ def stop(...)
530
+ cancel(...)
526
531
  end
527
532
 
528
- private def run_loop(&block)
533
+ # Run the event loop, until the scheduler is interrupted or finished.
534
+ #
535
+ # @parameter initial [Proc | Nil] The initial block to execute before the first loop iteration.
536
+ # @yields {...} The block to execute on each loop iteration.
537
+ # @raises {Interrupt} If the scheduler is interrupted.
538
+ private def run_loop(initial = nil, &block)
529
539
  interrupt = nil
530
540
 
531
541
  begin
532
542
  # In theory, we could use Exception here to be a little bit safer, but we've only shown the case for SignalException to be a problem, so let's not over-engineer this.
533
543
  Thread.handle_interrupt(::SignalException => :never) do
544
+ if initial
545
+ begin
546
+ initial.call
547
+ ensure
548
+ initial = nil
549
+ end
550
+ end
551
+
534
552
  until self.interrupted?
535
553
  # If we are finished, we need to exit:
536
554
  break unless yield
@@ -570,9 +588,15 @@ module Async
570
588
  begin
571
589
  @profiler&.start
572
590
 
573
- initial_task = self.async(...) if block_given?
591
+ initial_task = nil
574
592
 
575
- self.run_loop do
593
+ if block_given?
594
+ initial = proc do
595
+ initial_task = self.async(...)
596
+ end
597
+ end
598
+
599
+ self.run_loop(initial) do
576
600
  run_once
577
601
  end
578
602
 
@@ -661,6 +685,7 @@ module Async
661
685
  @children = nil
662
686
  @selector = nil
663
687
  @timers = nil
688
+ @blocked = 0
664
689
 
665
690
  # Close the scheduler:
666
691
  Fiber.set_scheduler(nil)
data/lib/async/task.rb CHANGED
@@ -8,6 +8,7 @@
8
8
  # Copyright, 2023, by Math Ieu.
9
9
  # Copyright, 2025, by Shigeru Nakajima.
10
10
  # Copyright, 2025-2026, by Shopify Inc.
11
+ # Copyright, 2026, by Kyle Tate.
11
12
 
12
13
  require "fiber"
13
14
  require "console"
@@ -333,7 +334,7 @@ module Async
333
334
 
334
335
  if self.cancelled?
335
336
  # If the task is already cancelled, a `cancel` state transition re-enters the same state which is a no-op. However, we will also attempt to cancel any running children too. This can happen if the children did not cancel correctly the first time around. Doing this should probably be considered a bug, but it's better to be safe than sorry.
336
- return cancelled!
337
+ return cancelled!(cause)
337
338
  end
338
339
 
339
340
  # If the fiber is alive, we need to cancel it:
@@ -369,7 +370,7 @@ module Async
369
370
  end
370
371
  else
371
372
  # We are not running, but children might be, so transition directly into cancelled state:
372
- cancel!
373
+ cancel!(cause)
373
374
  end
374
375
  end
375
376
 
@@ -479,7 +480,7 @@ module Async
479
480
  @promise.reject(exception)
480
481
  end
481
482
 
482
- def cancelled!
483
+ def cancelled!(cause = $!)
483
484
  # Console.info(self, status:) {"Task #{self} was cancelled with #{@children&.size.inspect} children!"}
484
485
 
485
486
  # Cancel the promise, specify nil here so that no exception is raised when waiting on the promise:
@@ -489,15 +490,16 @@ module Async
489
490
 
490
491
  begin
491
492
  # We are not running, but children might be so we should stop them:
492
- stop_children(true)
493
- rescue Cancel
493
+ stop_children(true, cause: cause)
494
+ rescue Cancel => exception
494
495
  cancelled = true
496
+ cause ||= exception.cause || exception
495
497
  # If we are cancelling children, and one of them tries to cancel the current task, we should ignore it. We will be cancelled later.
496
498
  retry
497
499
  end
498
500
 
499
501
  if cancelled
500
- raise Cancel, "Cancelling current task!"
502
+ raise Cancel, "Cancelling current task!", cause: cause
501
503
  end
502
504
  end
503
505
 
@@ -505,8 +507,8 @@ module Async
505
507
  cancelled!
506
508
  end
507
509
 
508
- def cancel!
509
- cancelled!
510
+ def cancel!(cause = $!)
511
+ cancelled!(cause)
510
512
 
511
513
  finish!
512
514
  end
@@ -519,8 +521,8 @@ module Async
519
521
  @fiber = Fiber.new(annotation: self.annotation) do
520
522
  begin
521
523
  completed!(yield)
522
- rescue Cancel
523
- cancelled!
524
+ rescue Cancel => exception
525
+ cancelled!(exception.cause || exception)
524
526
  rescue StandardError => error
525
527
  failed!(error)
526
528
  rescue Exception => exception
data/lib/async/version.rb CHANGED
@@ -5,5 +5,5 @@
5
5
 
6
6
  # @namespace
7
7
  module Async
8
- VERSION = "2.39.0"
8
+ VERSION = "2.44.1"
9
9
  end
data/lib/kernel/async.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2026, by Samuel Williams.
5
+ # Copyright, 2026, by Shopify Inc.
5
6
 
6
7
  require_relative "../async/reactor"
7
8
 
@@ -27,13 +28,15 @@ module Kernel
27
28
  elsif scheduler = Fiber.scheduler
28
29
  ::Async::Task.run(scheduler, ...)
29
30
  else
30
- # This calls Fiber.set_scheduler(self):
31
- reactor = ::Async::Reactor.new
32
-
33
- begin
34
- return reactor.run(...)
35
- ensure
36
- Fiber.set_scheduler(nil)
31
+ Fiber.blocking do
32
+ # This calls Fiber.set_scheduler(self):
33
+ reactor = ::Async::Reactor.new
34
+
35
+ begin
36
+ return reactor.run(...)
37
+ ensure
38
+ Fiber.set_scheduler(nil)
39
+ end
37
40
  end
38
41
  end
39
42
  end
data/lib/kernel/sync.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2025, by Samuel Williams.
4
+ # Copyright, 2019-2026, by Samuel Williams.
5
5
  # Copyright, 2020, by Brian Morearty.
6
6
  # Copyright, 2024, by Patrik Wenger.
7
- # Copyright, 2025, by Shopify Inc.
7
+ # Copyright, 2025-2026, by Shopify Inc.
8
8
 
9
9
  require_relative "../async/reactor"
10
10
 
@@ -27,16 +27,16 @@ module Kernel
27
27
  elsif scheduler = Fiber.scheduler
28
28
  ::Async::Task.run(scheduler, &block).wait
29
29
  else
30
- # This calls Fiber.set_scheduler(self):
31
- reactor = Async::Reactor.new
32
-
33
- begin
34
- # Use finished: false to suppress warnings since we're handling exceptions explicitly
35
- task = reactor.async(annotation: annotation, finished: false, &block)
36
- reactor.run
37
- return task.wait
38
- ensure
39
- Fiber.set_scheduler(nil)
30
+ Fiber.blocking do
31
+ # This calls Fiber.set_scheduler(self):
32
+ reactor = Async::Reactor.new
33
+
34
+ begin
35
+ # Use finished: false to suppress warnings since we're handling exceptions explicitly
36
+ return reactor.run(annotation: annotation, finished: false, &block).wait
37
+ ensure
38
+ Fiber.set_scheduler(nil)
39
+ end
40
40
  end
41
41
  end
42
42
  end
data/license.md CHANGED
@@ -36,6 +36,8 @@ Copyright, 2025, by Josh Teeter.
36
36
  Copyright, 2025, by Jatin Goyal.
37
37
  Copyright, 2025, by Yuhi Sato.
38
38
  Copyright, 2026, by Tavian Barnes.
39
+ Copyright, 2026, by Robert Mosolgo.
40
+ Copyright, 2026, by Kyle Tate.
39
41
 
40
42
  Permission is hereby granted, free of charge, to any person obtaining a copy
41
43
  of this software and associated documentation files (the "Software"), to deal
data/readme.md CHANGED
@@ -35,6 +35,26 @@ Please see the [project documentation](https://socketry.github.io/async/) for mo
35
35
 
36
36
  Please see the [project releases](https://socketry.github.io/async/releases/index) for all releases.
37
37
 
38
+ ### v2.44.0
39
+
40
+ - Fixed scheduler cleanup after forking while other fibers are blocked.
41
+
42
+ ### v2.43.0
43
+
44
+ - Propagate cancellation causes through task trees so child tasks observe the original cancellation cause.
45
+
46
+ ### v2.42.0
47
+
48
+ - `Sync` and `Async` can now be invoked from a non-blocking fiber that has no scheduler (e.g. inside an `Enumerator` or a bare `Fiber.new`). Previously this raised `RuntimeError: Running scheduler on non-blocking fiber!`. The reactor is now run within `Fiber.blocking`, so the scheduler always runs on a blocking fiber.
49
+
50
+ ### v2.41.0
51
+
52
+ - **Fixed**: Protect initial task from Interrupt exceptions.
53
+
54
+ ### v2.40.0
55
+
56
+ - Introduce `Async::Condition#waiting_count`. This allows you to see how many tasks are currently waiting on the condition, which can be useful for debugging and monitoring purposes.
57
+
38
58
  ### v2.39.0
39
59
 
40
60
  - `Async::Barrier#wait` now returns the number of tasks that were waited for, or `nil` if there were no tasks to wait for. This provides better feedback about the operation, and allows you to know how many tasks were involved in the wait.
@@ -58,27 +78,6 @@ Please see the [project releases](https://socketry.github.io/async/releases/inde
58
78
  - Introduce `Task#wait_all` which recursively waits for all children and self, excepting the current task.
59
79
  - Introduce `Task#join` as an alias for `Task#wait` for compatibility with `Thread#join` and similar interfaces.
60
80
 
61
- ### v2.35.3
62
-
63
- - `Async::Clock` now implements `#as_json` and `#to_json` for nicer log formatting.
64
-
65
- ### v2.35.2
66
-
67
- - Improved handling of `Process.fork` on Ruby 4+.
68
- - Improve `@promise` state handling in `Task#initialize`, preventing incomplete instances being visible to the scheduler.
69
-
70
- ### v2.35.1
71
-
72
- - Fix incorrect handling of spurious wakeups in `Async::Promise#wait`, which could lead to premature (incorrect) resolution of the promise.
73
-
74
- ### v2.35.0
75
-
76
- - `Process.fork` is now properly handled by the Async fiber scheduler, ensuring that the scheduler state is correctly reset in the child process after a fork. This prevents issues where the child process inherits the scheduler state from the parent, which could lead to unexpected behavior.
77
-
78
- ### v2.34.0
79
-
80
- - [`Kernel::Barrier` Convenience Interface](https://socketry.github.io/async/releases/index#kernel::barrier-convenience-interface)
81
-
82
81
  ## See Also
83
82
 
84
83
  - [async-http](https://github.com/socketry/async-http) — Asynchronous HTTP client/server.
@@ -97,6 +96,22 @@ We welcome contributions to this project.
97
96
  4. Push to the branch (`git push origin my-new-feature`).
98
97
  5. Create new Pull Request.
99
98
 
99
+ ### Running Tests
100
+
101
+ To run the test suite:
102
+
103
+ ``` shell
104
+ bundle exec sus
105
+ ```
106
+
107
+ ### Making Releases
108
+
109
+ To make a new release:
110
+
111
+ ``` shell
112
+ bundle exec bake gem:release:patch # or minor or major
113
+ ```
114
+
100
115
  ### Developer Certificate of Origin
101
116
 
102
117
  In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
data/releases.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Releases
2
2
 
3
+ ## v2.44.0
4
+
5
+ - Fixed scheduler cleanup after forking while other fibers are blocked.
6
+
7
+ ## v2.43.0
8
+
9
+ - Propagate cancellation causes through task trees so child tasks observe the original cancellation cause.
10
+
11
+ ## v2.42.0
12
+
13
+ - `Sync` and `Async` can now be invoked from a non-blocking fiber that has no scheduler (e.g. inside an `Enumerator` or a bare `Fiber.new`). Previously this raised `RuntimeError: Running scheduler on non-blocking fiber!`. The reactor is now run within `Fiber.blocking`, so the scheduler always runs on a blocking fiber.
14
+
15
+ ## v2.41.0
16
+
17
+ - **Fixed**: Protect initial task from Interrupt exceptions.
18
+
19
+ ## v2.40.0
20
+
21
+ - Introduce `Async::Condition#waiting_count`. This allows you to see how many tasks are currently waiting on the condition, which can be useful for debugging and monitoring purposes.
22
+
3
23
  ## v2.39.0
4
24
 
5
25
  - `Async::Barrier#wait` now returns the number of tasks that were waited for, or `nil` if there were no tasks to wait for. This provides better feedback about the operation, and allows you to know how many tasks were involved in the wait.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.39.0
4
+ version: 2.44.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -26,11 +26,13 @@ authors:
26
26
  - Julien Portalier
27
27
  - Jun Jiang
28
28
  - Ken Muryoi
29
+ - Kyle Tate
29
30
  - Leon Löchner
30
31
  - Mark Montroy
31
32
  - Masafumi Okura
32
33
  - Masayuki Yamamoto
33
34
  - Math Ieu
35
+ - Robert Mosolgo
34
36
  - Ryan Musgrave
35
37
  - Salim Semaoune
36
38
  - Shannon Skipper
@@ -115,34 +117,6 @@ dependencies:
115
117
  - - "~>"
116
118
  - !ruby/object:Gem::Version
117
119
  version: '1.11'
118
- - !ruby/object:Gem::Dependency
119
- name: metrics
120
- requirement: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - "~>"
123
- - !ruby/object:Gem::Version
124
- version: '0.12'
125
- type: :runtime
126
- prerelease: false
127
- version_requirements: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - "~>"
130
- - !ruby/object:Gem::Version
131
- version: '0.12'
132
- - !ruby/object:Gem::Dependency
133
- name: traces
134
- requirement: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - "~>"
137
- - !ruby/object:Gem::Version
138
- version: '0.18'
139
- type: :runtime
140
- prerelease: false
141
- version_requirements: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - "~>"
144
- - !ruby/object:Gem::Version
145
- version: '0.18'
146
120
  executables: []
147
121
  extensions: []
148
122
  extra_rdoc_files: []
@@ -199,6 +173,8 @@ homepage: https://github.com/socketry/async
199
173
  licenses:
200
174
  - MIT
201
175
  metadata:
176
+ bug_tracker_uri: https://github.com/socketry/async/issues
177
+ changelog_uri: https://github.com/socketry/async/blob/main/releases.md
202
178
  documentation_uri: https://socketry.github.io/async/
203
179
  funding_uri: https://github.com/sponsors/ioquatix/
204
180
  source_code_uri: https://github.com/socketry/async.git
@@ -216,7 +192,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
216
192
  - !ruby/object:Gem::Version
217
193
  version: '0'
218
194
  requirements: []
219
- rubygems_version: 3.6.9
195
+ rubygems_version: 4.0.10
220
196
  specification_version: 4
221
197
  summary: A concurrency framework for Ruby.
222
198
  test_files: []
metadata.gz.sig CHANGED
Binary file