async 2.21.0 → 2.21.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: 4a5bc2e233137c26a571123a0edc0cd22fe32278f780fe801c166a2c7968dc83
4
- data.tar.gz: 2c2aab55da8cfe72e2806a6bb1e4a18e3b20d7162ff929bcb91de019eb9c6da3
3
+ metadata.gz: '0821852f6bb8e8b82506934a6ad8e921270f121636453303a2fd6409cacd83e2'
4
+ data.tar.gz: 56992a10ff23bcba8dfa535f7c134ac2870a10dd62f7776ea9e109d5e14e0b05
5
5
  SHA512:
6
- metadata.gz: 9f3490b820e58aa4e6a3a95051bc36b699903f617918832f557b4a3369c7101a9c36cf878b34ab274caa1379591c30fef0f56cac79dbf1da482b16472c9cde1d
7
- data.tar.gz: 8c911bdb5a50dadc299ee4d7ed0cd6d16371b84fe622fb1542190fc5a586f914dca4a5396de481f826c8606d547df831357f783f3ee67da3de18e49dd5ad7dd6
6
+ metadata.gz: 17791af587852dbb46ab3cb6b5584c10d7073997d1ba21eabed248fe1dc6a9107902ac34cf8741efacdfa01e87d57aa515f22635310fa35697ee56acd5cd03ee
7
+ data.tar.gz: e0a7fc1d5cd13632ceaa93e9ccb41ff89ec38648d8ca6d5eb2831ea39cb014d2fb8046c3226d94d7101a5d289cecaff338fcf65c440627ae76cdd44a93e624a6
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ # The implementation lives in `queue.rb` but later we may move it here for better autoload/inference.
7
+ require_relative "queue"
data/lib/async/queue.rb CHANGED
@@ -45,7 +45,9 @@ module Async
45
45
  end
46
46
 
47
47
  # Compatibility with {::Queue#push}.
48
- alias << push
48
+ def <<(item)
49
+ self.push(item)
50
+ end
49
51
 
50
52
  # Add multiple items to the queue.
51
53
  def enqueue(*items)
@@ -64,7 +66,9 @@ module Async
64
66
  end
65
67
 
66
68
  # Compatibility with {::Queue#pop}.
67
- alias pop dequeue
69
+ def pop
70
+ self.dequeue
71
+ end
68
72
 
69
73
  # Process each item in the queue.
70
74
  #
@@ -125,7 +129,7 @@ module Async
125
129
  # If the queue is full, this method will block until there is space available.
126
130
  #
127
131
  # @parameter item [Object] The item to add to the queue.
128
- def <<(item)
132
+ def push(item)
129
133
  while limited?
130
134
  @full.wait
131
135
  end
@@ -7,6 +7,7 @@
7
7
 
8
8
  require_relative "clock"
9
9
  require_relative "task"
10
+ require_relative "worker_pool"
10
11
 
11
12
  require "io/event"
12
13
 
@@ -16,6 +17,10 @@ require "resolv"
16
17
  module Async
17
18
  # Handles scheduling of fibers. Implements the fiber scheduler interface.
18
19
  class Scheduler < Node
20
+ DEFAULT_WORKER_POOL = ENV.fetch("ASYNC_SCHEDULER_DEFAULT_WORKER_POOL", nil).then do |value|
21
+ value == "true" ? true : nil
22
+ end
23
+
19
24
  # Raised when an operation is attempted on a closed scheduler.
20
25
  class ClosedError < RuntimeError
21
26
  # Create a new error.
@@ -37,7 +42,7 @@ module Async
37
42
  # @public Since *Async v1*.
38
43
  # @parameter parent [Node | Nil] The parent node to use for task hierarchy.
39
44
  # @parameter selector [IO::Event::Selector] The selector to use for event handling.
40
- def initialize(parent = nil, selector: nil)
45
+ def initialize(parent = nil, selector: nil, worker_pool: DEFAULT_WORKER_POOL)
41
46
  super(parent)
42
47
 
43
48
  @selector = selector || ::IO::Event::Selector.new(Fiber.current)
@@ -49,6 +54,15 @@ module Async
49
54
  @idle_time = 0.0
50
55
 
51
56
  @timers = ::IO::Event::Timers.new
57
+ if worker_pool == true
58
+ @worker_pool = WorkerPool.new
59
+ else
60
+ @worker_pool = worker_pool
61
+ end
62
+
63
+ if @worker_pool
64
+ self.singleton_class.prepend(WorkerPool::BlockingOperationWait)
65
+ end
52
66
  end
53
67
 
54
68
  # Compute the scheduler load according to the busy and idle times that are updated by the run loop.
@@ -112,6 +126,11 @@ module Async
112
126
 
113
127
  selector&.close
114
128
 
129
+ worker_pool = @worker_pool
130
+ @worker_pool = nil
131
+
132
+ worker_pool&.close
133
+
115
134
  consume
116
135
  end
117
136
 
@@ -169,8 +188,11 @@ module Async
169
188
 
170
189
  # Invoked when a fiber tries to perform a blocking operation which cannot continue. A corresponding call {unblock} must be performed to allow this fiber to continue.
171
190
  #
172
-
191
+ # @public Since *Async v2*.
173
192
  # @asynchronous May only be called on same thread as fiber scheduler.
193
+ #
194
+ # @parameter blocker [Object] The object that is blocking the fiber.
195
+ # @parameter timeout [Float | Nil] The maximum time to block, or if nil, indefinitely.
174
196
  def block(blocker, timeout)
175
197
  # $stderr.puts "block(#{blocker}, #{Fiber.current}, #{timeout})"
176
198
  fiber = Fiber.current
@@ -338,25 +360,6 @@ module Async
338
360
  return @selector.process_wait(Fiber.current, pid, flags)
339
361
  end
340
362
 
341
- # Wait for the given work to be executed.
342
- #
343
- # @public Since *Async v2.19* and *Ruby v3.4*.
344
- # @asynchronous May be non-blocking.
345
- #
346
- # @parameter work [Proc] The work to execute on a background thread.
347
- # @returns [Object] The result of the work.
348
- def blocking_operation_wait(work)
349
- thread = Thread.new(&work)
350
-
351
- result = thread.join
352
-
353
- thread = nil
354
-
355
- return result
356
- ensure
357
- thread&.kill
358
- end
359
-
360
363
  # Run one iteration of the event loop.
361
364
  #
362
365
  # When terminating the event loop, we already know we are finished. So we don't need to check the task tree. This is a logical requirement because `run_once` ignores transient tasks. For example, a single top level transient task is not enough to keep the reactor running, but during termination we must still process it in order to terminate child tasks.
data/lib/async/task.rb CHANGED
@@ -181,7 +181,10 @@ module Async
181
181
  @status == :completed
182
182
  end
183
183
 
184
- alias complete? completed?
184
+ # Alias for {#completed?}.
185
+ def complete?
186
+ self.completed?
187
+ end
185
188
 
186
189
  # @attribute [Symbol] The status of the execution of the task, one of `:initialized`, `:running`, `:complete`, `:stopped` or `:failed`.
187
190
  attr :status
@@ -31,7 +31,10 @@ module Async
31
31
  condition.signal(value)
32
32
  end
33
33
 
34
- alias value= resolve
34
+ # Alias for {#resolve}.
35
+ def value=(value)
36
+ self.resolve(value)
37
+ end
35
38
 
36
39
  # Whether the value has been resolved.
37
40
  #
@@ -48,6 +51,9 @@ module Async
48
51
  return @value
49
52
  end
50
53
 
51
- alias value wait
54
+ # Alias for {#wait}.
55
+ def value
56
+ self.wait
57
+ end
52
58
  end
53
59
  end
data/lib/async/version.rb CHANGED
@@ -4,5 +4,5 @@
4
4
  # Copyright, 2017-2024, by Samuel Williams.
5
5
 
6
6
  module Async
7
- VERSION = "2.21.0"
7
+ VERSION = "2.21.2"
8
8
  end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require "etc"
7
+
8
+ module Async
9
+ # A simple work pool that offloads work to a background thread.
10
+ #
11
+ # @private
12
+ class WorkerPool
13
+ # Used to augment the scheduler to add support for blocking operations.
14
+ module BlockingOperationWait
15
+ # Wait for the given work to be executed.
16
+ #
17
+ # @public Since *Async v2.19* and *Ruby v3.4*.
18
+ # @asynchronous May be non-blocking.
19
+ #
20
+ # @parameter work [Proc] The work to execute on a background thread.
21
+ # @returns [Object] The result of the work.
22
+ def blocking_operation_wait(work)
23
+ @worker_pool.call(work)
24
+ end
25
+ end
26
+
27
+ # Execute the given work in a background thread.
28
+ class Promise
29
+ # Create a new promise.
30
+ #
31
+ # @parameter work [Proc] The work to be done.
32
+ def initialize(work)
33
+ @work = work
34
+ @state = :pending
35
+ @value = nil
36
+ @guard = ::Mutex.new
37
+ @condition = ::ConditionVariable.new
38
+ @thread = nil
39
+ end
40
+
41
+ # Execute the work and resolve the promise.
42
+ def call
43
+ work = nil
44
+
45
+ @guard.synchronize do
46
+ @thread = ::Thread.current
47
+
48
+ return unless work = @work
49
+ end
50
+
51
+ resolve(work.call)
52
+ rescue Exception => error
53
+ reject(error)
54
+ end
55
+
56
+ private def resolve(value)
57
+ @guard.synchronize do
58
+ @work = nil
59
+ @thread = nil
60
+ @value = value
61
+ @state = :resolved
62
+ @condition.broadcast
63
+ end
64
+ end
65
+
66
+ private def reject(error)
67
+ @guard.synchronize do
68
+ @work = nil
69
+ @thread = nil
70
+ @value = error
71
+ @state = :failed
72
+ @condition.broadcast
73
+ end
74
+ end
75
+
76
+ # Cancel the work and raise an exception in the background thread.
77
+ def cancel
78
+ return unless @work
79
+
80
+ @guard.synchronize do
81
+ @work = nil
82
+ @state = :cancelled
83
+ @thread&.raise(Interrupt)
84
+ end
85
+ end
86
+
87
+ # Wait for the work to be done.
88
+ #
89
+ # @returns [Object] The result of the work.
90
+ def wait
91
+ @guard.synchronize do
92
+ while @state == :pending
93
+ @condition.wait(@guard)
94
+ end
95
+
96
+ if @state == :failed
97
+ raise @value
98
+ else
99
+ return @value
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ # A background worker thread.
106
+ class Worker
107
+ # Create a new worker.
108
+ def initialize
109
+ @work = ::Thread::Queue.new
110
+ @thread = ::Thread.new(&method(:run))
111
+ end
112
+
113
+ # Execute work until the queue is closed.
114
+ def run
115
+ while work = @work.pop
116
+ work.call
117
+ end
118
+ end
119
+
120
+ # Close the worker thread.
121
+ def close
122
+ if thread = @thread
123
+ @thread = nil
124
+ thread.kill
125
+ end
126
+ end
127
+
128
+ # Call the work and notify the scheduler when it is done.
129
+ def call(work)
130
+ promise = Promise.new(work)
131
+
132
+ @work.push(promise)
133
+
134
+ begin
135
+ return promise.wait
136
+ ensure
137
+ promise.cancel
138
+ end
139
+ end
140
+ end
141
+
142
+ # Create a new work pool.
143
+ #
144
+ # @parameter size [Integer] The number of threads to use.
145
+ def initialize(size: Etc.nprocessors)
146
+ @ready = ::Thread::Queue.new
147
+
148
+ size.times do
149
+ @ready.push(Worker.new)
150
+ end
151
+ end
152
+
153
+ # Close the work pool. Kills all outstanding work.
154
+ def close
155
+ if ready = @ready
156
+ @ready = nil
157
+ ready.close
158
+
159
+ while worker = ready.pop
160
+ worker.close
161
+ end
162
+ end
163
+ end
164
+
165
+ # Offload work to a thread.
166
+ #
167
+ # @parameter work [Proc] The work to be done.
168
+ def call(work)
169
+ if ready = @ready
170
+ worker = ready.pop
171
+
172
+ begin
173
+ worker.call(work)
174
+ ensure
175
+ ready.push(worker)
176
+ end
177
+ else
178
+ raise RuntimeError, "No worker available!"
179
+ end
180
+ end
181
+ end
182
+ end
@@ -12,13 +12,17 @@ Traces::Provider(Async::Task) do
12
12
  trace_context = Traces.trace_context
13
13
  end
14
14
 
15
+ attributes = {
16
+ # We use the instance variable as it corresponds to the user-provided block.
17
+ "block" => @block,
18
+ "transient" => self.transient?,
19
+ }
20
+
15
21
  super do
16
22
  Traces.trace_context = trace_context
17
23
 
18
24
  if annotation = self.annotation
19
- attributes = {
20
- "annotation" => annotation
21
- }
25
+ attributes["annotation"] = annotation
22
26
  end
23
27
 
24
28
  Traces.trace("async.task", attributes: attributes) do
data/readme.md CHANGED
@@ -7,6 +7,7 @@ Async is a composable asynchronous I/O framework for Ruby based on [io-event](ht
7
7
  > beautifully designed." *– [janko](https://github.com/janko)*
8
8
 
9
9
  [![Development Status](https://github.com/socketry/async/workflows/Test/badge.svg)](https://github.com/socketry/async/actions?workflow=Test)
10
+ [<img src="https://api.gitsponsors.com/api/badge/img?id=87380483" height="20"/>](https://api.gitsponsors.com/api/badge/link?p=U4gCxvzG7eUksiJSe0MSlPHWWhBYryqj6i48tx5L7/r/2NgkAToKb6dEm31bAftU3H+7BVwk3VhUBtE4GHqHJTPfWPR6xo2BQVoT15rFAGAsLFgdT2kKopIfCGV/QDOm7BrkodS2//R7NUMksAdaCQ==)
10
11
 
11
12
  ## Features
12
13
 
@@ -35,6 +36,10 @@ Please see the [project documentation](https://socketry.github.io/async/) for mo
35
36
 
36
37
  Please see the [project releases](https://socketry.github.io/async/releases/index) for all releases.
37
38
 
39
+ ### v2.21.1
40
+
41
+ - [Worker Pool](https://socketry.github.io/async/releases/index#worker-pool)
42
+
38
43
  ### v2.20.0
39
44
 
40
45
  - [Traces and Metrics Providers](https://socketry.github.io/async/releases/index#traces-and-metrics-providers)
data/releases.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Releases
2
2
 
3
+ ## v2.21.1
4
+
5
+ ### Worker Pool
6
+
7
+ Ruby 3.4 will feature a new fiber scheduler hook, `blocking_operation_wait` which allows the scheduler to redirect the work given to `rb_nogvl` to a worker pool.
8
+
9
+ The Async scheduler optionally supports this feature using a worker pool, by using the following environment variable:
10
+
11
+ ASYNC_SCHEDULER_DEFAULT_WORKER_POOL=true
12
+
13
+ This will cause the scheduler to use a worker pool for general blocking operations, rather than blocking the event loop.
14
+
15
+ It should be noted that this isn't a net win, as the overhead of using a worker pool can be significant compared to the `rb_nogvl` work. As such, it is recommended to benchmark your application with and without the worker pool to determine if it is beneficial.
16
+
3
17
  ## v2.20.0
4
18
 
5
19
  ### Traces and Metrics Providers
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.21.0
4
+ version: 2.21.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -31,7 +31,6 @@ authors:
31
31
  - Sokolov Yura
32
32
  - Stefan Wrobel
33
33
  - Trevor Turk
34
- autorequire:
35
34
  bindir: bin
36
35
  cert_chain:
37
36
  - |
@@ -63,7 +62,7 @@ cert_chain:
63
62
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
64
63
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
65
64
  -----END CERTIFICATE-----
66
- date: 2024-11-21 00:00:00.000000000 Z
65
+ date: 2025-01-30 00:00:00.000000000 Z
67
66
  dependencies:
68
67
  - !ruby/object:Gem::Dependency
69
68
  name: console
@@ -113,8 +112,6 @@ dependencies:
113
112
  - - ">="
114
113
  - !ruby/object:Gem::Version
115
114
  version: 1.6.5
116
- description:
117
- email:
118
115
  executables: []
119
116
  extensions: []
120
117
  extra_rdoc_files: []
@@ -127,6 +124,7 @@ files:
127
124
  - lib/async/condition.rb
128
125
  - lib/async/console.rb
129
126
  - lib/async/idler.rb
127
+ - lib/async/limited_queue.rb
130
128
  - lib/async/list.rb
131
129
  - lib/async/node.rb
132
130
  - lib/async/notification.rb
@@ -141,6 +139,7 @@ files:
141
139
  - lib/async/version.rb
142
140
  - lib/async/waiter.md
143
141
  - lib/async/waiter.rb
142
+ - lib/async/worker_pool.rb
144
143
  - lib/async/wrapper.rb
145
144
  - lib/kernel/async.rb
146
145
  - lib/kernel/sync.rb
@@ -159,7 +158,6 @@ metadata:
159
158
  documentation_uri: https://socketry.github.io/async/
160
159
  funding_uri: https://github.com/sponsors/ioquatix/
161
160
  source_code_uri: https://github.com/socketry/async.git
162
- post_install_message:
163
161
  rdoc_options: []
164
162
  require_paths:
165
163
  - lib
@@ -174,8 +172,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
174
172
  - !ruby/object:Gem::Version
175
173
  version: '0'
176
174
  requirements: []
177
- rubygems_version: 3.5.22
178
- signing_key:
175
+ rubygems_version: 3.6.2
179
176
  specification_version: 4
180
177
  summary: A concurrency framework for Ruby.
181
178
  test_files: []
metadata.gz.sig CHANGED
Binary file