async 2.12.1 → 2.22.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: 1eef4bf800b6d52c0f273289186f9886975237ecdf9885f599617691283ca2e5
4
- data.tar.gz: 14a7e20dc72c178b89a93233a73fd49f182f7aec7926804b9b8bf37e8ceb9f36
3
+ metadata.gz: ef8aa467e82d3ddcd2c24a823ffba2e2b9e1680d005fade667ef36c882588dd1
4
+ data.tar.gz: c2df7e4669fe9d2f0bb8d6c6f7de673d1a89e41d8ffc99bf702f79cefbce7a3e
5
5
  SHA512:
6
- metadata.gz: 30176358a5c9efc3df3e525e0ba941f5771bb7619bef28e1b6229a547ad3a24cb5475483a257704c3eea2d9214dcca2e17932994e0859a1c5d4dfc6401b24d48
7
- data.tar.gz: 8b7353cf2ef0884c78a919775b84280d3234c7868c6c3b95aa00ae560a5825a07bd9533eda1afcc9776d1c94b7cc116e549e24144e4bab7c9bb3f086782a7456
6
+ metadata.gz: 2a98aaf33872cafab28e41acf187425926a3e0423ee5017bfd42fef66e64f55ad0ec6d6d1a053987813a3f190471e4521bac7c8895fb1ee352b8e984cf1dd326
7
+ data.tar.gz: 64205b564a8de8aeb22d0ae6834ea9dfd114439336e7bd076de489da0e045f002a9d90f51f72414c36ed006a599cd536d27ff852b5f8f9541f3c656c3d9e7035
checksums.yaml.gz.sig CHANGED
Binary file
data/lib/async/barrier.md CHANGED
@@ -18,7 +18,7 @@ Sync do
18
18
  # Sleep sort the numbers:
19
19
  numbers.each do |number|
20
20
  barrier.async do |task|
21
- task.sleep(number)
21
+ sleep(number)
22
22
  sorted << number
23
23
  end
24
24
  end
data/lib/async/barrier.rb CHANGED
@@ -1,19 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2022, by Samuel Williams.
4
+ # Copyright, 2019-2024, by Samuel Williams.
5
5
 
6
- require_relative 'list'
7
- require_relative 'task'
6
+ require_relative "list"
7
+ require_relative "task"
8
8
 
9
9
  module Async
10
10
  # A general purpose synchronisation primitive, which allows one task to wait for a number of other tasks to complete. It can be used in conjunction with {Semaphore}.
11
11
  #
12
- # @public Since `stable-v1`.
12
+ # @public Since *Async v1*.
13
13
  class Barrier
14
14
  # Initialize the barrier.
15
15
  # @parameter parent [Task | Semaphore | Nil] The parent for holding any children tasks.
16
- # @public Since `stable-v1`.
16
+ # @public Since *Async v1*.
17
17
  def initialize(parent: nil)
18
18
  @tasks = List.new
19
19
 
data/lib/async/clock.rb CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  module Async
7
7
  # A convenient wrapper around the internal monotonic clock.
8
- # @public Since `stable-v1`.
8
+ # @public Since *Async v1*.
9
9
  class Clock
10
10
  # Get the current elapsed monotonic time.
11
11
  def self.now
@@ -61,5 +61,14 @@ module Async
61
61
 
62
62
  return total
63
63
  end
64
+
65
+ # Reset the total elapsed time. If the clock is currently running, reset the start time to now.
66
+ def reset!
67
+ @total = 0
68
+
69
+ if @started
70
+ @started = Clock.now
71
+ end
72
+ end
64
73
  end
65
74
  end
@@ -15,7 +15,7 @@ Sync do
15
15
  end
16
16
 
17
17
  Async do |task|
18
- task.sleep(1)
18
+ sleep(1)
19
19
  Console.info "Signalling condition..."
20
20
  condition.signal("Hello World")
21
21
  end
@@ -1,16 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2017-2022, by Samuel Williams.
4
+ # Copyright, 2017-2024, by Samuel Williams.
5
5
  # Copyright, 2017, by Kent Gruber.
6
6
 
7
- require 'fiber'
8
- require_relative 'list'
7
+ require "fiber"
8
+ require_relative "list"
9
9
 
10
10
  module Async
11
11
  # A synchronization primitive, which allows fibers to wait until a particular condition is (edge) triggered.
12
- # @public Since `stable-v1`.
12
+ # @public Since *Async v1*.
13
13
  class Condition
14
+ # Create a new condition.
14
15
  def initialize
15
16
  @waiting = List.new
16
17
  end
@@ -39,12 +40,16 @@ module Async
39
40
  end
40
41
  end
41
42
 
42
- # Is any fiber waiting on this notification?
43
- # @returns [Boolean]
43
+ # @deprecated Replaced by {#waiting?}
44
44
  def empty?
45
45
  @waiting.empty?
46
46
  end
47
47
 
48
+ # @returns [Boolean] Is any fiber waiting on this notification?
49
+ def waiting?
50
+ @waiting.size > 0
51
+ end
52
+
48
53
  # Signal to a given task that it should resume operations.
49
54
  # @parameter value [Object | Nil] The value to return to the waiting fibers.
50
55
  def signal(value = nil)
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ module Async
7
+ # Shims for the console gem, redirecting warnings and above to `Kernel#warn`.
8
+ #
9
+ # If you require this file, the `async` library will not depend on the `console` gem.
10
+ #
11
+ # That includes any gems that sit within the `Async` namespace.
12
+ #
13
+ # This is an experimental feature.
14
+ module Console
15
+ # Log a message at the debug level. The shim is silent.
16
+ def self.debug(...)
17
+ end
18
+
19
+ # Log a message at the info level. The shim is silent.
20
+ def self.info(...)
21
+ end
22
+
23
+ # Log a message at the warn level. The shim redirects to `Kernel#warn`.
24
+ def self.warn(*arguments, exception: nil, **options)
25
+ if exception
26
+ super(*arguments, exception.full_message, **options)
27
+ else
28
+ super(*arguments, **options)
29
+ end
30
+ end
31
+
32
+ # Log a message at the error level. The shim redirects to `Kernel#warn`.
33
+ def self.error(...)
34
+ self.warn(...)
35
+ end
36
+
37
+ # Log a message at the fatal level. The shim redirects to `Kernel#warn`.
38
+ def self.fatal(...)
39
+ self.warn(...)
40
+ end
41
+ end
42
+ end
data/lib/async/idler.rb CHANGED
@@ -4,13 +4,29 @@
4
4
  # Copyright, 2024, by Samuel Williams.
5
5
 
6
6
  module Async
7
+ # A load balancing mechanism that can be used process work when the system is idle.
7
8
  class Idler
9
+ # Create a new idler.
10
+ #
11
+ # @public Since *Async v2*.
12
+ #
13
+ # @parameter maximum_load [Numeric] The maximum load before we start shedding work.
14
+ # @parameter backoff [Numeric] The initial backoff time, used for delaying work.
15
+ # @parameter parent [Interface(:async) | Nil] The parent task to use for async operations.
8
16
  def initialize(maximum_load = 0.8, backoff: 0.01, parent: nil)
9
17
  @maximum_load = maximum_load
10
18
  @backoff = backoff
11
19
  @parent = parent
12
20
  end
13
21
 
22
+ # Wait until the system is idle, then execute the given block in a new task.
23
+ #
24
+ # @asynchronous Executes the given block concurrently.
25
+ #
26
+ # @parameter arguments [Array] The arguments to pass to the block.
27
+ # @parameter parent [Interface(:async) | Nil] The parent task to use for async operations.
28
+ # @parameter options [Hash] The options to pass to the task.
29
+ # @yields {|task| ...} When the system is idle, the block will be executed in a new task.
14
30
  def async(*arguments, parent: (@parent or Task.current), **options, &block)
15
31
  wait
16
32
 
@@ -18,12 +34,16 @@ module Async
18
34
  parent.async(*arguments, **options, &block)
19
35
  end
20
36
 
37
+ # Wait until the system is idle, according to the maximum load specified.
38
+ #
39
+ # If the scheduler is overloaded, this method will sleep for an exponentially increasing amount of time.
21
40
  def wait
22
41
  scheduler = Fiber.scheduler
23
42
  backoff = nil
24
43
 
25
44
  while true
26
- load = scheduler.load
45
+ load = scheduler.load
46
+
27
47
  break if load < @maximum_load
28
48
 
29
49
  if backoff
@@ -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/list.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022, by Samuel Williams.
4
+ # Copyright, 2022-2024, by Samuel Williams.
5
5
 
6
6
  module Async
7
7
  # A general doublely linked list. This is used internally by {Async::Barrier} and {Async::Condition} to manage child tasks.
@@ -13,7 +13,7 @@ module Async
13
13
  @size = 0
14
14
  end
15
15
 
16
- # Print a short summary of the list.
16
+ # @returns [String] A short summary of the list.
17
17
  def to_s
18
18
  sprintf("#<%s:0x%x size=%d>", self.class.name, object_id, @size)
19
19
  end
@@ -36,12 +36,13 @@ module Async
36
36
  return items
37
37
  end
38
38
 
39
- # Points at the end of the list.
39
+ # @attribute [Node | Nil] Points at the end of the list.
40
40
  attr_accessor :head
41
41
 
42
- # Points at the start of the list.
42
+ # @attribute [Node | Nil] Points at the start of the list.
43
43
  attr_accessor :tail
44
44
 
45
+ # @attribute [Integer] The number of nodes in the list.
45
46
  attr :size
46
47
 
47
48
  # A callback that is invoked when an item is added to the list.
@@ -64,6 +65,7 @@ module Async
64
65
  return added(node)
65
66
  end
66
67
 
68
+ # Prepend a node to the start of the list.
67
69
  def prepend(node)
68
70
  if node.head
69
71
  raise ArgumentError, "Node is already in a list!"
@@ -224,6 +226,7 @@ module Async
224
226
  return nil
225
227
  end
226
228
 
229
+ # Shift the first node off the list, if it is not empty.
227
230
  def shift
228
231
  if node = first
229
232
  remove!(node)
data/lib/async/node.rb CHANGED
@@ -1,17 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2017-2023, by Samuel Williams.
4
+ # Copyright, 2017-2024, by Samuel Williams.
5
5
  # Copyright, 2017, by Kent Gruber.
6
6
  # Copyright, 2022, by Shannon Skipper.
7
7
 
8
- require 'fiber/annotation'
8
+ require "fiber/annotation"
9
9
 
10
- require_relative 'list'
10
+ require_relative "list"
11
11
 
12
12
  module Async
13
13
  # A list of children tasks.
14
14
  class Children < List
15
+ # Create an empty list of children tasks.
15
16
  def initialize
16
17
  super
17
18
  @transient_count = 0
@@ -33,6 +34,19 @@ module Async
33
34
  empty?
34
35
  end
35
36
 
37
+ # Adjust the number of transient children, assuming it has changed.
38
+ #
39
+ # Despite being public, this is not intended to be called directly. It is used internally by {Node#transient=}.
40
+ #
41
+ # @parameter transient [Boolean] Whether to increment or decrement the transient count.
42
+ def adjust_transient_count(transient)
43
+ if transient
44
+ @transient_count += 1
45
+ else
46
+ @transient_count -= 1
47
+ end
48
+ end
49
+
36
50
  private
37
51
 
38
52
  def added(node)
@@ -73,7 +87,7 @@ module Async
73
87
  end
74
88
  end
75
89
 
76
- # @returns [Node] the root node in the hierarchy.
90
+ # @returns [Node] The root node in the hierarchy.
77
91
  def root
78
92
  @parent&.root || self
79
93
  end
@@ -87,10 +101,10 @@ module Async
87
101
  # @attribute [Node] The parent node.
88
102
  attr :parent
89
103
 
90
- # @attribute children [Children | Nil] Optional list of children.
104
+ # @attribute [Children | Nil] Optional list of children.
91
105
  attr :children
92
106
 
93
- # A useful identifier for the current node.
107
+ # @attribute [String | Nil] A useful identifier for the current node.
94
108
  attr :annotation
95
109
 
96
110
  # Whether this node has any children.
@@ -109,6 +123,22 @@ module Async
109
123
  @transient
110
124
  end
111
125
 
126
+ # Change the transient state of the node.
127
+ #
128
+ # A transient node is not considered when determining if a node is finished, and propagates up if the parent is consumed.
129
+ #
130
+ # @parameter value [Boolean] Whether the node is transient.
131
+ def transient=(value)
132
+ if @transient != value
133
+ @transient = value
134
+
135
+ @parent&.children&.adjust_transient_count(value)
136
+ end
137
+ end
138
+
139
+ # Annotate the node with a description.
140
+ #
141
+ # @parameter annotation [String] The description to annotate the node with.
112
142
  def annotate(annotation)
113
143
  if block_given?
114
144
  begin
@@ -123,6 +153,9 @@ module Async
123
153
  end
124
154
  end
125
155
 
156
+ # A description of the node, including the annotation and object name.
157
+ #
158
+ # @returns [String] The description of the node.
126
159
  def description
127
160
  @object_name ||= "#{self.class}:#{format '%#018x', object_id}#{@transient ? ' transient' : nil}"
128
161
 
@@ -135,10 +168,14 @@ module Async
135
168
  end
136
169
  end
137
170
 
171
+ # Provides a backtrace for nodes that have an active execution context.
172
+ #
173
+ # @returns [Array(Thread::Backtrace::Locations) | Nil] The backtrace of the node, if available.
138
174
  def backtrace(*arguments)
139
175
  nil
140
176
  end
141
177
 
178
+ # @returns [String] A description of the node.
142
179
  def to_s
143
180
  "\#<#{self.description}>"
144
181
  end
@@ -255,10 +292,15 @@ module Async
255
292
  end
256
293
  end
257
294
 
295
+ # Whether the node has been stopped.
258
296
  def stopped?
259
297
  @children.nil?
260
298
  end
261
299
 
300
+ # Print the hierarchy of the task tree from the given node.
301
+ #
302
+ # @parameter out [IO] The output stream to write to.
303
+ # @parameter backtrace [Boolean] Whether to print the backtrace of each node.
262
304
  def print_hierarchy(out = $stdout, backtrace: true)
263
305
  self.traverse do |node, level|
264
306
  indent = "\t" * level
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2022, by Samuel Williams.
4
+ # Copyright, 2018-2024, by Samuel Williams.
5
5
 
6
- require_relative 'condition'
6
+ require_relative "condition"
7
7
 
8
8
  module Async
9
9
  # A synchronization primitive, which allows fibers to wait until a notification is received. Does not block the task which signals the notification. Waiting tasks are resumed on next iteration of the reactor.
10
- # @public Since `stable-v1`.
10
+ # @public Since *Async v1*.
11
11
  class Notification < Condition
12
12
  # Signal to a given task that it should resume operations.
13
13
  def signal(value = nil, task: Task.current)
@@ -29,5 +29,7 @@ module Async
29
29
  end
30
30
  end
31
31
  end
32
+
33
+ private_constant :Signal
32
34
  end
33
35
  end
data/lib/async/queue.rb CHANGED
@@ -1,76 +1,122 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2022, by Samuel Williams.
4
+ # Copyright, 2018-2024, by Samuel Williams.
5
5
  # Copyright, 2019, by Ryan Musgrave.
6
6
  # Copyright, 2020-2022, by Bruno Sutic.
7
7
 
8
- require_relative 'notification'
8
+ require_relative "notification"
9
9
 
10
10
  module Async
11
11
  # A queue which allows items to be processed in order.
12
- # @public Since `stable-v1`.
13
- class Queue < Notification
14
- def initialize(parent: nil)
15
- super()
16
-
12
+ #
13
+ # It has a compatible interface with {Notification} and {Condition}, except that it's multi-value.
14
+ #
15
+ # @public Since *Async v1*.
16
+ class Queue
17
+ # Create a new queue.
18
+ #
19
+ # @parameter parent [Interface(:async) | Nil] The parent task to use for async operations.
20
+ # @parameter available [Notification] The notification to use for signaling when items are available.
21
+ def initialize(parent: nil, available: Notification.new)
17
22
  @items = []
18
23
  @parent = parent
24
+ @available = available
19
25
  end
20
26
 
27
+ # @attribute [Array] The items in the queue.
21
28
  attr :items
22
29
 
30
+ # @returns [Integer] The number of items in the queue.
23
31
  def size
24
32
  @items.size
25
33
  end
26
-
34
+
35
+ # @returns [Boolean] Whether the queue is empty.
27
36
  def empty?
28
37
  @items.empty?
29
38
  end
30
39
 
31
- def <<(item)
40
+ # Add an item to the queue.
41
+ def push(item)
32
42
  @items << item
33
43
 
34
- self.signal unless self.empty?
44
+ @available.signal unless self.empty?
35
45
  end
36
46
 
47
+ # Compatibility with {::Queue#push}.
48
+ def <<(item)
49
+ self.push(item)
50
+ end
51
+
52
+ # Add multiple items to the queue.
37
53
  def enqueue(*items)
38
54
  @items.concat(items)
39
55
 
40
- self.signal unless self.empty?
56
+ @available.signal unless self.empty?
41
57
  end
42
58
 
59
+ # Remove and return the next item from the queue.
43
60
  def dequeue
44
61
  while @items.empty?
45
- self.wait
62
+ @available.wait
46
63
  end
47
64
 
48
65
  @items.shift
49
66
  end
50
67
 
51
- def async(parent: (@parent or Task.current), &block)
68
+ # Compatibility with {::Queue#pop}.
69
+ def pop
70
+ self.dequeue
71
+ end
72
+
73
+ # Process each item in the queue.
74
+ #
75
+ # @asynchronous Executes the given block concurrently for each item.
76
+ #
77
+ # @parameter arguments [Array] The arguments to pass to the block.
78
+ # @parameter parent [Interface(:async) | Nil] The parent task to use for async operations.
79
+ # @parameter options [Hash] The options to pass to the task.
80
+ # @yields {|task| ...} When the system is idle, the block will be executed in a new task.
81
+ def async(parent: (@parent or Task.current), **options, &block)
52
82
  while item = self.dequeue
53
- parent.async(item, &block)
83
+ parent.async(item, **options, &block)
54
84
  end
55
85
  end
56
86
 
87
+ # Enumerate each item in the queue.
57
88
  def each
58
89
  while item = self.dequeue
59
90
  yield item
60
91
  end
61
92
  end
93
+
94
+ # Signal the queue with a value, the same as {#enqueue}.
95
+ def signal(value = nil)
96
+ self.enqueue(value)
97
+ end
98
+
99
+ # Wait for an item to be available, the same as {#dequeue}.
100
+ def wait
101
+ self.dequeue
102
+ end
62
103
  end
63
104
 
64
- # @public Since `stable-v1`.
105
+ # A queue which limits the number of items that can be enqueued.
106
+ # @public Since *Async v1*.
65
107
  class LimitedQueue < Queue
66
- def initialize(limit = 1, **options)
108
+ # Create a new limited queue.
109
+ #
110
+ # @parameter limit [Integer] The maximum number of items that can be enqueued.
111
+ # @parameter full [Notification] The notification to use for signaling when the queue is full.
112
+ def initialize(limit = 1, full: Notification.new, **options)
67
113
  super(**options)
68
114
 
69
115
  @limit = limit
70
-
71
- @full = Notification.new
116
+ @full = full
72
117
  end
73
118
 
119
+ # @attribute [Integer] The maximum number of items that can be enqueued.
74
120
  attr :limit
75
121
 
76
122
  # @returns [Boolean] Whether trying to enqueue an item would block.
@@ -78,7 +124,12 @@ module Async
78
124
  @items.size >= @limit
79
125
  end
80
126
 
81
- def <<(item)
127
+ # Add an item to the queue.
128
+ #
129
+ # If the queue is full, this method will block until there is space available.
130
+ #
131
+ # @parameter item [Object] The item to add to the queue.
132
+ def push(item)
82
133
  while limited?
83
134
  @full.wait
84
135
  end
@@ -86,7 +137,12 @@ module Async
86
137
  super
87
138
  end
88
139
 
89
- def enqueue *items
140
+ # Add multiple items to the queue.
141
+ #
142
+ # If the queue is full, this method will block until there is space available.
143
+ #
144
+ # @parameter items [Array] The items to add to the queue.
145
+ def enqueue(*items)
90
146
  while !items.empty?
91
147
  while limited?
92
148
  @full.wait
@@ -95,10 +151,15 @@ module Async
95
151
  available = @limit - @items.size
96
152
  @items.concat(items.shift(available))
97
153
 
98
- self.signal unless self.empty?
154
+ @available.signal unless self.empty?
99
155
  end
100
156
  end
101
157
 
158
+ # Remove and return the next item from the queue.
159
+ #
160
+ # If the queue is empty, this method will block until an item is available.
161
+ #
162
+ # @returns [Object] The next item in the queue.
102
163
  def dequeue
103
164
  item = super
104
165
 
data/lib/async/reactor.rb CHANGED
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2017-2022, by Samuel Williams.
4
+ # Copyright, 2017-2024, by Samuel Williams.
5
5
  # Copyright, 2017, by Kent Gruber.
6
6
  # Copyright, 2018, by Sokolov Yura.
7
7
 
8
- require_relative 'scheduler'
8
+ require_relative "scheduler"
9
9
 
10
10
  module Async
11
11
  # A wrapper around the the scheduler which binds it to the current thread automatically.
@@ -15,12 +15,14 @@ module Async
15
15
  Async(...)
16
16
  end
17
17
 
18
+ # Initialize the reactor and assign it to the current Fiber scheduler.
18
19
  def initialize(...)
19
20
  super
20
21
 
21
22
  Fiber.set_scheduler(self)
22
23
  end
23
24
 
25
+ # Close the reactor and remove it from the current Fiber scheduler.
24
26
  def scheduler_close
25
27
  self.close
26
28
  end