async 2.21.0 → 2.21.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: 4a5bc2e233137c26a571123a0edc0cd22fe32278f780fe801c166a2c7968dc83
4
- data.tar.gz: 2c2aab55da8cfe72e2806a6bb1e4a18e3b20d7162ff929bcb91de019eb9c6da3
3
+ metadata.gz: 6dda1de8f976c85bf6dcbd0156c27c30f85f88d5ffaf32f3397b9838aa920bab
4
+ data.tar.gz: 0f09cde08880db4456bc03e9a4351903b9a96ca7099e7305c76fdee7f8edc0ed
5
5
  SHA512:
6
- metadata.gz: 9f3490b820e58aa4e6a3a95051bc36b699903f617918832f557b4a3369c7101a9c36cf878b34ab274caa1379591c30fef0f56cac79dbf1da482b16472c9cde1d
7
- data.tar.gz: 8c911bdb5a50dadc299ee4d7ed0cd6d16371b84fe622fb1542190fc5a586f914dca4a5396de481f826c8606d547df831357f783f3ee67da3de18e49dd5ad7dd6
6
+ metadata.gz: 980cd5e5832ddd71846785e10b23d18d11cb1f0eee33ccbdf33ed40feb4ac51dd99f7cf3d7f93055d0ad52de2abe0103c1b433d41f5bac1728c04435e35655db
7
+ data.tar.gz: d9c7c1c69e49acd89738f44900502bc83aea08341b84a9f8d9e02588261889a24a935dfa6509394514a575a4b17d8dcf0ca512518e8b0e089369a8fd37b44d52
checksums.yaml.gz.sig CHANGED
Binary file
@@ -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/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.1"
8
8
  end
@@ -0,0 +1,169 @@
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
+ module BlockingOperationWait
14
+ # Wait for the given work to be executed.
15
+ #
16
+ # @public Since *Async v2.19* and *Ruby v3.4*.
17
+ # @asynchronous May be non-blocking.
18
+ #
19
+ # @parameter work [Proc] The work to execute on a background thread.
20
+ # @returns [Object] The result of the work.
21
+ def blocking_operation_wait(work)
22
+ @worker_pool.call(work)
23
+ end
24
+ end
25
+
26
+ class Promise
27
+ def initialize(work)
28
+ @work = work
29
+ @state = :pending
30
+ @value = nil
31
+ @guard = ::Mutex.new
32
+ @condition = ::ConditionVariable.new
33
+ @thread = nil
34
+ end
35
+
36
+ def call
37
+ work = nil
38
+
39
+ @guard.synchronize do
40
+ @thread = ::Thread.current
41
+
42
+ return unless work = @work
43
+ end
44
+
45
+ resolve(work.call)
46
+ rescue Exception => error
47
+ reject(error)
48
+ end
49
+
50
+ private def resolve(value)
51
+ @guard.synchronize do
52
+ @work = nil
53
+ @thread = nil
54
+ @value = value
55
+ @state = :resolved
56
+ @condition.broadcast
57
+ end
58
+ end
59
+
60
+ private def reject(error)
61
+ @guard.synchronize do
62
+ @work = nil
63
+ @thread = nil
64
+ @value = error
65
+ @state = :failed
66
+ @condition.broadcast
67
+ end
68
+ end
69
+
70
+ def cancel
71
+ return unless @work
72
+
73
+ @guard.synchronize do
74
+ @work = nil
75
+ @state = :cancelled
76
+ @thread&.raise(Interrupt)
77
+ end
78
+ end
79
+
80
+ def wait
81
+ @guard.synchronize do
82
+ while @state == :pending
83
+ @condition.wait(@guard)
84
+ end
85
+
86
+ if @state == :failed
87
+ raise @value
88
+ else
89
+ return @value
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ # A handle to the work being done.
96
+ class Worker
97
+ def initialize
98
+ @work = ::Thread::Queue.new
99
+ @thread = ::Thread.new(&method(:run))
100
+ end
101
+
102
+ def run
103
+ while work = @work.pop
104
+ work.call
105
+ end
106
+ end
107
+
108
+ def close
109
+ if thread = @thread
110
+ @thread = nil
111
+ thread.kill
112
+ end
113
+ end
114
+
115
+ # Call the work and notify the scheduler when it is done.
116
+ def call(work)
117
+ promise = Promise.new(work)
118
+
119
+ @work.push(promise)
120
+
121
+ begin
122
+ return promise.wait
123
+ ensure
124
+ promise.cancel
125
+ end
126
+ end
127
+ end
128
+
129
+ # Create a new work pool.
130
+ #
131
+ # @parameter size [Integer] The number of threads to use.
132
+ def initialize(size: Etc.nprocessors)
133
+ @ready = ::Thread::Queue.new
134
+
135
+ size.times do
136
+ @ready.push(Worker.new)
137
+ end
138
+ end
139
+
140
+ # Close the work pool. Kills all outstanding work.
141
+ def close
142
+ if ready = @ready
143
+ @ready = nil
144
+ ready.close
145
+
146
+ while worker = ready.pop
147
+ worker.close
148
+ end
149
+ end
150
+ end
151
+
152
+ # Offload work to a thread.
153
+ #
154
+ # @parameter work [Proc] The work to be done.
155
+ def call(work)
156
+ if ready = @ready
157
+ worker = ready.pop
158
+
159
+ begin
160
+ worker.call(work)
161
+ ensure
162
+ ready.push(worker)
163
+ end
164
+ else
165
+ raise RuntimeError, "No worker available!"
166
+ end
167
+ end
168
+ end
169
+ end
data/readme.md CHANGED
@@ -35,6 +35,10 @@ 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.21.1
39
+
40
+ - [Worker Pool](https://socketry.github.io/async/releases/index#worker-pool)
41
+
38
42
  ### v2.20.0
39
43
 
40
44
  - [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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -63,7 +63,7 @@ cert_chain:
63
63
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
64
64
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
65
65
  -----END CERTIFICATE-----
66
- date: 2024-11-21 00:00:00.000000000 Z
66
+ date: 2024-11-27 00:00:00.000000000 Z
67
67
  dependencies:
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: console
@@ -141,6 +141,7 @@ files:
141
141
  - lib/async/version.rb
142
142
  - lib/async/waiter.md
143
143
  - lib/async/waiter.rb
144
+ - lib/async/worker_pool.rb
144
145
  - lib/async/wrapper.rb
145
146
  - lib/kernel/async.rb
146
147
  - lib/kernel/sync.rb
metadata.gz.sig CHANGED
Binary file