async 2.8.2 → 2.10.0

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: 953c51a9b7552058772c90c36cc99d5a6cd26de3c32d3b424bc5929de81a1960
4
- data.tar.gz: 63b115b69ea49ba7192982de38b936fc6d07623aa6139ce51ee2000c949ad759
3
+ metadata.gz: 707788679dfee0d00262df41dca98842740769f25f38b4ec72f3a0fe0af466fb
4
+ data.tar.gz: 543acd4f573f1665cda010fd6b15d7d24ec14eb5de39cc873ef8b4d1b1cc7f8e
5
5
  SHA512:
6
- metadata.gz: 3023d786331d88fbb94c36b0ab1597f123e5794d410773b9005dfc6df8212c91d106cbeb8506c1c6725e059ac6e82ed1e77667c595d033eb2ff5ea9d9de681f2
7
- data.tar.gz: 31c4192c27d349f045fa25ba2165948630da063abe5369512def7c54d653961ee5c7d2e82ec36808910a26f81f51c23d6674e1b4993888936410d7649cbd2ec0
6
+ metadata.gz: a6be161ee5c42ebcdc0b26ed054b749900060fab3a0601255d7fb038feb063282cd13e8917f79700fc8379996903ea7cb9796c083f348b70f85f6ae363e348ca
7
+ data.tar.gz: ded0df5f2a950f143819bfe94537b188c0e66426344f0d21e0f5e9ed240651b2cafed41d6d5da36a123793cc6406b0ffbc5b8adb1e834faddd168f96855b02c4
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ module Async
7
+ class Idler
8
+ def initialize(maximum_load = 0.8, backoff: 0.01, parent: nil)
9
+ @maximum_load = maximum_load
10
+ @backoff = backoff
11
+ @parent = parent
12
+ end
13
+
14
+ def async(*arguments, parent: (@parent or Task.current), **options, &block)
15
+ wait
16
+
17
+ # It is crucial that we optimistically execute the child task, so that we prevent a tight loop invoking this method from consuming all available resources.
18
+ parent.async(*arguments, **options, &block)
19
+ end
20
+
21
+ def wait
22
+ scheduler = Fiber.scheduler
23
+ backoff = nil
24
+
25
+ while true
26
+ load = scheduler.load
27
+ break if load < @maximum_load
28
+
29
+ if backoff
30
+ sleep(backoff)
31
+ backoff *= 2.0
32
+ else
33
+ scheduler.yield
34
+ backoff = @backoff
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -37,9 +37,33 @@ module Async
37
37
 
38
38
  @blocked = 0
39
39
 
40
+ @busy_time = 0.0
41
+ @idle_time = 0.0
42
+
40
43
  @timers = ::Timers::Group.new
41
44
  end
42
45
 
46
+ # Compute the scheduler load according to the busy and idle times that are updated by the run loop.
47
+ # @returns [Float] The load of the scheduler. 0.0 means no load, 1.0 means fully loaded or over-loaded.
48
+ def load
49
+ total_time = @busy_time + @idle_time
50
+
51
+ # If the total time is zero, then the load is zero:
52
+ return 0.0 if total_time.zero?
53
+
54
+ # We normalize to a 1 second window:
55
+ if total_time > 1.0
56
+ ratio = 1.0 / total_time
57
+ @busy_time *= ratio
58
+ @idle_time *= ratio
59
+
60
+ # We don't need to divide here as we've already normalised it to a 1s window:
61
+ return @busy_time
62
+ else
63
+ return @busy_time / total_time
64
+ end
65
+ end
66
+
43
67
  def scheduler_close
44
68
  # If the execution context (thread) was handling an exception, we want to exit as quickly as possible:
45
69
  unless $!
@@ -267,6 +291,8 @@ module Async
267
291
  # @parameter timeout [Float | Nil] The maximum timeout, or if nil, indefinite.
268
292
  # @returns [Boolean] Whether there is more work to do.
269
293
  private def run_once!(timeout = 0)
294
+ start_time = Async::Clock.now
295
+
270
296
  interval = @timers.wait_interval
271
297
 
272
298
  # If there is no interval to wait (thus no timers), and no tasks, we could be done:
@@ -288,6 +314,15 @@ module Async
288
314
 
289
315
  @timers.fire
290
316
 
317
+ # Compute load:
318
+ end_time = Async::Clock.now
319
+ total_duration = end_time - start_time
320
+ idle_duration = @selector.idle_duration
321
+ busy_duration = total_duration - idle_duration
322
+
323
+ @busy_time += busy_duration
324
+ @idle_time += idle_duration
325
+
291
326
  # The reactor still has work to do:
292
327
  return true
293
328
  end
data/lib/async/task.rb CHANGED
@@ -67,6 +67,8 @@ module Async
67
67
  @status = :initialized
68
68
  @result = nil
69
69
  @finished = finished
70
+
71
+ @defer_stop = nil
70
72
  end
71
73
 
72
74
  def reactor
@@ -208,10 +210,17 @@ module Async
208
210
  # @parameter later [Boolean] Whether to stop the task later, or immediately.
209
211
  def stop(later = false)
210
212
  if self.stopped?
211
- # If we already stopped this task... don't try to stop it again:
213
+ # If the task is already stopped, a `stop` state transition re-enters the same state which is a no-op. However, we will also attempt to stop any running children too. This can happen if the children did not stop correctly the first time around. Doing this should probably be considered a bug, but it's better to be safe than sorry.
212
214
  return stopped!
213
215
  end
214
216
 
217
+ # If we are deferring stop...
218
+ if @defer_stop == false
219
+ # Don't stop now... but update the state so we know we need to stop later.
220
+ @defer_stop = true
221
+ return false
222
+ end
223
+
215
224
  # If the fiber is alive, we need to stop it:
216
225
  if @fiber&.alive?
217
226
  if self.current?
@@ -239,6 +248,42 @@ module Async
239
248
  end
240
249
  end
241
250
 
251
+ # Defer the handling of stop. During the execution of the given block, if a stop is requested, it will be deferred until the block exits. This is useful for ensuring graceful shutdown of servers and other long-running tasks. You should wrap the response handling code in a defer_stop block to ensure that the task is stopped when the response is complete but not before.
252
+ #
253
+ # You can nest calls to defer_stop, but the stop will only be deferred until the outermost block exits.
254
+ #
255
+ # If stop is invoked a second time, it will be immediately executed.
256
+ #
257
+ # @yields {} The block of code to execute.
258
+ # @public Since `stable-v1`.
259
+ def defer_stop
260
+ # Tri-state variable for controlling stop:
261
+ # - nil: defer_stop has not been called.
262
+ # - false: defer_stop has been called and we are not stopping.
263
+ # - true: defer_stop has been called and we will stop when exiting the block.
264
+ if @defer_stop.nil?
265
+ # If we are not deferring stop already, we can defer it now:
266
+ @defer_stop = false
267
+
268
+ begin
269
+ yield
270
+ rescue Stop
271
+ # If we are exiting due to a stop, we shouldn't try to invoke stop again:
272
+ @defer_stop = nil
273
+ raise
274
+ ensure
275
+ # If we were asked to stop, we should do so now:
276
+ if @defer_stop
277
+ @defer_stop = nil
278
+ self.stop
279
+ end
280
+ end
281
+ else
282
+ # If we are deferring stop already, entering it again is a no-op.
283
+ yield
284
+ end
285
+ end
286
+
242
287
  # Lookup the {Task} for the current fiber. Raise `RuntimeError` if none is available.
243
288
  # @returns [Task]
244
289
  # @raises[RuntimeError] If task was not {set!} for the current fiber.
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.8.2"
7
+ VERSION = "2.10.0"
8
8
  end
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.8.2
4
+ version: 2.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -61,7 +61,7 @@ cert_chain:
61
61
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
62
62
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
63
63
  -----END CERTIFICATE-----
64
- date: 2024-02-24 00:00:00.000000000 Z
64
+ date: 2024-03-27 00:00:00.000000000 Z
65
65
  dependencies:
66
66
  - !ruby/object:Gem::Dependency
67
67
  name: console
@@ -97,14 +97,20 @@ dependencies:
97
97
  requirements:
98
98
  - - "~>"
99
99
  - !ruby/object:Gem::Version
100
- version: '1.1'
100
+ version: '1.5'
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: 1.5.1
101
104
  type: :runtime
102
105
  prerelease: false
103
106
  version_requirements: !ruby/object:Gem::Requirement
104
107
  requirements:
105
108
  - - "~>"
106
109
  - !ruby/object:Gem::Version
107
- version: '1.1'
110
+ version: '1.5'
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: 1.5.1
108
114
  - !ruby/object:Gem::Dependency
109
115
  name: timers
110
116
  requirement: !ruby/object:Gem::Requirement
@@ -131,6 +137,7 @@ files:
131
137
  - lib/async/clock.rb
132
138
  - lib/async/condition.md
133
139
  - lib/async/condition.rb
140
+ - lib/async/idler.rb
134
141
  - lib/async/list.rb
135
142
  - lib/async/node.rb
136
143
  - lib/async/notification.rb
metadata.gz.sig CHANGED
Binary file