From 122cbdf6e741ce0467c3a49ff08917db5eb6b74f Mon Sep 17 00:00:00 2001 From: Bryan Powell Date: Fri, 19 Mar 2021 09:04:35 -0700 Subject: [PATCH 01/29] Don't assume the wrapper exists --- lib/async/scheduler.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/async/scheduler.rb b/lib/async/scheduler.rb index e6baa96a..02bdc016 100644 --- a/lib/async/scheduler.rb +++ b/lib/async/scheduler.rb @@ -71,7 +71,7 @@ def io_wait(io, events, timeout = nil) rescue TimeoutError return nil ensure - wrapper.reactor = nil + wrapper&.reactor = nil end # Wait for the specified process ID to exit. From e97c5dd0932177b7170ac20821da25139a0e1d69 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Fri, 16 Jul 2021 17:27:53 +1200 Subject: [PATCH 02/29] Patch version bump. --- lib/async/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/async/version.rb b/lib/async/version.rb index 83c4038b..0d2532ad 100644 --- a/lib/async/version.rb +++ b/lib/async/version.rb @@ -21,5 +21,5 @@ # THE SOFTWARE. module Async - VERSION = "1.29.1" + VERSION = "1.29.2" end From 587b0a2ed441f1f0d52fa1f3598cc2a657dc44ff Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 18 Jul 2021 09:34:57 +1200 Subject: [PATCH 03/29] Add support for `Barrier#stop`. --- lib/async/barrier.rb | 6 ++++++ spec/async/barrier_spec.rb | 21 +++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/async/barrier.rb b/lib/async/barrier.rb index df47b0e6..1bc03f18 100644 --- a/lib/async/barrier.rb +++ b/lib/async/barrier.rb @@ -56,5 +56,11 @@ def wait task.wait end end + + def stop + while task = @tasks.shift + task.stop + end + end end end diff --git a/spec/async/barrier_spec.rb b/spec/async/barrier_spec.rb index ab21fc7e..f6e011b4 100644 --- a/spec/async/barrier_spec.rb +++ b/spec/async/barrier_spec.rb @@ -31,7 +31,7 @@ RSpec.describe Async::Barrier do include_context Async::RSpec::Reactor - context '#async' do + describe '#async' do let(:repeats) {40} let(:delay) {0.1} @@ -61,7 +61,7 @@ end end - context '#wait' do + describe '#wait' do it 'should wait for tasks even after exceptions' do task1 = subject.async do raise "Boom" @@ -95,6 +95,23 @@ end end + describe '#stop' do + it "can stop several tasks" do + task1 = subject.async do |task| + task.sleep(10) + end + + task2 = subject.async do |task| + task.sleep(10) + end + + subject.stop + + expect(task1).to be_stopped + expect(task2).to be_stopped + end + end + context 'with semaphore' do let(:capacity) {2} let(:semaphore) {Async::Semaphore.new(capacity)} From 56e69e9b67c38ff112f8e32cfbff29c721d5df19 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Wed, 21 Jul 2021 14:24:41 +1200 Subject: [PATCH 04/29] Minor version bump. --- lib/async/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/async/version.rb b/lib/async/version.rb index 0d2532ad..2bcc754a 100644 --- a/lib/async/version.rb +++ b/lib/async/version.rb @@ -21,5 +21,5 @@ # THE SOFTWARE. module Async - VERSION = "1.29.2" + VERSION = "1.30.0" end From acef0fd1d3f499ac38e1ab96ca171b01abe010ef Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 29 Jul 2021 13:59:54 +1200 Subject: [PATCH 05/29] Fix race conditions when multiple tasks are waiting and/or stopping the barrier. # Conflicts: # lib/async/barrier.rb --- lib/async/barrier.rb | 21 ++++++++++++----- spec/async/barrier_spec.rb | 47 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/lib/async/barrier.rb b/lib/async/barrier.rb index 1bc03f18..c67e560d 100644 --- a/lib/async/barrier.rb +++ b/lib/async/barrier.rb @@ -50,17 +50,26 @@ def empty? @tasks.empty? end - # Wait for tasks in FIFO order. + # Wait for all tasks. + # @asynchronous Will wait for tasks to finish executing. def wait - while task = @tasks.shift - task.wait + # TODO: This would be better with linked list. + while @tasks.any? + task = @tasks.first + + begin + task.wait + ensure + # Remove the task from the waiting list if it's finished: + @tasks.shift if @tasks.first == task + end end end def stop - while task = @tasks.shift - task.stop - end + # We have to be careful to avoid enumerating tasks while adding/removing to it: + tasks = @tasks.dup + tasks.each(&:stop) end end end diff --git a/spec/async/barrier_spec.rb b/spec/async/barrier_spec.rb index f6e011b4..a6d2b74b 100644 --- a/spec/async/barrier_spec.rb +++ b/spec/async/barrier_spec.rb @@ -110,6 +110,53 @@ expect(task1).to be_stopped expect(task2).to be_stopped end + + it "can stop several tasks when waiting on barrier" do + task1 = subject.async do |task| + task.sleep(10) + end + + task2 = subject.async do |task| + task.sleep(10) + end + + task3 = reactor.async do + subject.wait + end + + subject.stop + + task1.wait + task2.wait + + expect(task1).to be_stopped + expect(task2).to be_stopped + + task3.wait + end + + it "several tasks can wait on the same barrier" do + task1 = subject.async do |task| + task.sleep(10) + end + + task2 = reactor.async do |task| + subject.wait + end + + task3 = reactor.async do + subject.wait + end + + subject.stop + + task1.wait + + expect(task1).to be_stopped + + task2.wait + task3.wait + end end context 'with semaphore' do From 7406e14151ab745323359af4647c96478b9b8cc1 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 29 Jul 2021 14:14:26 +1200 Subject: [PATCH 06/29] Patch version bump. --- lib/async/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/async/version.rb b/lib/async/version.rb index 2bcc754a..2514b68f 100644 --- a/lib/async/version.rb +++ b/lib/async/version.rb @@ -21,5 +21,5 @@ # THE SOFTWARE. module Async - VERSION = "1.30.0" + VERSION = "1.30.1" end From 79bd5360e5a8f1afe7e209f230f53fc2dc159770 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sat, 6 Nov 2021 09:18:20 +1300 Subject: [PATCH 07/29] Don't use alias. Fixes #129. --- lib/async/condition.rb | 1 - lib/async/queue.rb | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/async/condition.rb b/lib/async/condition.rb index 0be6c1e6..9940f92e 100644 --- a/lib/async/condition.rb +++ b/lib/async/condition.rb @@ -21,7 +21,6 @@ # THE SOFTWARE. require 'fiber' -require 'forwardable' require_relative 'node' module Async diff --git a/lib/async/queue.rb b/lib/async/queue.rb index 53eb5f2b..e3ab2d63 100644 --- a/lib/async/queue.rb +++ b/lib/async/queue.rb @@ -48,7 +48,9 @@ def enqueue(item) self.signal unless self.empty? end - alias << enqueue + def <<(item) + enqueue(item) + end def dequeue while @items.empty? From 9b6550791293c2825a7bed28c606314f731c1a72 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sat, 6 Nov 2021 09:22:56 +1300 Subject: [PATCH 08/29] Modernize gem. --- .github/workflows/development.yml | 8 ++++---- .github/workflows/documentation.yml | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml index 37175b59..8e72c5dd 100644 --- a/.github/workflows/development.yml +++ b/.github/workflows/development.yml @@ -4,6 +4,7 @@ on: [push, pull_request] jobs: test: + name: ${{matrix.ruby}} on ${{matrix.os}} runs-on: ${{matrix.os}}-latest continue-on-error: ${{matrix.experimental}} @@ -14,10 +15,9 @@ jobs: - macos ruby: - - 2.5 - - 2.6 - - 2.7 - - 3.0 + - "2.6" + - "2.7" + - "3.0" experimental: [false] env: [""] diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 5c081abc..61956e97 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -3,7 +3,7 @@ name: Documentation on: push: branches: - - master + - main jobs: deploy: @@ -15,7 +15,7 @@ jobs: env: BUNDLE_WITH: maintenance with: - ruby-version: 2.7 + ruby-version: 3.0 bundler-cache: true - name: Installing packages @@ -28,5 +28,5 @@ jobs: - name: Deploy documentation uses: JamesIves/github-pages-deploy-action@4.0.0 with: - branch: gh-pages + branch: docs folder: docs From 8b9664be61debcb687c5bab23ed68faacd82376a Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sat, 6 Nov 2021 12:19:57 +1300 Subject: [PATCH 09/29] Add `Async::Variable` for level-triggered values. --- lib/async/variable.rb | 55 ++++++++++++++++++++++++++++++++ spec/async/condition_examples.rb | 13 ++++++-- spec/async/variable_spec.rb | 54 +++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 lib/async/variable.rb create mode 100644 spec/async/variable_spec.rb diff --git a/lib/async/variable.rb b/lib/async/variable.rb new file mode 100644 index 00000000..95eb672b --- /dev/null +++ b/lib/async/variable.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +# Copyright, 2017, by Samuel G. D. Williams. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +require_relative 'condition' + +module Async + class Variable + def initialize(condition = Condition.new) + @condition = condition + @value = nil + end + + def resolve(value = true) + @value = value + condition = @condition + @condition = nil + + self.freeze + + condition.signal(value) + end + + def resolved? + @condition.nil? + end + + def value + @condition&.wait + return @value + end + + def wait + self.value + end + end +end diff --git a/spec/async/condition_examples.rb b/spec/async/condition_examples.rb index 0fcbb82d..6aaeb2b1 100644 --- a/spec/async/condition_examples.rb +++ b/spec/async/condition_examples.rb @@ -20,6 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +require 'async/variable' + RSpec.shared_examples Async::Condition do it 'can signal waiting task' do state = nil @@ -71,6 +73,9 @@ end context "with timeout" do + let!(:ready) {Async::Variable.new(subject)} + let!(:waiting) {Async::Variable.new(described_class.new)} + before do @state = nil end @@ -80,7 +85,9 @@ task.with_timeout(0.1) do begin @state = :waiting - subject.wait + waiting.resolve + + ready.wait @state = :signalled rescue Async::TimeoutError @state = :timeout @@ -96,7 +103,9 @@ end it 'can signal while waiting' do - subject.signal + waiting.wait + ready.resolve + task.wait expect(@state).to be == :signalled diff --git a/spec/async/variable_spec.rb b/spec/async/variable_spec.rb new file mode 100644 index 00000000..7e943bd8 --- /dev/null +++ b/spec/async/variable_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# Copyright, 2017, by Samuel G. D. Williams. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +require 'async/variable' + +RSpec.shared_examples_for Async::Variable do |value| + it "can resolve the value to #{value.inspect}" do + subject.resolve(value) + is_expected.to be_resolved + end + + it "can wait for the value to be resolved" do + Async do + expect(subject.wait).to be value + end + + subject.resolve(value) + end + + it "can't resolve it a 2nd time" do + subject.resolve(value) + expect do + subject.resolve(value) + end.to raise_error(FrozenError) + end +end + +RSpec.describe Async::Variable do + include_context Async::RSpec::Reactor + + it_behaves_like Async::Variable, true + it_behaves_like Async::Variable, false + it_behaves_like Async::Variable, nil + it_behaves_like Async::Variable, Object.new +end From a34ddb8cce72cfe6be1a776bf614a1899761316d Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Wed, 4 May 2022 17:57:24 +1200 Subject: [PATCH 10/29] Fix handling of barriers when waiting on a task fails with an unrelated exception. --- lib/async/barrier.rb | 7 +++++-- spec/async/barrier_spec.rb | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/async/barrier.rb b/lib/async/barrier.rb index c67e560d..b82a98eb 100644 --- a/lib/async/barrier.rb +++ b/lib/async/barrier.rb @@ -60,8 +60,11 @@ def wait begin task.wait ensure - # Remove the task from the waiting list if it's finished: - @tasks.shift if @tasks.first == task + # We don't know for sure that the exception was due to the task completion. + unless task.running? + # Remove the task from the waiting list if it's finished: + @tasks.shift if @tasks.first == task + end end end end diff --git a/spec/async/barrier_spec.rb b/spec/async/barrier_spec.rb index a6d2b74b..80ff663b 100644 --- a/spec/async/barrier_spec.rb +++ b/spec/async/barrier_spec.rb @@ -93,6 +93,27 @@ expect(order).to be == [0, 1, 2, 3, 4] end + + # It's possible for Barrier#wait to be interrupted with an unexpected exception, and this should not cause the barrier to incorrectly remove that task from the wait list. + it 'waits for tasks with timeouts' do + begin + reactor.with_timeout(0.25) do + 5.times do |i| + subject.async do |task| + task.sleep(i/10.0) + end + end + + expect(subject.tasks.size).to be == 5 + subject.wait + end + rescue Async::TimeoutError + # Expected. + ensure + expect(subject.tasks.size).to be == 2 + subject.stop + end + end end describe '#stop' do From 681aab95008b53ac005677bbe6ed89708064ab3b Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Wed, 4 May 2022 19:18:41 +1200 Subject: [PATCH 11/29] Prefer bake-gem for release management. --- gems.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gems.rb b/gems.rb index 6110fab5..2e7913d2 100644 --- a/gems.rb +++ b/gems.rb @@ -5,7 +5,7 @@ gemspec group :maintenance, optional: true do - gem "bake-bundler" + gem "bake-gem" gem "bake-modernize" gem "utopia-project" From dc8270f0c14f95cd6a71a95c098e7f4a495d214f Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Wed, 4 May 2022 19:19:29 +1200 Subject: [PATCH 12/29] Bump patch version. --- lib/async/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/async/version.rb b/lib/async/version.rb index 2514b68f..a4885fb4 100644 --- a/lib/async/version.rb +++ b/lib/async/version.rb @@ -21,5 +21,5 @@ # THE SOFTWARE. module Async - VERSION = "1.30.1" + VERSION = "1.30.2" end From 1a20746e1aaf107fe0b73cff88a37e10fc6d64f3 Mon Sep 17 00:00:00 2001 From: Bruno Sutic Date: Wed, 4 May 2022 11:54:43 +0200 Subject: [PATCH 13/29] Only include *.rb files in gem releases (#142) --- async.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/async.gemspec b/async.gemspec index f5625e5c..8cd096be 100644 --- a/async.gemspec +++ b/async.gemspec @@ -11,7 +11,7 @@ Gem::Specification.new do |spec| spec.homepage = "https://github.com/socketry/async" - spec.files = Dir.glob('{lib}/**/*', File::FNM_DOTMATCH, base: __dir__) + spec.files = Dir.glob('{lib}/**/*.rb', File::FNM_DOTMATCH, base: __dir__) spec.required_ruby_version = ">= 2.5.0" From f2ad15756dc0eb3f21ca3394848bc64fe62c5be3 Mon Sep 17 00:00:00 2001 From: Bruno Sutic Date: Wed, 4 May 2022 12:17:51 +0200 Subject: [PATCH 14/29] Enable enqueuing multiple items to Async::Queue (#81) Co-authored-by: Samuel Williams --- lib/async/queue.rb | 25 ++++++++++++++---- spec/async/queue_spec.rb | 55 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/lib/async/queue.rb b/lib/async/queue.rb index e3ab2d63..3629ac41 100644 --- a/lib/async/queue.rb +++ b/lib/async/queue.rb @@ -42,14 +42,16 @@ def empty? @items.empty? end - def enqueue(item) - @items.push(item) + def <<(item) + @items << item self.signal unless self.empty? end - def <<(item) - enqueue(item) + def enqueue(*items) + @items.concat(items) + + self.signal unless self.empty? end def dequeue @@ -89,7 +91,7 @@ def limited? @items.size >= @limit end - def enqueue item + def <<(item) while limited? @full.wait end @@ -97,6 +99,19 @@ def enqueue item super end + def enqueue *items + while !items.empty? + while limited? + @full.wait + end + + available = @limit - @items.size + @items.concat(items.shift(available)) + + self.signal unless self.empty? + end + end + def dequeue item = super diff --git a/spec/async/queue_spec.rb b/spec/async/queue_spec.rb index 1e62a00c..e08d7073 100644 --- a/spec/async/queue_spec.rb +++ b/spec/async/queue_spec.rb @@ -42,6 +42,18 @@ end end + it 'can enqueue multiple items' do + items = Array.new(10) { rand(10) } + + reactor.async do |task| + subject.enqueue(*items) + end + + items.each do |item| + expect(subject.dequeue).to be == item + end + end + it 'can dequeue items asynchronously' do reactor.async do |task| subject << 1 @@ -53,6 +65,14 @@ end end + describe '#<<' do + it 'adds an item to the queue' do + subject << :item + expect(subject.size).to be == 1 + expect(subject.dequeue).to be == :item + end + end + describe '#size' do it 'returns queue size' do reactor.async do |task| @@ -120,7 +140,17 @@ subject.enqueue(10) expect(subject).to be_limited end - + + it 'enqueues items up to a limit' do + items = Array.new(2) { rand(10) } + reactor.async do + subject.enqueue(*items) + end + + expect(subject.size).to be 1 + expect(subject.dequeue).to be == items.first + end + it 'should resume waiting tasks in order' do total_resumed = 0 total_dequeued = 0 @@ -141,4 +171,27 @@ expect(total_resumed).to be == total_dequeued end end + + describe '#<<' do + context 'when queue is limited' do + before do + subject << :item1 + expect(subject.size).to be == 1 + expect(subject).to be_limited + end + + it 'waits until a queue is dequeued' do + reactor.async do + subject << :item2 + end + + reactor.async do |task| + task.sleep 0.01 + expect(subject.items).to contain_exactly :item1 + subject.dequeue + expect(subject.items).to contain_exactly :item2 + end + end + end + end end From fd8c67441e82fc879637e67c952e78d0df0a8e95 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Wed, 15 Jun 2022 17:08:08 +1200 Subject: [PATCH 15/29] Log as warning rather than error, and with explicit message. --- lib/async/task.rb | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/async/task.rb b/lib/async/task.rb index 6be28a05..9aa22679 100644 --- a/lib/async/task.rb +++ b/lib/async/task.rb @@ -230,18 +230,19 @@ def complete? private # This is a very tricky aspect of tasks to get right. I've modelled it after `Thread` but it's slightly different in that the exception can propagate back up through the reactor. If the user writes code which raises an exception, that exception should always be visible, i.e. cause a failure. If it's not visible, such code fails silently and can be very difficult to debug. - # As an explcit choice, the user can start a task which doesn't propagate exceptions. This only applies to `StandardError` and derived tasks. This allows tasks to internally capture their error state which is raised when invoking `Task#result` similar to how `Thread#join` works. This mode makes {ruby Async::Task} behave more like a promise, and you would need to ensure that someone calls `Task#result` otherwise you might miss important errors. - def fail!(exception = nil, propagate = true) + def fail!(exception = false, propagate = true) @status = :failed @result = exception - if propagate - raise - elsif @finished.nil? - # If no one has called wait, we log this as an error: - Console.logger.error(self) {$!} - else - Console.logger.debug(self) {$!} + if exception + if propagate + raise exception + elsif @finished.nil? + # If no one has called wait, we log this as a warning: + Console.logger.warn(self, "Task may have ended with unhandled exception.", exception) + else + Console.logger.debug(self, exception) + end end end From dc14d1b73344a46ee0d0d2a1616f759028e5823c Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Wed, 15 Jun 2022 19:59:44 +1200 Subject: [PATCH 16/29] Bump patch version. --- lib/async/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/async/version.rb b/lib/async/version.rb index a4885fb4..7c64d437 100644 --- a/lib/async/version.rb +++ b/lib/async/version.rb @@ -21,5 +21,5 @@ # THE SOFTWARE. module Async - VERSION = "1.30.2" + VERSION = "1.30.3" end From 1b21523e5b25a7a749055bcc70f828c9f79ddcb6 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 2 Mar 2023 22:34:17 +1300 Subject: [PATCH 17/29] Backport `Semaphore#limit=` (#215). --- lib/async/semaphore.rb | 19 ++++++++++++++++++ spec/async/semaphore_spec.rb | 37 ++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/lib/async/semaphore.rb b/lib/async/semaphore.rb index 87ad49c9..753849b7 100644 --- a/lib/async/semaphore.rb +++ b/lib/async/semaphore.rb @@ -40,6 +40,25 @@ def initialize(limit = 1, parent: nil) # The tasks waiting on this semaphore. attr :waiting + # Allow setting the limit. This is useful for cases where the semaphore is used to limit the number of concurrent tasks, but the number of tasks is not known in advance or needs to be modified. + # + # On increasing the limit, some tasks may be immediately resumed. On decreasing the limit, some tasks may execute until the count is < than the limit. + # + # @parameter limit [Integer] The new limit. + def limit= limit + difference = limit - @limit + @limit = limit + + # We can't suspend + if difference > 0 + difference.times do + break unless fiber = @waiting.shift + + fiber.resume + end + end + end + # Is the semaphore currently acquired? def empty? @count.zero? diff --git a/spec/async/semaphore_spec.rb b/spec/async/semaphore_spec.rb index 18d435b5..5897e5f9 100644 --- a/spec/async/semaphore_spec.rb +++ b/spec/async/semaphore_spec.rb @@ -116,6 +116,43 @@ end end + context '#limit=' do + it "releases tasks when limit is increased" do + subject.acquire + expect(subject.count).to be == 1 + expect(subject.blocking?).to be_truthy + + task = Async do + subject.acquire + end + + subject.limit = 2 + task.wait + + expect(subject.count).to be == 2 + end + + it "blocks tasks when limit is decreased" do + subject.limit = 2 + subject.acquire + subject.acquire + + expect(subject.count).to be == 2 + expect(subject.blocking?).to be_truthy + + task = Async do + subject.acquire + end + + subject.limit = 1 + subject.release + subject.release + task.wait + + expect(subject.count).to be == 1 + end + end + context '#empty?' do it 'should be empty unless acquired' do expect(subject).to be_empty From 323c66a6991cac7d13bd888b9d5fcd32d821a5f0 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 2 Mar 2023 22:41:43 +1300 Subject: [PATCH 18/29] Fix coverage and add Ruby 3.1, 3.2. --- .github/workflows/development.yml | 7 ++++--- bake.rb | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml index 8e72c5dd..47381cb4 100644 --- a/.github/workflows/development.yml +++ b/.github/workflows/development.yml @@ -15,9 +15,10 @@ jobs: - macos ruby: - - "2.6" - "2.7" - "3.0" + - "3.1" + - "3.2" experimental: [false] env: [""] @@ -34,8 +35,8 @@ jobs: ruby: head experimental: true - os: ubuntu - ruby: 2.6 - env: COVERAGE=PartialSummary,Coveralls + ruby: "3.2" + env: COVERAGE=PartialSummary experimental: true steps: diff --git a/bake.rb b/bake.rb index d4899c0e..7894a25c 100644 --- a/bake.rb +++ b/bake.rb @@ -5,8 +5,8 @@ def external Bundler.with_clean_env do clone_and_test("async-io") - clone_and_test("async-pool") - clone_and_test("async-websocket") + clone_and_test("async-pool", "sus") + clone_and_test("async-websocket", "sus") clone_and_test("async-dns") clone_and_test("async-http") clone_and_test("falcon") @@ -16,7 +16,7 @@ def external private -def clone_and_test(name) +def clone_and_test(name, command = "rspec") require 'fileutils' path = "external/#{name}" @@ -35,5 +35,5 @@ def clone_and_test(name) file.puts('gem "async", path: "../../"') end - system("cd #{path} && bundle install && bundle exec rspec") or abort("Tests failed!") + system("cd #{path} && bundle install && bundle exec #{command}") or abort("Tests for #{name} failed!") end From 9e49bee9412b7ae6124ba5b6d0b9ff3fcc3b3ed8 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 2 Mar 2023 23:18:55 +1300 Subject: [PATCH 19/29] Bump minor version. --- lib/async/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/async/version.rb b/lib/async/version.rb index 7c64d437..8384ce84 100644 --- a/lib/async/version.rb +++ b/lib/async/version.rb @@ -21,5 +21,5 @@ # THE SOFTWARE. module Async - VERSION = "1.30.3" + VERSION = "1.31.0" end From 8f590415c08943372e0227f01d3c32e92dbcbed0 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 12 Mar 2023 20:33:48 +1300 Subject: [PATCH 20/29] Improve robustness of test, fixes #218. --- spec/async/semaphore_spec.rb | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/spec/async/semaphore_spec.rb b/spec/async/semaphore_spec.rb index 5897e5f9..c9c76071 100644 --- a/spec/async/semaphore_spec.rb +++ b/spec/async/semaphore_spec.rb @@ -62,7 +62,7 @@ 3.times.map do |i| semaphore.async do |task| order << i - task.sleep(0.1) + task.yield order << i end end.collect(&:result) @@ -72,17 +72,22 @@ it 'allows tasks to execute concurrently' do semaphore = Async::Semaphore.new(3) - order = [] + concurrency = 0 + latch = Async::Condition.new 3.times.map do |i| semaphore.async do |task| - order << i - task.sleep(0.1) - order << i + concurrency += 1 + + if concurrency == 3 + latch.signal + else + latch.wait + end end - end.collect(&:result) + end.each(&:wait) - expect(order).to be == [0, 1, 2, 0, 1, 2] + expect(concurrency).to be == 3 end end From fe8aa063a7c5bdf24fd39409b45d949191513e15 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 13 Jun 2023 22:09:15 +0900 Subject: [PATCH 21/29] More improvements to test, fixes #218. --- spec/async/scheduler_spec.rb | 22 ++++++++++------------ spec/async/task_spec.rb | 11 ++++++----- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/spec/async/scheduler_spec.rb b/spec/async/scheduler_spec.rb index 354c0c59..2d6faab6 100644 --- a/spec/async/scheduler_spec.rb +++ b/spec/async/scheduler_spec.rb @@ -56,21 +56,19 @@ let(:message) {"Helloooooo World!"} it "can send message via pipe" do - input, output = IO.pipe - - reactor.async do - sleep(0.001) - - message.each_char do |character| - output.write(character) + IO.pipe do |input, output| + reactor.async do + sleep(0.001) + + message.each_char do |character| + output.write(character) + end + + output.close end - output.close + expect(input.read).to be == message end - - expect(input.read).to be == message - - input.close end it "can fetch website using Net::HTTP" do diff --git a/spec/async/task_spec.rb b/spec/async/task_spec.rb index 12679577..cc61ba0b 100644 --- a/spec/async/task_spec.rb +++ b/spec/async/task_spec.rb @@ -300,12 +300,13 @@ it "can sleep for the requested duration" do state = nil - reactor.async do |task| - task.sleep(duration) - state = :finished - end - + # Measure the entire time, it's got to be at least bigger than the requested duration: time = Async::Clock.measure do + reactor.async do |task| + task.sleep(duration) + state = :finished + end + reactor.run end From ab28e6079bed6a99284dff20c8c9a805e593806a Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Wed, 27 Mar 2024 23:44:42 +1300 Subject: [PATCH 22/29] Backport `Task#defer_stop`. --- lib/async/task.rb | 44 ++++++++++++++++++++++++++++++ spec/async/task_spec.rb | 59 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/lib/async/task.rb b/lib/async/task.rb index 9aa22679..ed32885d 100644 --- a/lib/async/task.rb +++ b/lib/async/task.rb @@ -84,6 +84,8 @@ def initialize(reactor, parent = Task.current?, logger: nil, finished: nil, **op @logger = logger || @parent.logger @fiber = make_fiber(&block) + + @defer_stop = nil end attr :logger @@ -162,6 +164,13 @@ def stop(later = false) return end + # If we are deferring stop... + if @defer_stop == false + # Don't stop now... but update the state so we know we need to stop later. + @defer_stop = true + return false + end + if self.running? if self.current? if later @@ -182,6 +191,41 @@ def stop(later = false) end end + # 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. + # + # You can nest calls to defer_stop, but the stop will only be deferred until the outermost block exits. + # + # If stop is invoked a second time, it will be immediately executed. + # + # @yields {} The block of code to execute. + def defer_stop + # Tri-state variable for controlling stop: + # - nil: defer_stop has not been called. + # - false: defer_stop has been called and we are not stopping. + # - true: defer_stop has been called and we will stop when exiting the block. + if @defer_stop.nil? + # If we are not deferring stop already, we can defer it now: + @defer_stop = false + + begin + yield + rescue Stop + # If we are exiting due to a stop, we shouldn't try to invoke stop again: + @defer_stop = nil + raise + ensure + # If we were asked to stop, we should do so now: + if @defer_stop + @defer_stop = nil + self.stop + end + end + else + # If we are deferring stop already, entering it again is a no-op. + yield + end + end + # Lookup the {Task} for the current fiber. Raise `RuntimeError` if none is available. # @return [Async::Task] # @raise [RuntimeError] if task was not {set!} for the current fiber. diff --git a/spec/async/task_spec.rb b/spec/async/task_spec.rb index cc61ba0b..a57c1843 100644 --- a/spec/async/task_spec.rb +++ b/spec/async/task_spec.rb @@ -22,6 +22,7 @@ require 'async' require 'async/clock' +require 'async/notification' RSpec.describe Async::Task do let(:reactor) {Async::Reactor.new} @@ -496,4 +497,62 @@ def sleep_forever expect(apples_task.to_s).to include "complete" end end + + describe '#defer_stop' do + it "can defer stopping a task" do + child_task = reactor.async do |task| + task.defer_stop do + task.sleep(10) + end + end + + reactor.run_once(0) + + child_task.stop + expect(child_task).to be_running + + child_task.stop + expect(child_task).to be_stopped + end + + it "will stop the task if it was deferred" do + condition = Async::Notification.new + + child_task = reactor.async do |task| + task.defer_stop do + condition.wait + end + end + + reactor.run_once(0) + + child_task.stop(true) + expect(child_task).to be_running + + reactor.async do + condition.signal + end + + reactor.run_once(0) + expect(child_task).to be_stopped + end + + it "can defer stop in a deferred stop" do + child_task = reactor.async do |task| + task.defer_stop do + task.defer_stop do + task.sleep(10) + end + end + end + + reactor.run_once(0) + + child_task.stop + expect(child_task).to be_running + + child_task.stop + expect(child_task).to be_stopped + end + end end From c349f471daa1a1ca11da83f6b62038275fff00c3 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 28 Mar 2024 00:38:34 +1300 Subject: [PATCH 23/29] Remove async-websocket from external tests. --- bake.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/bake.rb b/bake.rb index 7894a25c..77acf907 100644 --- a/bake.rb +++ b/bake.rb @@ -6,7 +6,6 @@ def external Bundler.with_clean_env do clone_and_test("async-io") clone_and_test("async-pool", "sus") - clone_and_test("async-websocket", "sus") clone_and_test("async-dns") clone_and_test("async-http") clone_and_test("falcon") From e2f84c7ee38c231210303808094a8b0661df2fa4 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 28 Mar 2024 00:45:49 +1300 Subject: [PATCH 24/29] Fix external tests that use sus. --- bake.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bake.rb b/bake.rb index 77acf907..5786dabf 100644 --- a/bake.rb +++ b/bake.rb @@ -7,9 +7,9 @@ def external clone_and_test("async-io") clone_and_test("async-pool", "sus") clone_and_test("async-dns") - clone_and_test("async-http") - clone_and_test("falcon") - clone_and_test("async-rest") + clone_and_test("async-http", "sus") + clone_and_test("falcon", "sus") + clone_and_test("async-rest", "sus") end end From bd91daa9b0853ba0bc22a492801788a800b67bcc Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 28 Mar 2024 00:57:55 +1300 Subject: [PATCH 25/29] Remove async-http from external tests. --- bake.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/bake.rb b/bake.rb index 5786dabf..94698f33 100644 --- a/bake.rb +++ b/bake.rb @@ -7,7 +7,6 @@ def external clone_and_test("async-io") clone_and_test("async-pool", "sus") clone_and_test("async-dns") - clone_and_test("async-http", "sus") clone_and_test("falcon", "sus") clone_and_test("async-rest", "sus") end From 7336fa48eac5bfa7e4a2a5eb041cab4162aab0ee Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 28 Mar 2024 01:01:19 +1300 Subject: [PATCH 26/29] Remove other gems from external tests. --- bake.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bake.rb b/bake.rb index 94698f33..b222aba2 100644 --- a/bake.rb +++ b/bake.rb @@ -5,10 +5,6 @@ def external Bundler.with_clean_env do clone_and_test("async-io") - clone_and_test("async-pool", "sus") - clone_and_test("async-dns") - clone_and_test("falcon", "sus") - clone_and_test("async-rest", "sus") end end From 80dac9d092dbc45cfa310ad37d675a2a56509165 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 28 Mar 2024 01:07:42 +1300 Subject: [PATCH 27/29] Bump minor version. --- lib/async/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/async/version.rb b/lib/async/version.rb index 8384ce84..f133b64a 100644 --- a/lib/async/version.rb +++ b/lib/async/version.rb @@ -21,5 +21,5 @@ # THE SOFTWARE. module Async - VERSION = "1.31.0" + VERSION = "1.32.0" end From a720921e424f131c286f7abac52b3c6bddd4f530 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 28 Mar 2024 12:19:13 +1300 Subject: [PATCH 28/29] Reduce logging noise if reactor is inspected. --- lib/async/reactor.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/async/reactor.rb b/lib/async/reactor.rb index 41caf91f..d769ae55 100644 --- a/lib/async/reactor.rb +++ b/lib/async/reactor.rb @@ -93,6 +93,10 @@ def initialize(parent = nil, selector: self.class.selector, logger: nil) @unblocked = [] end + def inspect + "#<#{self.class} children=#{@children&.size} stopped=#{stopped?}>" + end + attr :scheduler attr :logger From c8c8bb915be781d4ba1951d883941819fab2db2f Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 28 Mar 2024 12:20:17 +1300 Subject: [PATCH 29/29] Bump patch version. --- lib/async/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/async/version.rb b/lib/async/version.rb index f133b64a..9ea403d0 100644 --- a/lib/async/version.rb +++ b/lib/async/version.rb @@ -21,5 +21,5 @@ # THE SOFTWARE. module Async - VERSION = "1.32.0" + VERSION = "1.32.1" end