├── .editorconfig ├── .github └── workflows │ ├── documentation-coverage.yaml │ ├── documentation.yaml │ ├── rubocop.yaml │ ├── test-coverage.yaml │ ├── test-external.yaml │ ├── test-io_uring.yaml │ ├── test-select.yaml │ ├── test-worker-pool.yaml │ └── test.yaml ├── .gitignore ├── .mailmap ├── .rubocop.yml ├── assets ├── license.md ├── logo-lossless.webp ├── logo-v1.svg └── logo.webp ├── async.gemspec ├── bake.rb ├── benchmark ├── async_vs_lightio.rb ├── core │ ├── fiber-creation.rb │ ├── results.md │ └── thread-creation.rb ├── fiber_count.rb ├── rubies │ ├── README.md │ └── benchmark.rb ├── thread_count.rb ├── thread_vs_fiber.rb └── timers │ ├── after_0.rb │ ├── after_cancel.rb │ ├── after_n.rb │ ├── gems.locked │ └── gems.rb ├── config ├── environment.yaml ├── external.yaml ├── metrics.rb ├── sus.rb └── traces.rb ├── examples ├── buffer │ └── buffer.rb ├── bugs │ └── write_lock.rb ├── callback │ └── loop.rb ├── capture │ ├── README.md │ └── capture.rb ├── count │ ├── fibers.rb │ └── threads.rb ├── dataloader │ ├── dataloader.rb │ ├── gems.locked │ ├── gems.rb │ └── main.rb ├── debug │ ├── gems.locked │ ├── gems.rb │ └── sample.rb ├── dining-philosophers │ └── philosophers.rb ├── hup-test │ ├── child.rb │ └── main.rb ├── load │ └── test.rb ├── queue │ └── producer.rb └── stop │ ├── condition.rb │ └── sleep.rb ├── fixtures └── async │ ├── a_condition.rb │ ├── a_queue.rb │ └── chainable_async.rb ├── gems.rb ├── guides ├── asynchronous-tasks │ └── readme.md ├── best-practices │ └── readme.md ├── compatibility │ └── readme.md ├── debugging │ └── readme.md ├── getting-started │ └── readme.md ├── links.yaml └── scheduler │ └── readme.md ├── lib ├── async.rb ├── async │ ├── barrier.md │ ├── barrier.rb │ ├── clock.rb │ ├── condition.md │ ├── condition.rb │ ├── console.rb │ ├── idler.rb │ ├── limited_queue.rb │ ├── list.rb │ ├── node.rb │ ├── notification.rb │ ├── queue.rb │ ├── reactor.rb │ ├── scheduler.rb │ ├── semaphore.md │ ├── semaphore.rb │ ├── task.md │ ├── task.rb │ ├── timeout.rb │ ├── variable.rb │ ├── version.rb │ ├── waiter.md │ ├── waiter.rb │ └── worker_pool.rb ├── kernel │ ├── async.rb │ └── sync.rb ├── metrics │ └── provider │ │ ├── async.rb │ │ └── async │ │ └── task.rb └── traces │ └── provider │ ├── async.rb │ └── async │ ├── barrier.rb │ └── task.rb ├── license.md ├── readme.md ├── release.cert ├── releases.md ├── tea.yaml └── test ├── async ├── barrier.rb ├── children.rb ├── clock.rb ├── condition.rb ├── idler.rb ├── limited_queue.rb ├── list.rb ├── node.rb ├── notification.rb ├── queue.rb ├── reactor.rb ├── reactor │ └── nested.rb ├── scheduler.rb ├── scheduler │ ├── address.rb │ ├── condition_variable.rb │ ├── io.rb │ ├── kernel.rb │ ├── thread.rb │ └── timeout.rb ├── semaphore.rb ├── task.rb ├── timeout.rb ├── variable.rb ├── waiter.rb └── worker_pool.rb ├── enumerator.rb ├── fiber.rb ├── io.rb ├── io └── buffer.rb ├── kernel.rb ├── kernel ├── async.rb └── sync.rb ├── net └── http.rb ├── process.rb ├── tempfile.rb ├── thread.rb ├── thread └── queue.rb └── traces └── provider └── async └── task.rb /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | 7 | [*.{yml,yaml}] 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.github/workflows/documentation-coverage.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation Coverage 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | COVERAGE: PartialSummary 11 | 12 | jobs: 13 | validate: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: "3.4" 21 | bundler-cache: true 22 | 23 | - name: Validate coverage 24 | timeout-minutes: 5 25 | run: bundle exec bake decode:index:coverage lib 26 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages: 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | # Allow one concurrent deployment: 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: true 18 | 19 | env: 20 | CONSOLE_OUTPUT: XTerm 21 | BUNDLE_WITH: maintenance 22 | 23 | jobs: 24 | generate: 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - uses: ruby/setup-ruby@v1 31 | with: 32 | ruby-version: "3.4" 33 | bundler-cache: true 34 | 35 | - name: Installing packages 36 | run: sudo apt-get install wget 37 | 38 | - name: Generate documentation 39 | timeout-minutes: 5 40 | run: bundle exec bake utopia:project:static --force no 41 | 42 | - name: Upload documentation artifact 43 | uses: actions/upload-pages-artifact@v3 44 | with: 45 | path: docs 46 | 47 | deploy: 48 | runs-on: ubuntu-latest 49 | 50 | environment: 51 | name: github-pages 52 | url: ${{steps.deployment.outputs.page_url}} 53 | 54 | needs: generate 55 | steps: 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v4 59 | -------------------------------------------------------------------------------- /.github/workflows/rubocop.yaml: -------------------------------------------------------------------------------- 1 | name: RuboCop 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | 11 | jobs: 12 | check: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: ruby/setup-ruby@v1 18 | with: 19 | ruby-version: ruby 20 | bundler-cache: true 21 | 22 | - name: Run RuboCop 23 | timeout-minutes: 10 24 | run: bundle exec rubocop 25 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | name: Test Coverage 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | COVERAGE: PartialSummary 11 | 12 | jobs: 13 | test: 14 | name: ${{matrix.ruby}} on ${{matrix.os}} with ${{matrix.io_event_selector}} 15 | runs-on: ${{matrix.os}}-latest 16 | 17 | strategy: 18 | matrix: 19 | include: 20 | - os: ubuntu 21 | ruby: "3.2" 22 | io_event_selector: EPoll 23 | - os: ubuntu 24 | ruby: "3.3" 25 | io_event_selector: EPoll 26 | - os: ubuntu 27 | ruby: "3.4" 28 | io_event_selector: EPoll 29 | - os: ubuntu 30 | ruby: "3.4" 31 | io_event_selector: URing 32 | - os: ubuntu 33 | ruby: "3.4" 34 | io_event_selector: URing 35 | fiber_profiler_capture: "true" 36 | - os: ubuntu 37 | ruby: "head" 38 | io_event_selector: URing 39 | - os: ubuntu 40 | ruby: "head" 41 | io_event_selector: URing 42 | async_scheduler_worker_pool: "true" 43 | 44 | env: 45 | IO_EVENT_SELECTOR: ${{matrix.io_event_selector}} 46 | ASYNC_SCHEDULER_WORKER_POOL: ${{matrix.async_scheduler_worker_pool}} 47 | FIBER_PROFILER_CAPTURE: ${{matrix.fiber_profiler_capture}} 48 | 49 | steps: 50 | - uses: actions/checkout@v4 51 | - uses: ruby/setup-ruby-pkgs@v1 52 | with: 53 | ruby-version: ${{matrix.ruby}} 54 | bundler-cache: true 55 | cache-version: io_uring 56 | apt-get: liburing-dev 57 | 58 | - name: Run tests 59 | timeout-minutes: 5 60 | run: bundle exec bake test 61 | 62 | - uses: actions/upload-artifact@v4 63 | with: 64 | include-hidden-files: true 65 | if-no-files-found: error 66 | name: coverage-${{matrix.os}}-${{matrix.ruby}}-${{matrix.io_event_selector}}-${{matrix.async_scheduler_worker_pool}}-${{matrix.fiber_profiler_capture}} 67 | path: .covered.db 68 | 69 | validate: 70 | needs: test 71 | runs-on: ubuntu-latest 72 | 73 | steps: 74 | - uses: actions/checkout@v4 75 | - uses: ruby/setup-ruby@v1 76 | with: 77 | ruby-version: "3.4" 78 | bundler-cache: true 79 | 80 | - uses: actions/download-artifact@v4 81 | 82 | - name: Validate coverage 83 | timeout-minutes: 5 84 | run: bundle exec bake covered:validate --paths */.covered.db \; 85 | -------------------------------------------------------------------------------- /.github/workflows/test-external.yaml: -------------------------------------------------------------------------------- 1 | name: Test External 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | 11 | jobs: 12 | test: 13 | name: ${{matrix.ruby}} on ${{matrix.os}} 14 | runs-on: ${{matrix.os}}-latest 15 | 16 | strategy: 17 | matrix: 18 | os: 19 | - ubuntu 20 | - macos 21 | 22 | ruby: 23 | - "3.2" 24 | - "3.3" 25 | - "3.4" 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: ${{matrix.ruby}} 32 | bundler-cache: true 33 | 34 | - name: Run tests 35 | timeout-minutes: 10 36 | run: bundle exec bake test:external 37 | -------------------------------------------------------------------------------- /.github/workflows/test-io_uring.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | IO_EVENT_SELECTOR: URing 11 | 12 | jobs: 13 | test: 14 | name: ${{matrix.ruby}} on ${{matrix.os}} / IO_EVENT_SELECTOR=URing 15 | runs-on: ${{matrix.os}}-latest 16 | 17 | strategy: 18 | matrix: 19 | os: 20 | - ubuntu 21 | 22 | ruby: 23 | - "3.3" 24 | - "3.4" 25 | - "head" 26 | 27 | steps: 28 | - uses: actions/checkout@v3 29 | 30 | - name: Install packages (Ubuntu) 31 | if: matrix.os == 'ubuntu' 32 | run: sudo apt-get install -y liburing-dev 33 | 34 | - uses: ruby/setup-ruby@v1 35 | with: 36 | ruby-version: ${{matrix.ruby}} 37 | bundler-cache: true 38 | cache-version: io_uring 39 | 40 | - name: Backends 41 | run: bundle exec ruby -r"io/event" -e "puts IO::Event::Selector.constants" 42 | 43 | - name: Run tests 44 | timeout-minutes: 10 45 | run: bundle exec bake test 46 | 47 | # - name: Run external tests 48 | # timeout-minutes: 10 49 | # run: bundle exec bake test:external 50 | -------------------------------------------------------------------------------- /.github/workflows/test-select.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | IO_EVENT_SELECTOR: Select 11 | 12 | jobs: 13 | test: 14 | name: ${{matrix.ruby}} on ${{matrix.os}} / IO_EVENT_SELECTOR=Select 15 | runs-on: ${{matrix.os}}-latest 16 | 17 | strategy: 18 | matrix: 19 | os: 20 | - ubuntu 21 | - macos 22 | 23 | ruby: 24 | - "3.2" 25 | - "3.3" 26 | - "3.4" 27 | - "head" 28 | 29 | steps: 30 | - uses: actions/checkout@v3 31 | - uses: ruby/setup-ruby@v1 32 | with: 33 | ruby-version: ${{matrix.ruby}} 34 | bundler-cache: true 35 | 36 | - name: Run tests 37 | timeout-minutes: 10 38 | run: bundle exec bake test 39 | 40 | # Maybe buggy. 41 | # - name: Run external tests 42 | # timeout-minutes: 10 43 | # run: bundle exec bake test:external 44 | -------------------------------------------------------------------------------- /.github/workflows/test-worker-pool.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | ASYNC_SCHEDULER_WORKER_POOL: true 11 | 12 | jobs: 13 | test: 14 | name: ${{matrix.ruby}} on ${{matrix.os}} / ASYNC_SCHEDULER_WORKER_POOL=true 15 | runs-on: ${{matrix.os}}-latest 16 | 17 | strategy: 18 | matrix: 19 | os: 20 | - ubuntu 21 | 22 | ruby: 23 | - "3.4" 24 | - "head" 25 | 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{matrix.ruby}} 31 | bundler-cache: true 32 | 33 | - name: Run tests 34 | timeout-minutes: 10 35 | run: bundle exec bake test 36 | 37 | - name: Run external tests 38 | timeout-minutes: 10 39 | run: bundle exec bake test:external 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | 11 | jobs: 12 | test: 13 | name: ${{matrix.ruby}} on ${{matrix.os}} 14 | runs-on: ${{matrix.os}}-latest 15 | continue-on-error: ${{matrix.experimental}} 16 | 17 | strategy: 18 | matrix: 19 | os: 20 | - ubuntu 21 | - macos 22 | 23 | ruby: 24 | - "3.2" 25 | - "3.3" 26 | - "3.4" 27 | 28 | experimental: [false] 29 | 30 | include: 31 | - os: ubuntu 32 | ruby: truffleruby 33 | experimental: true 34 | - os: ubuntu 35 | ruby: jruby 36 | experimental: true 37 | - os: ubuntu 38 | ruby: head 39 | experimental: true 40 | 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: ruby/setup-ruby@v1 44 | with: 45 | ruby-version: ${{matrix.ruby}} 46 | bundler-cache: true 47 | 48 | - name: Run tests 49 | timeout-minutes: 10 50 | run: bundle exec bake test 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /pkg/ 3 | /gems.locked 4 | /.covered.db 5 | /external 6 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Jun Jiang 2 | Ken Muryoi 3 | Jeremy Jung 4 | Sokolov Yura 5 | Masafumi Okura 6 | Masayuki Yamamoto 7 | Leon Löchner <60091705+leonnicolas@users.noreply.github.com> 8 | Math Ieu 9 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | DisabledByDefault: true 3 | 4 | Layout/IndentationStyle: 5 | Enabled: true 6 | EnforcedStyle: tabs 7 | 8 | Layout/InitialIndentation: 9 | Enabled: true 10 | 11 | Layout/IndentationWidth: 12 | Enabled: true 13 | Width: 1 14 | 15 | Layout/IndentationConsistency: 16 | Enabled: true 17 | EnforcedStyle: normal 18 | 19 | Layout/BlockAlignment: 20 | Enabled: true 21 | 22 | Layout/EndAlignment: 23 | Enabled: true 24 | EnforcedStyleAlignWith: start_of_line 25 | 26 | Layout/BeginEndAlignment: 27 | Enabled: true 28 | EnforcedStyleAlignWith: start_of_line 29 | 30 | Layout/ElseAlignment: 31 | Enabled: true 32 | 33 | Layout/DefEndAlignment: 34 | Enabled: true 35 | 36 | Layout/CaseIndentation: 37 | Enabled: true 38 | 39 | Layout/CommentIndentation: 40 | Enabled: true 41 | 42 | Layout/EmptyLinesAroundClassBody: 43 | Enabled: true 44 | 45 | Layout/EmptyLinesAroundModuleBody: 46 | Enabled: true 47 | 48 | Style/FrozenStringLiteralComment: 49 | Enabled: true 50 | 51 | Style/StringLiterals: 52 | Enabled: true 53 | EnforcedStyle: double_quotes 54 | -------------------------------------------------------------------------------- /assets/license.md: -------------------------------------------------------------------------------- 1 | # Assets 2 | 3 | ## Logo 4 | 5 | Copyright, 2022, by Malene Laugesen. 6 | 7 | This work is licensed under a [Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License](https://creativecommons.org/licenses/by-nc-nd/4.0/). 8 | -------------------------------------------------------------------------------- /assets/logo-lossless.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketry/async/f30c2f881e6700dc73064b4cfa90ad047341db5f/assets/logo-lossless.webp -------------------------------------------------------------------------------- /assets/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socketry/async/f30c2f881e6700dc73064b4cfa90ad047341db5f/assets/logo.webp -------------------------------------------------------------------------------- /async.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/async/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "async" 7 | spec.version = Async::VERSION 8 | 9 | spec.summary = "A concurrency framework for Ruby." 10 | spec.authors = ["Samuel Williams", "Bruno Sutic", "Jeremy Jung", "Olle Jonsson", "Patrik Wenger", "Devin Christensen", "Emil Tin", "Jamie McCarthy", "Kent Gruber", "Brian Morearty", "Colin Kelley", "Dimitar Peychinov", "Gert Goet", "Jiang Jinyang", "Julien Portalier", "Jun Jiang", "Ken Muryoi", "Leon Löchner", "Masafumi Okura", "Masayuki Yamamoto", "Math Ieu", "Ryan Musgrave", "Salim Semaoune", "Shannon Skipper", "Sokolov Yura", "Stefan Wrobel", "Trevor Turk"] 11 | spec.license = "MIT" 12 | 13 | spec.cert_chain = ["release.cert"] 14 | spec.signing_key = File.expand_path("~/.gem/release.pem") 15 | 16 | spec.homepage = "https://github.com/socketry/async" 17 | 18 | spec.metadata = { 19 | "documentation_uri" => "https://socketry.github.io/async/", 20 | "funding_uri" => "https://github.com/sponsors/ioquatix/", 21 | "source_code_uri" => "https://github.com/socketry/async.git", 22 | } 23 | 24 | spec.files = Dir.glob(["{lib}/**/*", "*.md"], File::FNM_DOTMATCH, base: __dir__) 25 | 26 | spec.required_ruby_version = ">= 3.1" 27 | 28 | spec.add_dependency "console", "~> 1.29" 29 | spec.add_dependency "fiber-annotation" 30 | spec.add_dependency "io-event", "~> 1.9" 31 | spec.add_dependency "traces", "~> 0.15" 32 | spec.add_dependency "metrics", "~> 0.12" 33 | end 34 | -------------------------------------------------------------------------------- /bake.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2020-2024, by Samuel Williams. 5 | 6 | # Update the project documentation with the new version number. 7 | # 8 | # @parameter version [String] The new version number. 9 | def after_gem_release_version_increment(version) 10 | context["releases:update"].call(version) 11 | context["utopia:project:readme:update"].call 12 | end 13 | -------------------------------------------------------------------------------- /benchmark/async_vs_lightio.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2018-2024, by Samuel Williams. 6 | 7 | require "async" 8 | require "lightio" 9 | 10 | require "benchmark/ips" 11 | 12 | # 13 | # It's hard to know exactly how to interpret these results. When running parallel 14 | # instances, resource contention is more likely to be a problem, and yet with 15 | # async, the performance between a single task and several tasks is roughly the 16 | # same, while in the case of lightio, there is an obvious performance gap. 17 | # 18 | # The main takeaway is that contention causes issues and if systems are not 19 | # designed with that in mind, it will impact performance. 20 | # 21 | # $ ruby async_vs_lightio.rb 22 | # Warming up -------------------------------------- 23 | # lightio (synchronous) 24 | # 2.439k i/100ms 25 | # async (synchronous) 2.115k i/100ms 26 | # lightio (parallel) 211.000 i/100ms 27 | # async (parallel) 449.000 i/100ms 28 | # Calculating ------------------------------------- 29 | # lightio (synchronous) 30 | # 64.502k (± 3.9%) i/s - 643.896k in 10.002151s 31 | # async (synchronous) 161.195k (± 1.6%) i/s - 1.612M in 10.000976s 32 | # lightio (parallel) 49.827k (±17.5%) i/s - 477.704k in 9.999579s 33 | # async (parallel) 166.862k (± 6.2%) i/s - 1.662M in 10.000365s 34 | # 35 | # Comparison: 36 | # async (parallel): 166862.3 i/s 37 | # async (synchronous): 161194.6 i/s - same-ish: difference falls within error 38 | # lightio (synchronous): 64502.5 i/s - 2.59x slower 39 | # lightio (parallel): 49827.3 i/s - 3.35x slower 40 | 41 | 42 | DURATION = 0.000001 43 | 44 | def run_async(count, repeats = 10000) 45 | Async::Reactor.run do |task| 46 | count.times.map do 47 | task.async do |subtask| 48 | repeats.times do 49 | sleep(DURATION) 50 | end 51 | end 52 | end.each(&:wait) 53 | end 54 | end 55 | 56 | def run_lightio(count, repeats = 10000) 57 | count.times.map do 58 | LightIO::Beam.new do 59 | repeats.times do 60 | LightIO.sleep(DURATION) 61 | end 62 | end 63 | end.each(&:join) 64 | end 65 | 66 | Benchmark.ips do |benchmark| 67 | benchmark.time = 10 68 | benchmark.warmup = 2 69 | 70 | benchmark.report("lightio (synchronous)") do |count| 71 | run_lightio(1, count) 72 | end 73 | 74 | benchmark.report("async (synchronous)") do |count| 75 | run_async(1, count) 76 | end 77 | 78 | benchmark.report("lightio (parallel)") do |count| 79 | run_lightio(32, count/32) 80 | end 81 | 82 | benchmark.report("async (parallel)") do |count| 83 | run_async(32, count/32) 84 | end 85 | 86 | benchmark.compare! 87 | end 88 | -------------------------------------------------------------------------------- /benchmark/core/fiber-creation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2023, by Samuel Williams. 5 | 6 | puts RUBY_VERSION 7 | 8 | times = [] 9 | 10 | 10.times do 11 | start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) 12 | 13 | fibers = 10_000.times.map do 14 | Fiber.new do 15 | true 16 | end 17 | end 18 | 19 | fibers.each(&:resume) 20 | 21 | duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time 22 | duration_us = duration * 1_000_000 23 | duration_per_iteration = duration_us / fibers.size 24 | 25 | times << duration_per_iteration 26 | puts "Fiber duration: #{duration_per_iteration.round(2)}us" 27 | end 28 | 29 | puts "Average: #{(times.sum / times.size).round(2)}us" 30 | puts " Best: #{times.min.round(2)}us" 31 | -------------------------------------------------------------------------------- /benchmark/core/results.md: -------------------------------------------------------------------------------- 1 | ## Ruby 2.6.10 2 | 3 | ``` 4 | samuel@aiko ~/P/s/a/b/core (main)> ruby fiber-creation.rb 5 | 2.6.10 6 | Fiber duration: 3.34us 7 | Fiber duration: 2.57us 8 | Fiber duration: 2.06us 9 | Fiber duration: 2.23us 10 | Fiber duration: 1.96us 11 | Fiber duration: 2.13us 12 | Fiber duration: 1.99us 13 | Fiber duration: 2.13us 14 | Fiber duration: 1.97us 15 | Fiber duration: 1.98us 16 | Average: 2.23us 17 | Best: 1.96us 18 | samuel@aiko ~/P/s/a/b/core (main)> ruby thread-creation.rb 19 | 2.6.10 20 | Thread duration: 47.01us 21 | Thread duration: 44.85us 22 | Thread duration: 50.91us 23 | Thread duration: 45.06us 24 | Thread duration: 47.04us 25 | Thread duration: 48.75us 26 | Thread duration: 52.04us 27 | Thread duration: 54.62us 28 | Thread duration: 48.35us 29 | Thread duration: 50.79us 30 | Average: 48.94us 31 | Best: 44.85us 32 | ``` 33 | 34 | ## Ruby 2.7.7 35 | 36 | ``` 37 | samuel@aiko ~/P/s/a/b/core (main)> ruby fiber-creation.rb 38 | 2.7.7 39 | Fiber duration: 0.93us 40 | Fiber duration: 0.89us 41 | Fiber duration: 0.81us 42 | Fiber duration: 0.83us 43 | Fiber duration: 0.82us 44 | Fiber duration: 0.77us 45 | Fiber duration: 0.73us 46 | Fiber duration: 0.79us 47 | Fiber duration: 0.88us 48 | Fiber duration: 0.8us 49 | Average: 0.83us 50 | Best: 0.73us 51 | samuel@aiko ~/P/s/a/b/core (main)> ruby thread-creation.rb 52 | 2.7.7 53 | Thread duration: 13.44us 54 | Thread duration: 11.32us 55 | Thread duration: 11.05us 56 | Thread duration: 10.71us 57 | Thread duration: 9.22us 58 | Thread duration: 9.89us 59 | Thread duration: 9.48us 60 | Thread duration: 10.25us 61 | Thread duration: 9.92us 62 | Thread duration: 9.52us 63 | Average: 10.48us 64 | Best: 9.22us 65 | ``` 66 | 67 | ## Ruby 3.0.4 68 | 69 | ``` 70 | samuel@aiko ~/P/s/a/b/core (main)> ruby fiber-creation.rb 71 | 3.0.4 72 | Fiber duration: 0.88us 73 | Fiber duration: 0.85us 74 | Fiber duration: 0.78us 75 | Fiber duration: 0.76us 76 | Fiber duration: 0.81us 77 | Fiber duration: 0.71us 78 | Fiber duration: 0.87us 79 | Fiber duration: 0.89us 80 | Fiber duration: 0.76us 81 | Fiber duration: 0.96us 82 | Average: 0.83us 83 | Best: 0.71us 84 | samuel@aiko ~/P/s/a/b/core (main)> ruby thread-creation.rb 85 | 3.0.4 86 | Thread duration: 13.72us 87 | Thread duration: 10.92us 88 | Thread duration: 9.97us 89 | Thread duration: 9.44us 90 | Thread duration: 9.28us 91 | Thread duration: 9.38us 92 | Thread duration: 9.31us 93 | Thread duration: 9.45us 94 | Thread duration: 9.4us 95 | Thread duration: 9.39us 96 | Average: 10.03us 97 | Best: 9.28us 98 | ``` 99 | 100 | ## Ruby 3.1.3 101 | 102 | ``` 103 | samuel@aiko ~/P/s/a/b/core (main)> ruby fiber-creation.rb 104 | 3.1.3 105 | Fiber duration: 0.94us 106 | Fiber duration: 0.89us 107 | Fiber duration: 0.86us 108 | Fiber duration: 0.87us 109 | Fiber duration: 0.71us 110 | Fiber duration: 0.78us 111 | Fiber duration: 0.79us 112 | Fiber duration: 0.91us 113 | Fiber duration: 0.75us 114 | Fiber duration: 0.75us 115 | Average: 0.82us 116 | Best: 0.71us 117 | samuel@aiko ~/P/s/a/b/core (main)> ruby thread-creation.rb 118 | 3.1.3 119 | Thread duration: 13.42us 120 | Thread duration: 11.78us 121 | Thread duration: 11.84us 122 | Thread duration: 10.92us 123 | Thread duration: 10.98us 124 | Thread duration: 9.34us 125 | Thread duration: 11.2us 126 | Thread duration: 11.29us 127 | Thread duration: 14.21us 128 | Thread duration: 11.86us 129 | Average: 11.68us 130 | Best: 9.34us 131 | ``` 132 | 133 | ## Ruby 3.2.1 134 | 135 | ``` 136 | samuel@aiko ~/P/s/a/b/core (main)> ruby fiber-creation.rb 137 | 3.2.1 138 | Fiber duration: 1.07us 139 | Fiber duration: 0.91us 140 | Fiber duration: 0.83us 141 | Fiber duration: 0.86us 142 | Fiber duration: 0.81us 143 | Fiber duration: 0.83us 144 | Fiber duration: 0.82us 145 | Fiber duration: 0.83us 146 | Fiber duration: 0.84us 147 | Fiber duration: 0.81us 148 | Average: 0.86us 149 | Best: 0.81us 150 | samuel@aiko ~/P/s/a/b/core (main)> ruby thread-creation.rb 151 | 3.2.1 152 | Thread duration: 13.3us 153 | Thread duration: 11.71us 154 | Thread duration: 13.17us 155 | Thread duration: 10.61us 156 | Thread duration: 8.94us 157 | Thread duration: 11.99us 158 | Thread duration: 12.63us 159 | Thread duration: 12.04us 160 | Thread duration: 10.63us 161 | Thread duration: 12.73us 162 | Average: 11.77us 163 | Best: 8.94us 164 | ``` 165 | -------------------------------------------------------------------------------- /benchmark/core/thread-creation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2023, by Samuel Williams. 5 | 6 | puts RUBY_VERSION 7 | 8 | times = [] 9 | 10 | 10.times do 11 | start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) 12 | 13 | threads = 20_000.times.map do 14 | Thread.new do 15 | true 16 | end 17 | end 18 | 19 | threads.each(&:join) 20 | 21 | duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time 22 | duration_us = duration * 1_000_000 23 | duration_per_iteration = duration_us / threads.size 24 | 25 | times << duration_per_iteration 26 | puts "Thread duration: #{duration_per_iteration.round(2)}us" 27 | end 28 | 29 | puts "Average: #{(times.sum / times.size).round(2)}us" 30 | puts " Best: #{times.min.round(2)}us" 31 | -------------------------------------------------------------------------------- /benchmark/fiber_count.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019-2022, by Samuel Williams. 5 | 6 | fibers = [] 7 | 8 | (1..).each do |i| 9 | fibers << Fiber.new{} 10 | fibers.last.resume 11 | puts i 12 | end 13 | 14 | -------------------------------------------------------------------------------- /benchmark/rubies/README.md: -------------------------------------------------------------------------------- 1 | # (All) Rubies Benchmark 2 | 3 | This is a simple benchmark, which reads and writes data over a pipe. 4 | 5 | It is designed to work as far back as Ruby 1.9.3 at the expense of code clarity. It also works on JRuby and TruffleRuby. 6 | 7 | ## Usage 8 | 9 | The simplest way is to use RVM. 10 | 11 | rvm all do ./benchmark.rb 12 | 13 | ## Results 14 | 15 | General improvements. 16 | 17 | ruby 1.9.3p551 (2014-11-13 revision 48407) [x86_64-linux] 18 | # 19 | 20 | ruby 2.0.0p648 (2015-12-16 revision 53162) [x86_64-linux] 21 | # 22 | 23 | ruby 2.1.10p492 (2016-04-01 revision 54464) [x86_64-linux] 24 | # 25 | 26 | ruby 2.2.10p489 (2018-03-28 revision 63023) [x86_64-linux] 27 | # 28 | 29 | ruby 2.3.8p459 (2018-10-18 revision 65136) [x86_64-linux] 30 | # 31 | 32 | ruby 2.4.6p354 (2019-04-01 revision 67394) [x86_64-linux] 33 | # 34 | 35 | ruby 2.5.3p105 (2018-10-18 revision 65156) [x86_64-linux] 36 | # 37 | 38 | Native fiber implementation & reduced syscalls (https://bugs.ruby-lang.org/issues/14739). 39 | 40 | ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-linux] 41 | # 42 | 43 | Performance regression (https://bugs.ruby-lang.org/issues/16009). 44 | 45 | ruby 2.7.0preview1 (2019-05-31 trunk c55db6aa271df4a689dc8eb0039c929bf6ed43ff) [x86_64-linux] 46 | # 47 | 48 | Improve fiber performance using pool alloation strategy (https://bugs.ruby-lang.org/issues/15997). 49 | 50 | ruby 2.7.0dev (2019-10-02T08:19:14Z trunk 9759e3c9f0) [x86_64-linux] 51 | # 52 | -------------------------------------------------------------------------------- /benchmark/rubies/benchmark.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2019-2024, by Samuel Williams. 6 | 7 | require "socket" 8 | require "fiber" 9 | 10 | puts 11 | puts RUBY_DESCRIPTION 12 | 13 | if RUBY_VERSION < "2.0" 14 | class String 15 | def b 16 | self 17 | end 18 | end 19 | end 20 | 21 | # TODO: make these much larger, see if we're effectively batching 22 | # even if we don't mean to... 23 | QUERY_TEXT = "STATUS".freeze 24 | RESPONSE_TEXT = "OK".freeze 25 | 26 | NUM_WORKERS = (ARGV[0] || 10_000).to_i 27 | NUM_REQUESTS = (ARGV[1] || 100).to_i 28 | 29 | # Fiber reactor code taken from 30 | # https://www.codeotaku.com/journal/2018-11/fibers-are-the-right-solution/index 31 | class Reactor 32 | def initialize 33 | @readable = {} 34 | @writable = {} 35 | end 36 | 37 | def run 38 | while @readable.any? or @writable.any? 39 | readable, writable = IO.select(@readable.keys, @writable.keys, []) 40 | 41 | readable.each do |io| 42 | @readable[io].resume 43 | end 44 | 45 | writable.each do |io| 46 | @writable[io].resume 47 | end 48 | end 49 | end 50 | 51 | def wait_readable(io) 52 | @readable[io] = Fiber.current 53 | Fiber.yield 54 | @readable.delete(io) 55 | end 56 | 57 | def wait_writable(io) 58 | @writable[io] = Fiber.current 59 | Fiber.yield 60 | @writable.delete(io) 61 | end 62 | end 63 | 64 | class Wrapper 65 | def initialize(io, reactor) 66 | @io = io 67 | @reactor = reactor 68 | end 69 | 70 | if RUBY_VERSION >= "2.3" 71 | def read_nonblock(length, buffer) 72 | while true 73 | case result = @io.read_nonblock(length, buffer, exception: false) 74 | when :wait_readable 75 | @reactor.wait_readable(@io) 76 | when :wait_writable 77 | @reactor.wait_writable(@io) 78 | else 79 | return result 80 | end 81 | end 82 | 83 | end 84 | 85 | def write_nonblock(buffer) 86 | while true 87 | case result = @io.write_nonblock(buffer, exception: false) 88 | when :wait_readable 89 | @reactor.wait_readable(@io) 90 | when :wait_writable 91 | @reactor.wait_writable(@io) 92 | else 93 | return result 94 | end 95 | end 96 | end 97 | else 98 | def read_nonblock(length, buffer) 99 | while true 100 | begin 101 | return @io.read_nonblock(length, buffer) 102 | rescue IO::WaitReadable 103 | @reactor.wait_readable(@io) 104 | rescue IO::WaitWritable 105 | @reactor.wait_writable(@io) 106 | end 107 | end 108 | end 109 | 110 | def write_nonblock(buffer) 111 | while true 112 | begin 113 | return @io.write_nonblock(buffer) 114 | rescue IO::WaitReadable 115 | @reactor.wait_readable(@io) 116 | rescue IO::WaitWritable 117 | @reactor.wait_writable(@io) 118 | end 119 | end 120 | end 121 | end 122 | 123 | def read(length, buffer = nil) 124 | if buffer 125 | buffer.clear 126 | else 127 | buffer = String.new.b 128 | end 129 | 130 | result = self.read_nonblock(length - buffer.bytesize, buffer) 131 | 132 | if result == length 133 | return result 134 | end 135 | 136 | chunk = String.new.b 137 | while chunk = self.read_nonblock(length - buffer.bytesize, chunk) 138 | buffer << chunk 139 | 140 | break if buffer.bytesize == length 141 | end 142 | 143 | return buffer 144 | end 145 | 146 | def write(buffer) 147 | remaining = buffer.dup 148 | 149 | while true 150 | result = self.write_nonblock(remaining) 151 | 152 | if result == remaining.bytesize 153 | return buffer.bytesize 154 | else 155 | remaining = remaining.byteslice(result, remaining.bytesize - result) 156 | end 157 | end 158 | end 159 | end 160 | 161 | reactor = Reactor.new 162 | 163 | worker_read = [] 164 | worker_write = [] 165 | 166 | master_read = [] 167 | master_write = [] 168 | 169 | workers = [] 170 | 171 | # puts "Setting up pipes..." 172 | NUM_WORKERS.times do |i| 173 | r, w = IO.pipe 174 | worker_read.push Wrapper.new(r, reactor) 175 | master_write.push Wrapper.new(w, reactor) 176 | 177 | r, w = IO.pipe 178 | worker_write.push Wrapper.new(w, reactor) 179 | master_read.push Wrapper.new(r, reactor) 180 | end 181 | 182 | # puts "Setting up fibers..." 183 | NUM_WORKERS.times do |i| 184 | f = Fiber.new do 185 | # Worker code 186 | NUM_REQUESTS.times do |req_num| 187 | q = worker_read[i].read(QUERY_TEXT.size) 188 | if q != QUERY_TEXT 189 | raise "Fail! Expected #{QUERY_TEXT.inspect} but got #{q.inspect} on request #{req_num.inspect}!" 190 | end 191 | worker_write[i].write(RESPONSE_TEXT) 192 | end 193 | end 194 | workers.push f 195 | end 196 | 197 | workers.each { |f| f.resume } 198 | 199 | master_fiber = Fiber.new do 200 | NUM_WORKERS.times do |worker_num| 201 | f = Fiber.new do 202 | NUM_REQUESTS.times do |req_num| 203 | master_write[worker_num].write(QUERY_TEXT) 204 | buffer = master_read[worker_num].read(RESPONSE_TEXT.size) 205 | if buffer != RESPONSE_TEXT 206 | raise "Error! Fiber no. #{worker_num} on req #{req_num} expected #{RESPONSE_TEXT.inspect} but got #{buf.inspect}!" 207 | end 208 | end 209 | end 210 | f.resume 211 | end 212 | end 213 | 214 | master_fiber.resume 215 | 216 | # puts "Starting reactor..." 217 | reactor.run 218 | 219 | # puts "Done, finished all reactor Fibers!" 220 | 221 | puts Process.times 222 | 223 | # Exit 224 | -------------------------------------------------------------------------------- /benchmark/thread_count.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019-2022, by Samuel Williams. 5 | 6 | threads = [] 7 | 8 | (1..).each do |i| 9 | threads << Thread.new{sleep} 10 | puts i 11 | end 12 | 13 | -------------------------------------------------------------------------------- /benchmark/thread_vs_fiber.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2019-2024, by Samuel Williams. 6 | 7 | require "benchmark/ips" 8 | 9 | GC.disable 10 | 11 | Benchmark.ips do |benchmark| 12 | benchmark.time = 1 13 | benchmark.warmup = 1 14 | 15 | benchmark.report("Thread.new{}") do |count| 16 | while count > 0 17 | Thread.new{count -= 1}.join 18 | end 19 | end 20 | 21 | benchmark.report("Fiber.new{}") do |count| 22 | while count > 0 23 | Fiber.new{count -= 1}.resume 24 | end 25 | end 26 | 27 | benchmark.compare! 28 | end 29 | -------------------------------------------------------------------------------- /benchmark/timers/after_0.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2024, by Samuel Williams. 6 | 7 | require "benchmark/ips" 8 | 9 | require "timers" 10 | require "io/event/timers" 11 | 12 | Benchmark.ips do |benchmark| 13 | benchmark.time = 1 14 | benchmark.warmup = 1 15 | 16 | benchmark.report("Timers::Group") do |count| 17 | timers = Timers::Group.new 18 | 19 | while count > 0 20 | timers.after(0) {count -= 1} 21 | timers.fire 22 | end 23 | end 24 | 25 | benchmark.report("IO::Event::Timers") do |count| 26 | timers = IO::Event::Timers.new 27 | 28 | while count > 0 29 | timers.after(0) {count -= 1} 30 | timers.fire 31 | end 32 | end 33 | 34 | benchmark.compare! 35 | end 36 | -------------------------------------------------------------------------------- /benchmark/timers/after_cancel.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2024, by Samuel Williams. 6 | 7 | require "benchmark/ips" 8 | 9 | require "timers" 10 | require "io/event/timers" 11 | 12 | Benchmark.ips do |benchmark| 13 | benchmark.time = 1 14 | benchmark.warmup = 1 15 | 16 | benchmark.report("Timers::Group") do |count| 17 | timers = Timers::Group.new 18 | 19 | while count > 0 20 | timer = timers.after(0) {} 21 | timer.cancel 22 | count -= 1 23 | end 24 | 25 | timers.fire 26 | end 27 | 28 | benchmark.report("IO::Event::Timers") do |count| 29 | timers = IO::Event::Timers.new 30 | 31 | while count > 0 32 | timer = timers.after(0) {} 33 | timer.cancel! 34 | count -= 1 35 | end 36 | 37 | timers.fire 38 | end 39 | 40 | benchmark.compare! 41 | end 42 | -------------------------------------------------------------------------------- /benchmark/timers/after_n.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2024, by Samuel Williams. 6 | 7 | require "benchmark/ips" 8 | 9 | require "timers" 10 | require "io/event/timers" 11 | 12 | Benchmark.ips do |benchmark| 13 | benchmark.time = 1 14 | benchmark.warmup = 1 15 | 16 | benchmark.report("Timers::Group") do |count| 17 | timers = Timers::Group.new 18 | 19 | while count > 0 20 | timers.after(0) {} 21 | count -= 1 22 | end 23 | 24 | timers.fire 25 | end 26 | 27 | benchmark.report("IO::Event::Timers") do |count| 28 | timers = IO::Event::Timers.new 29 | 30 | while count > 0 31 | timers.after(0) {} 32 | count -= 1 33 | end 34 | 35 | timers.fire 36 | end 37 | 38 | benchmark.compare! 39 | end 40 | -------------------------------------------------------------------------------- /benchmark/timers/gems.locked: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | benchmark-ips (2.13.0) 5 | io-event (1.6.0) 6 | timers (4.3.5) 7 | 8 | PLATFORMS 9 | arm64-darwin-23 10 | ruby 11 | 12 | DEPENDENCIES 13 | benchmark-ips (= 2.13) 14 | io-event (~> 1.6) 15 | timers (~> 4.3) 16 | 17 | BUNDLED WITH 18 | 2.5.5 19 | -------------------------------------------------------------------------------- /benchmark/timers/gems.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024, by Samuel Williams. 5 | 6 | source "https://rubygems.org" 7 | 8 | gem "benchmark-ips", "2.13" 9 | 10 | gem "timers", "~> 4.3" 11 | gem "io-event", "~> 1.6" 12 | -------------------------------------------------------------------------------- /config/environment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | UTOPIA_PROJECT_GISCUS_REPO: "socketry/async" 3 | UTOPIA_PROJECT_GISCUS_REPO_ID: "MDEwOlJlcG9zaXRvcnk4NzM4MDQ4Mw==" 4 | UTOPIA_PROJECT_GISCUS_CATEGORY: "Documentation" 5 | UTOPIA_PROJECT_GISCUS_CATEGORY_ID: "DIC_kwDOBTVSA84B-XEu" 6 | -------------------------------------------------------------------------------- /config/external.yaml: -------------------------------------------------------------------------------- 1 | async-pool: 2 | url: https://github.com/socketry/async-pool.git 3 | command: bundle exec sus 4 | async-websocket: 5 | url: https://github.com/socketry/async-websocket.git 6 | command: bundle exec sus 7 | async-dns: 8 | url: https://github.com/socketry/async-dns.git 9 | command: bundle exec sus 10 | async-http: 11 | url: https://github.com/socketry/async-http.git 12 | command: bundle exec sus 13 | falcon: 14 | url: https://github.com/socketry/falcon.git 15 | command: bundle exec sus 16 | async-rest: 17 | url: https://github.com/socketry/async-rest.git 18 | command: bundle exec sus 19 | -------------------------------------------------------------------------------- /config/metrics.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024, by Samuel Williams. 5 | 6 | def prepare 7 | require "metrics/provider/async" 8 | end 9 | -------------------------------------------------------------------------------- /config/sus.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2024, by Samuel Williams. 5 | 6 | require "covered/sus" 7 | include Covered::Sus 8 | 9 | ENV["TRACES_BACKEND"] ||= "traces/backend/test" 10 | ENV["METRICS_BACKEND"] ||= "metrics/backend/test" 11 | 12 | def prepare_instrumentation! 13 | require "traces" 14 | require "metrics" 15 | end 16 | 17 | def before_tests(...) 18 | prepare_instrumentation! 19 | 20 | super 21 | end 22 | -------------------------------------------------------------------------------- /config/traces.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024, by Samuel Williams. 5 | 6 | def prepare 7 | require "traces/provider/async" 8 | end 9 | -------------------------------------------------------------------------------- /examples/buffer/buffer.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2021-2024, by Samuel Williams. 6 | 7 | # abort "Need IO::Buffer" unless IO.const_defined?(:Buffer) 8 | 9 | require_relative "../../lib/async" 10 | 11 | file = File.open("/dev/zero") 12 | 13 | Async do 14 | binding.irb 15 | end 16 | -------------------------------------------------------------------------------- /examples/bugs/write_lock.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2023-2024, by Samuel Williams. 6 | 7 | # This bug only generally shows up on Linux, when using io_uring, as it has more fine-grained locking. The issue is that `puts` can acquire and release a write lock, and if one thread releases that lock while the reactor on the waitq thread is closing, it can call `unblock` with `@selector = nil` which fails or causes odd behaviour. 8 | 9 | require_relative "../../lib/async" 10 | 11 | def wait_for_interrupt(thread_index, repeat) 12 | sequence = [] 13 | 14 | events = Thread::Queue.new 15 | reactor = Async::Reactor.new 16 | 17 | thread = Thread.new do 18 | if events.pop 19 | puts "#{thread_index}+#{repeat} Sending Interrupt!" 20 | reactor.interrupt 21 | end 22 | end 23 | 24 | reactor.async do 25 | events << true 26 | puts "#{thread_index}+#{repeat} Reactor ready!" 27 | 28 | # Wait to be interrupted: 29 | sleep(1) 30 | 31 | puts "#{thread_index}+#{repeat} Missing interrupt!" 32 | end 33 | 34 | reactor.run 35 | 36 | thread.join 37 | end 38 | 39 | 100.times.map do |thread_index| 40 | Thread.new do 41 | 1000.times do |repeat| 42 | wait_for_interrupt(thread_index, repeat) 43 | end 44 | end 45 | end.each(&:join) 46 | -------------------------------------------------------------------------------- /examples/callback/loop.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2018-2024, by Samuel Williams. 6 | 7 | # frozen_string_literal: true 8 | 9 | require "async/reactor" 10 | 11 | class Callback 12 | def initialize 13 | @reactor = Async::Reactor.new 14 | end 15 | 16 | def close 17 | @reactor.close 18 | end 19 | 20 | # If duration is 0, it will happen immediately after the task is started. 21 | def run(duration = 0, &block) 22 | if block_given? 23 | @reactor.async(&block) 24 | end 25 | 26 | @reactor.run_once(duration) 27 | end 28 | end 29 | 30 | 31 | callback = Callback.new 32 | 33 | begin 34 | callback.run do |task| 35 | while true 36 | sleep(2) 37 | puts "Hello from task!" 38 | end 39 | end 40 | 41 | while true 42 | callback.run(0) 43 | puts "Sleeping for 1 second" 44 | sleep(1) 45 | end 46 | ensure 47 | callback.close 48 | end 49 | -------------------------------------------------------------------------------- /examples/capture/README.md: -------------------------------------------------------------------------------- 1 | # Capture 2 | 3 | ## Falcon 4 | 5 | ``` 6 | % wrk -t 8 -c 32 http://localhost:9292/ 7 | Running 10s test @ http://localhost:9292/ 8 | 8 threads and 32 connections 9 | Thread Stats Avg Stdev Max +/- Stdev 10 | Latency 106.31ms 10.20ms 211.79ms 98.00% 11 | Req/Sec 37.94 5.43 40.00 84.24% 12 | 3003 requests in 10.01s, 170.16KB read 13 | Requests/sec: 299.98 14 | Transfer/sec: 17.00KB 15 | ``` 16 | 17 | ``` 18 | 0.0s: Process 28065 start times: 19 | | # 20 | ^C15.11s: strace -p 28065 21 | | ["sendto", {:"% time"=>57.34, :seconds=>0.595047, :"usecs/call"=>14, :calls=>39716, :errors=>32, :syscall=>"sendto"}] 22 | | ["recvfrom", {:"% time"=>42.58, :seconds=>0.441867, :"usecs/call"=>12, :calls=>36718, :errors=>70, :syscall=>"recvfrom"}] 23 | | ["read", {:"% time"=>0.07, :seconds=>0.000723, :"usecs/call"=>7, :calls=>98, :errors=>nil, :syscall=>"read"}] 24 | | ["write", {:"% time"=>0.01, :seconds=>0.000112, :"usecs/call"=>56, :calls=>2, :errors=>nil, :syscall=>"write"}] 25 | | [:total, {:"% time"=>100.0, :seconds=>1.037749, :"usecs/call"=>nil, :calls=>76534, :errors=>102, :syscall=>"total"}] 26 | 15.11s: Process 28065 end times: 27 | | # 28 | 15.11s: Process Waiting: 1.0377s out of 1.55s 29 | | Wait percentage: 66.95% 30 | ``` 31 | 32 | ## Puma 33 | 34 | ``` 35 | wrk -t 8 -c 32 http://localhost:9292/ 36 | Running 10s test @ http://localhost:9292/ 37 | 8 threads and 32 connections 38 | Thread Stats Avg Stdev Max +/- Stdev 39 | Latency 108.83ms 3.50ms 146.38ms 86.58% 40 | Req/Sec 34.43 6.70 40.00 92.68% 41 | 1371 requests in 10.01s, 81.67KB read 42 | Requests/sec: 136.94 43 | Transfer/sec: 8.16KB 44 | ``` 45 | 46 | ``` 47 | 0.0s: Process 28448 start times: 48 | | # 49 | ^C24.89s: strace -p 28448 50 | | ["recvfrom", {:"% time"=>64.65, :seconds=>0.595275, :"usecs/call"=>13, :calls=>44476, :errors=>27769, :syscall=>"recvfrom"}] 51 | | ["sendto", {:"% time"=>30.68, :seconds=>0.282467, :"usecs/call"=>18, :calls=>15288, :errors=>nil, :syscall=>"sendto"}] 52 | | ["write", {:"% time"=>4.66, :seconds=>0.042921, :"usecs/call"=>15, :calls=>2772, :errors=>nil, :syscall=>"write"}] 53 | | ["read", {:"% time"=>0.02, :seconds=>0.000157, :"usecs/call"=>8, :calls=>19, :errors=>1, :syscall=>"read"}] 54 | | [:total, {:"% time"=>100.0, :seconds=>0.92082, :"usecs/call"=>nil, :calls=>62555, :errors=>27770, :syscall=>"total"}] 55 | 24.89s: Process 28448 end times: 56 | | # 57 | 24.89s: Process Waiting: 0.9208s out of 2.56s 58 | | Wait percentage: 35.97% 59 | ``` 60 | -------------------------------------------------------------------------------- /examples/capture/capture.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2019-2024, by Samuel Williams. 6 | 7 | require "irb" 8 | require "console" 9 | 10 | pids = ARGV.collect(&:to_i) 11 | 12 | TICKS = Process.clock_getres(:TIMES_BASED_CLOCK_PROCESS_CPUTIME_ID, :hertz).to_f 13 | 14 | def getrusage(pid) 15 | fields = File.read("/proc/#{pid}/stat").split(/\s+/) 16 | 17 | return Process::Tms.new( 18 | fields[14].to_f / TICKS, 19 | fields[15].to_f / TICKS, 20 | fields[16].to_f / TICKS, 21 | fields[17].to_f / TICKS, 22 | ) 23 | end 24 | 25 | def parse(value) 26 | case value 27 | when /^\s*\d+\.\d+/ 28 | Float(value) 29 | when /^\s*\d+/ 30 | Integer(value) 31 | else 32 | value = value.strip 33 | if value.empty? 34 | nil 35 | else 36 | value 37 | end 38 | end 39 | end 40 | 41 | def strace(pid, duration = 60) 42 | input, output = IO.pipe 43 | 44 | pid = Process.spawn("strace", "-p", pid.to_s, "-cqf", "-w", "-e", "!futex", err: output) 45 | 46 | output.close 47 | 48 | Signal.trap(:INT) do 49 | Process.kill(:INT, pid) 50 | Signal.trap(:INT, :DEFAULT) 51 | end 52 | 53 | Thread.new do 54 | sleep duration 55 | Process.kill(:INT, pid) 56 | end 57 | 58 | summary = {} 59 | 60 | if first_line = input.gets 61 | if rule = input.gets # horizontal separator 62 | pattern = Regexp.new( 63 | rule.split(/\s/).map{|s| "(.{1,#{s.size}})"}.join(" ") 64 | ) 65 | 66 | header = pattern.match(first_line).captures.map{|key| key.strip.to_sym} 67 | end 68 | 69 | while line = input.gets 70 | break if line == rule 71 | row = pattern.match(line).captures.map{|value| parse(value)} 72 | fields = header.zip(row).to_h 73 | 74 | summary[fields[:syscall]] = fields 75 | end 76 | 77 | if line = input.gets 78 | row = pattern.match(line).captures.map{|value| parse(value)} 79 | fields = header.zip(row).to_h 80 | summary[:total] = fields 81 | end 82 | end 83 | 84 | _, status = Process.waitpid2(pid) 85 | 86 | Console.error(status) do |buffer| 87 | buffer.puts first_line 88 | end unless status.success? 89 | 90 | return summary 91 | end 92 | 93 | pids.each do |pid| 94 | start_times = getrusage(pid) 95 | Console.info("Process #{pid} start times:", start_times) 96 | 97 | # sleep 60 98 | summary = strace(pid) 99 | 100 | Console.info("strace -p #{pid}") do |buffer| 101 | summary.each do |fields| 102 | buffer.puts fields.inspect 103 | end 104 | end 105 | 106 | end_times = getrusage(pid) 107 | Console.info("Process #{pid} end times:", end_times) 108 | 109 | if total = summary[:total] 110 | process_duration = end_times.utime - start_times.utime 111 | wait_duration = summary[:total][:seconds] 112 | 113 | Console.info("Process Waiting: #{wait_duration.round(4)}s out of #{process_duration.round(4)}s") do |buffer| 114 | buffer.puts "Wait percentage: #{(wait_duration / process_duration * 100.0).round(2)}%" 115 | end 116 | else 117 | Console.warn("No system calls detected.") 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /examples/count/fibers.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2024, by Samuel Williams. 6 | 7 | require "async" 8 | require "benchmark" 9 | 10 | transitions = [] 11 | 12 | puts "=========== FIBERS ===========" 13 | puts 14 | 15 | count = 0 16 | time = Benchmark.measure do 17 | Sync do 18 | 5.times do 19 | [ 20 | Async do 21 | transitions << "A1" 22 | puts "Task 1: count is #{count}" 23 | 24 | transitions << "A2" 25 | count += 1 26 | 27 | transitions << "A3" 28 | sleep(0.1) 29 | 30 | transitions << "A4" 31 | end, 32 | Async do 33 | transitions << "B1" 34 | puts "Task 2: count is #{count}" 35 | 36 | transitions << "B2" 37 | count += 1 38 | 39 | transitions << "B3" 40 | sleep(0.1) 41 | 42 | transitions << "B4" 43 | end 44 | ].map(&:wait) 45 | end 46 | end 47 | end 48 | 49 | puts "#{time.real.round(2)} seconds to run. Final count is #{count}" 50 | puts transitions.join(" ") 51 | -------------------------------------------------------------------------------- /examples/count/threads.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2024, by Samuel Williams. 6 | 7 | require "benchmark" 8 | 9 | transitions = [] 10 | 11 | puts "=========== THREADS ===========" 12 | puts 13 | 14 | count = 0 15 | time = Benchmark.measure do 16 | 5.times do 17 | [ 18 | Thread.new do 19 | transitions << "A1" 20 | puts "Task 1: count is #{count}" 21 | 22 | transitions << "A2" 23 | count += 1 24 | 25 | transitions << "A3" 26 | sleep(0.1) 27 | 28 | transitions << "A4" 29 | end, 30 | Thread.new do 31 | transitions << "B1" 32 | puts "Task 2: count is #{count}" 33 | 34 | transitions << "B2" 35 | count += 1 36 | 37 | transitions << "B3" 38 | sleep(0.1) 39 | 40 | transitions << "B4" 41 | end 42 | ].map(&:join) 43 | end 44 | end 45 | 46 | puts "#{time.real.round(2)} seconds to run. Final count is #{count}" 47 | puts transitions.join(" ") 48 | -------------------------------------------------------------------------------- /examples/dataloader/dataloader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2024, by Samuel Williams. 5 | 6 | require "net/http" 7 | require "uri" 8 | 9 | class Dataloader 10 | def initialize 11 | @loading = 0 12 | @results = Thread::Queue.new 13 | end 14 | 15 | def load(url) 16 | @loading += 1 17 | Fiber.schedule do 18 | puts "Making request to #{url}" 19 | result = Net::HTTP.get(URI url) 20 | ensure 21 | puts "Finished making request to #{url}." 22 | @loading -= 1 23 | @results << result 24 | end 25 | end 26 | 27 | def wait 28 | raise RuntimeError if @loading == 0 29 | 30 | return @results.pop 31 | end 32 | 33 | def wait_all 34 | raise RuntimeError if @loading == 0 35 | 36 | results = [] 37 | results << @results.pop until @loading == 0 38 | 39 | return results 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /examples/dataloader/gems.locked: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | async (2.1.0) 5 | console (~> 1.10) 6 | io-event (~> 1.0.0) 7 | timers (~> 4.1) 8 | console (1.15.3) 9 | fiber-local 10 | fiber-local (1.0.0) 11 | io-event (1.0.9) 12 | timers (4.3.4) 13 | 14 | PLATFORMS 15 | x86_64-linux 16 | 17 | DEPENDENCIES 18 | async 19 | 20 | BUNDLED WITH 21 | 2.3.22 22 | -------------------------------------------------------------------------------- /examples/dataloader/gems.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2023, by Samuel Williams. 5 | 6 | source "https://rubygems.org" 7 | 8 | gem "async" 9 | -------------------------------------------------------------------------------- /examples/dataloader/main.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2022-2024, by Samuel Williams. 6 | 7 | require "async" 8 | require_relative "dataloader" 9 | 10 | Async do 11 | dataloader = Dataloader.new 12 | 13 | dataloader.load("https://www.google.com") 14 | dataloader.load("https://www.microsoft.com") 15 | dataloader.load("https://www.github.com") 16 | 17 | pp dataloader.wait_all 18 | end 19 | -------------------------------------------------------------------------------- /examples/debug/gems.locked: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../.. 3 | specs: 4 | async (2.5.1) 5 | console (~> 1.10) 6 | io-event (~> 1.1) 7 | timers (~> 4.1) 8 | 9 | PATH 10 | remote: /Users/samuel/Developer/ruby/debug 11 | specs: 12 | debug (1.8.0) 13 | irb (>= 1.5.0) 14 | reline (>= 0.3.1) 15 | 16 | GEM 17 | remote: https://rubygems.org/ 18 | specs: 19 | console (1.16.2) 20 | fiber-local 21 | fiber-local (1.0.0) 22 | io-console (0.6.0) 23 | io-event (1.2.2) 24 | irb (1.6.3) 25 | reline (>= 0.3.0) 26 | reline (0.3.2) 27 | io-console (~> 0.5) 28 | timers (4.3.5) 29 | 30 | PLATFORMS 31 | arm64-darwin-22 32 | 33 | DEPENDENCIES 34 | async! 35 | debug! 36 | 37 | BUNDLED WITH 38 | 2.4.7 39 | -------------------------------------------------------------------------------- /examples/debug/gems.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2023, by Samuel Williams. 5 | 6 | source "https://rubygems.org" 7 | 8 | gem "debug", path: "/Users/samuel/Developer/ruby/debug" 9 | gem "async", path: "../.." 10 | -------------------------------------------------------------------------------- /examples/debug/sample.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2023-2024, by Samuel Williams. 5 | 6 | require "async" 7 | 8 | Async do |t| 9 | t.async do 10 | puts "1\n" 11 | end 12 | 13 | t.async do 14 | puts "2\n" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /examples/dining-philosophers/philosophers.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2024, by Samuel Williams. 6 | 7 | require "async" 8 | require "async/semaphore" 9 | 10 | class Philosopher 11 | def initialize(name, left_fork, right_fork) 12 | @name = name 13 | @left_fork = left_fork 14 | @right_fork = right_fork 15 | end 16 | 17 | def think 18 | puts "#{@name} is thinking." 19 | sleep(rand(1..3)) 20 | puts "#{@name} has finished thinking." 21 | end 22 | 23 | def eat 24 | puts "#{@name} is eating." 25 | sleep(rand(1..3)) 26 | puts "#{@name} has finished eating." 27 | end 28 | 29 | def dine 30 | Sync do |task| 31 | think 32 | 33 | @left_fork.acquire do 34 | @right_fork.acquire do 35 | eat 36 | end 37 | end 38 | end 39 | end 40 | end 41 | 42 | # Each philosopher has a name, a left fork, and a right fork. 43 | # - The think method simulates thinking by sleeping for a random duration. 44 | # - The eat method simulates eating by sleeping for a random duration. 45 | # - The dine method is a loop where the philosopher alternates between thinking and eating. 46 | # - It uses async blocks to pick up the left and right forks before eating. 47 | 48 | # This code ensures that philosophers can think and eat concurrently while properly handling the synchronization of forks to avoid conflicts. 49 | Async do |task| 50 | # We create an array of Async::Semaphore objects to represent the forks. Each semaphore is initialized with a count of 1, representing a single fork. 51 | forks = Array.new(5) {Async::Semaphore.new(1)} 52 | 53 | # We create an array of philosophers, each of whom gets two forks (their left and right neighbors). 54 | philosophers = Array.new(5) do |i| 55 | Philosopher.new("Philosopher #{i + 1}", forks[i], forks[(i + 1) % 5]) 56 | end 57 | 58 | # We start an async task for each philosopher to run their dine method concurrently. 59 | philosophers.each do |philosopher| 60 | task.async do 61 | philosopher.dine 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /examples/hup-test/child.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2023-2024, by Samuel Williams. 6 | 7 | require_relative "../../lib/async" 8 | require "console" 9 | 10 | Async do |task| 11 | while true 12 | task.async do 13 | Console.info("Child running.") 14 | sleep 0.1 15 | end.wait 16 | end 17 | end 18 | 19 | # ruby3-tcp-server-mini-benchmark$ ruby loop.rb async-scheduler.rb 20 | # spawn 21 | # /usr/bin/wrk -t1 -c1 -d1s http://localhost:9090 22 | # waiting for process to die 23 | # spawn 24 | # /usr/bin/wrk -t1 -c1 -d1s http://localhost:9090 25 | # waiting for process to die 26 | # ^Cloop.rb:24:in `waitpid': Interrupt 27 | # from loop.rb:24:in `block in
' 28 | # from loop.rb:15:in `loop' 29 | # from loop.rb:15:in `
' 30 | # /home/jsaak/.gem/ruby/3.2.1/gems/async-2.6.1/lib/async/list.rb:250:in `initialize': Interrupt 31 | # from /home/jsaak/.gem/ruby/3.2.1/gems/async-2.6.1/lib/async/list.rb:298:in `new' 32 | # from /home/jsaak/.gem/ruby/3.2.1/gems/async-2.6.1/lib/async/list.rb:298:in `each' 33 | # from /home/jsaak/.gem/ruby/3.2.1/gems/async-2.6.1/lib/async/list.rb:176:in `each' 34 | # from /home/jsaak/.gem/ruby/3.2.1/gems/async-2.6.1/lib/async/node.rb:240:in `terminate' 35 | # from /home/jsaak/.gem/ruby/3.2.1/gems/async-2.6.1/lib/async/scheduler.rb:52:in `close' 36 | # from /home/jsaak/.gem/ruby/3.2.1/gems/async-2.6.1/lib/async/scheduler.rb:46:in `ensure in scheduler_close' 37 | # from /home/jsaak/.gem/ruby/3.2.1/gems/async-2.6.1/lib/async/scheduler.rb:46:in `scheduler_close' 38 | # /home/jsaak/.gem/ruby/3.2.1/gems/async-2.6.1/lib/async/node.rb:110:in `transient?': SIGHUP (SignalException) 39 | # from /home/jsaak/.gem/ruby/3.2.1/gems/async-2.6.1/lib/async/node.rb:47:in `removed' 40 | # from /home/jsaak/.gem/ruby/3.2.1/gems/async-2.6.1/lib/async/list.rb:132:in `remove!' 41 | # from /home/jsaak/.gem/ruby/3.2.1/gems/async-2.6.1/lib/async/list.rb:121:in `remove' 42 | # from /home/jsaak/.gem/ruby/3.2.1/gems/async-2.6.1/lib/async/node.rb:182:in `remove_child' 43 | # from /home/jsaak/.gem/ruby/3.2.1/gems/async-2.6.1/lib/async/node.rb:197:in `consume' 44 | # from /home/jsaak/.gem/ruby/3.2.1/gems/async-2.6.1/lib/async/task.rb:287:in `finish!' 45 | # from /home/jsaak/.gem/ruby/3.2.1/gems/async-2.6.1/lib/async/task.rb:360:in `block in schedule' 46 | 47 | -------------------------------------------------------------------------------- /examples/hup-test/main.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2023-2024, by Samuel Williams. 6 | 7 | require "console" 8 | 9 | while true 10 | pid = Process.spawn("./child.rb") 11 | Console.info("Spawned child.", pid: pid) 12 | sleep 2 13 | Console.info("Sending HUP to child.", pid: pid) 14 | Process.kill(:HUP, pid) 15 | status = Process.waitpid(pid) 16 | Console.info("Child exited.", pid: pid, status: status) 17 | end 18 | -------------------------------------------------------------------------------- /examples/load/test.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2024, by Samuel Williams. 6 | 7 | require_relative "../../lib/async" 8 | require_relative "../../lib/async/idler" 9 | 10 | Async do 11 | idler = Async::Idler.new(0.8) 12 | 13 | Async do 14 | while true 15 | idler.async do 16 | $stdout.write "." 17 | while true 18 | sleep 0.1 19 | end 20 | end 21 | end 22 | end 23 | 24 | scheduler = Fiber.scheduler 25 | while true 26 | load = scheduler.load 27 | 28 | $stdout.write "\nLoad: #{load} " 29 | sleep 1.0 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /examples/queue/producer.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2019-2024, by Samuel Williams. 6 | 7 | require "async" 8 | require "async/queue" 9 | 10 | Async do 11 | # Queue of up to 10 items: 12 | items = Async::LimitedQueue.new(10) 13 | 14 | # Five producers: 15 | 5.times do 16 | Async do |task| 17 | while true 18 | t = rand 19 | sleep(t) 20 | items.enqueue(t) 21 | end 22 | end 23 | end 24 | 25 | # A single consumer: 26 | Async do |task| 27 | while item = items.dequeue 28 | puts "dequeue -> #{item}" 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /examples/stop/condition.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2019-2024, by Samuel Williams. 6 | 7 | require_relative "../../lib/async"; require_relative "../../lib/async/queue" 8 | 9 | Async do |consumer| 10 | consumer.annotate "consumer" 11 | condition = Async::Condition.new 12 | 13 | producer = Async do |subtask| 14 | subtask.annotate "subtask" 15 | 16 | (1..).each do |value| 17 | puts "producer yielding" 18 | subtask.yield # (1) Fiber.yield, (3) Reactor -> producer.resume 19 | condition.signal(value) # (4) consumer.resume(value) 20 | end 21 | 22 | puts "producer exiting" 23 | end 24 | 25 | value = condition.wait # (2) value = Fiber.yield 26 | puts "producer.stop" 27 | producer.stop # (5) [producer is resumed already] producer.stop 28 | 29 | puts "consumer exiting" 30 | end 31 | 32 | puts "Done." 33 | -------------------------------------------------------------------------------- /examples/stop/sleep.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2019-2024, by Samuel Williams. 6 | 7 | require_relative "../../lib/async" 8 | 9 | require "async/http/endpoint" 10 | require "async/http/server" 11 | 12 | require "async/http/internet" 13 | 14 | # To query the web server: 15 | # curl http://localhost:9292/kittens 16 | 17 | Async do |parent| 18 | endpoint = Async::HTTP::Endpoint.parse("http://localhost:9292") 19 | internet = Async::HTTP::Internet.new 20 | 21 | server = Async::HTTP::Server.for(endpoint) do |request| 22 | if request.path =~ /\/(.*)/ 23 | keyword = $1 24 | 25 | response = internet.get("https://www.google.com/search?q=#{keyword}") 26 | 27 | count = response.read.scan(keyword).size 28 | 29 | Protocol::HTTP::Response[200, [], ["Google found #{count} instance(s) of #{keyword}.\n"]] 30 | else 31 | Protocol::HTTP::Response[404, [], []] 32 | end 33 | end 34 | 35 | tasks = server.run 36 | 37 | #while true 38 | sleep(10) 39 | parent.reactor.print_hierarchy 40 | #end 41 | 42 | parent.stop # -> Async::Stop 43 | 44 | tasks.each(&:stop) 45 | end 46 | -------------------------------------------------------------------------------- /fixtures/async/a_condition.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2024, by Samuel Williams. 5 | 6 | require "async/variable" 7 | 8 | module Async 9 | ACondition = Sus::Shared("a condition") do 10 | let(:condition) {subject.new} 11 | 12 | it "can signal waiting task" do 13 | state = nil 14 | 15 | reactor.async do 16 | state = :waiting 17 | condition.wait 18 | state = :resumed 19 | end 20 | 21 | expect(state).to be == :waiting 22 | 23 | condition.signal 24 | 25 | reactor.yield 26 | 27 | expect(state).to be == :resumed 28 | end 29 | 30 | it "should be able to signal stopped task" do 31 | expect(condition).to be(:empty?) 32 | expect(condition).not.to be(:waiting?) 33 | 34 | task = reactor.async do 35 | condition.wait 36 | end 37 | 38 | expect(condition).not.to be(:empty?) 39 | expect(condition).to be(:waiting?) 40 | 41 | task.stop 42 | 43 | condition.signal 44 | end 45 | 46 | it "resumes tasks in order" do 47 | order = [] 48 | 49 | 5.times do |i| 50 | task = reactor.async do 51 | condition.wait 52 | order << i 53 | end 54 | end 55 | 56 | condition.signal 57 | 58 | reactor.yield 59 | 60 | expect(order).to be == [0, 1, 2, 3, 4] 61 | end 62 | 63 | with "timeout" do 64 | let(:ready) {Async::Variable.new(condition)} 65 | let(:waiting) {Async::Variable.new(subject.new)} 66 | 67 | def before 68 | @state = nil 69 | 70 | @task = reactor.async do |task| 71 | task.with_timeout(0.01) do 72 | begin 73 | @state = :waiting 74 | waiting.resolve 75 | 76 | ready.wait 77 | @state = :signalled 78 | rescue Async::TimeoutError 79 | @state = :timeout 80 | end 81 | end 82 | end 83 | 84 | super 85 | end 86 | 87 | it "can timeout while waiting" do 88 | @task.wait 89 | 90 | expect(@state).to be == :timeout 91 | end 92 | 93 | it "can signal while waiting" do 94 | waiting.wait 95 | ready.resolve 96 | 97 | @task.wait 98 | 99 | expect(@state).to be == :signalled 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /fixtures/async/a_queue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2024, by Samuel Williams. 5 | # Copyright, 2019, by Ryan Musgrave. 6 | # Copyright, 2020-2022, by Bruno Sutic. 7 | 8 | require "async" 9 | require "async/queue" 10 | require "sus/fixtures/async" 11 | require "async/semaphore" 12 | 13 | require "async/chainable_async" 14 | 15 | module Async 16 | AQueue = Sus::Shared("a queue") do 17 | let(:queue) {subject.new} 18 | 19 | with "#push" do 20 | it "adds an item to the queue" do 21 | queue.push(:item) 22 | expect(queue.size).to be == 1 23 | expect(queue.dequeue).to be == :item 24 | end 25 | end 26 | 27 | with "#pop" do 28 | it "removes an item from the queue" do 29 | queue.push(:item) 30 | expect(queue.pop).to be == :item 31 | expect(queue.size).to be == 0 32 | end 33 | end 34 | 35 | with "#each" do 36 | it "can enumerate queue items" do 37 | reactor.async do |task| 38 | 10.times do |item| 39 | sleep(0.0001) 40 | queue.enqueue(item) 41 | end 42 | 43 | queue.enqueue(nil) 44 | end 45 | 46 | items = [] 47 | queue.each do |item| 48 | items << item 49 | end 50 | 51 | expect(items).to be == 10.times.to_a 52 | end 53 | end 54 | 55 | it "should process items in order" do 56 | reactor.async do |task| 57 | 10.times do |i| 58 | sleep(0.001) 59 | queue.enqueue(i) 60 | end 61 | end 62 | 63 | 10.times do |j| 64 | expect(queue.dequeue).to be == j 65 | end 66 | end 67 | 68 | it "can enqueue multiple items" do 69 | items = Array.new(10) { rand(10) } 70 | 71 | reactor.async do |task| 72 | queue.enqueue(*items) 73 | end 74 | 75 | items.each do |item| 76 | expect(queue.dequeue).to be == item 77 | end 78 | end 79 | 80 | it "can dequeue items asynchronously" do 81 | reactor.async do |task| 82 | queue << 1 83 | queue << nil 84 | end 85 | 86 | queue.async do |task, item| 87 | expect(item).to be == 1 88 | end 89 | end 90 | 91 | with "#<<" do 92 | it "adds an item to the queue" do 93 | queue << :item 94 | expect(queue.size).to be == 1 95 | expect(queue.dequeue).to be == :item 96 | end 97 | end 98 | 99 | with "#size" do 100 | it "returns queue size" do 101 | expect(queue.size).to be == 0 102 | queue.enqueue("Hello World") 103 | expect(queue.size).to be == 1 104 | end 105 | end 106 | 107 | with "#signal" do 108 | it "can signal with an item" do 109 | queue.signal(:item) 110 | expect(queue.dequeue).to be == :item 111 | end 112 | end 113 | 114 | with "#wait" do 115 | it "can wait for an item" do 116 | reactor.async do |task| 117 | queue.enqueue(:item) 118 | end 119 | 120 | expect(queue.wait).to be == :item 121 | end 122 | end 123 | 124 | with "an empty queue" do 125 | it "is expected to be empty" do 126 | expect(queue).to be(:empty?) 127 | end 128 | end 129 | 130 | with "task finishing queue" do 131 | it "can signal task completion" do 132 | 3.times do 133 | Async(finished: queue) do 134 | :result 135 | end 136 | end 137 | 138 | 3.times do 139 | task = queue.dequeue 140 | expect(task.wait).to be == :result 141 | end 142 | end 143 | end 144 | 145 | with "semaphore" do 146 | let(:capacity) {2} 147 | let(:semaphore) {Async::Semaphore.new(capacity)} 148 | let(:repeats) {capacity * 2} 149 | 150 | it "should process several items limited by a semaphore" do 151 | count = 0 152 | 153 | Async do 154 | repeats.times do 155 | queue.enqueue :item 156 | end 157 | 158 | queue.enqueue nil 159 | end 160 | 161 | queue.async(parent: semaphore) do |task| 162 | count += 1 163 | end 164 | 165 | expect(count).to be == repeats 166 | end 167 | end 168 | 169 | it_behaves_like Async::ChainableAsync do 170 | def before 171 | chainable.enqueue(:item) 172 | 173 | # The limited queue may block. 174 | Async do 175 | chainable.enqueue(nil) 176 | end 177 | 178 | super 179 | end 180 | end 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /fixtures/async/chainable_async.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019-2024, by Samuel Williams. 5 | 6 | module Async 7 | ChainableAsync = Sus::Shared("chainable async") do 8 | let(:parent) {Object.new} 9 | let(:chainable) {subject.new(parent: parent)} 10 | 11 | it "should chain async to parent" do 12 | expect(parent).to receive(:async).and_return(nil) 13 | 14 | chainable.async do 15 | # Nothing. 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /gems.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2024, by Samuel Williams. 5 | # Copyright, 2017, by Kent Gruber. 6 | # Copyright, 2024, by Patrik Wenger. 7 | 8 | source "https://rubygems.org" 9 | 10 | gemspec 11 | 12 | # gem "io-event", git: "https://github.com/socketry/io-event.git" 13 | 14 | # In order to capture both code paths in coverage, we need to optionally load this gem: 15 | if ENV["FIBER_PROFILER_CAPTURE"] == "true" 16 | gem "fiber-profiler" 17 | end 18 | 19 | group :maintenance, optional: true do 20 | gem "bake-gem" 21 | gem "bake-modernize" 22 | 23 | gem "utopia-project" 24 | gem "bake-releases" 25 | end 26 | 27 | group :test do 28 | gem "sus", "~> 0.31" 29 | gem "covered" 30 | gem "decode" 31 | gem "rubocop" 32 | 33 | gem "sus-fixtures-async" 34 | gem "sus-fixtures-console" 35 | gem "sus-fixtures-time" 36 | 37 | gem "bake-test" 38 | gem "bake-test-external" 39 | 40 | gem "benchmark-ips" 41 | end 42 | -------------------------------------------------------------------------------- /guides/best-practices/readme.md: -------------------------------------------------------------------------------- 1 | # Best Practices 2 | 3 | This guide gives an overview of best practices for using Async. 4 | 5 | ## Use a top-level `Sync` to denote the root of your program 6 | 7 | The `Sync` method ensures your code is running in a reactor or creates one if necessary, and has synchronous semantics, i.e. you do not need to wait on the result of the block. 8 | 9 | ```ruby 10 | require 'async' 11 | 12 | class Packages 13 | def initialize(urls) 14 | @urls = urls 15 | end 16 | 17 | def fetch 18 | Sync do |task| 19 | @urls.map do |url| 20 | task.async do 21 | fetch(url) 22 | end 23 | end.map(&:wait) 24 | end 25 | end 26 | end 27 | ``` 28 | 29 | ## Use a barrier to wait for all tasks to complete 30 | 31 | A barrier ensures you don't leak tasks and that all tasks are completed or stopped before progressing. 32 | 33 | ```ruby 34 | require 'async' 35 | require 'async/barrier' 36 | 37 | class Packages 38 | def initialize(urls) 39 | @urls = urls 40 | end 41 | 42 | def fetch 43 | barrier = Async::Barrier.new 44 | 45 | Sync do 46 | @urls.map do |url| 47 | barrier.async do 48 | fetch(url) 49 | end 50 | end.map(&:wait) 51 | ensure 52 | barrier.stop 53 | end 54 | end 55 | end 56 | ``` 57 | 58 | ## Use a semaphore to limit the number of concurrent tasks 59 | 60 | Unbounded concurrency is a common source of bugs. Use a semaphore to limit the number of concurrent tasks. 61 | 62 | ```ruby 63 | require 'async' 64 | require 'async/barrier' 65 | require 'async/semaphore' 66 | 67 | class Packages 68 | def initialize(urls) 69 | @urls = urls 70 | end 71 | 72 | def fetch 73 | barrier = Async::Barrier.new 74 | 75 | Sync do 76 | # Only 10 tasks are created at a time: 77 | semaphore = Async::Semaphore.new(10, parent: barrier) 78 | 79 | @urls.map do |url| 80 | semaphore.async do 81 | fetch(url) 82 | end 83 | end.map(&:wait) 84 | ensure 85 | barrier.stop 86 | end 87 | end 88 | end 89 | ``` 90 | -------------------------------------------------------------------------------- /guides/compatibility/readme.md: -------------------------------------------------------------------------------- 1 | # Compatibility 2 | 3 | This guide gives an overview of the compatibility of Async with Ruby and other frameworks. 4 | 5 | ## Ruby 6 | 7 | Async has two main branches, `stable-v1` and `main`. 8 | 9 | ### Stable V1 10 | 11 | > [!CAUTION] 12 | > The `stable-v1` branch is now considered legacy and is no longer actively maintained. It is recommended to use the `main` branch for new projects. 13 | 14 | The `stable-v1` branch of async is compatible with Ruby 2.5+ & TruffleRuby, and partially compatible with JRuby. 15 | 16 | Because it was designed with the interfaces available in Ruby 2.x, the following limitations apply: 17 | 18 | - {ruby Async::Task} implements context switching using {ruby Fiber.yield} and {ruby Fiber.resume}. This means that {ruby Async} may not be compatible with code which uses fibers for flow control, e.g. {ruby Enumerator}. 19 | - {ruby Async::Reactor} is unable to intercept blocking operations with native interfaces. You need to use the wrappers provided by {ruby Async::IO}. 20 | - DNS resolution is blocking. 21 | 22 | ### Main 23 | 24 | The `main` branch of async is compatible with Ruby 3.1.1+, and partially compatible with TruffleRuby. JRuby is currently incompatble. 25 | 26 | Because it was designed with the interfaces available in Ruby 3.x, it supports the fiber scheduler which provides transparent concurrency. 27 | 28 | - {ruby Async::Task} uses {ruby Fiber#transfer} for scheduling so it is compatible with all other usage of Fiber. 29 | - {ruby Async::Reactor} implements the Fiber scheduler interface and is compatible with a wide range of non-blocking operations, including DNS, {ruby Process.wait}, etc. 30 | - External C libraries that use blocking operations may still block. 31 | 32 | ## Rails 33 | 34 | Rails itself is generally compatible with Async and the fiber scheduler, but certain parts of Rails are not compatible with Async and have ossified ("accidental standardization") around thread-per-request as a general model. These issues are fully addressed in Rails v7.1+, which supports Rack 3 and fiber-per-request. 35 | 36 | - ActiveRecord with the latest version of the `pg` gem supports concurrent database queries. 37 | - ActiveRecord with `mysql2` gem does not support asynchronous queries. Potentially fixed by . 38 | - `ActiveSupport::CurrentAttributes` is per-isolated execution context. This means that child threads or fibers won't share the state. If you desire this, use `Fiber.storage` instead. 39 | -------------------------------------------------------------------------------- /guides/debugging/readme.md: -------------------------------------------------------------------------------- 1 | # Debugging 2 | 3 | This guide explains how to debug issues with programs that use Async. 4 | 5 | ## Debugging Techniques 6 | 7 | ### Debugging with `puts` 8 | 9 | The simplest way to debug an Async program is to use `puts` to print messages to the console. This is useful for understanding the flow of your program and the values of variables. However, it can be difficult to use `puts` to debug programs that use asynchronous code, as the output may be interleaved. To prevent this, wrap it in `Fiber.blocking{}`: 10 | 11 | ```ruby 12 | require 'async' 13 | 14 | Async do 15 | 3.times do |i| 16 | sleep i 17 | Fiber.blocking{puts "Slept for #{i} seconds."} 18 | end 19 | end 20 | ``` 21 | 22 | Using `Fiber.blocking{}` prevents any context switching until the block is complete, ensuring that the output is not interleaved and that flow control is strictly sequential. You should not use `Fiber.blocking{}` in production code, as it will block the reactor. 23 | 24 | ### Debugging with IRB 25 | 26 | You can use IRB to debug your Async program. In some cases, you will want to stop the world and inspect the state of your program. You can do this by wrapping `binding.irb` inside a `Fiber.blocking{}` block: 27 | 28 | ```ruby 29 | Async do 30 | 3.times do |i| 31 | sleep i 32 | # The event loop will stop at this point and you can inspect the state of your program. 33 | Fiber.blocking{binding.irb} 34 | end 35 | end 36 | ``` 37 | 38 | If you don't use `Fiber.blocking{}`, the event loop will continue to run and you will end up with three instances of `binding.irb` running. 39 | 40 | ### Debugging with `Async::Debug` 41 | 42 | The `async-debug` gem provides a visual debugger for Async programs. It is a powerful tool that allows you to inspect the state of your program and see the hierarchy of your program: 43 | 44 | ```ruby 45 | require 'async' 46 | require 'async/debug' 47 | 48 | Sync do 49 | debugger = Async::Debug.serve 50 | 51 | 3.times do 52 | Async do |task| 53 | while true 54 | duration = rand 55 | task.annotate("Sleeping for #{duration} second...") 56 | sleep(duration) 57 | end 58 | end 59 | end 60 | end 61 | ``` 62 | 63 | When you run this program, it will start a web server on `http://localhost:9000`. You can open this URL in your browser to see the state of your program. 64 | -------------------------------------------------------------------------------- /guides/getting-started/readme.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | This guide shows how to add async to your project and run code asynchronously. 4 | 5 | ## Installation 6 | 7 | Add the gem to your project: 8 | 9 | ~~~ bash 10 | $ bundle add async 11 | ~~~ 12 | 13 | ## Core Concepts 14 | 15 | `async` has several core concepts: 16 | 17 | - A {ruby Async::Task} instance which captures your sequential computations. 18 | - A {ruby Async::Reactor} instance which implements the fiber scheduler interface and event loop. 19 | - A {ruby Fiber} is an object which executes user code with cooperative concurrency, i.e. you can transfer execution from one fiber to another and back again. 20 | 21 | ### What is a scheduler? 22 | 23 | A scheduler is an interface which manages the execution of fibers. It is responsible for intercepting blocking operations and redirecting them to an event loop. 24 | 25 | ### What is an event loop? 26 | 27 | An event loop is part of the implementation of a scheduler which is responsible for waiting for events to occur, and waking up fibers when they are ready to run. 28 | 29 | ### What is a selector? 30 | 31 | A selector is part of the implementation of an event loop which is responsible for interacting with the operating system and waiting for specific events to occur. This is often referred to as "select"ing ready events from a set of file descriptors, but in practice has expanded to encompass a wide range of blocking operations. 32 | 33 | ### What is a reactor? 34 | 35 | A reactor is a specific implementation of the scheduler interface, which includes an event loop and selector, and is responsible for managing the execution of fibers. 36 | 37 | ## Creating an Asynchronous Task 38 | 39 | The main entry point for creating tasks is the {ruby Kernel#Async} method. Because this method is defined on `Kernel`, it's available in all parts of your program. 40 | 41 | ~~~ ruby 42 | require 'async' 43 | 44 | Async do |task| 45 | puts "Hello World!" 46 | end 47 | ~~~ 48 | 49 | A {ruby Async::Task} runs using a {ruby Fiber} and blocking operations e.g. `sleep`, `read`, `write` yield control until the operation can complete. When a blocking operation yields control, it means another fiber can execute, giving the illusion of simultaneous execution. 50 | 51 | ### When should I use `Async`? 52 | 53 | You should use `Async` when you desire explicit concurrency in your program. That means you want to run multiple tasks at the same time, and you want to be able to wait for the results of those tasks. 54 | 55 | - You should use `Async` when you want to perform network operations concurrently, such as HTTP requests or database queries. 56 | - You should use `Async` when you want to process independent requests concurrently, such as a web server. 57 | - You should use `Async` when you want to handle multiple connections concurrently, such as a chat server. 58 | 59 | You should consider the boundary around your program and the request handling. For example, one task per operation, request or connection, is usually appropriate. 60 | 61 | ### Waiting for Results 62 | 63 | Similar to a promise, {ruby Async::Task} produces results. In order to wait for these results, you must invoke {ruby Async::Task#wait}: 64 | 65 | ``` ruby 66 | require 'async' 67 | 68 | task = Async do 69 | rand 70 | end 71 | 72 | puts "The number was: #{task.wait}" 73 | ``` 74 | 75 | ## Creating a Fiber Scheduler 76 | 77 | The first (top level) async block will also create an instance of {ruby Async::Reactor} which is a subclass of {ruby Async::Scheduler} to handle the event loop. You can also do this directly using {ruby Fiber.set_scheduler}: 78 | 79 | ~~~ ruby 80 | require 'async/scheduler' 81 | 82 | scheduler = Async::Scheduler.new 83 | Fiber.set_scheduler(scheduler) 84 | 85 | Fiber.schedule do 86 | 1.upto(3) do |i| 87 | Fiber.schedule do 88 | sleep 1 89 | puts "Hello World" 90 | end 91 | end 92 | end 93 | ~~~ 94 | 95 | ## Synchronous Execution in an existing Fiber Scheduler 96 | 97 | Unless you need fan-out, map-reduce style concurrency, you can actually use a slightly more efficient {ruby Kernel::Sync} execution model. This method will run your block in the current event loop if one exists, or create an event loop if not. You can use it for code which uses asynchronous primitives, but itself does not need to be asynchronous with respect to other tasks. 98 | 99 | ```ruby 100 | require 'async/http/internet' 101 | 102 | def fetch(url) 103 | Sync do 104 | internet = Async::HTTP::Internet.new 105 | return internet.get(url).read 106 | end 107 | end 108 | 109 | # At the level of your program, this method will create an event loop: 110 | fetch(...) 111 | 112 | Sync do 113 | # The event loop already exists, and will be reused: 114 | fetch(...) 115 | end 116 | ``` 117 | 118 | In other words, `Sync{...}` is very similar in behaviour to `Async{...}.wait`, but significantly more efficient. 119 | 120 | ## Enforcing Embedded Execution 121 | 122 | In some methods, you may want to implement a fan-out or map-reduce. That requires a parent scheduler. There are two ways you can do this: 123 | 124 | ```ruby 125 | def fetch_all(urls, parent: Async::Task.current) 126 | urls.map do |url| 127 | parent.async do 128 | fetch(url) 129 | end 130 | end.map(&:wait) 131 | end 132 | ``` 133 | 134 | or: 135 | 136 | ```ruby 137 | def fetch_all(urls) 138 | Sync do |parent| 139 | urls.map do |url| 140 | parent.async do 141 | fetch(url) 142 | end 143 | end.map(&:wait) 144 | end 145 | end 146 | ``` 147 | 148 | The former allows you to inject the parent, which could be a barrier or semaphore, while the latter will create a new parent scheduler if one does not exist. In both cases, you guarantee that the map operation will be executed in the parent task (of some sort). 149 | 150 | ## Compatibility 151 | 152 | The Fiber Scheduler interface is compatible with most pure Ruby code and well-behaved C code. For example, you can use {ruby Net::HTTP} for performing concurrent HTTP requests: 153 | 154 | ```ruby 155 | urls = [...] 156 | 157 | Async do 158 | # Perform several concurrent requests: 159 | responses = urls.map do |url| 160 | Async do 161 | Net::HTTP.get(url) 162 | end 163 | end.map(&:wait) 164 | end 165 | ``` 166 | 167 | Unfortunately, some libraries do not integrate well with the fiber scheduler: either they are blocking, processor bound, or use thread locals for execution state. To use these libraries, you may be able to use a background thread. 168 | 169 | ```ruby 170 | Async do 171 | result = Thread.new do 172 | # Code which is otherwise unsafe... 173 | end.value # Wait for the result of the thread, internally non-blocking. 174 | end 175 | ``` 176 | -------------------------------------------------------------------------------- /guides/links.yaml: -------------------------------------------------------------------------------- 1 | getting-started: 2 | order: 1 3 | compatibility: 4 | order: 4 5 | scheduler: 6 | order: 3 7 | asynchronous-tasks: 8 | order: 2 9 | best-practices: 10 | order: 5 11 | debugging: 12 | order: 6 13 | -------------------------------------------------------------------------------- /guides/scheduler/readme.md: -------------------------------------------------------------------------------- 1 | # Scheduler 2 | 3 | This guide gives an overview of how the scheduler is implemented. 4 | 5 | ## Overview 6 | 7 | The {ruby Async::Scheduler} uses an event loop to execute tasks. When tasks are waiting on blocking operations like IO, the scheduler will use the operating system's native event system to wait for the operation to complete. This allows the scheduler to efficiently handle many tasks. 8 | 9 | ### Tasks 10 | 11 | Tasks are the building blocks of concurrent programs. They are lightweight and can be scheduled by the event loop. Tasks can be nested, and the parent task is used to determine the current reactor. Tasks behave like promises, in the sense you can wait on them to complete, and they might fail with an exception. 12 | 13 | ~~~ ruby 14 | require 'async' 15 | 16 | def sleepy(duration, task: Async::Task.current) 17 | task.async do |subtask| 18 | subtask.annotate "I'm going to sleep #{duration}s..." 19 | sleep duration 20 | puts "I'm done sleeping!" 21 | end 22 | end 23 | 24 | def nested_sleepy(task: Async::Task.current) 25 | task.async do |subtask| 26 | subtask.annotate "Invoking sleepy 5 times..." 27 | 5.times do |index| 28 | sleepy(index, task: subtask) 29 | end 30 | end 31 | end 32 | 33 | Async do |task| 34 | task.annotate "Invoking nested_sleepy..." 35 | subtask = nested_sleepy 36 | 37 | # Print out all running tasks in a tree: 38 | task.print_hierarchy($stderr) 39 | 40 | # Kill the subtask 41 | subtask.stop 42 | end 43 | ~~~ 44 | 45 | ### Thread Safety 46 | 47 | Most methods of the reactor and related tasks are not thread-safe, so you'd typically have [one reactor per thread or process](https://github.com/socketry/async-container). 48 | 49 | ### Embedding Schedulers 50 | 51 | {ruby Async::Scheduler#run} will run until the reactor runs out of work to do. To run a single iteration of the reactor, use {ruby Async::Scheduler#run_once}. 52 | 53 | ~~~ ruby 54 | require 'async' 55 | 56 | Console.logger.debug! 57 | reactor = Async::Scheduler.new 58 | 59 | # Run the reactor for 1 second: 60 | reactor.async do |task| 61 | task.sleep 1 62 | puts "Finished!" 63 | end 64 | 65 | while reactor.run_once 66 | # Round and round we go! 67 | end 68 | ~~~ 69 | 70 | You can use this approach to embed the reactor in another event loop. For some integrations, you may want to specify the maximum time to wait to {ruby Async::Scheduler#run_once}. 71 | 72 | ### Stopping a Scheduler 73 | 74 | {ruby Async::Scheduler#stop} will stop the current scheduler and all children tasks. 75 | 76 | ### Fiber Scheduler Integration 77 | 78 | In order to integrate with native Ruby blocking operations, the {ruby Async::Scheduler} uses a {ruby Fiber::Scheduler} interface. 79 | 80 | ```ruby 81 | require 'async' 82 | 83 | scheduler = Async::Scheduler.new 84 | Fiber.set_scheduler(scheduler) 85 | 86 | Fiber.schedule do 87 | puts "Hello World!" 88 | end 89 | ``` 90 | 91 | ## Design 92 | 93 | ### Optimistic vs Pessimistic Scheduling 94 | 95 | There are two main strategies for scheduling tasks: optimistic and pessimistic. An optimistic scheduler is usually greedy and will try to execute tasks as soon as they are scheduled using a direct transfer of control flow. A pessimistic scheduler will schedule tasks into the event loop ready list and will only execute them on the next iteration of the event loop. 96 | 97 | ```ruby 98 | Async do 99 | puts "Hello " 100 | 101 | Async do 102 | puts "World" 103 | end 104 | 105 | puts "!" 106 | end 107 | ``` 108 | 109 | An optimstic scheduler will print "Hello World!", while a pessimistic scheduler will print "Hello !World". In practice you should not design your code to rely on the order of execution, but it's important to understand the difference. It is an unspecifed implementation detail of the scheduler. 110 | -------------------------------------------------------------------------------- /lib/async.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2024, by Samuel Williams. 5 | # Copyright, 2020, by Salim Semaoune. 6 | 7 | require_relative "async/version" 8 | require_relative "async/reactor" 9 | 10 | require_relative "kernel/async" 11 | require_relative "kernel/sync" 12 | 13 | # Asynchronous programming framework. 14 | module Async 15 | end 16 | -------------------------------------------------------------------------------- /lib/async/barrier.md: -------------------------------------------------------------------------------- 1 | A synchronization primitive, which allows one task to wait for a number of other tasks to complete. It can be used in conjunction with {Semaphore}. 2 | 3 | 4 | ## Example 5 | 6 | ~~~ ruby 7 | require 'async' 8 | require 'async/barrier' 9 | 10 | barrier = Async::Barrier.new 11 | Sync do 12 | Console.info("Barrier Example: sleep sort.") 13 | 14 | # Generate an array of 10 numbers: 15 | numbers = 10.times.map{rand(10)} 16 | sorted = [] 17 | 18 | # Sleep sort the numbers: 19 | numbers.each do |number| 20 | barrier.async do |task| 21 | sleep(number) 22 | sorted << number 23 | end 24 | end 25 | 26 | # Wait for all the numbers to be sorted: 27 | barrier.wait 28 | 29 | Console.info("Sorted", sorted) 30 | ensure 31 | # Ensure all the tasks are stopped when we exit: 32 | barrier.stop 33 | end 34 | ~~~ 35 | 36 | ### Output 37 | 38 | ~~~ 39 | 0.0s info: Barrier Example: sleep sort. 40 | 9.0s info: Sorted 41 | | [3, 3, 3, 4, 4, 5, 5, 5, 8, 9] 42 | ~~~ 43 | -------------------------------------------------------------------------------- /lib/async/barrier.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019-2024, by Samuel Williams. 5 | 6 | require_relative "list" 7 | require_relative "task" 8 | 9 | module Async 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 | # 12 | # @public Since *Async v1*. 13 | class Barrier 14 | # Initialize the barrier. 15 | # @parameter parent [Task | Semaphore | Nil] The parent for holding any children tasks. 16 | # @public Since *Async v1*. 17 | def initialize(parent: nil) 18 | @tasks = List.new 19 | 20 | @parent = parent 21 | end 22 | 23 | class TaskNode < List::Node 24 | def initialize(task) 25 | @task = task 26 | end 27 | 28 | attr :task 29 | end 30 | 31 | private_constant :TaskNode 32 | 33 | # Number of tasks being held by the barrier. 34 | def size 35 | @tasks.size 36 | end 37 | 38 | # All tasks which have been invoked into the barrier. 39 | attr :tasks 40 | 41 | # Execute a child task and add it to the barrier. 42 | # @asynchronous Executes the given block concurrently. 43 | def async(*arguments, parent: (@parent or Task.current), **options, &block) 44 | task = parent.async(*arguments, **options, &block) 45 | 46 | @tasks.append(TaskNode.new(task)) 47 | 48 | return task 49 | end 50 | 51 | # Whether there are any tasks being held by the barrier. 52 | # @returns [Boolean] 53 | def empty? 54 | @tasks.empty? 55 | end 56 | 57 | # Wait for all tasks to complete by invoking {Task#wait} on each waiting task, which may raise an error. As long as the task has completed, it will be removed from the barrier. 58 | # @asynchronous Will wait for tasks to finish executing. 59 | def wait 60 | @tasks.each do |waiting| 61 | task = waiting.task 62 | begin 63 | task.wait 64 | ensure 65 | @tasks.remove?(waiting) unless task.alive? 66 | end 67 | end 68 | end 69 | 70 | # Stop all tasks held by the barrier. 71 | # @asynchronous May wait for tasks to finish executing. 72 | def stop 73 | @tasks.each do |waiting| 74 | waiting.task.stop 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/async/clock.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2022, by Samuel Williams. 5 | 6 | module Async 7 | # A convenient wrapper around the internal monotonic clock. 8 | # @public Since *Async v1*. 9 | class Clock 10 | # Get the current elapsed monotonic time. 11 | def self.now 12 | ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) 13 | end 14 | 15 | # Measure the execution of a block of code. 16 | # @yields {...} The block to execute. 17 | # @returns [Numeric] The total execution time. 18 | def self.measure 19 | start_time = self.now 20 | 21 | yield 22 | 23 | return self.now - start_time 24 | end 25 | 26 | # Start measuring elapsed time from now. 27 | # @returns [Clock] 28 | def self.start 29 | self.new.tap(&:start!) 30 | end 31 | 32 | # Create a new clock with the initial total time. 33 | # @parameter total [Numeric] The initial clock duration. 34 | def initialize(total = 0) 35 | @total = total 36 | @started = nil 37 | end 38 | 39 | # Start measuring a duration. 40 | def start! 41 | @started ||= Clock.now 42 | end 43 | 44 | # Stop measuring a duration and append the duration to the current total. 45 | def stop! 46 | if @started 47 | @total += (Clock.now - @started) 48 | @started = nil 49 | end 50 | 51 | return @total 52 | end 53 | 54 | # The total elapsed time including any current duration. 55 | def total 56 | total = @total 57 | 58 | if @started 59 | total += (Clock.now - @started) 60 | end 61 | 62 | return total 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 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/async/condition.md: -------------------------------------------------------------------------------- 1 | A synchronization primitive, which allows fibers to wait until a particular condition is (edge) triggered. Zero or more fibers can wait on a condition. When the condition is signalled, the fibers will be resumed in order. 2 | 3 | ## Example 4 | 5 | ~~~ ruby 6 | require 'async' 7 | 8 | Sync do 9 | condition = Async::Condition.new 10 | 11 | Async do 12 | Console.info "Waiting for condition..." 13 | value = condition.wait 14 | Console.info "Condition was signalled: #{value}" 15 | end 16 | 17 | Async do |task| 18 | sleep(1) 19 | Console.info "Signalling condition..." 20 | condition.signal("Hello World") 21 | end 22 | end 23 | ~~~ 24 | 25 | ### Output 26 | 27 | ~~~ 28 | 0.0s info: Waiting for condition... 29 | 1.0s info: Signalling condition... 30 | 1.0s info: Condition was signalled: Hello World 31 | ~~~ 32 | -------------------------------------------------------------------------------- /lib/async/condition.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2024, by Samuel Williams. 5 | # Copyright, 2017, by Kent Gruber. 6 | 7 | require "fiber" 8 | require_relative "list" 9 | 10 | module Async 11 | # A synchronization primitive, which allows fibers to wait until a particular condition is (edge) triggered. 12 | # @public Since *Async v1*. 13 | class Condition 14 | # Create a new condition. 15 | def initialize 16 | @waiting = List.new 17 | end 18 | 19 | class FiberNode < List::Node 20 | def initialize(fiber) 21 | @fiber = fiber 22 | end 23 | 24 | def transfer(*arguments) 25 | @fiber.transfer(*arguments) 26 | end 27 | 28 | def alive? 29 | @fiber.alive? 30 | end 31 | end 32 | 33 | private_constant :FiberNode 34 | 35 | # Queue up the current fiber and wait on yielding the task. 36 | # @returns [Object] 37 | def wait 38 | @waiting.stack(FiberNode.new(Fiber.current)) do 39 | Fiber.scheduler.transfer 40 | end 41 | end 42 | 43 | # @deprecated Replaced by {#waiting?} 44 | def empty? 45 | @waiting.empty? 46 | end 47 | 48 | # @returns [Boolean] Is any fiber waiting on this notification? 49 | def waiting? 50 | @waiting.size > 0 51 | end 52 | 53 | # Signal to a given task that it should resume operations. 54 | # @parameter value [Object | Nil] The value to return to the waiting fibers. 55 | def signal(value = nil) 56 | return if @waiting.empty? 57 | 58 | waiting = self.exchange 59 | 60 | waiting.each do |fiber| 61 | Fiber.scheduler.resume(fiber, value) if fiber.alive? 62 | end 63 | 64 | return nil 65 | end 66 | 67 | protected 68 | 69 | def exchange 70 | waiting = @waiting 71 | @waiting = List.new 72 | return waiting 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/async/console.rb: -------------------------------------------------------------------------------- 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 43 | -------------------------------------------------------------------------------- /lib/async/idler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024, by Samuel Williams. 5 | 6 | module Async 7 | # A load balancing mechanism that can be used process work when the system is idle. 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. 16 | def initialize(maximum_load = 0.8, backoff: 0.01, parent: nil) 17 | @maximum_load = maximum_load 18 | @backoff = backoff 19 | @parent = parent 20 | end 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. 30 | def async(*arguments, parent: (@parent or Task.current), **options, &block) 31 | wait 32 | 33 | # 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. 34 | parent.async(*arguments, **options, &block) 35 | end 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. 40 | def wait 41 | scheduler = Fiber.scheduler 42 | backoff = nil 43 | 44 | while true 45 | load = scheduler.load 46 | 47 | break if load < @maximum_load 48 | 49 | if backoff 50 | sleep(backoff) 51 | backoff *= 2.0 52 | else 53 | scheduler.yield 54 | backoff = @backoff 55 | end 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/async/limited_queue.rb: -------------------------------------------------------------------------------- 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" 8 | -------------------------------------------------------------------------------- /lib/async/list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2024, by Samuel Williams. 5 | 6 | module Async 7 | # A general doublely linked list. This is used internally by {Async::Barrier} and {Async::Condition} to manage child tasks. 8 | class List 9 | # Initialize a new, empty, list. 10 | def initialize 11 | @head = self 12 | @tail = self 13 | @size = 0 14 | end 15 | 16 | # @returns [String] A short summary of the list. 17 | def to_s 18 | sprintf("#<%s:0x%x size=%d>", self.class.name, object_id, @size) 19 | end 20 | 21 | alias inspect to_s 22 | 23 | # Fast, safe, unbounded accumulation of children. 24 | def to_a 25 | items = [] 26 | current = self 27 | 28 | while current.tail != self 29 | unless current.tail.is_a?(Iterator) 30 | items << current.tail 31 | end 32 | 33 | current = current.tail 34 | end 35 | 36 | return items 37 | end 38 | 39 | # @attribute [Node | Nil] Points at the end of the list. 40 | attr_accessor :head 41 | 42 | # @attribute [Node | Nil] Points at the start of the list. 43 | attr_accessor :tail 44 | 45 | # @attribute [Integer] The number of nodes in the list. 46 | attr :size 47 | 48 | # A callback that is invoked when an item is added to the list. 49 | def added(node) 50 | @size += 1 51 | return node 52 | end 53 | 54 | # Append a node to the end of the list. 55 | def append(node) 56 | if node.head 57 | raise ArgumentError, "Node is already in a list!" 58 | end 59 | 60 | node.tail = self 61 | @head.tail = node 62 | node.head = @head 63 | @head = node 64 | 65 | return added(node) 66 | end 67 | 68 | # Prepend a node to the start of the list. 69 | def prepend(node) 70 | if node.head 71 | raise ArgumentError, "Node is already in a list!" 72 | end 73 | 74 | node.head = self 75 | @tail.head = node 76 | node.tail = @tail 77 | @tail = node 78 | 79 | return added(node) 80 | end 81 | 82 | # Add the node, yield, and the remove the node. 83 | # @yields {|node| ...} Yields the node. 84 | # @returns [Object] Returns the result of the block. 85 | def stack(node, &block) 86 | append(node) 87 | return yield(node) 88 | ensure 89 | remove!(node) 90 | end 91 | 92 | # A callback that is invoked when an item is removed from the list. 93 | def removed(node) 94 | @size -= 1 95 | return node 96 | end 97 | 98 | # Remove the node if it is in a list. 99 | # 100 | # You should be careful to only remove nodes that are part of this list. 101 | # 102 | # @returns [Node] Returns the node if it was removed, otherwise nil. 103 | def remove?(node) 104 | if node.head 105 | return remove!(node) 106 | end 107 | 108 | return nil 109 | end 110 | 111 | # Remove the node. If it was already removed, this will raise an error. 112 | # 113 | # You should be careful to only remove nodes that are part of this list. 114 | # 115 | # @raises [ArgumentError] If the node is not part of this list. 116 | # @returns [Node] Returns the node if it was removed, otherwise nil. 117 | def remove(node) 118 | # One downside of this interface is we don't actually check if the node is part of the list defined by `self`. This means that there is a potential for a node to be removed from a different list using this method, which in can throw off book-keeping when lists track size, etc. 119 | unless node.head 120 | raise ArgumentError, "Node is not in a list!" 121 | end 122 | 123 | remove!(node) 124 | end 125 | 126 | private def remove!(node) 127 | node.head.tail = node.tail 128 | node.tail.head = node.head 129 | 130 | # This marks the node as being removed, and causes remove to fail if called a 2nd time. 131 | node.head = nil 132 | # node.tail = nil 133 | 134 | return removed(node) 135 | end 136 | 137 | # @returns [Boolean] Returns true if the list is empty. 138 | def empty? 139 | @size == 0 140 | end 141 | 142 | # def validate!(node = nil) 143 | # previous = self 144 | # current = @tail 145 | # found = node.equal?(self) 146 | 147 | # while true 148 | # break if current.equal?(self) 149 | 150 | # if current.head != previous 151 | # raise "Invalid previous linked list node!" 152 | # end 153 | 154 | # if current.is_a?(List) and !current.equal?(self) 155 | # raise "Invalid list in list node!" 156 | # end 157 | 158 | # if node 159 | # found ||= current.equal?(node) 160 | # end 161 | 162 | # previous = current 163 | # current = current.tail 164 | # end 165 | 166 | # if node and !found 167 | # raise "Node not found in list!" 168 | # end 169 | # end 170 | 171 | # Iterate over each node in the linked list. It is generally safe to remove the current node, any previous node or any future node during iteration. 172 | # 173 | # @yields {|node| ...} Yields each node in the list. 174 | # @returns [List] Returns self. 175 | def each(&block) 176 | return to_enum unless block_given? 177 | 178 | Iterator.each(self, &block) 179 | 180 | return self 181 | end 182 | 183 | # Determine whether the given node is included in the list. 184 | # 185 | # @parameter needle [Node] The node to search for. 186 | # @returns [Boolean] Returns true if the node is in the list. 187 | def include?(needle) 188 | self.each do |item| 189 | return true if needle.equal?(item) 190 | end 191 | 192 | return false 193 | end 194 | 195 | # @returns [Node] Returns the first node in the list, if it is not empty. 196 | def first 197 | # validate! 198 | 199 | current = @tail 200 | 201 | while !current.equal?(self) 202 | if current.is_a?(Iterator) 203 | current = current.tail 204 | else 205 | return current 206 | end 207 | end 208 | 209 | return nil 210 | end 211 | 212 | # @returns [Node] Returns the last node in the list, if it is not empty. 213 | def last 214 | # validate! 215 | 216 | current = @head 217 | 218 | while !current.equal?(self) 219 | if current.is_a?(Iterator) 220 | current = current.head 221 | else 222 | return current 223 | end 224 | end 225 | 226 | return nil 227 | end 228 | 229 | # Shift the first node off the list, if it is not empty. 230 | def shift 231 | if node = first 232 | remove!(node) 233 | end 234 | end 235 | 236 | # A linked list Node. 237 | class Node 238 | attr_accessor :head 239 | attr_accessor :tail 240 | 241 | alias inspect to_s 242 | end 243 | 244 | class Iterator < Node 245 | def initialize(list) 246 | @list = list 247 | 248 | # Insert the iterator as the first item in the list: 249 | @tail = list.tail 250 | @tail.head = self 251 | list.tail = self 252 | @head = list 253 | end 254 | 255 | def remove! 256 | @head.tail = @tail 257 | @tail.head = @head 258 | @head = nil 259 | @tail = nil 260 | @list = nil 261 | end 262 | 263 | def move_next 264 | # Move to the next item (which could be an iterator or the end): 265 | @tail.head = @head 266 | @head.tail = @tail 267 | @head = @tail 268 | @tail = @tail.tail 269 | @head.tail = self 270 | @tail.head = self 271 | end 272 | 273 | def move_current 274 | while true 275 | # Are we at the end of the list? 276 | if @tail.equal?(@list) 277 | return nil 278 | end 279 | 280 | if @tail.is_a?(Iterator) 281 | move_next 282 | else 283 | return @tail 284 | end 285 | end 286 | end 287 | 288 | def each 289 | while current = move_current 290 | yield current 291 | 292 | if current.equal?(@tail) 293 | move_next 294 | end 295 | end 296 | end 297 | 298 | def self.each(list, &block) 299 | return if list.empty? 300 | 301 | iterator = Iterator.new(list) 302 | 303 | iterator.each(&block) 304 | ensure 305 | iterator&.remove! 306 | end 307 | end 308 | 309 | private_constant :Iterator 310 | end 311 | end 312 | -------------------------------------------------------------------------------- /lib/async/notification.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2024, by Samuel Williams. 5 | 6 | require_relative "condition" 7 | 8 | module Async 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 *Async v1*. 11 | class Notification < Condition 12 | # Signal to a given task that it should resume operations. 13 | def signal(value = nil, task: Task.current) 14 | return if @waiting.empty? 15 | 16 | Fiber.scheduler.push Signal.new(self.exchange, value) 17 | 18 | return nil 19 | end 20 | 21 | Signal = Struct.new(:waiting, :value) do 22 | def alive? 23 | true 24 | end 25 | 26 | def transfer 27 | waiting.each do |fiber| 28 | fiber.transfer(value) if fiber.alive? 29 | end 30 | end 31 | end 32 | 33 | private_constant :Signal 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/async/queue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2024, by Samuel Williams. 5 | # Copyright, 2019, by Ryan Musgrave. 6 | # Copyright, 2020-2022, by Bruno Sutic. 7 | 8 | require_relative "notification" 9 | 10 | module Async 11 | # A queue which allows items to be processed in order. 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) 22 | @items = [] 23 | @parent = parent 24 | @available = available 25 | end 26 | 27 | # @attribute [Array] The items in the queue. 28 | attr :items 29 | 30 | # @returns [Integer] The number of items in the queue. 31 | def size 32 | @items.size 33 | end 34 | 35 | # @returns [Boolean] Whether the queue is empty. 36 | def empty? 37 | @items.empty? 38 | end 39 | 40 | # Add an item to the queue. 41 | def push(item) 42 | @items << item 43 | 44 | @available.signal unless self.empty? 45 | end 46 | 47 | # Compatibility with {::Queue#push}. 48 | def <<(item) 49 | self.push(item) 50 | end 51 | 52 | # Add multiple items to the queue. 53 | def enqueue(*items) 54 | @items.concat(items) 55 | 56 | @available.signal unless self.empty? 57 | end 58 | 59 | # Remove and return the next item from the queue. 60 | def dequeue 61 | while @items.empty? 62 | @available.wait 63 | end 64 | 65 | @items.shift 66 | end 67 | 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) 82 | while item = self.dequeue 83 | parent.async(item, **options, &block) 84 | end 85 | end 86 | 87 | # Enumerate each item in the queue. 88 | def each 89 | while item = self.dequeue 90 | yield item 91 | end 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 103 | end 104 | 105 | # A queue which limits the number of items that can be enqueued. 106 | # @public Since *Async v1*. 107 | class LimitedQueue < Queue 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) 113 | super(**options) 114 | 115 | @limit = limit 116 | @full = full 117 | end 118 | 119 | # @attribute [Integer] The maximum number of items that can be enqueued. 120 | attr :limit 121 | 122 | # @returns [Boolean] Whether trying to enqueue an item would block. 123 | def limited? 124 | @items.size >= @limit 125 | end 126 | 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) 133 | while limited? 134 | @full.wait 135 | end 136 | 137 | super 138 | end 139 | 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) 146 | while !items.empty? 147 | while limited? 148 | @full.wait 149 | end 150 | 151 | available = @limit - @items.size 152 | @items.concat(items.shift(available)) 153 | 154 | @available.signal unless self.empty? 155 | end 156 | end 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. 163 | def dequeue 164 | item = super 165 | 166 | @full.signal 167 | 168 | return item 169 | end 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /lib/async/reactor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2024, by Samuel Williams. 5 | # Copyright, 2017, by Kent Gruber. 6 | # Copyright, 2018, by Sokolov Yura. 7 | 8 | require_relative "scheduler" 9 | 10 | module Async 11 | # A wrapper around the the scheduler which binds it to the current thread automatically. 12 | class Reactor < Scheduler 13 | # @deprecated Replaced by {Kernel::Async}. 14 | def self.run(...) 15 | Async(...) 16 | end 17 | 18 | # Initialize the reactor and assign it to the current Fiber scheduler. 19 | def initialize(...) 20 | super 21 | 22 | Fiber.set_scheduler(self) 23 | end 24 | 25 | # Close the reactor and remove it from the current Fiber scheduler. 26 | def scheduler_close 27 | self.close 28 | end 29 | 30 | public :sleep 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/async/semaphore.md: -------------------------------------------------------------------------------- 1 | A synchronization primitive, which limits access to a given resource, such as a limited number of database connections, open files, or network connections. 2 | 3 | ## Example 4 | 5 | ~~~ ruby 6 | require 'async' 7 | require 'async/semaphore' 8 | require 'net/http' 9 | 10 | Sync do 11 | # Only allow two concurrent tasks at a time: 12 | semaphore = Async::Semaphore.new(2) 13 | 14 | # Generate an array of 10 numbers: 15 | terms = ['ruby', 'python', 'go', 'java', 'c++'] 16 | 17 | # Search for the terms: 18 | terms.each do |term| 19 | semaphore.async do |task| 20 | Console.info("Searching for #{term}...") 21 | response = Net::HTTP.get(URI "https://www.google.com/search?q=#{term}") 22 | Console.info("Got response #{response.size} bytes.") 23 | end 24 | end 25 | end 26 | ~~~ 27 | 28 | ### Output 29 | 30 | ~~~ 31 | 0.0s info: Searching for ruby... [ec=0x3c] [pid=50523] 32 | 0.04s info: Searching for python... [ec=0x21c] [pid=50523] 33 | 1.7s info: Got response 182435 bytes. [ec=0x3c] [pid=50523] 34 | 1.71s info: Searching for go... [ec=0x834] [pid=50523] 35 | 3.0s info: Got response 204854 bytes. [ec=0x21c] [pid=50523] 36 | 3.0s info: Searching for java... [ec=0xf64] [pid=50523] 37 | 4.32s info: Got response 103235 bytes. [ec=0x834] [pid=50523] 38 | 4.32s info: Searching for c++... [ec=0x12d4] [pid=50523] 39 | 4.65s info: Got response 109697 bytes. [ec=0xf64] [pid=50523] 40 | 6.64s info: Got response 87249 bytes. [ec=0x12d4] [pid=50523] 41 | ~~~ 42 | -------------------------------------------------------------------------------- /lib/async/semaphore.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2024, by Samuel Williams. 5 | 6 | require_relative "list" 7 | 8 | module Async 9 | # A synchronization primitive, which limits access to a given resource. 10 | # @public Since *Async v1*. 11 | class Semaphore 12 | # @parameter limit [Integer] The maximum number of times the semaphore can be acquired before it blocks. 13 | # @parameter parent [Task | Semaphore | Nil] The parent for holding any children tasks. 14 | def initialize(limit = 1, parent: nil) 15 | @count = 0 16 | @limit = limit 17 | @waiting = List.new 18 | 19 | @parent = parent 20 | end 21 | 22 | # The current number of tasks that have acquired the semaphore. 23 | attr :count 24 | 25 | # The maximum number of tasks that can acquire the semaphore. 26 | attr :limit 27 | 28 | # The tasks waiting on this semaphore. 29 | attr :waiting 30 | 31 | # 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. 32 | # 33 | # 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. 34 | # 35 | # @parameter limit [Integer] The new limit. 36 | def limit= limit 37 | difference = limit - @limit 38 | @limit = limit 39 | 40 | # We can't suspend 41 | if difference > 0 42 | difference.times do 43 | break unless node = @waiting.first 44 | 45 | node.resume 46 | end 47 | end 48 | end 49 | 50 | # Is the semaphore currently acquired? 51 | def empty? 52 | @count.zero? 53 | end 54 | 55 | # Whether trying to acquire this semaphore would block. 56 | def blocking? 57 | @count >= @limit 58 | end 59 | 60 | # Run an async task. Will wait until the semaphore is ready until spawning and running the task. 61 | def async(*arguments, parent: (@parent or Task.current), **options) 62 | wait 63 | 64 | parent.async(**options) do |task| 65 | @count += 1 66 | 67 | begin 68 | yield task, *arguments 69 | ensure 70 | self.release 71 | end 72 | end 73 | end 74 | 75 | # Acquire the semaphore, block if we are at the limit. 76 | # If no block is provided, you must call release manually. 77 | # @yields {...} When the semaphore can be acquired. 78 | # @returns The result of the block if invoked. 79 | def acquire 80 | wait 81 | 82 | @count += 1 83 | 84 | return unless block_given? 85 | 86 | begin 87 | return yield 88 | ensure 89 | self.release 90 | end 91 | end 92 | 93 | # Release the semaphore. Must match up with a corresponding call to `acquire`. Will release waiting fibers in FIFO order. 94 | def release 95 | @count -= 1 96 | 97 | while (@limit - @count) > 0 and node = @waiting.first 98 | node.resume 99 | end 100 | end 101 | 102 | private 103 | 104 | class FiberNode < List::Node 105 | def initialize(fiber) 106 | @fiber = fiber 107 | end 108 | 109 | def resume 110 | if @fiber.alive? 111 | Fiber.scheduler.resume(@fiber) 112 | end 113 | end 114 | end 115 | 116 | private_constant :FiberNode 117 | 118 | # Wait until the semaphore becomes available. 119 | def wait 120 | return unless blocking? 121 | 122 | @waiting.stack(FiberNode.new(Fiber.current)) do 123 | Fiber.scheduler.transfer while blocking? 124 | end 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /lib/async/task.md: -------------------------------------------------------------------------------- 1 | A sequence of instructions, defined by a block, which is executed sequentially and managed by the scheduler. A task can be in one of the following states: `initialized`, `running`, `completed`, `failed`, `cancelled` or `stopped`. 2 | 3 | ```mermaid 4 | stateDiagram-v2 5 | [*] --> Initialized 6 | Initialized --> Running : Run 7 | 8 | Running --> Completed : Return Value 9 | Running --> Failed : Exception 10 | 11 | Completed --> [*] 12 | Failed --> [*] 13 | 14 | Running --> Stopped : Stop 15 | Stopped --> [*] 16 | Completed --> Stopped : Stop 17 | Failed --> Stopped : Stop 18 | Initialized --> Stopped : Stop 19 | ``` 20 | 21 | ## Example 22 | 23 | ```ruby 24 | require 'async' 25 | 26 | # Create an asynchronous task that sleeps for 1 second: 27 | Async do |task| 28 | sleep(1) 29 | end 30 | ``` 31 | -------------------------------------------------------------------------------- /lib/async/timeout.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2025, by Samuel Williams. 5 | 6 | module Async 7 | # Represents a flexible timeout that can be rescheduled or extended. 8 | # @public Since *Async v2.24*. 9 | class Timeout 10 | # Initialize a new timeout. 11 | def initialize(timers, handle) 12 | @timers = timers 13 | @handle = handle 14 | end 15 | 16 | # @returns [Numeric] The time remaining until the timeout occurs, in seconds. 17 | def duration 18 | @handle.time - @timers.now 19 | end 20 | 21 | # Update the duration of the timeout. 22 | # 23 | # The duration is relative to the current time, e.g. setting the duration to 5 means the timeout will occur in 5 seconds from now. 24 | # 25 | # @parameter value [Numeric] The new duration to assign to the timeout, in seconds. 26 | def duration=(value) 27 | self.reschedule(@timers.now + value) 28 | end 29 | 30 | # Adjust the timeout by the specified duration. 31 | # 32 | # The duration is relative to the timeout time, e.g. adjusting the timeout by 5 increases the current duration by 5 seconds. 33 | # 34 | # @parameter duration [Numeric] The duration to adjust the timeout by, in seconds. 35 | # @returns [Numeric] The new time at which the timeout will occur. 36 | def adjust(duration) 37 | self.reschedule(time + duration) 38 | end 39 | 40 | # @returns [Numeric] The time at which the timeout will occur, in seconds since {now}. 41 | def time 42 | @handle.time 43 | end 44 | 45 | # Assign a new time to the timeout, rescheduling it if necessary. 46 | # 47 | # @parameter value [Numeric] The new time to assign to the timeout. 48 | # @returns [Numeric] The new time at which the timeout will occur. 49 | def time=(value) 50 | self.reschedule(value) 51 | end 52 | 53 | # @returns [Numeric] The current time in the scheduler, relative to the time of this timeout, in seconds. 54 | def now 55 | @timers.now 56 | end 57 | 58 | # Cancel the timeout, preventing it from executing. 59 | def cancel! 60 | @handle.cancel! 61 | end 62 | 63 | # @returns [Boolean] Whether the timeout has been cancelled. 64 | def cancelled? 65 | @handle.cancelled? 66 | end 67 | 68 | # Raised when attempting to reschedule a cancelled timeout. 69 | class CancelledError < RuntimeError 70 | end 71 | 72 | # Reschedule the timeout to occur at the specified time. 73 | # 74 | # @parameter time [Numeric] The new time to schedule the timeout for. 75 | # @returns [Numeric] The new time at which the timeout will occur. 76 | private def reschedule(time) 77 | if block = @handle&.block 78 | @handle.cancel! 79 | 80 | @handle = @timers.schedule(time, block) 81 | 82 | return time 83 | else 84 | raise CancelledError, "Cannot reschedule a cancelled timeout!" 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/async/variable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2024, by Samuel Williams. 5 | 6 | require_relative "condition" 7 | 8 | module Async 9 | # A synchronization primitive that allows one task to wait for another task to resolve a value. 10 | class Variable 11 | # Create a new variable. 12 | # 13 | # @parameter condition [Condition] The condition to use for synchronization. 14 | def initialize(condition = Condition.new) 15 | @condition = condition 16 | @value = nil 17 | end 18 | 19 | # Resolve the value. 20 | # 21 | # Signals all waiting tasks. 22 | # 23 | # @parameter value [Object] The value to resolve. 24 | def resolve(value = true) 25 | @value = value 26 | condition = @condition 27 | @condition = nil 28 | 29 | self.freeze 30 | 31 | condition.signal(value) 32 | end 33 | 34 | # Alias for {#resolve}. 35 | def value=(value) 36 | self.resolve(value) 37 | end 38 | 39 | # Whether the value has been resolved. 40 | # 41 | # @returns [Boolean] Whether the value has been resolved. 42 | def resolved? 43 | @condition.nil? 44 | end 45 | 46 | # Wait for the value to be resolved. 47 | # 48 | # @returns [Object] The resolved value. 49 | def wait 50 | @condition&.wait 51 | return @value 52 | end 53 | 54 | # Alias for {#wait}. 55 | def value 56 | self.wait 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/async/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2024, by Samuel Williams. 5 | 6 | module Async 7 | VERSION = "2.24.0" 8 | end 9 | -------------------------------------------------------------------------------- /lib/async/waiter.md: -------------------------------------------------------------------------------- 1 | A synchronization primitive, which allows you to wait for tasks to complete in order of completion. This is useful for implementing a task pool, where you want to wait for the first task to complete, and then cancel the rest. 2 | 3 | If you try to wait for more things than you have added, you will deadlock. 4 | 5 | ## Example 6 | 7 | ~~~ ruby 8 | require 'async' 9 | require 'async/semaphore' 10 | require 'async/barrier' 11 | require 'async/waiter' 12 | 13 | Sync do 14 | barrier = Async::Barrier.new 15 | waiter = Async::Waiter.new(parent: barrier) 16 | semaphore = Async::Semaphore.new(2, parent: waiter) 17 | 18 | # Sleep sort the numbers: 19 | generator = Async do 20 | while true 21 | semaphore.async do |task| 22 | number = rand(1..10) 23 | sleep(number) 24 | end 25 | end 26 | end 27 | 28 | numbers = [] 29 | 30 | 4.times do 31 | # Wait for all the numbers to be sorted: 32 | numbers << waiter.wait 33 | end 34 | 35 | # Don't generate any more numbers: 36 | generator.stop 37 | 38 | # Stop all tasks which we don't care about: 39 | barrier.stop 40 | 41 | Console.info("Smallest", numbers) 42 | end 43 | ~~~ 44 | 45 | ### Output 46 | 47 | ~~~ 48 | 0.0s info: Smallest 49 | | [3, 3, 1, 2] 50 | ~~~ 51 | -------------------------------------------------------------------------------- /lib/async/waiter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2024, by Samuel Williams. 5 | # Copyright, 2024, by Patrik Wenger. 6 | 7 | module Async 8 | # A composable synchronization primitive, which allows one task to wait for a number of other tasks to complete. It can be used in conjunction with {Semaphore} and/or {Barrier}. 9 | class Waiter 10 | # Create a waiter instance. 11 | # 12 | # @parameter parent [Interface(:async) | Nil] The parent task to use for asynchronous operations. 13 | # @parameter finished [Async::Condition] The condition to signal when a task completes. 14 | def initialize(parent: nil, finished: Async::Condition.new) 15 | @finished = finished 16 | @done = [] 17 | 18 | @parent = parent 19 | end 20 | 21 | # Execute a child task and add it to the waiter. 22 | # @asynchronous Executes the given block concurrently. 23 | def async(parent: (@parent or Task.current), **options, &block) 24 | parent.async(**options) do |task| 25 | yield(task) 26 | ensure 27 | @done << task 28 | @finished.signal 29 | end 30 | end 31 | 32 | # Wait for the first `count` tasks to complete. 33 | # @parameter count [Integer | Nil] The number of tasks to wait for. 34 | # @returns [Array(Async::Task)] If an integer is given, the tasks which have completed. 35 | # @returns [Async::Task] Otherwise, the first task to complete. 36 | def first(count = nil) 37 | minimum = count || 1 38 | 39 | while @done.size < minimum 40 | @finished.wait 41 | end 42 | 43 | return @done.shift(*count) 44 | end 45 | 46 | # Wait for the first `count` tasks to complete. 47 | # @parameter count [Integer | Nil] The number of tasks to wait for. 48 | def wait(count = nil) 49 | if count 50 | first(count).map(&:wait) 51 | else 52 | first.wait 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/async/worker_pool.rb: -------------------------------------------------------------------------------- 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.21* 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 183 | -------------------------------------------------------------------------------- /lib/kernel/async.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019-2024, by Samuel Williams. 5 | 6 | require_relative "../async/reactor" 7 | 8 | module Kernel 9 | # Run the given block of code in a task, asynchronously, creating a reactor if necessary. 10 | # 11 | # The preferred method to invoke asynchronous behavior at the top level. 12 | # 13 | # - When invoked within an existing reactor task, it will run the given block 14 | # asynchronously. Will return the task once it has been scheduled. 15 | # - When invoked at the top level, will create and run a reactor, and invoke 16 | # the block as an asynchronous task. Will block until the reactor finishes 17 | # running. 18 | # 19 | # @yields {|task| ...} The block that will execute asynchronously. 20 | # @parameter task [Async::Task] The task that is executing the given block. 21 | # 22 | # @public Since *Async v1*. 23 | # @asynchronous May block until given block completes executing. 24 | def Async(...) 25 | if current = ::Async::Task.current? 26 | return current.async(...) 27 | elsif scheduler = Fiber.scheduler 28 | ::Async::Task.run(scheduler, ...) 29 | else 30 | # This calls Fiber.set_scheduler(self): 31 | reactor = ::Async::Reactor.new 32 | 33 | begin 34 | return reactor.run(...) 35 | ensure 36 | Fiber.set_scheduler(nil) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/kernel/sync.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019-2024, by Samuel Williams. 5 | # Copyright, 2020, by Brian Morearty. 6 | # Copyright, 2024, by Patrik Wenger. 7 | 8 | require_relative "../async/reactor" 9 | 10 | # Extensions to all Ruby objects. 11 | module Kernel 12 | # Run the given block of code synchronously, but within a reactor if not already in one. 13 | # 14 | # @yields {|task| ...} The block that will execute asynchronously. 15 | # @parameter task [Async::Task] The task that is executing the given block. 16 | # 17 | # @public Since *Async v1*. 18 | # @asynchronous Will block until given block completes executing. 19 | def Sync(annotation: nil, &block) 20 | if task = ::Async::Task.current? 21 | if annotation 22 | task.annotate(annotation) {yield task} 23 | else 24 | yield task 25 | end 26 | elsif scheduler = Fiber.scheduler 27 | ::Async::Task.run(scheduler, &block).wait 28 | else 29 | # This calls Fiber.set_scheduler(self): 30 | reactor = Async::Reactor.new 31 | 32 | begin 33 | return reactor.run(annotation: annotation, finished: ::Async::Condition.new, &block).wait 34 | ensure 35 | Fiber.set_scheduler(nil) 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/metrics/provider/async.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024, by Samuel Williams. 5 | 6 | require_relative "async/task" 7 | -------------------------------------------------------------------------------- /lib/metrics/provider/async/task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024, by Samuel Williams. 5 | 6 | require_relative "../../../async/task" 7 | require "metrics/provider" 8 | 9 | Metrics::Provider(Async::Task) do 10 | ASYNC_TASK_SCHEDULED = Metrics.metric("async.task.scheduled", :counter, description: "The number of tasks scheduled.") 11 | ASYNC_TASK_FINISHED = Metrics.metric("async.task.finished", :counter, description: "The number of tasks finished.") 12 | 13 | def schedule(&block) 14 | ASYNC_TASK_SCHEDULED.emit(1) 15 | 16 | super(&block) 17 | ensure 18 | ASYNC_TASK_FINISHED.emit(1) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/traces/provider/async.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024, by Samuel Williams. 5 | 6 | require_relative "async/task" 7 | require_relative "async/barrier" 8 | -------------------------------------------------------------------------------- /lib/traces/provider/async/barrier.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022, by Samuel Williams. 5 | 6 | require_relative "../../../async/barrier" 7 | require "traces/provider" 8 | 9 | Traces::Provider(Async::Barrier) do 10 | def wait 11 | attributes = { 12 | "size" => self.size 13 | } 14 | 15 | Traces.trace("async.barrier.wait", attributes: attributes) {super} 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/traces/provider/async/task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022, by Samuel Williams. 5 | 6 | require_relative "../../../async/task" 7 | require "traces/provider" 8 | 9 | Traces::Provider(Async::Task) do 10 | def schedule(&block) 11 | # If we are not actively tracing anything, then we can skip this: 12 | unless Traces.active? 13 | return super(&block) 14 | end 15 | 16 | unless self.transient? 17 | trace_context = Traces.trace_context 18 | end 19 | 20 | attributes = { 21 | # We use the instance variable as it corresponds to the user-provided block. 22 | "block" => @block, 23 | "transient" => self.transient?, 24 | } 25 | 26 | # Run the trace in the context of the child task: 27 | super do 28 | Traces.trace_context = trace_context 29 | 30 | if annotation = self.annotation 31 | attributes["annotation"] = annotation 32 | end 33 | 34 | Traces.trace("async.task", attributes: attributes) do 35 | # Yes, this is correct, we already called super above: 36 | yield 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright, 2017-2024, by Samuel Williams. 4 | Copyright, 2017, by Kent Gruber. 5 | Copyright, 2017, by Devin Christensen. 6 | Copyright, 2018, by Sokolov Yura. 7 | Copyright, 2018, by Jiang Jinyang. 8 | Copyright, 2019, by Jeremy Jung. 9 | Copyright, 2019, by Ryan Musgrave. 10 | Copyright, 2020-2023, by Olle Jonsson. 11 | Copyright, 2020, by Salim Semaoune. 12 | Copyright, 2020, by Brian Morearty. 13 | Copyright, 2020, by Stefan Wrobel. 14 | Copyright, 2020-2024, by Patrik Wenger. 15 | Copyright, 2020, by Ken Muryoi. 16 | Copyright, 2020, by Jun Jiang. 17 | Copyright, 2020-2022, by Bruno Sutic. 18 | Copyright, 2021, by Julien Portalier. 19 | Copyright, 2022, by Shannon Skipper. 20 | Copyright, 2022, by Masafumi Okura. 21 | Copyright, 2022, by Trevor Turk. 22 | Copyright, 2022, by Masayuki Yamamoto. 23 | Copyright, 2023, by Leon Löchner. 24 | Copyright, 2023, by Colin Kelley. 25 | Copyright, 2023, by Math Ieu. 26 | Copyright, 2023, by Emil Tin. 27 | Copyright, 2023, by Gert Goet. 28 | Copyright, 2024, by Dimitar Peychinov. 29 | Copyright, 2024, by Jamie McCarthy. 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining a copy 32 | of this software and associated documentation files (the "Software"), to deal 33 | in the Software without restriction, including without limitation the rights 34 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 35 | copies of the Software, and to permit persons to whom the Software is 36 | furnished to do so, subject to the following conditions: 37 | 38 | The above copyright notice and this permission notice shall be included in all 39 | copies or substantial portions of the Software. 40 | 41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 42 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 43 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 44 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 45 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 46 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 47 | SOFTWARE. 48 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ![Async](assets/logo.webp) 2 | 3 | Async is a composable asynchronous I/O framework for Ruby based on [io-event](https://github.com/socketry/io-event). 4 | 5 | > "Lately I've been looking into `async`, as one of my projects – 6 | > [tus-ruby-server](https://github.com/janko/tus-ruby-server) – would really benefit from non-blocking I/O. It's really 7 | > beautifully designed." *– [janko](https://github.com/janko)* 8 | 9 | [![Development Status](https://github.com/socketry/async/workflows/Test/badge.svg)](https://github.com/socketry/async/actions?workflow=Test) 10 | 11 | ## Features 12 | 13 | - Scalable event-driven I/O for Ruby. Thousands of clients per process\! 14 | - Light weight fiber-based concurrency. No need for callbacks\! 15 | - Multi-thread/process containers for parallelism. 16 | - Growing eco-system of event-driven components. 17 | 18 | ## Usage 19 | 20 | Please see the [project documentation](https://socketry.github.io/async/) for more details. 21 | 22 | - [Getting Started](https://socketry.github.io/async/guides/getting-started/index) - This guide shows how to add async to your project and run code asynchronously. 23 | 24 | - [Asynchronous Tasks](https://socketry.github.io/async/guides/asynchronous-tasks/index) - This guide explains how asynchronous tasks work and how to use them. 25 | 26 | - [Scheduler](https://socketry.github.io/async/guides/scheduler/index) - This guide gives an overview of how the scheduler is implemented. 27 | 28 | - [Compatibility](https://socketry.github.io/async/guides/compatibility/index) - This guide gives an overview of the compatibility of Async with Ruby and other frameworks. 29 | 30 | - [Best Practices](https://socketry.github.io/async/guides/best-practices/index) - This guide gives an overview of best practices for using Async. 31 | 32 | - [Debugging](https://socketry.github.io/async/guides/debugging/index) - This guide explains how to debug issues with programs that use Async. 33 | 34 | ## Releases 35 | 36 | Please see the [project releases](https://socketry.github.io/async/releases/index) for all releases. 37 | 38 | ### v2.24.0 39 | 40 | - Ruby v3.1 support is dropped. 41 | - `Async::Wrapper` which was previously deprecated, is now removed. 42 | - [Flexible Timeouts](https://socketry.github.io/async/releases/index#flexible-timeouts) 43 | 44 | ### v2.23.0 45 | 46 | - Rename `ASYNC_SCHEDULER_DEFAULT_WORKER_POOL` to `ASYNC_SCHEDULER_WORKER_POOL`. 47 | - [Fiber Stall Profiler](https://socketry.github.io/async/releases/index#fiber-stall-profiler) 48 | 49 | ### v2.21.1 50 | 51 | - [Worker Pool](https://socketry.github.io/async/releases/index#worker-pool) 52 | 53 | ### v2.20.0 54 | 55 | - [Traces and Metrics Providers](https://socketry.github.io/async/releases/index#traces-and-metrics-providers) 56 | 57 | ### v2.19.0 58 | 59 | - [Async::Scheduler Debugging](https://socketry.github.io/async/releases/index#async::scheduler-debugging) 60 | - [Console Shims](https://socketry.github.io/async/releases/index#console-shims) 61 | 62 | ### v2.18.0 63 | 64 | - Add support for `Sync(annotation:)`, so that you can annotate the block with a description of what it does, even if it doesn't create a new task. 65 | 66 | ### v2.17.0 67 | 68 | - Introduce `Async::Queue#push` and `Async::Queue#pop` for compatibility with `::Queue`. 69 | 70 | ### v2.16.0 71 | 72 | - [Better Handling of Async and Sync in Nested Fibers](https://socketry.github.io/async/releases/index#better-handling-of-async-and-sync-in-nested-fibers) 73 | 74 | ## See Also 75 | 76 | - [async-http](https://github.com/socketry/async-http) — Asynchronous HTTP client/server. 77 | - [async-websocket](https://github.com/socketry/async-websocket) — Asynchronous client and server websockets. 78 | - [async-dns](https://github.com/socketry/async-dns) — Asynchronous DNS resolver and server. 79 | - [falcon](https://github.com/socketry/falcon) — A rack compatible server built on top of `async-http`. 80 | - [rubydns](https://github.com/ioquatix/rubydns) — An easy to use Ruby DNS server. 81 | - [slack-ruby-bot](https://github.com/slack-ruby/slack-ruby-bot) — A client for making slack bots. 82 | 83 | ## Contributing 84 | 85 | We welcome contributions to this project. 86 | 87 | 1. Fork it. 88 | 2. Create your feature branch (`git checkout -b my-new-feature`). 89 | 3. Commit your changes (`git commit -am 'Add some feature'`). 90 | 4. Push to the branch (`git push origin my-new-feature`). 91 | 5. Create new Pull Request. 92 | 93 | ### Developer Certificate of Origin 94 | 95 | In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed. 96 | 97 | ### Community Guidelines 98 | 99 | This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers. 100 | -------------------------------------------------------------------------------- /release.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11 3 | ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK 4 | CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz 5 | MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd 6 | MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj 7 | bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB 8 | igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2 9 | 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW 10 | sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE 11 | e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN 12 | XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss 13 | RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn 14 | tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM 15 | zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW 16 | xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O 17 | BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs 18 | aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs 19 | aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE 20 | cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl 21 | xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/ 22 | c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp 23 | 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws 24 | JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP 25 | eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt 26 | Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8 27 | voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg= 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0x03d7E2c0cf7813867DDb318674B66CC53B8497dA' 6 | quorum: 1 7 | -------------------------------------------------------------------------------- /test/async/barrier.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019-2024, by Samuel Williams. 5 | 6 | require "async/barrier" 7 | require "async/clock" 8 | require "sus/fixtures/async" 9 | require "sus/fixtures/time" 10 | require "async/semaphore" 11 | 12 | require "async/chainable_async" 13 | 14 | describe Async::Barrier do 15 | include Sus::Fixtures::Async::ReactorContext 16 | 17 | let(:barrier) {subject.new} 18 | 19 | with "#async" do 20 | let(:repeats) {40} 21 | let(:delay) {0.01} 22 | 23 | it "should wait for all jobs to complete" do 24 | finished = 0 25 | 26 | repeats.times.map do |i| 27 | barrier.async do |task| 28 | sleep(delay) 29 | finished += 1 30 | 31 | # This task is a child task but not part of the barrier. 32 | task.async do 33 | sleep(delay*3) 34 | end 35 | end 36 | end 37 | 38 | expect(barrier).not.to be(:empty?) 39 | expect(finished).to be < repeats 40 | 41 | duration = Async::Clock.measure{barrier.wait} 42 | 43 | expect(duration).to be_within(repeats * Sus::Fixtures::Time::QUANTUM).of(delay) 44 | expect(finished).to be == repeats 45 | expect(barrier).to be(:empty?) 46 | end 47 | end 48 | 49 | with "#wait" do 50 | it "should wait for tasks even after exceptions" do 51 | task1 = barrier.async do |task| 52 | expect(task).to receive(:warn).and_return(nil) 53 | 54 | raise "Boom" 55 | end 56 | 57 | task2 = barrier.async do 58 | end 59 | 60 | expect{barrier.wait}.to raise_exception(RuntimeError, message: be =~ /Boom/) 61 | 62 | barrier.wait until barrier.empty? 63 | 64 | expect{task1.wait}.to raise_exception(RuntimeError, message: be =~ /Boom/) 65 | 66 | expect(barrier).to be(:empty?) 67 | 68 | expect(task1).to be(:failed?) 69 | expect(task2).to be(:finished?) 70 | end 71 | 72 | it "waits for tasks in order" do 73 | order = [] 74 | 75 | 5.times do |i| 76 | barrier.async do 77 | order << i 78 | end 79 | end 80 | 81 | barrier.wait 82 | 83 | expect(order).to be == [0, 1, 2, 3, 4] 84 | end 85 | 86 | # 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. 87 | it "waits for tasks with timeouts" do 88 | begin 89 | reactor.with_timeout(5/100.0/2) do 90 | 5.times do |i| 91 | barrier.async do |task| 92 | sleep(i/100.0) 93 | end 94 | end 95 | 96 | expect(barrier.tasks.size).to be == 5 97 | barrier.wait 98 | end 99 | rescue Async::TimeoutError 100 | # Expected. 101 | ensure 102 | expect(barrier.tasks.size).to be == 2 103 | barrier.stop 104 | end 105 | end 106 | end 107 | 108 | with "#stop" do 109 | it "can stop several tasks" do 110 | task1 = barrier.async do |task| 111 | sleep(10) 112 | end 113 | 114 | task2 = barrier.async do |task| 115 | sleep(10) 116 | end 117 | 118 | barrier.stop 119 | 120 | expect(task1).to be(:stopped?) 121 | expect(task2).to be(:stopped?) 122 | end 123 | 124 | it "can stop several tasks when waiting on barrier" do 125 | task1 = barrier.async do |task| 126 | sleep(10) 127 | end 128 | 129 | task2 = barrier.async do |task| 130 | sleep(10) 131 | end 132 | 133 | task3 = reactor.async do 134 | barrier.wait 135 | end 136 | 137 | barrier.stop 138 | 139 | task1.wait 140 | task2.wait 141 | 142 | expect(task1).to be(:stopped?) 143 | expect(task2).to be(:stopped?) 144 | 145 | task3.wait 146 | end 147 | 148 | it "several tasks can wait on the same barrier" do 149 | task1 = barrier.async do |task| 150 | sleep(10) 151 | end 152 | 153 | task2 = reactor.async do |task| 154 | barrier.wait 155 | end 156 | 157 | task3 = reactor.async do 158 | barrier.wait 159 | end 160 | 161 | barrier.stop 162 | 163 | task1.wait 164 | 165 | expect(task1).to be(:stopped?) 166 | 167 | task2.wait 168 | task3.wait 169 | end 170 | end 171 | 172 | with "semaphore" do 173 | let(:capacity) {2} 174 | let(:semaphore) {Async::Semaphore.new(capacity)} 175 | let(:repeats) {capacity * 2} 176 | 177 | it "should execute several tasks and wait using a barrier" do 178 | repeats.times do 179 | barrier.async(parent: semaphore) do |task| 180 | sleep 0.01 181 | end 182 | end 183 | 184 | expect(barrier.size).to be == repeats 185 | barrier.wait 186 | end 187 | end 188 | 189 | it_behaves_like Async::ChainableAsync 190 | end 191 | -------------------------------------------------------------------------------- /test/async/children.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2024, by Samuel Williams. 5 | # Copyright, 2017, by Kent Gruber. 6 | 7 | require "async/node" 8 | 9 | describe Async::Children do 10 | let(:children) {subject.new} 11 | 12 | with "no children" do 13 | it "should be empty" do 14 | expect(children).to be(:empty?) 15 | expect(children).to be(:nil?) 16 | expect(children).not.to be(:transients?) 17 | end 18 | end 19 | 20 | with "one child" do 21 | it "can add a child" do 22 | child = Async::Node.new 23 | children.append(child) 24 | 25 | expect(children).not.to be(:empty?) 26 | end 27 | 28 | it "can't remove a child that hasn't been inserted" do 29 | child = Async::Node.new 30 | 31 | expect{children.remove(child)}.to raise_exception(ArgumentError, message: be =~ /not in a list/) 32 | end 33 | 34 | it "can't remove the child twice" do 35 | child = Async::Node.new 36 | children.append(child) 37 | 38 | children.remove(child) 39 | 40 | expect{children.remove(child)}.to raise_exception(ArgumentError, message: be =~ /not in a list/) 41 | end 42 | end 43 | 44 | with "transient children" do 45 | let(:parent) {Async::Node.new} 46 | let(:children) {parent.children} 47 | 48 | it "can add a transient child" do 49 | child = Async::Node.new(parent, transient: true) 50 | expect(children).to be(:transients?) 51 | 52 | child.transient = false 53 | expect(children).not.to be(:transients?) 54 | expect(parent).not.to be(:finished?) 55 | 56 | child.transient = true 57 | expect(children).to be(:transients?) 58 | expect(parent).to be(:finished?) 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/async/clock.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2024, by Samuel Williams. 5 | 6 | require "async/clock" 7 | require "sus/fixtures/time/quantum" 8 | 9 | describe Async::Clock do 10 | let(:clock) {subject.new} 11 | 12 | it "can measure durations" do 13 | duration = Async::Clock.measure do 14 | sleep 0.01 15 | end 16 | 17 | expect(duration).to be_within(Sus::Fixtures::Time::QUANTUM).of(0.01) 18 | end 19 | 20 | it "can get current offset" do 21 | expect(Async::Clock.now).to be_a Float 22 | end 23 | 24 | it "can accumulate durations" do 25 | 2.times do 26 | clock.start! 27 | sleep(0.01) 28 | clock.stop! 29 | end 30 | 31 | expect(clock.total).to be_within(2 * Sus::Fixtures::Time::QUANTUM).of(0.02) 32 | end 33 | 34 | with "#total" do 35 | with "initial duration" do 36 | let(:clock) {subject.new(1.5)} 37 | let(:total) {clock.total} 38 | 39 | it "computes a sum total" do 40 | expect(total).to be == 1.5 41 | end 42 | end 43 | 44 | it "can accumulate time" do 45 | clock.start! 46 | total = clock.total 47 | expect(total).to be >= 0 48 | sleep(0.0001) 49 | expect(clock.total).to be >= total 50 | end 51 | end 52 | 53 | with ".start" do 54 | let(:clock) {subject.start} 55 | let(:total) {clock.total} 56 | 57 | it "computes a sum total" do 58 | expect(total).to be >= 0.0 59 | end 60 | end 61 | 62 | with "#reset!" do 63 | it "resets the total time" do 64 | clock.start! 65 | sleep(0.0001) 66 | clock.stop! 67 | expect(clock.total).to be > 0.0 68 | clock.reset! 69 | expect(clock.total).to be == 0.0 70 | end 71 | 72 | it "resets the start time" do 73 | clock.start! 74 | clock.reset! 75 | sleep(0.0001) 76 | expect(clock.total).to be > 0.0 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /test/async/condition.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017, by Kent Gruber. 5 | # Copyright, 2017-2024, by Samuel Williams. 6 | 7 | require "sus/fixtures/async" 8 | require "async/condition" 9 | 10 | require "async/a_condition" 11 | 12 | describe Async::Condition do 13 | include Sus::Fixtures::Async::ReactorContext 14 | 15 | let(:condition) {subject.new} 16 | 17 | it "should continue after condition is signalled" do 18 | task = reactor.async do 19 | condition.wait 20 | end 21 | 22 | expect(task).to be(:running?) 23 | 24 | # This will cause the task to exit: 25 | condition.signal 26 | 27 | expect(task).to be(:completed?) 28 | end 29 | 30 | it "can stop nested task" do 31 | producer = nil 32 | 33 | consumer = reactor.async do |task| 34 | condition = Async::Condition.new 35 | 36 | producer = task.async do |subtask| 37 | subtask.yield 38 | condition.signal 39 | sleep(10) 40 | end 41 | 42 | condition.wait 43 | expect do 44 | producer.stop 45 | end.not.to raise_exception 46 | end 47 | 48 | consumer.wait 49 | producer.wait 50 | 51 | expect(producer.status).to be == :stopped 52 | expect(consumer.status).to be == :completed 53 | end 54 | 55 | it_behaves_like Async::ACondition 56 | end 57 | -------------------------------------------------------------------------------- /test/async/idler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024, by Samuel Williams. 5 | 6 | require "async/idler" 7 | require "sus/fixtures/async" 8 | 9 | require "async/chainable_async" 10 | 11 | describe Async::Idler do 12 | include Sus::Fixtures::Async::ReactorContext 13 | let(:idler) {subject.new(0.5)} 14 | 15 | it "can schedule tasks up to the desired load" do 16 | expect(Fiber.scheduler.load).to be < 0.1 17 | 18 | # Generate the load: 19 | Async do 20 | while true 21 | idler.async do 22 | while true 23 | sleep 0.1 24 | end 25 | end 26 | end 27 | end 28 | 29 | # This test must be longer than the idle calculation window (1s)... 30 | sleep 1.1 31 | 32 | # Verify that the load is within the desired range: 33 | expect(Fiber.scheduler.load).to be_within(0.1).of(0.5) 34 | end 35 | 36 | it_behaves_like Async::ChainableAsync 37 | end 38 | -------------------------------------------------------------------------------- /test/async/limited_queue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2024, by Samuel Williams. 5 | # Copyright, 2019, by Ryan Musgrave. 6 | # Copyright, 2020-2022, by Bruno Sutic. 7 | 8 | require "async/limited_queue" 9 | 10 | require "sus/fixtures/async" 11 | require "async/a_queue" 12 | 13 | describe Async::LimitedQueue do 14 | include Sus::Fixtures::Async::ReactorContext 15 | 16 | it_behaves_like Async::AQueue 17 | 18 | let(:queue) {subject.new} 19 | 20 | it "should become limited" do 21 | expect(queue).not.to be(:limited?) 22 | queue.enqueue(10) 23 | expect(queue).to be(:limited?) 24 | end 25 | 26 | it "enqueues items up to a limit" do 27 | items = Array.new(2) { rand(10) } 28 | reactor.async do 29 | queue.enqueue(*items) 30 | end 31 | 32 | expect(queue.size).to be == 1 33 | expect(queue.dequeue).to be == items.first 34 | end 35 | 36 | it "should resume waiting tasks in order" do 37 | total_resumed = 0 38 | total_dequeued = 0 39 | 40 | Async do |producer| 41 | 10.times do 42 | producer.async do 43 | queue.enqueue("foo") 44 | total_resumed += 1 45 | end 46 | end 47 | end 48 | 49 | 10.times do 50 | item = queue.dequeue 51 | total_dequeued += 1 52 | 53 | expect(total_resumed).to be == total_dequeued 54 | end 55 | end 56 | 57 | with "#<<" do 58 | with "a limited queue" do 59 | def before 60 | queue << :item1 61 | expect(queue.size).to be == 1 62 | expect(queue).to be(:limited?) 63 | 64 | super 65 | end 66 | 67 | it "waits until a queue is dequeued" do 68 | reactor.async do 69 | queue << :item2 70 | end 71 | 72 | expect(queue.dequeue).to be == :item1 73 | expect(queue.dequeue).to be == :item2 74 | end 75 | 76 | with "#pop" do 77 | it "waits until a queue is dequeued" do 78 | reactor.async do 79 | queue << :item2 80 | end 81 | 82 | expect(queue.pop).to be == :item1 83 | expect(queue.pop).to be == :item2 84 | end 85 | end 86 | end 87 | end 88 | end 89 | 90 | -------------------------------------------------------------------------------- /test/async/list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2024, by Samuel Williams. 5 | 6 | require "async/list" 7 | 8 | class Item < Async::List::Node 9 | def initialize(value) 10 | super() 11 | @value = value 12 | end 13 | 14 | attr_accessor :value 15 | end 16 | 17 | describe Async::List do 18 | let(:list) {Async::List.new} 19 | 20 | with "#append" do 21 | it "can append items" do 22 | list.append(Item.new(1)) 23 | list.append(Item.new(2)) 24 | list.append(Item.new(3)) 25 | 26 | expect(list.each.map(&:value)).to be == [1, 2, 3] 27 | expect(list.to_a.map(&:value)).to be == [1, 2, 3] 28 | expect(list.to_s).to be =~ /size=3/ 29 | end 30 | 31 | it "can't append the same item twice" do 32 | item = Item.new(1) 33 | list.append(item) 34 | 35 | expect do 36 | list.append(item) 37 | end.to raise_exception(ArgumentError, message: be =~ /already in a list/) 38 | end 39 | end 40 | 41 | with "#prepend" do 42 | it "can prepend items" do 43 | list.prepend(Item.new(1)) 44 | list.prepend(Item.new(2)) 45 | list.prepend(Item.new(3)) 46 | 47 | expect(list.each.map(&:value)).to be == [3, 2, 1] 48 | end 49 | 50 | it "can't prepend the same item twice" do 51 | item = Item.new(1) 52 | list.prepend(item) 53 | 54 | expect do 55 | list.prepend(item) 56 | end.to raise_exception(ArgumentError, message: be =~ /already in a list/) 57 | end 58 | end 59 | 60 | with "#remove" do 61 | it "can remove items" do 62 | item = Item.new(1) 63 | 64 | list.append(item) 65 | list.remove(item) 66 | 67 | expect(list.each.map(&:value)).to be(:empty?) 68 | end 69 | 70 | it "can't remove an item twice" do 71 | item = Item.new(1) 72 | 73 | list.append(item) 74 | list.remove(item) 75 | 76 | expect do 77 | list.remove(item) 78 | end.to raise_exception(ArgumentError, message: be =~ /not in a list/) 79 | end 80 | 81 | it "can remove an item from the middle" do 82 | item = Item.new(1) 83 | 84 | list.append(Item.new(2)) 85 | list.append(item) 86 | list.append(Item.new(3)) 87 | 88 | list.remove(item) 89 | 90 | expect(list.each.map(&:value)).to be == [2, 3] 91 | end 92 | end 93 | 94 | with "#each" do 95 | it "can iterate over nodes while deleting them" do 96 | nodes = [Item.new(1), Item.new(2), Item.new(3)] 97 | nodes.each do |node| 98 | list.append(node) 99 | end 100 | 101 | enumerated = [] 102 | 103 | index = 0 104 | list.each do |node| 105 | enumerated << node 106 | 107 | # This tests that enumeration is tolerant of deletion: 108 | if index == 1 109 | # When we are indexing child 1, it means the current node is child 0 - deleting it shouldn't break enumeration: 110 | list.remove(nodes.first) 111 | end 112 | 113 | index += 1 114 | end 115 | 116 | expect(enumerated).to be == nodes 117 | end 118 | 119 | it "can get #first and #last while enumerating" do 120 | list.append(first = Item.new(1)) 121 | list.append(last = Item.new(2)) 122 | 123 | list.each do |item| 124 | if item.equal?(last) 125 | # This ensures the last node in the list is an iterator: 126 | list.remove(last) 127 | expect(list.last).to be == first 128 | end 129 | end 130 | end 131 | end 132 | 133 | with "#first" do 134 | it "returns nil for an empty list" do 135 | expect(list.first).to be_nil 136 | end 137 | 138 | it "can return the first item" do 139 | item = Item.new(1) 140 | 141 | list.append(item) 142 | list.append(Item.new(2)) 143 | list.append(Item.new(3)) 144 | 145 | expect(list.first).to be == item 146 | end 147 | end 148 | 149 | with "#last" do 150 | it "returns nil for an empty list" do 151 | expect(list.last).to be_nil 152 | end 153 | 154 | it "can return the last item" do 155 | item = Item.new(1) 156 | 157 | list.append(Item.new(2)) 158 | list.append(Item.new(3)) 159 | list.append(item) 160 | 161 | expect(list.last).to be == item 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /test/async/notification.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2024, by Samuel Williams. 5 | 6 | require "sus/fixtures/async" 7 | require "async/notification" 8 | 9 | require "async/a_condition" 10 | 11 | describe Async::Notification do 12 | include Sus::Fixtures::Async::ReactorContext 13 | 14 | let(:notification) {subject.new} 15 | 16 | it "should continue after notification is signalled" do 17 | sequence = [] 18 | 19 | task = reactor.async do 20 | sequence << :waiting 21 | notification.wait 22 | sequence << :resumed 23 | end 24 | 25 | expect(task.status).to be == :running 26 | 27 | sequence << :running 28 | # This will cause the task to exit: 29 | notification.signal 30 | sequence << :signalled 31 | 32 | expect(task.status).to be == :running 33 | 34 | sequence << :yielding 35 | reactor.yield 36 | sequence << :finished 37 | 38 | expect(task.status).to be == :completed 39 | 40 | expect(sequence).to be == [ 41 | :waiting, 42 | :running, 43 | :signalled, 44 | :yielding, 45 | :resumed, 46 | :finished 47 | ] 48 | end 49 | 50 | it_behaves_like Async::ACondition 51 | end 52 | -------------------------------------------------------------------------------- /test/async/queue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2024, by Samuel Williams. 5 | # Copyright, 2019, by Ryan Musgrave. 6 | # Copyright, 2020-2022, by Bruno Sutic. 7 | 8 | require "async/queue" 9 | 10 | require "sus/fixtures/async" 11 | require "async/a_queue" 12 | 13 | describe Async::Queue do 14 | include Sus::Fixtures::Async::ReactorContext 15 | 16 | it_behaves_like Async::AQueue 17 | end 18 | -------------------------------------------------------------------------------- /test/async/reactor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2024, by Samuel Williams. 5 | # Copyright, 2017, by Devin Christensen. 6 | 7 | require "async" 8 | require "async/variable" 9 | require "sus/fixtures/async" 10 | 11 | describe Async::Reactor do 12 | let(:reactor) {subject.new} 13 | 14 | after do 15 | Fiber.set_scheduler(nil) 16 | end 17 | 18 | with "#run" do 19 | it "can run tasks on different fibers" do 20 | outer_fiber = Fiber.current 21 | inner_fiber = nil 22 | 23 | subject.run do |task| 24 | sleep(0) 25 | inner_fiber = Fiber.current 26 | end 27 | 28 | expect(inner_fiber).not.to be_nil 29 | expect(outer_fiber).not.to be == inner_fiber 30 | end 31 | end 32 | 33 | with "#close" do 34 | it "can close empty reactor" do 35 | reactor.close 36 | 37 | expect(reactor).to be(:closed?) 38 | end 39 | 40 | it "terminates transient tasks" do 41 | task = reactor.async(transient: true) do 42 | sleep 43 | ensure 44 | sleep 45 | end 46 | 47 | expect(reactor).to be(:finished?) 48 | expect(reactor.run_once(0)).to be == true 49 | 50 | reactor.close 51 | end 52 | 53 | it "terminates transient tasks with nested tasks" do 54 | task = reactor.async(transient: true) do |parent| 55 | parent.async do |child| 56 | sleep(1) 57 | end 58 | end 59 | 60 | reactor.run_once 61 | expect(reactor).to be(:finished?) 62 | reactor.close 63 | end 64 | 65 | it "terminates nested tasks" do 66 | top = reactor.async do |parent| 67 | parent.async do |child| 68 | sleep(1) 69 | end 70 | end 71 | 72 | reactor.run_once 73 | reactor.close 74 | end 75 | end 76 | 77 | with "#run" do 78 | it "can run the reactor" do 79 | # Run the reactor for 1 second: 80 | task = reactor.async do |task| 81 | task.yield 82 | end 83 | 84 | expect(task).to be(:running?) 85 | 86 | # This will resume the task, and then the reactor will be finished. 87 | reactor.run 88 | 89 | expect(task).to be(:finished?) 90 | end 91 | 92 | it "can run one iteration" do 93 | state = :started 94 | 95 | reactor.async do |task| 96 | task.yield 97 | state = :finished 98 | end 99 | 100 | expect(state).to be == :started 101 | 102 | reactor.run 103 | 104 | expect(state).to be == :finished 105 | end 106 | end 107 | 108 | with "#print_hierarchy" do 109 | it "can print hierarchy" do 110 | reactor.async do |parent| 111 | parent.async do |child| 112 | child.yield 113 | end 114 | 115 | output = StringIO.new 116 | reactor.print_hierarchy(output, backtrace: false) 117 | lines = output.string.lines 118 | 119 | expect(lines[0]).to be =~ /#= 0 10 | expect(timeout.duration).to (be > 0).and(be <= 1) 11 | end 12 | end 13 | 14 | with "#now" do 15 | it "can get the current time" do 16 | scheduler.with_timeout(1) do |timeout| 17 | expect(timeout.now).to be >= 0 18 | expect(timeout.now).to be <= timeout.time 19 | end 20 | end 21 | end 22 | 23 | with "#adjust" do 24 | it "can adjust the timeout" do 25 | scheduler.with_timeout(1) do |timeout| 26 | timeout.adjust(1) 27 | expect(timeout.duration).to (be > 1).and(be <= 2) 28 | end 29 | end 30 | end 31 | 32 | with "#duration=" do 33 | it "can set the timeout duration" do 34 | scheduler.with_timeout(1) do |timeout| 35 | timeout.duration = 2 36 | expect(timeout.duration).to (be > 1).and(be <= 2) 37 | end 38 | end 39 | 40 | it "can increase the timeout duration" do 41 | scheduler.with_timeout(1) do |timeout| 42 | timeout.duration += 2 43 | expect(timeout.duration).to (be > 2).and(be <= 3) 44 | end 45 | end 46 | end 47 | 48 | with "#time=" do 49 | it "can set the timeout time" do 50 | scheduler.with_timeout(1) do |timeout| 51 | timeout.time = timeout.time + 1 52 | expect(timeout.duration).to (be > 1).and(be < 2) 53 | end 54 | end 55 | end 56 | 57 | with "#cancel!" do 58 | it "can cancel the timeout" do 59 | scheduler.with_timeout(1) do |timeout| 60 | timeout.cancel! 61 | expect(timeout).to be(:cancelled?) 62 | end 63 | end 64 | 65 | it "can't reschedule a cancelled timeout" do 66 | scheduler.with_timeout(1) do |timeout| 67 | timeout.cancel! 68 | expect do 69 | timeout.adjust(1) 70 | end.to raise_exception(Async::Timeout::CancelledError) 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /test/async/variable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2024, by Samuel Williams. 5 | 6 | require "sus/fixtures/async" 7 | require "async/variable" 8 | 9 | VariableContext = Sus::Shared("a variable") do 10 | let(:variable) {Async::Variable.new} 11 | 12 | it "can resolve the value" do 13 | variable.resolve(value) 14 | expect(variable).to be(:resolved?) 15 | end 16 | 17 | it "can wait for the value to be resolved" do 18 | Async do 19 | expect(variable.wait).to be == value 20 | end 21 | 22 | variable.resolve(value) 23 | end 24 | 25 | it "can't resolve it a 2nd time" do 26 | variable.resolve(value) 27 | expect do 28 | variable.resolve(value) 29 | end.to raise_exception(FrozenError) 30 | end 31 | end 32 | 33 | include Sus::Fixtures::Async::ReactorContext 34 | 35 | describe true do 36 | let(:value) {subject} 37 | it_behaves_like VariableContext 38 | end 39 | 40 | describe false do 41 | let(:value) {subject} 42 | it_behaves_like VariableContext 43 | end 44 | 45 | describe nil do 46 | let(:value) {subject} 47 | it_behaves_like VariableContext 48 | end 49 | 50 | describe Object do 51 | let(:value) {subject.new} 52 | it_behaves_like VariableContext 53 | end 54 | -------------------------------------------------------------------------------- /test/async/waiter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2024, by Samuel Williams. 5 | # Copyright, 2024, by Patrik Wenger. 6 | 7 | require "async/waiter" 8 | require "sus/fixtures/async" 9 | 10 | describe Async::Waiter do 11 | include Sus::Fixtures::Async::ReactorContext 12 | 13 | let(:waiter) {subject.new} 14 | 15 | it "can wait for the first task to complete" do 16 | waiter.async do 17 | :result 18 | end 19 | 20 | expect(waiter.wait).to be == :result 21 | end 22 | 23 | it "can wait for a subset of tasks" do 24 | 3.times do 25 | waiter.async do 26 | sleep(rand * 0.01) 27 | end 28 | end 29 | 30 | done = waiter.wait(2) 31 | expect(done.size).to be == 2 32 | 33 | done = waiter.wait(1) 34 | expect(done.size).to be == 1 35 | end 36 | 37 | it "can wait for tasks even when exceptions occur" do 38 | waiter.async do |task| 39 | expect(task).to receive(:warn).with_options(have_keys( 40 | exception: be_a(RuntimeError), 41 | )).and_return(nil) 42 | 43 | raise "Something went wrong" 44 | end 45 | 46 | expect do 47 | waiter.wait 48 | end.to raise_exception(RuntimeError) 49 | end 50 | 51 | with "barrier parent" do 52 | let(:barrier) { Async::Barrier.new } 53 | let(:waiter) { subject.new(parent: barrier) } 54 | 55 | it "passes annotation to barrier" do 56 | expect(barrier).to receive(:async).with(annotation: "waited upon task") 57 | waiter.async(annotation: "waited upon task") { } 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/async/worker_pool.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2024, by Samuel Williams. 5 | # Copyright, 2024, by Patrik Wenger. 6 | 7 | require "async/worker_pool" 8 | require "sus/fixtures/async" 9 | 10 | describe Async::WorkerPool do 11 | let(:worker_pool) {subject.new(size: 1)} 12 | 13 | it "offloads work to a thread" do 14 | result = worker_pool.call(proc do 15 | Thread.current 16 | end) 17 | 18 | expect(result).not.to be == Thread.current 19 | end 20 | 21 | it "gracefully handles errors" do 22 | expect do 23 | worker_pool.call(proc do 24 | raise ArgumentError, "Oops!" 25 | end) 26 | end.to raise_exception(ArgumentError, message: be == "Oops!") 27 | end 28 | 29 | it "can cancel work" do 30 | sleeping = ::Thread::Queue.new 31 | 32 | thread = Thread.new do 33 | Thread.current.report_on_exception = false 34 | 35 | worker_pool.call(proc do 36 | sleeping.push(true) 37 | sleep(1) 38 | end) 39 | end 40 | 41 | # Wait for the worker to start: 42 | sleeping.pop 43 | 44 | thread.raise(Interrupt) 45 | 46 | expect do 47 | thread.join 48 | end.to raise_exception(Interrupt) 49 | end 50 | 51 | with "#close" do 52 | it "can be closed" do 53 | worker_pool.close 54 | 55 | expect do 56 | worker_pool.call(proc{}) 57 | end.to raise_exception(RuntimeError) 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/enumerator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2024, by Samuel Williams. 5 | 6 | require "async" 7 | 8 | describe Enumerator do 9 | def some_yielder(task) 10 | yield 1 11 | sleep(0.002) 12 | yield 2 13 | end 14 | 15 | def enum(task) 16 | to_enum(:some_yielder, task) 17 | end 18 | 19 | it "should play well with Enumerator as internal iterator" do 20 | # no fiber really used in internal iterator, 21 | # but let this test be here for completness 22 | result = nil 23 | 24 | Async do |task| 25 | result = enum(task).to_a 26 | end 27 | 28 | expect(result).to be == [1, 2] 29 | end 30 | 31 | it "should play well with Enumerator as external iterator" do 32 | result = [] 33 | 34 | Async do |task| 35 | enumerator = enum(task) 36 | result << enumerator.next 37 | result << enumerator.next 38 | result << begin enumerator.next rescue $! end 39 | end 40 | 41 | expect(result[0]).to be == 1 42 | expect(result[1]).to be == 2 43 | expect(result[2]).to be_a StopIteration 44 | end 45 | 46 | it "should play well with Enumerator.zip(Enumerator) method" do 47 | Async do |task| 48 | result = [:a, :b, :c, :d].each.zip(enum(task)) 49 | expect(result).to be == [[:a, 1], [:b, 2], [:c, nil], [:d, nil]] 50 | end 51 | end 52 | 53 | it "should play well with explicit Fiber usage" do 54 | result = [] 55 | 56 | Async do |task| 57 | fiber = Fiber.new do 58 | Fiber.yield 1 59 | sleep(0.002) 60 | Fiber.yield 2 61 | end 62 | 63 | result << fiber.resume 64 | result << fiber.resume 65 | result << fiber.resume 66 | end 67 | 68 | expect(result[0]).to be == 1 69 | expect(result[1]).to be == 2 70 | expect(result[2]).to be == nil 71 | end 72 | 73 | it "can stop lazy enumerator" do 74 | # This test will hang on older Rubies without the bug fix: 75 | skip_unless_minimum_ruby_version("3.3.4") 76 | 77 | enumerator = Enumerator.new do |yielder| 78 | yielder.yield 1 79 | sleep 80 | yielder.yield 2 81 | end 82 | 83 | Sync do |task| 84 | child_task = task.async do 85 | enumerator.next 86 | enumerator.next 87 | end 88 | 89 | child_task.stop 90 | 91 | expect(child_task).to be(:stopped?) 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /test/fiber.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2024, by Samuel Williams. 5 | 6 | require "async" 7 | require "async/variable" 8 | 9 | describe Fiber do 10 | with ".new" do 11 | it "can stop a task with a nested resumed fiber" do 12 | skip_unless_minimum_ruby_version("3.3.4") 13 | 14 | variable = Async::Variable.new 15 | error = nil 16 | 17 | Sync do |task| 18 | child_task = task.async do 19 | Fiber.new do 20 | # Wait here... 21 | variable.value 22 | rescue Async::Stop => error 23 | # This is expected. 24 | raise 25 | end.resume 26 | end 27 | 28 | child_task.stop 29 | expect(child_task).to be(:stopped?) 30 | end 31 | 32 | expect(error).to be_a(Async::Stop) 33 | end 34 | 35 | it "can nest child tasks within a resumed fiber" do 36 | skip_unless_minimum_ruby_version("3.3.4") 37 | 38 | variable = Async::Variable.new 39 | error = nil 40 | 41 | Sync do |task| 42 | child_task = task.async do 43 | Fiber.new do 44 | Async do 45 | variable.value 46 | end.wait 47 | end.resume 48 | end 49 | 50 | expect(child_task).to be(:running?) 51 | 52 | variable.value = true 53 | end 54 | end 55 | end 56 | 57 | with ".schedule" do 58 | it "can create several tasks" do 59 | sequence = [] 60 | 61 | Thread.new do 62 | scheduler = Async::Scheduler.new 63 | Fiber.set_scheduler(scheduler) 64 | 65 | Fiber.schedule do 66 | 3.times do |i| 67 | Fiber.schedule do 68 | sleep (i / 1000.0) 69 | sequence << i 70 | end 71 | end 72 | end 73 | end.join 74 | 75 | expect(sequence).to be == [0, 1, 2] 76 | end 77 | 78 | def spawn_child_ruby(code) 79 | lib_path = File.expand_path("../lib", __dir__) 80 | 81 | IO.popen(["ruby", "-I#{lib_path}"], "r+", err: [:child, :out]) do |process| 82 | process.write(code) 83 | process.close_write 84 | 85 | return process.read 86 | end 87 | end 88 | 89 | it "correctly handles exceptions in process" do 90 | buffer = spawn_child_ruby(<<~RUBY) 91 | require 'async' 92 | 93 | scheduler = Async::Scheduler.new 94 | Fiber.set_scheduler(scheduler) 95 | 96 | Fiber.schedule do 97 | sleep(1) 98 | puts "Finished sleeping!" 99 | end 100 | 101 | raise "Boom!" 102 | RUBY 103 | 104 | expect(buffer).to be(:include?, "Boom!") 105 | expect(buffer).not.to be(:include?, "Finished sleeping!") 106 | end 107 | 108 | it "correctly handles exceptions" do 109 | finished_sleeping = nil 110 | 111 | thread = Thread.new do 112 | # Stop the thread logging on exception: 113 | Thread.current.report_on_exception = false 114 | 115 | scheduler = Async::Scheduler.new 116 | Fiber.set_scheduler(scheduler) 117 | 118 | finished_sleeping = false 119 | 120 | Fiber.schedule do 121 | sleep(10) 122 | finished_sleeping = true 123 | end 124 | 125 | raise "Boom!" 126 | end 127 | 128 | expect{thread.join}.to raise_exception(RuntimeError, 129 | message: be == "Boom!" 130 | ) 131 | 132 | expect(finished_sleeping).to be == false 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /test/io.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2024, by Samuel Williams. 5 | 6 | require "sus/fixtures/async" 7 | 8 | describe IO do 9 | include Sus::Fixtures::Async::ReactorContext 10 | 11 | describe ".pipe" do 12 | let(:message) {"Helloooooo World!"} 13 | 14 | it "can send message via pipe" do 15 | input, output = IO.pipe 16 | 17 | reactor.async do 18 | sleep(0.001) 19 | 20 | message.each_char do |character| 21 | output.write(character) 22 | end 23 | 24 | output.close 25 | end 26 | 27 | expect(input.read).to be == message 28 | 29 | ensure 30 | input.close 31 | output.close 32 | end 33 | 34 | it "can read with timeout" do 35 | skip_unless_constant_defined(:TimeoutError, IO) 36 | 37 | input, output = IO.pipe 38 | input.timeout = 0.001 39 | 40 | expect do 41 | line = input.gets 42 | end.to raise_exception(::IO::TimeoutError) 43 | end 44 | 45 | it "can write with timeout" do 46 | skip_unless_constant_defined(:TimeoutError, IO) 47 | 48 | big = "x" * 1024 * 1024 49 | 50 | input, output = IO.pipe 51 | output.timeout = 0.001 52 | 53 | expect do 54 | while true 55 | output.write(big) 56 | end 57 | end.to raise_exception(::IO::TimeoutError) 58 | end 59 | 60 | it "can wait readable with default timeout" do 61 | skip_unless_constant_defined(:TimeoutError, IO) 62 | 63 | input, output = IO.pipe 64 | input.timeout = 0.001 65 | 66 | expect do 67 | # This behaviour is not consistent with non-fiber scheduler IO. 68 | # However, this is the best we can do without fixing CRuby. 69 | input.wait_readable 70 | end.to raise_exception(::IO::TimeoutError) 71 | end 72 | 73 | it "can wait readable with explicit timeout" do 74 | input, output = IO.pipe 75 | 76 | expect(input.wait_readable(0)).to be_nil 77 | end 78 | end 79 | 80 | describe "/dev/null" do 81 | # Ruby < 3.3.1 will fail this test with the `io_write` scheduler hook enabled, as it will try to io_wait on /dev/null which will fail on some platforms (kqueue). 82 | it "can write to /dev/null" do 83 | out = File.open("/dev/null", "w") 84 | 85 | # Needs to write about 8,192 bytes to trigger the internal flush: 86 | 1000.times do 87 | out.puts "Hello World!" 88 | end 89 | ensure 90 | out.close 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /test/io/buffer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024, by Samuel Williams. 5 | 6 | describe IO::Buffer do 7 | it "can copy a large buffer (releasing the GVL)" do 8 | source = IO::Buffer.new(1024 * 1024 * 10) 9 | destination = IO::Buffer.new(source.size) 10 | 11 | source.copy(destination) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/kernel.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2024, by Samuel Williams. 5 | 6 | require "sus/fixtures/async" 7 | 8 | describe Kernel do 9 | include Sus::Fixtures::Async::ReactorContext 10 | 11 | with "#sleep" do 12 | it "can intercept sleep" do 13 | expect(reactor).to receive(:kernel_sleep).with(0.001) 14 | 15 | sleep(0.001) 16 | end 17 | end 18 | 19 | with "#system" do 20 | it "can execute child process" do 21 | expect(reactor).to receive(:process_wait) 22 | 23 | result = ::Kernel.system("true") 24 | 25 | expect(result).to be == true 26 | expect($?).to be(:success?) 27 | end 28 | 29 | it "can fail to execute child process" do 30 | expect(reactor).to receive(:process_wait) 31 | 32 | result = ::Kernel.system("does-not-exist") 33 | 34 | expect(result).to be == nil 35 | expect($?).not.to be(:success?) 36 | end 37 | end 38 | 39 | with "#`" do 40 | it "can execute child process and capture output" do 41 | expect(`echo OK`).to be == "OK\n" 42 | expect($?).to be(:success?) 43 | end 44 | 45 | it "can execute child process with delay and capture output" do 46 | expect(`sleep 0.01; echo OK`).to be == "OK\n" 47 | expect($?).to be(:success?) 48 | end 49 | 50 | it "can echo several times" do 51 | 10.times do 52 | expect(`echo test`).to be == "test\n" 53 | expect($?).to be(:success?) 54 | expect($?.inspect).to be =~ /exit 0/ 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/kernel/async.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019-2024, by Samuel Williams. 5 | # Copyright, 2024, by Patrik Wenger. 6 | 7 | require "kernel/async" 8 | 9 | describe Kernel do 10 | describe ".Async" do 11 | it "can run an asynchronous task" do 12 | Async do |task| 13 | expect(task).to be_a Async::Task 14 | end 15 | end 16 | 17 | it "passes transient: options through to initial task" do 18 | Async(transient: true) do |task| 19 | expect(task).to be(:transient?) 20 | end 21 | end 22 | 23 | it "passes annotation: option through to initial task" do 24 | Async(annotation: "foobar") do |task| 25 | expect(task.annotation).to be == "foobar" 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/kernel/sync.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2019-2024, by Samuel Williams. 5 | # Copyright, 2020, by Brian Morearty. 6 | # Copyright, 2024, by Patrik Wenger. 7 | 8 | require "kernel/async" 9 | require "kernel/sync" 10 | 11 | describe Kernel do 12 | with "#Sync" do 13 | let(:value) {10} 14 | 15 | it "can run a synchronous task" do 16 | result = Sync do |task| 17 | expect(Async::Task.current).not.to be == nil 18 | expect(Async::Task.current).to be == task 19 | 20 | next value 21 | end 22 | 23 | expect(result).to be == value 24 | end 25 | 26 | it "passes annotation through to initial task" do 27 | Sync(annotation: "foobar") do |task| 28 | expect(task.annotation).to be == "foobar" 29 | end 30 | end 31 | 32 | it "can run inside reactor" do 33 | Async do |task| 34 | result = Sync do |sync_task| 35 | expect(Async::Task.current).to be == task 36 | expect(sync_task).to be == task 37 | 38 | next value 39 | end 40 | 41 | expect(result).to be == value 42 | end 43 | end 44 | 45 | with "parent task" do 46 | it "replaces and restores existing task's annotation" do 47 | annotations = [] 48 | 49 | Async(annotation: "foo") do |t1| 50 | annotations << t1.annotation 51 | 52 | Sync(annotation: "bar") do |t2| 53 | expect(t2).to be_equal(t1) 54 | annotations << t1.annotation 55 | end 56 | 57 | annotations << t1.annotation 58 | end.wait 59 | 60 | expect(annotations).to be == %w[foo bar foo] 61 | end 62 | end 63 | 64 | 65 | it "can propagate error without logging them" do 66 | expect do 67 | Sync do |task| 68 | expect(task).not.to receive(:warn) 69 | 70 | raise StandardError, "brain not provided" 71 | end 72 | end.to raise_exception(StandardError, message: be =~ /brain/) 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/net/http.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2024, by Samuel Williams. 5 | 6 | require "sus/fixtures/async" 7 | require "net/http" 8 | require "async/barrier" 9 | require "openssl" 10 | 11 | describe Net::HTTP do 12 | include Sus::Fixtures::Async::ReactorContext 13 | let(:timeout) {10} 14 | 15 | it "can make several concurrent requests" do 16 | barrier = Async::Barrier.new 17 | events = [] 18 | 19 | 3.times do |i| 20 | barrier.async do 21 | events << i 22 | response = Net::HTTP.get(URI "https://github.com/") 23 | expect(response).not.to be == nil 24 | events << i 25 | end 26 | end 27 | 28 | barrier.wait 29 | 30 | # The requests all get started concurrently: 31 | expect(events.first(3)).to be == [0, 1, 2] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/process.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2024, by Samuel Williams. 5 | 6 | require "sus/fixtures/async" 7 | 8 | describe Process do 9 | include Sus::Fixtures::Async::ReactorContext 10 | 11 | describe ".wait2" do 12 | it "can wait on child process" do 13 | expect(reactor).to receive(:process_wait) 14 | 15 | pid = ::Process.spawn("true") 16 | _, status = Process.wait2(pid) 17 | expect(status).to be(:success?) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/tempfile.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2024, by Samuel Williams. 5 | 6 | require "tempfile" 7 | require "sus/fixtures/async" 8 | 9 | describe Tempfile do 10 | include Sus::Fixtures::Async::ReactorContext 11 | 12 | it "should be able to read and write" do 13 | tempfile = Tempfile.new 14 | 15 | 1_000.times{tempfile.write("Hello World!")} 16 | tempfile.flush 17 | 18 | tempfile.seek(0) 19 | 20 | expect(tempfile.read(12)).to be == "Hello World!" 21 | ensure 22 | tempfile.close 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/thread.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2024, by Samuel Williams. 5 | 6 | require "thread" 7 | require "sus/fixtures/async" 8 | 9 | describe Thread do 10 | include Sus::Fixtures::Async::ReactorContext 11 | 12 | it "can join thread" do 13 | queue = Thread::Queue.new 14 | thread = Thread.new{queue.pop} 15 | 16 | waiting = 0 17 | 18 | 3.times do 19 | Async do 20 | waiting += 1 21 | thread.join 22 | waiting -= 1 23 | end 24 | end 25 | 26 | expect(waiting).to be == 3 27 | queue.close 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/thread/queue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2024, by Samuel Williams. 5 | 6 | require "sus/fixtures/async" 7 | require "thread" 8 | 9 | describe Thread::Queue do 10 | include Sus::Fixtures::Async::ReactorContext 11 | 12 | let(:item) {"Hello World"} 13 | 14 | it "can pass items between thread and fiber" do 15 | queue = Thread::Queue.new 16 | 17 | Async do 18 | expect(queue.pop).to be == item 19 | end 20 | 21 | ::Thread.new do 22 | expect(Fiber).to be(:blocking?) 23 | queue.push(item) 24 | end.join 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/traces/provider/async/task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2025, by Samuel Williams. 5 | 6 | require "traces" 7 | return unless Traces.enabled? 8 | 9 | require "async" 10 | require "traces/provider/async/task" 11 | 12 | describe Async::Task do 13 | it "traces tasks within active tracing" do 14 | context = nil 15 | 16 | Thread.new do 17 | Traces.trace("test") do 18 | Async do 19 | context = Traces.trace_context 20 | end 21 | end 22 | end.join 23 | 24 | expect(context).not.to be == nil 25 | end 26 | 27 | it "doesn't trace tasks outside of active tracing" do 28 | expect(Traces).to receive(:active?).and_return(false) 29 | 30 | context = nil 31 | 32 | Async do 33 | context = Traces.trace_context 34 | end 35 | 36 | expect(context).to be == nil 37 | end 38 | end 39 | --------------------------------------------------------------------------------