pax_global_header00006660000000000000000000000064141004074020014502gustar00rootroot0000000000000052 comment=7406e14151ab745323359af4647c96478b9b8cc1 ruby-async-1.30.1/000077500000000000000000000000001410040740200136605ustar00rootroot00000000000000ruby-async-1.30.1/.editorconfig000066400000000000000000000000641410040740200163350ustar00rootroot00000000000000root = true [*] indent_style = tab indent_size = 2 ruby-async-1.30.1/.github/000077500000000000000000000000001410040740200152205ustar00rootroot00000000000000ruby-async-1.30.1/.github/workflows/000077500000000000000000000000001410040740200172555ustar00rootroot00000000000000ruby-async-1.30.1/.github/workflows/development.yml000066400000000000000000000023241410040740200223230ustar00rootroot00000000000000name: Development on: [push, pull_request] jobs: test: runs-on: ${{matrix.os}}-latest continue-on-error: ${{matrix.experimental}} strategy: matrix: os: - ubuntu - macos ruby: - 2.5 - 2.6 - 2.7 - 3.0 experimental: [false] env: [""] include: - os: ubuntu ruby: truffleruby experimental: true - os: ubuntu ruby: jruby env: JRUBY_OPTS="--debug -X+O" experimental: true - os: ubuntu ruby: head experimental: true - os: ubuntu ruby: 2.6 env: COVERAGE=PartialSummary,Coveralls experimental: true steps: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Run tests timeout-minutes: 5 run: ${{matrix.env}} bundle exec rspec - name: Run external tests timeout-minutes: 5 if: matrix.experimental == false && matrix.os == 'ubuntu' run: ${{matrix.env}} bundle exec bake external ruby-async-1.30.1/.github/workflows/documentation.yml000066400000000000000000000011731410040740200226530ustar00rootroot00000000000000name: Documentation on: push: branches: - master jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 env: BUNDLE_WITH: maintenance with: ruby-version: 2.7 bundler-cache: true - name: Installing packages run: sudo apt-get install wget - name: Generate documentation timeout-minutes: 5 run: bundle exec bake utopia:project:static - name: Deploy documentation uses: JamesIves/github-pages-deploy-action@4.0.0 with: branch: gh-pages folder: docs ruby-async-1.30.1/.gitignore000066400000000000000000000002031410040740200156430ustar00rootroot00000000000000.tags /.bundle/ /.yardoc /gems.locked /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ /external/ .rspec_status .covered.db ruby-async-1.30.1/.rspec000066400000000000000000000000671410040740200150000ustar00rootroot00000000000000--format documentation --warnings --require spec_helperruby-async-1.30.1/README.md000066400000000000000000000065241410040740200151460ustar00rootroot00000000000000# ![Async](logo.svg) Async is a composable asynchronous I/O framework for Ruby based on [nio4r](https://github.com/socketry/nio4r) and [timers](https://github.com/socketry/timers). > "Lately I've been looking into `async`, as one of my projects – [tus-ruby-server](https://github.com/janko/tus-ruby-server) – would really benefit from non-blocking I/O. It's really beautifully designed." *– [janko](https://github.com/janko)* [![Development Status](https://github.com/socketry/async/workflows/Development/badge.svg)](https://github.com/socketry/async/actions?workflow=Development) ## Features - Scalable event-driven I/O for Ruby. Thousands of clients per process\! - Light weight fiber-based concurrency. No need for callbacks\! - Multi-thread/process containers for parallelism. - Growing eco-system of event-driven components. ## Usage Please see the [project documentation](https://socketry.github.io/async). ## Contributing We welcome contributions to this project. 1. Fork it. 2. Create your feature branch (`git checkout -b my-new-feature`). 3. Commit your changes (`git commit -am 'Add some feature'`). 4. Push to the branch (`git push origin my-new-feature`). 5. Create new Pull Request. ## See Also - [async-io](https://github.com/socketry/async-io) — Asynchronous networking and sockets. - [async-http](https://github.com/socketry/async-http) — Asynchronous HTTP client/server. - [async-process](https://github.com/socketry/async-process) — Asynchronous process spawning/waiting. - [async-websocket](https://github.com/socketry/async-websocket) — Asynchronous client and server websockets. - [async-dns](https://github.com/socketry/async-dns) — Asynchronous DNS resolver and server. - [async-rspec](https://github.com/socketry/async-rspec) — Shared contexts for running async specs. ### Projects Using Async - [ciri](https://github.com/ciri-ethereum/ciri) — An Ethereum implementation written in Ruby. - [falcon](https://github.com/socketry/falcon) — A rack compatible server built on top of `async-http`. - [rubydns](https://github.com/ioquatix/rubydns) — A easy to use Ruby DNS server. - [slack-ruby-bot](https://github.com/slack-ruby/slack-ruby-bot) — A client for making slack bots. ## License Released under the MIT license. Copyright, 2017, by [Samuel G. D. Williams](http://www.codeotaku.com). Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ruby-async-1.30.1/async.gemspec000066400000000000000000000014771410040740200163530ustar00rootroot00000000000000 require_relative "lib/async/version" Gem::Specification.new do |spec| spec.name = "async" spec.version = Async::VERSION spec.summary = "A concurrency framework for Ruby." spec.authors = ["Samuel Williams"] spec.license = "MIT" spec.homepage = "https://github.com/socketry/async" spec.files = Dir.glob('{lib}/**/*', File::FNM_DOTMATCH, base: __dir__) spec.required_ruby_version = ">= 2.5.0" spec.add_dependency "console", "~> 1.10" spec.add_dependency "nio4r", "~> 2.3" spec.add_dependency "timers", "~> 4.1" spec.add_development_dependency "async-rspec", "~> 1.1" spec.add_development_dependency "bake" spec.add_development_dependency "benchmark-ips" spec.add_development_dependency "bundler" spec.add_development_dependency "covered", "~> 0.10" spec.add_development_dependency "rspec", "~> 3.6" end ruby-async-1.30.1/bake.rb000066400000000000000000000017321410040740200151120ustar00rootroot00000000000000# frozen_string_literal: true def external require 'bundler' Bundler.with_clean_env do clone_and_test("async-io") clone_and_test("async-pool") clone_and_test("async-websocket") clone_and_test("async-dns") clone_and_test("async-http") clone_and_test("falcon") clone_and_test("async-rest") end end private def clone_and_test(name) require 'fileutils' path = "external/#{name}" FileUtils.rm_rf path FileUtils.mkdir_p path system("git clone https://git@github.com/socketry/#{name} #{path}") # I tried using `bundle config --local local.async ../` but it simply doesn't work. # system("bundle", "config", "--local", "local.async", __dir__, chdir: path) gemfile_paths = ["#{path}/Gemfile", "#{path}/gems.rb"] gemfile_path = gemfile_paths.find{|path| File.exist?(path)} File.open(gemfile_path, "a") do |file| file.puts('gem "async", path: "../../"') end system("cd #{path} && bundle install && bundle exec rspec") or abort("Tests failed!") end ruby-async-1.30.1/benchmark/000077500000000000000000000000001410040740200156125ustar00rootroot00000000000000ruby-async-1.30.1/benchmark/async_vs_lightio.rb000077500000000000000000000044031410040740200215070ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require 'async' require 'lightio' require 'benchmark/ips' # # It's hard to know exactly how to interpret these results. When running parallel # instances, resource contention is more likely to be a problem, and yet with # async, the performance between a single task and several tasks is roughly the # same, while in the case of lightio, there is an obvious performance gap. # # The main takeaway is that contention causes issues and if systems are not # designed with that in mind, it will impact performance. # # $ ruby async_vs_lightio.rb # Warming up -------------------------------------- # lightio (synchronous) # 2.439k i/100ms # async (synchronous) 2.115k i/100ms # lightio (parallel) 211.000 i/100ms # async (parallel) 449.000 i/100ms # Calculating ------------------------------------- # lightio (synchronous) # 64.502k (± 3.9%) i/s - 643.896k in 10.002151s # async (synchronous) 161.195k (± 1.6%) i/s - 1.612M in 10.000976s # lightio (parallel) 49.827k (±17.5%) i/s - 477.704k in 9.999579s # async (parallel) 166.862k (± 6.2%) i/s - 1.662M in 10.000365s # # Comparison: # async (parallel): 166862.3 i/s # async (synchronous): 161194.6 i/s - same-ish: difference falls within error # lightio (synchronous): 64502.5 i/s - 2.59x slower # lightio (parallel): 49827.3 i/s - 3.35x slower DURATION = 0.000001 def run_async(count, repeats = 10000) Async::Reactor.run do |task| count.times.map do task.async do |subtask| repeats.times do subtask.sleep(DURATION) end end end.each(&:wait) end end def run_lightio(count, repeats = 10000) count.times.map do LightIO::Beam.new do repeats.times do LightIO.sleep(DURATION) end end end.each(&:join) end Benchmark.ips do |benchmark| benchmark.time = 10 benchmark.warmup = 2 benchmark.report("lightio (synchronous)") do |count| run_lightio(1, count) end benchmark.report("async (synchronous)") do |count| run_async(1, count) end benchmark.report("lightio (parallel)") do |count| run_lightio(32, count/32) end benchmark.report("async (parallel)") do |count| run_async(32, count/32) end benchmark.compare! end ruby-async-1.30.1/benchmark/fiber_count.rb000066400000000000000000000001661410040740200204410ustar00rootroot00000000000000# frozen_string_literal: true fibers = [] (1..).each do |i| fibers << Fiber.new{} fibers.last.resume puts i end ruby-async-1.30.1/benchmark/rubies/000077500000000000000000000000001410040740200171035ustar00rootroot00000000000000ruby-async-1.30.1/benchmark/rubies/README.md000066400000000000000000000037041410040740200203660ustar00rootroot00000000000000# (All) Rubies Benchmark This is a simple benchmark, which reads and writes data over a pipe. 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. ## Usage The simplest way is to use RVM. rvm all do ./benchmark.rb ## Results General improvements. ruby 1.9.3p551 (2014-11-13 revision 48407) [x86_64-linux] # ruby 2.0.0p648 (2015-12-16 revision 53162) [x86_64-linux] # ruby 2.1.10p492 (2016-04-01 revision 54464) [x86_64-linux] # ruby 2.2.10p489 (2018-03-28 revision 63023) [x86_64-linux] # ruby 2.3.8p459 (2018-10-18 revision 65136) [x86_64-linux] # ruby 2.4.6p354 (2019-04-01 revision 67394) [x86_64-linux] # ruby 2.5.3p105 (2018-10-18 revision 65156) [x86_64-linux] # Native fiber implementation & reduced syscalls (https://bugs.ruby-lang.org/issues/14739). ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-linux] # Performance regression (https://bugs.ruby-lang.org/issues/16009). ruby 2.7.0preview1 (2019-05-31 trunk c55db6aa271df4a689dc8eb0039c929bf6ed43ff) [x86_64-linux] # Improve fiber performance using pool alloation strategy (https://bugs.ruby-lang.org/issues/15997). ruby 2.7.0dev (2019-10-02T08:19:14Z trunk 9759e3c9f0) [x86_64-linux] # ruby-async-1.30.1/benchmark/rubies/benchmark.rb000077500000000000000000000102401410040740200213620ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require 'socket' require 'fiber' puts puts RUBY_DESCRIPTION if RUBY_VERSION < "2.0" class String def b self end end end # TODO: make these much larger, see if we're effectively batching # even if we don't mean to... QUERY_TEXT = "STATUS".freeze RESPONSE_TEXT = "OK".freeze NUM_WORKERS = (ARGV[0] || 10_000).to_i NUM_REQUESTS = (ARGV[1] || 100).to_i # Fiber reactor code taken from # https://www.codeotaku.com/journal/2018-11/fibers-are-the-right-solution/index class Reactor def initialize @readable = {} @writable = {} end def run while @readable.any? or @writable.any? readable, writable = IO.select(@readable.keys, @writable.keys, []) readable.each do |io| @readable[io].resume end writable.each do |io| @writable[io].resume end end end def wait_readable(io) @readable[io] = Fiber.current Fiber.yield @readable.delete(io) end def wait_writable(io) @writable[io] = Fiber.current Fiber.yield @writable.delete(io) end end class Wrapper def initialize(io, reactor) @io = io @reactor = reactor end if RUBY_VERSION >= "2.3" def read_nonblock(length, buffer) while true case result = @io.read_nonblock(length, buffer, exception: false) when :wait_readable @reactor.wait_readable(@io) when :wait_writable @reactor.wait_writable(@io) else return result end end end def write_nonblock(buffer) while true case result = @io.write_nonblock(buffer, exception: false) when :wait_readable @reactor.wait_readable(@io) when :wait_writable @reactor.wait_writable(@io) else return result end end end else def read_nonblock(length, buffer) while true begin return @io.read_nonblock(length, buffer) rescue IO::WaitReadable @reactor.wait_readable(@io) rescue IO::WaitWritable @reactor.wait_writable(@io) end end end def write_nonblock(buffer) while true begin return @io.write_nonblock(buffer) rescue IO::WaitReadable @reactor.wait_readable(@io) rescue IO::WaitWritable @reactor.wait_writable(@io) end end end end def read(length, buffer = nil) if buffer buffer.clear else buffer = String.new.b end result = self.read_nonblock(length - buffer.bytesize, buffer) if result == length return result end chunk = String.new.b while chunk = self.read_nonblock(length - buffer.bytesize, chunk) buffer << chunk break if buffer.bytesize == length end return buffer end def write(buffer) remaining = buffer.dup while true result = self.write_nonblock(remaining) if result == remaining.bytesize return buffer.bytesize else remaining = remaining.byteslice(result, remaining.bytesize - result) end end end end reactor = Reactor.new worker_read = [] worker_write = [] master_read = [] master_write = [] workers = [] # puts "Setting up pipes..." NUM_WORKERS.times do |i| r, w = IO.pipe worker_read.push Wrapper.new(r, reactor) master_write.push Wrapper.new(w, reactor) r, w = IO.pipe worker_write.push Wrapper.new(w, reactor) master_read.push Wrapper.new(r, reactor) end # puts "Setting up fibers..." NUM_WORKERS.times do |i| f = Fiber.new do # Worker code NUM_REQUESTS.times do |req_num| q = worker_read[i].read(QUERY_TEXT.size) if q != QUERY_TEXT raise "Fail! Expected #{QUERY_TEXT.inspect} but got #{q.inspect} on request #{req_num.inspect}!" end worker_write[i].write(RESPONSE_TEXT) end end workers.push f end workers.each { |f| f.resume } master_fiber = Fiber.new do NUM_WORKERS.times do |worker_num| f = Fiber.new do NUM_REQUESTS.times do |req_num| master_write[worker_num].write(QUERY_TEXT) buffer = master_read[worker_num].read(RESPONSE_TEXT.size) if buffer != RESPONSE_TEXT raise "Error! Fiber no. #{worker_num} on req #{req_num} expected #{RESPONSE_TEXT.inspect} but got #{buf.inspect}!" end end end f.resume end end master_fiber.resume # puts "Starting reactor..." reactor.run # puts "Done, finished all reactor Fibers!" puts Process.times # Exitruby-async-1.30.1/benchmark/thread_count.rb000066400000000000000000000001521410040740200206140ustar00rootroot00000000000000# frozen_string_literal: true threads = [] (1..).each do |i| threads << Thread.new{sleep} puts i end ruby-async-1.30.1/benchmark/thread_vs_fiber.rb000077500000000000000000000027741410040740200213020ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # Copyright, 2019, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'benchmark/ips' GC.disable Benchmark.ips do |benchmark| benchmark.time = 1 benchmark.warmup = 1 benchmark.report("Thread.new{}") do |count| while count > 0 Thread.new{count -= 1}.join end end benchmark.report("Fiber.new{}") do |count| while count > 0 Fiber.new{count -= 1}.resume end end benchmark.compare! end ruby-async-1.30.1/examples/000077500000000000000000000000001410040740200154765ustar00rootroot00000000000000ruby-async-1.30.1/examples/async_method.rb000077500000000000000000000020031410040740200204760ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require_relative '../lib/async' module Async::Methods def sleep(*args) Async::Task.current.sleep(*args) end def async(name) original_method = self.method(name) define_method(name) do |*args| Async::Reactor.run do |task| original_method.call(*args) end end end def await(&block) block.call.wait end def barrier! Async::Task.current.children.each(&:wait) end end include Async::Methods async def count_chickens(area_name) 3.times do |i| sleep rand puts "Found a chicken in the #{area_name}!" end end async def find_chicken(areas) puts "Searching for chicken..." sleep rand * 5 return areas.sample end async def count_all_chckens # These methods all run at the same time. count_chickens("garden") count_chickens("house") count_chickens("tree") # Wait for all previous async work to complete... barrier! puts "There was a chicken in the #{find_chicken(["garden", "house", "tree"]).wait}" end count_all_chckens ruby-async-1.30.1/examples/callback/000077500000000000000000000000001410040740200172325ustar00rootroot00000000000000ruby-async-1.30.1/examples/callback/loop.rb000066400000000000000000000011551410040740200205320ustar00rootroot00000000000000# frozen_string_literal: true require 'async/reactor' class Callback def initialize @reactor = Async::Reactor.new end def close @reactor.close end # If duration is 0, it will happen immediately after the task is started. def run(duration = 0) @reactor.run do |task| @reactor.after(duration) do @reactor.stop end yield(task) if block_given? end end end callback = Callback.new begin callback.run do |task| while true task.sleep(2) puts "Hello from task!" end end while true callback.run(0) puts "Sleeping for 1 second" sleep(1) end ensure callback.close end ruby-async-1.30.1/examples/capture/000077500000000000000000000000001410040740200171415ustar00rootroot00000000000000ruby-async-1.30.1/examples/capture/README.md000066400000000000000000000051171410040740200204240ustar00rootroot00000000000000# Capture ## Falcon ``` % wrk -t 8 -c 32 http://localhost:9292/ Running 10s test @ http://localhost:9292/ 8 threads and 32 connections Thread Stats Avg Stdev Max +/- Stdev Latency 106.31ms 10.20ms 211.79ms 98.00% Req/Sec 37.94 5.43 40.00 84.24% 3003 requests in 10.01s, 170.16KB read Requests/sec: 299.98 Transfer/sec: 17.00KB ``` ``` 0.0s: Process 28065 start times: | # ^C15.11s: strace -p 28065 | ["sendto", {:"% time"=>57.34, :seconds=>0.595047, :"usecs/call"=>14, :calls=>39716, :errors=>32, :syscall=>"sendto"}] | ["recvfrom", {:"% time"=>42.58, :seconds=>0.441867, :"usecs/call"=>12, :calls=>36718, :errors=>70, :syscall=>"recvfrom"}] | ["read", {:"% time"=>0.07, :seconds=>0.000723, :"usecs/call"=>7, :calls=>98, :errors=>nil, :syscall=>"read"}] | ["write", {:"% time"=>0.01, :seconds=>0.000112, :"usecs/call"=>56, :calls=>2, :errors=>nil, :syscall=>"write"}] | [:total, {:"% time"=>100.0, :seconds=>1.037749, :"usecs/call"=>nil, :calls=>76534, :errors=>102, :syscall=>"total"}] 15.11s: Process 28065 end times: | # 15.11s: Process Waiting: 1.0377s out of 1.55s | Wait percentage: 66.95% ``` ## Puma ``` wrk -t 8 -c 32 http://localhost:9292/ Running 10s test @ http://localhost:9292/ 8 threads and 32 connections Thread Stats Avg Stdev Max +/- Stdev Latency 108.83ms 3.50ms 146.38ms 86.58% Req/Sec 34.43 6.70 40.00 92.68% 1371 requests in 10.01s, 81.67KB read Requests/sec: 136.94 Transfer/sec: 8.16KB ``` ``` 0.0s: Process 28448 start times: | # ^C24.89s: strace -p 28448 | ["recvfrom", {:"% time"=>64.65, :seconds=>0.595275, :"usecs/call"=>13, :calls=>44476, :errors=>27769, :syscall=>"recvfrom"}] | ["sendto", {:"% time"=>30.68, :seconds=>0.282467, :"usecs/call"=>18, :calls=>15288, :errors=>nil, :syscall=>"sendto"}] | ["write", {:"% time"=>4.66, :seconds=>0.042921, :"usecs/call"=>15, :calls=>2772, :errors=>nil, :syscall=>"write"}] | ["read", {:"% time"=>0.02, :seconds=>0.000157, :"usecs/call"=>8, :calls=>19, :errors=>1, :syscall=>"read"}] | [:total, {:"% time"=>100.0, :seconds=>0.92082, :"usecs/call"=>nil, :calls=>62555, :errors=>27770, :syscall=>"total"}] 24.89s: Process 28448 end times: | # 24.89s: Process Waiting: 0.9208s out of 2.56s | Wait percentage: 35.97% ``` ruby-async-1.30.1/examples/capture/capture.rb000077500000000000000000000046471410040740200211470ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require 'irb' require 'console' pids = ARGV.collect(&:to_i) TICKS = Process.clock_getres(:TIMES_BASED_CLOCK_PROCESS_CPUTIME_ID, :hertz).to_f def getrusage(pid) fields = File.read("/proc/#{pid}/stat").split(/\s+/) return Process::Tms.new( fields[14].to_f / TICKS, fields[15].to_f / TICKS, fields[16].to_f / TICKS, fields[17].to_f / TICKS, ) end def parse(value) case value when /^\s*\d+\.\d+/ Float(value) when /^\s*\d+/ Integer(value) else value = value.strip if value.empty? nil else value end end end def strace(pid, duration = 60) input, output = IO.pipe pid = Process.spawn("strace", "-p", pid.to_s, "-cqf", "-w", "-e", "!futex", err: output) output.close Signal.trap(:INT) do Process.kill(:INT, pid) Signal.trap(:INT, :DEFAULT) end Thread.new do sleep duration Process.kill(:INT, pid) end summary = {} if first_line = input.gets if rule = input.gets # horizontal separator pattern = Regexp.new( rule.split(/\s/).map{|s| "(.{1,#{s.size}})"}.join(' ') ) header = pattern.match(first_line).captures.map{|key| key.strip.to_sym} end while line = input.gets break if line == rule row = pattern.match(line).captures.map{|value| parse(value)} fields = header.zip(row).to_h summary[fields[:syscall]] = fields end if line = input.gets row = pattern.match(line).captures.map{|value| parse(value)} fields = header.zip(row).to_h summary[:total] = fields end end _, status = Process.waitpid2(pid) Console.logger.error(status) do |buffer| buffer.puts first_line end unless status.success? return summary end pids.each do |pid| start_times = getrusage(pid) Console.logger.info("Process #{pid} start times:", start_times) # sleep 60 summary = strace(pid) Console.logger.info("strace -p #{pid}") do |buffer| summary.each do |fields| buffer.puts fields.inspect end end end_times = getrusage(pid) Console.logger.info("Process #{pid} end times:", end_times) if total = summary[:total] process_duration = end_times.utime - start_times.utime wait_duration = summary[:total][:seconds] Console.logger.info("Process Waiting: #{wait_duration.round(4)}s out of #{process_duration.round(4)}s") do |buffer| buffer.puts "Wait percentage: #{(wait_duration / process_duration * 100.0).round(2)}%" end else Console.logger.warn("No system calls detected.") end end ruby-async-1.30.1/examples/fibers.rb000066400000000000000000000103451410040740200173000ustar00rootroot00000000000000# frozen_string_literal: true require 'fiber' class IO READABLE = 1 WRITABLE = 2 # rb_wait_for_single_fd (int fd, int events, struct timeval *tv) def self.wait(descriptor, events, duration) fiber = Fiber.current reactor = fiber.reactor monitor = reactor.add_io(fiber, descriptor, state) fiber.with_timeout(duration) do result = Fiber.yield raise result if result.is_a? Exception end return result ensure reactor.remove_io(monitor) end def wait_readable(duration = nil) wait_any(READABLE) end def wait_writable(duration = nil) wait_any(WRITABLE) end def wait_until(events = READABLE|WRITABLE, duration = nil) IO.wait_for_io(self.fileno, events, duration) end end class Fiber # Raised when a task times out. class TimeoutError < RuntimeError end # This should be inherited by nested fibers. attr :reactor def timeout(duration) reactor = self.reactor backtrace = caller timer = reactor.add_timer(duration) do if self.alive? error = Fiber::TimeoutError.new("execution expired") error.set_backtrace backtrace self.resume error end end yield ensure reactor.cancel_timer(timer) end end # Can be standard implementation, but could also be provided by external gem/library. class Fiber::Reactor # Add IO to the reactor. The reactor will call `fiber.resume` when the event is triggered. # Returns an opaque monitor object which can be passed to `remove_io` to stop waiting for events. def add_io(fiber, io, state) # The motivation for add_io and remove_io is that it's how a lot of the underlying APIs work, where remove_io just takes the file descriptor. # It also avoids the need for any memory allocation, and maps well to how it's typically used (i.e. in an implementation of `IO#read`). # An efficient implementation might do it's job and then just: return io end def remove_io(monitor) end # The reactor will call the block at some point after duration time has elapsed. # Returns an opaque timer object which can be passed to `cancel_timer` to avoid this happening. def add_timer(duration, &block) end def cancel_timer(timer) end # Run until idle (no registered io/timers), or duration time has passed if specified. def run(duration = nil) end # Stop the reactor as soon as possible. Can be called from another thread. def stop end def close # Close the reactor so it can no longer be used. end end # Basic non-blocking task: reactor = Fiber::Reactor.new # User could provide their own reactor, it might even do other things, but the basic interface above should continue to work. Fiber.new(reactor: reactor) do # Blocking operations call Fiber.yield, which goes to... end.resume # ...here, which starts running the reactor which can also be controlled (e.g. duration, stopping) reactor.run # Here is a rough outline of the reactor concept implementation using NIO4R # Can be standard implementation, but could also be provided by external gem/library. class NIO::Reactor def initialize @selector = NIO::Selector.new @timers = Timers::Group.new @stopped = true end EVENTS = [ :r, :w, :rw ] def add_io(fiber, io, event) monitor = @selector.register(io, EVENTS[event]) monitor.value = fiber end def remove_io(monitor) monitor.cancel end # The reactor will call `fiber.resume(Fiber::TimeoutError)` at some point after duration time has elapsed. # Returns an opaque timer object which can be passed to `cancel_timer` to avoid this happening. def add_timer(fiber, duration) @timers.after(duration, &block) end def cancel_timer(timer) timer.cancel end # Run until idle (no registered io/timers), or duration time has passed if specified. def run(duration = nil) @timers.wait do |interval| # - nil: no timers # - -ve: timers expired already # - 0: timers ready to fire # - +ve: timers waiting to fire interval = 0 if interval && interval < 0 # If there is nothing to do, then finish: return if @fibers.empty? && interval.nil? if monitors = @selector.select(interval) monitors.each do |monitor| if fiber = monitor.value fiber.resume end end end end until @stopped end def stop @stopped = true @selector.wakeup end def close @seletor.close end end ruby-async-1.30.1/examples/queue/000077500000000000000000000000001410040740200166225ustar00rootroot00000000000000ruby-async-1.30.1/examples/queue/producer.rb000077500000000000000000000006351410040740200210010ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require 'async' require 'async/queue' Async do # Queue of up to 10 items: items = Async::LimitedQueue.new(10) # Five producers: 5.times do Async do |task| while true t = rand task.sleep(t) items.enqueue(t) end end end # A single consumer: Async do |task| while item = items.dequeue puts "dequeue -> #{item}" end end end ruby-async-1.30.1/examples/sleep_sort.rb000077500000000000000000000016301410040740200202050ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require_relative '../lib/async' def sleep_sort(items) Async::Reactor.run do |task| # Where to save the sorted items: sorted_items = [] items.each do |item| # Spawn an async task... task.async do |nested_task| # Which goes to sleep for the specified duration: nested_task.sleep(item) # And then appends the item to the sorted array: sorted_items << item end end # Wait for all children to complete. task.children.each(&:wait) # Return the result: sorted_items end.wait # Wait for the entire process to complete. end # Calling at the top level blocks the thread: puts sleep_sort(5.times.collect{rand}).inspect # Calling in your own reactor allows you to control the asynchronus behaviour: Async::Reactor.run do |task| 3.times do task.async do puts sleep_sort(5.times.collect{rand}).inspect end end end ruby-async-1.30.1/examples/stop/000077500000000000000000000000001410040740200164635ustar00rootroot00000000000000ruby-async-1.30.1/examples/stop/condition.rb000066400000000000000000000013171410040740200210000ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # require 'async'; require 'async/queue' require_relative '../../lib/async'; require_relative '../../lib/async/queue' Async do |consumer| consumer.annotate "consumer" condition = Async::Condition.new producer = Async do |subtask| subtask.annotate "subtask" (1..).each do |value| puts "producer yielding" subtask.yield # (1) Fiber.yield, (3) Reactor -> producer.resume condition.signal(value) # (4) consumer.resume(value) end puts "producer exiting" end value = condition.wait # (2) value = Fiber.yield puts "producer.stop" producer.stop # (5) [producer is resumed already] producer.stop puts "consumer exiting" end puts "Done." ruby-async-1.30.1/examples/stop/sleep.rb000066400000000000000000000016041410040740200201210ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true require_relative '../../lib/async' require 'async/http/endpoint' require 'async/http/server' require 'async/http/internet' # To query the web server: # curl http://localhost:9292/kittens Async do |parent| endpoint = Async::HTTP::Endpoint.parse("http://localhost:9292") internet = Async::HTTP::Internet.new server = Async::HTTP::Server.for(endpoint) do |request| if request.path =~ /\/(.*)/ keyword = $1 response = internet.get("https://www.google.com/search?q=#{keyword}") count = response.read.scan(keyword).size Protocol::HTTP::Response[200, [], ["Google found #{count} instance(s) of #{keyword}.\n"]] else Protocol::HTTP::Response[404, [], []] end end tasks = server.run #while true parent.sleep(10) parent.reactor.print_hierarchy #end parent.stop # -> Async::Stop tasks.each(&:stop) end ruby-async-1.30.1/gems.rb000066400000000000000000000004141410040740200151370ustar00rootroot00000000000000# frozen_string_literal: true source 'https://rubygems.org' gemspec group :maintenance, optional: true do gem "bake-bundler" gem "bake-modernize" gem "utopia-project" end # gem "async-rspec", path: "../async-rspec" # gem "rspec-files", path: "../rspec-files" ruby-async-1.30.1/guides/000077500000000000000000000000001410040740200151405ustar00rootroot00000000000000ruby-async-1.30.1/guides/event-loop/000077500000000000000000000000001410040740200172305ustar00rootroot00000000000000ruby-async-1.30.1/guides/event-loop/README.md000066400000000000000000000037711410040740200205170ustar00rootroot00000000000000# Event Loop This guide gives an overview of how the event loop is implemented. ## Overview {ruby Async::Reactor} provides the event loop and sits at the root of any task tree. Work is scheduled by adding {ruby Async::Task} instances to the reactor. When you invoke {ruby Async::Reactor#async}, the parent task is determined by calling {ruby Async::Task.current?} which uses fiber local storage. A slightly more efficient method is to use {ruby Async::Task#async}, which uses `self` as the parent task. ~~~ ruby require 'async' def sleepy(duration, task: Async::Task.current) task.async do |subtask| subtask.annotate "I'm going to sleep #{duration}s..." subtask.sleep duration puts "I'm done sleeping!" end end def nested_sleepy(task: Async::Task.current) task.async do |subtask| subtask.annotate "Invoking sleepy 5 times..." 5.times do |index| sleepy(index, task: subtask) end end end Async do |task| task.annotate "Invoking nested_sleepy..." subtask = nested_sleepy # Print out all running tasks in a tree: task.print_hierarchy($stderr) # Kill the subtask subtask.stop end ~~~ ### Thread Safety 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). ### Embedding Reactors `Async::Reactor#run` will run until the reactor runs out of work to do. To run a single iteration of the reactor, use `Async::Reactor#run_once` ~~~ ruby require 'async' Async.logger.debug! reactor = Async::Reactor.new # Run the reactor for 1 second: reactor.async do |task| task.sleep 1 puts "Finished!" end while reactor.run_once # Round and round we go! end ~~~ You can use this approach to embed the reactor in another event loop. ### Stopping Reactors `Async::Reactor#stop` will stop the current reactor and all children tasks. ### Interrupting Reactors `Async::Reactor#interrupt` can be called safely from a different thread (or signal handler) and will cause the reactor to invoke `#stop`. ruby-async-1.30.1/guides/getting-started/000077500000000000000000000000001410040740200202455ustar00rootroot00000000000000ruby-async-1.30.1/guides/getting-started/README.md000066400000000000000000000150611410040740200215270ustar00rootroot00000000000000# Getting Started This guide explains how to use `async` for event-driven systems. ## Installation Add the gem to your project: ~~~ bash $ bundle add async ~~~ ## Core Concepts `async` has several core concepts: - A {ruby Async::Task} instance which captures your sequential computations. - A {ruby Async::Reactor} instance which implements the core event loop. ## Creating Tasks 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 areas of your program. ~~~ ruby require 'async' Async do |task| puts "Hello World!" end ~~~ An {ruby Async::Task} runs using a {ruby Fiber} and blocking operations e.g. `sleep`, `read`, `write` yield control until the operation can complete. At the top level, `Async do ... end` will create an event loop, and nested `Async` blocks will reuse the existing event loop. This allows the caller to have either blocking or non-blocking behaviour. ~~~ ruby require 'async' def sleepy(duration = 1) Async do |task| task.sleep duration puts "I'm done sleeping, time for action!" end end # Synchronous operation: sleepy # Asynchronous operation: Async do # These two functions will sleep simultaneously. sleepy sleepy end ~~~ If you want to guarantee synchronous execution, you can use {ruby Kernel#Sync} which is semantically identical to `Async` except that in all cases it will wait until the given block completes execution. ### Nested Tasks Sometimes it's convenient to explicitly nest tasks. There are a variety of reasons to do this, including grouping tasks in order to wait for completion. In the most basic case, you can make a child task using the {ruby Async::Task#async} method. ~~~ ruby require 'async' def nested_sleepy(task: Async::Task.current) # Block caller task.sleep 0.1 # Schedule nested task: subtask = task.async(annotation: "Sleeping") do |subtask| puts "I'm going to sleep..." subtask.sleep 1.0 ensure puts "I'm waking up!" end end Async(annotation: "Top Level") do |task| subtask = nested_sleepy(task: task) task.reactor.print_hierarchy # # # end ~~~ This example creates a child `subtask` from the given parent `task`. It's the most efficient way to schedule a task. The task is executed until the first blocking operation, at which point it will yield control and `#async` will return. The result of this method is the task itself. ## Waiting For Results Like promises, {ruby Async::Task} produces results. In order to wait for these results, you must invoke {ruby Async::Task#wait}: ~~~ ruby require 'async' task = Async do rand end puts task.wait ~~~ ### Waiting For Multiple Tasks You can use {ruby Async::Barrier#async} to create multiple child tasks, and wait for them all to complete using {ruby Async::Barrier#wait}. {ruby Async::Barrier} and {ruby Async::Semaphore} are designed to be compatible with each other, and with other tasks that nest `#async` invocations. There are other similar situations where you may want to pass in a parent task, e.g. {ruby Async::IO::Endpoint#bind}. ~~~ ruby barrier = Async::Barrier.new semaphore = Async::Semaphore.new(2) semaphore.async(parent: barrier) do # ... end ~~~ A `parent:` in this context is anything that responds to `#async` in the same way that {ruby Async::Task} responds to `#async`. In situations where you strictly depend on the interface of {ruby Async::Task}, use the `task: Task.current` pattern. ### Stopping Tasks Use {ruby Async::Task#stop} to stop tasks. This function raises {ruby Async::Stop} on the target task and all descendent tasks. ~~~ ruby require 'async' Async do sleepy = Async do |task| task.sleep 1000 end sleepy.stop end ~~~ When you design a server, you should return the task back to the caller. They can use this task to stop the server if needed, independently of any other unrelated tasks within the reactor, and it will correctly clean up all related tasks. ## Resource Management In order to ensure your resources are cleaned up correctly, make sure you wrap resources appropriately, e.g.: ~~~ ruby Async::Reactor.run do begin socket = connect(remote_address) # May raise Async::Stop socket.write(...) # May raise Async::Stop socket.read(...) # May raise Async::Stop ensure socket.close if socket end end ~~~ As tasks run synchronously until they yield back to the reactor, you can guarantee this model works correctly. While in theory `IO#autoclose` allows you to automatically close file descriptors when they go out of scope via the GC, it may produce unpredictable behavour (exhaustion of file descriptors, flushing data at odd times), so it's not recommended. ## Exception Handling {ruby Async::Task} captures and logs exceptions. All unhandled exceptions will cause the enclosing task to enter the `:failed` state. Non-`StandardError` exceptions are re-raised immediately and will generally cause the reactor to fail. This ensures that exceptions will always be visible and cause the program to fail appropriately. ~~~ ruby require 'async' task = Async do # Exception will be logged and task will be failed. raise "Boom" end puts task.status # failed puts task.result # raises RuntimeError: Boom ~~~ ### Propagating Exceptions If a task has finished due to an exception, calling `Task#wait` will re-raise the exception. ~~~ ruby require 'async' Async do task = Async do raise "Boom" end begin task.wait # Re-raises above exception. rescue puts "It went #{$!}!" end end ~~~ ## Timeouts You can wrap asynchronous operations in a timeout. This ensures that malicious services don't cause your code to block indefinitely. ~~~ ruby require 'async' Async do |task| task.with_timeout(1) do task.sleep 100 rescue Async::TimeoutError puts "I timed out!" end end ~~~ ### Reoccurring Timers Sometimes you need to do some periodic work in a loop. ~~~ ruby require 'async' Async do |task| while true puts Time.now task.sleep 1 end end ~~~ ## Caveats ### Enumerators Due to limitations within Ruby and the nature of this library, it is not possible to use `to_enum` on methods which invoke asynchronous behaviour. We hope to [fix this issue in the future](https://github.com/socketry/async/issues/23). ### Blocking Methods in Standard Library Blocking Ruby methods such as `pop` in the `Queue` class require access to their own threads and will not yield control back to the reactor which can result in a deadlock. As a substitute for the standard library `Queue`, the {ruby Async::Queue} class can be used. ruby-async-1.30.1/guides/links.yaml000066400000000000000000000000621410040740200171420ustar00rootroot00000000000000getting-started: order: 1 event-loop: order: 2ruby-async-1.30.1/lib/000077500000000000000000000000001410040740200144265ustar00rootroot00000000000000ruby-async-1.30.1/lib/async.rb000066400000000000000000000026731410040740200161000ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require_relative "async/version" require_relative "async/logger" require_relative "async/reactor" require_relative "kernel/async" require_relative "kernel/sync" module Async # Invoke `Reactor.run` with all arguments/block. def self.run(*arguments, &block) Reactor.run(*arguments, &block) end end ruby-async-1.30.1/lib/async/000077500000000000000000000000001410040740200155435ustar00rootroot00000000000000ruby-async-1.30.1/lib/async/barrier.rb000066400000000000000000000043001410040740200175130ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2019, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require_relative 'task' module Async # A barrier is used to synchronize multiple tasks, waiting for them all to complete before continuing. class Barrier def initialize(parent: nil) @tasks = [] @parent = parent end # All tasks which have been invoked into the barrier. attr :tasks def size @tasks.size end def async(*arguments, parent: (@parent or Task.current), **options, &block) task = parent.async(*arguments, **options, &block) @tasks << task return task end def empty? @tasks.empty? end # Wait for all tasks. # @asynchronous Will wait for tasks to finish executing. def wait # TODO: This would be better with linked list. while @tasks.any? task = @tasks.first begin task.wait ensure # Remove the task from the waiting list if it's finished: @tasks.shift if @tasks.first == task end end end def stop # We have to be careful to avoid enumerating tasks while adding/removing to it: tasks = @tasks.dup tasks.each(&:stop) end end end ruby-async-1.30.1/lib/async/clock.rb000066400000000000000000000035311410040740200171650ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2018, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. module Async class Clock # Get the current elapsed monotonic time. def self.now ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) end # Measure the execution of a block of code. def self.measure start_time = self.now yield return self.now - start_time end def self.start self.new.tap(&:start!) end def initialize(total = 0) @total = total @started = nil end def start! @started ||= Clock.now end def stop! if @started @total += (Clock.now - @started) @started = nil end return @total end def total total = @total if @started total += (Clock.now - @started) end return total end end end ruby-async-1.30.1/lib/async/condition.rb000066400000000000000000000046571410040740200200720ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'fiber' require 'forwardable' require_relative 'node' module Async # A synchronization primative, which allows fibers to wait until a particular condition is triggered. Signalling the condition directly resumes the waiting fibers and thus blocks the caller. class Condition def initialize @waiting = [] end # Queue up the current fiber and wait on yielding the task. # @return [Object] def wait fiber = Fiber.current @waiting << fiber Task.yield # It would be nice if there was a better construct for this. We only need to invoke #delete if the task was not resumed normally. This can only occur with `raise` and `throw`. But there is no easy way to detect this. # ensure when not return or ensure when raise, throw rescue Exception @waiting.delete(fiber) raise end # Is any fiber waiting on this notification? # @return [Boolean] def empty? @waiting.empty? end # Signal to a given task that it should resume operations. # @param value The value to return to the waiting fibers. # @see Task.yield which is responsible for handling value. # @return [void] def signal(value = nil) waiting = @waiting @waiting = [] waiting.each do |fiber| fiber.resume(value) if fiber.alive? end return nil end end end ruby-async-1.30.1/lib/async/debug/000077500000000000000000000000001410040740200166315ustar00rootroot00000000000000ruby-async-1.30.1/lib/async/debug/monitor.rb000066400000000000000000000031221410040740200206430ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2018, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'delegate' module Async module Debug class Monitor < Delegator def initialize(monitor, selector) @monitor = monitor @selector = selector end def __getobj__ @monitor end def close @selector.deregister(self) @monitor.close end def inspect "\#<#{self.class} io=#{@monitor.io.inspect} interests=#{@monitor.interests.inspect} readiness=#{@monitor.readiness.inspect}>" end end end end ruby-async-1.30.1/lib/async/debug/selector.rb000066400000000000000000000046661410040740200210120ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2018, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require_relative 'monitor' require_relative '../logger' require 'nio' require 'set' module Async module Debug class LeakError < RuntimeError def initialize(monitors) super "Trying to close selector with active monitors: #{monitors.inspect}! This may cause your socket or file descriptor to leak." end end class Selector def initialize(selector = NIO::Selector.new) @selector = selector @monitors = Set.new end def register(object, interests) Async.logger.debug(self) {"Registering #{object.inspect} for #{interests}."} unless io = ::IO.try_convert(object) raise RuntimeError, "Could not convert #{io} into IO!" end monitor = Monitor.new(@selector.register(object, interests), self) @monitors.add(monitor) return monitor end def deregister(monitor) Async.logger.debug(self) {"Deregistering #{monitor.inspect}."} unless @monitors.delete?(monitor) raise RuntimeError, "Trying to remove monitor for #{monitor.inspect} but it was not registered!" end end def wakeup @selector.wakeup end def close if @monitors.any? raise LeakError, @monitors end ensure @selector.close end def select(*arguments) @selector.select(*arguments) end end end end ruby-async-1.30.1/lib/async/logger.rb000066400000000000000000000023301410040740200173450ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'console' require_relative 'task' module Async extend Console end ruby-async-1.30.1/lib/async/node.rb000066400000000000000000000173331410040740200170240ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. module Async # A double linked list. class List def initialize # The list behaves like a list node, so @tail points to the next item (the first one) and head points to the previous item (the last one). This may be slightly confusing but it makes the interface more natural. @head = nil @tail = nil @size = 0 end attr :size attr_accessor :head attr_accessor :tail # Inserts an item at the end of the list. def insert(item) unless @tail @tail = item @head = item # Consistency: item.head = nil item.tail = nil else @head.tail = item item.head = @head # Consistency: item.tail = nil @head = item end @size += 1 return self end def delete(item) if @tail.equal?(item) @tail = @tail.tail else item.head.tail = item.tail end if @head.equal?(item) @head = @head.head else item.tail.head = item.head end item.head = nil item.tail = nil @size -= 1 return self end def each(&block) return to_enum unless block_given? current = self while node = current.tail yield node # If the node has deleted itself or any subsequent node, it will no longer be the next node, so don't use it for continued traversal: if current.tail.equal?(node) current = node end end end def include?(needle) self.each do |item| return true if needle.equal?(item) end return false end def first @tail end def last @head end def empty? @tail.nil? end def nil? @tail.nil? end end private_constant :List class Children < List def initialize super @transient_count = 0 end # Does this node have (direct) transient children? def transients? @transient_count > 0 end def insert(item) if item.transient? @transient_count += 1 end super end def delete(item) if item.transient? @transient_count -= 1 end super end def finished? @size == @transient_count end end # Represents a node in a tree, used for nested {Task} instances. class Node # Create a new node in the tree. # @param parent [Node, nil] This node will attach to the given parent. def initialize(parent = nil, annotation: nil, transient: false) @parent = nil @children = nil @annotation = annotation @object_name = nil @transient = transient @head = nil @tail = nil if parent parent.add_child(self) end end # You should not directly rely on these pointers but instead use `#children`. # List pointers: attr_accessor :head attr_accessor :tail # @attr parent [Node, nil] attr :parent # @attr children [List] Optional list of children. attr :children # A useful identifier for the current node. attr :annotation # Whether there are children? def children? @children != nil && !@children.empty? end # Is this node transient? def transient? @transient end def annotate(annotation) if block_given? previous_annotation = @annotation @annotation = annotation yield @annotation = previous_annotation else @annotation = annotation end end def description @object_name ||= "#{self.class}:0x#{object_id.to_s(16)}#{@transient ? ' transient' : nil}" if @annotation "#{@object_name} #{@annotation}" else @object_name end end def backtrace(*arguments) nil end def to_s "\#<#{description}>" end # Change the parent of this node. # @param parent [Node, nil] the parent to attach to, or nil to detach. # @return [self] def parent=(parent) return if @parent.equal?(parent) if @parent @parent.delete_child(self) @parent = nil end if parent parent.add_child(self) end return self end protected def set_parent parent @parent = parent end protected def add_child child @children ||= Children.new @children.insert(child) child.set_parent(self) end protected def delete_child(child) @children.delete(child) child.set_parent(nil) end # Whether the node can be consumed safely. By default, checks if the # children set is empty. # @return [Boolean] def finished? @children.nil? || @children.finished? end # If the node has a parent, and is {finished?}, then remove this node from # the parent. def consume if parent = @parent and finished? parent.delete_child(self) if @children @children.each do |child| if child.finished? delete_child(child) else # In theory we don't need to do this... because we are throwing away the list. However, if you don't correctly update the list when moving the child to the parent, it foobars the enumeration, and subsequent nodes will be skipped, or in the worst case you might start enumerating the parents nodes. delete_child(child) parent.add_child(child) end end @children = nil end parent.consume end end # Traverse the tree. # @yield [node, level] The node and the level relative to the given root. def traverse(level = 0, &block) yield self, level @children&.each do |child| child.traverse(level + 1, &block) end end # Immediately terminate all children tasks, including transient tasks. # Internally invokes `stop(false)` on all children. def terminate # Attempt to stop the current task immediately, and all children: stop(false) # If that doesn't work, take more serious action: @children&.each do |child| child.terminate end end # Attempt to stop the current node immediately, including all non-transient children. # Invokes {#stop_children} to stop all children. # @parameter later [Boolean] Whether to defer stopping until some point in the future. def stop(later = false) # The implementation of this method may defer calling `stop_children`. stop_children(later) end # Attempt to stop all non-transient children. private def stop_children(later = false) @children&.each do |child| child.stop(later) unless child.transient? end end def print_hierarchy(out = $stdout, backtrace: true) self.traverse do |node, level| indent = "\t" * level out.puts "#{indent}#{node}" print_backtrace(out, indent, node) if backtrace end end private def print_backtrace(out, indent, node) if backtrace = node.backtrace backtrace.each_with_index do |line, index| out.puts "#{indent}#{index.zero? ? "→ " : " "}#{line}" end end end end end ruby-async-1.30.1/lib/async/notification.rb000066400000000000000000000035451410040740200205650ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require_relative 'condition' module Async # 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. class Notification < Condition # Signal to a given task that it should resume operations. # @return [void] def signal(value = nil, task: Task.current) return if @waiting.empty? task.reactor << Signal.new(@waiting, value) @waiting = [] return nil end Signal = Struct.new(:waiting, :value) do def alive? true end def resume waiting.each do |fiber| fiber.resume(value) if fiber.alive? end end end end end ruby-async-1.30.1/lib/async/queue.rb000066400000000000000000000044251410040740200172210ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require_relative 'notification' module Async # A queue which allows items to be processed in order. class Queue < Notification def initialize(parent: nil) super() @items = [] @parent = parent end attr :items def size @items.size end def empty? @items.empty? end def enqueue(item) @items.push(item) self.signal unless self.empty? end alias << enqueue def dequeue while @items.empty? self.wait end @items.shift end def async(parent: (@parent or Task.current), &block) while item = self.dequeue parent.async(item, &block) end end def each while item = self.dequeue yield item end end end class LimitedQueue < Queue def initialize(limit = 1, **options) super(**options) @limit = limit @full = Notification.new end attr :limit # @return [Boolean] Whether trying to enqueue an item would block. def limited? @items.size >= @limit end def enqueue item while limited? @full.wait end super end def dequeue item = super @full.signal return item end end end ruby-async-1.30.1/lib/async/reactor.rb000066400000000000000000000230321410040740200175270ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require_relative 'logger' require_relative 'task' require_relative 'wrapper' require_relative 'scheduler' require 'nio' require 'timers' require 'forwardable' module Async # Raised if a timeout occurs on a specific Fiber. Handled gracefully by `Task`. class TimeoutError < StandardError end # An asynchronous, cooperatively scheduled event reactor. class Reactor < Node extend Forwardable # The preferred method to invoke asynchronous behavior at the top level. # # - When invoked within an existing reactor task, it will run the given block # asynchronously. Will return the task once it has been scheduled. # - When invoked at the top level, will create and run a reactor, and invoke # the block as an asynchronous task. Will block until the reactor finishes # running. def self.run(*arguments, **options, &block) if current = Task.current? return current.async(*arguments, **options, &block) else reactor = self.new begin return reactor.run(*arguments, **options, &block) ensure reactor.close end end end def self.selector if backend = ENV['ASYNC_BACKEND']&.to_sym if NIO::Selector.backends.include?(backend) return NIO::Selector.new(backend) else warn "Could not find ASYNC_BACKEND=#{backend}!" end end return NIO::Selector.new end def initialize(parent = nil, selector: self.class.selector, logger: nil) super(parent) @selector = selector @timers = Timers::Group.new @logger = logger @ready = [] @running = [] if Scheduler.supported? @scheduler = Scheduler.new(self) else @scheduler = nil end @interrupted = false @guard = Mutex.new @blocked = 0 @unblocked = [] end attr :scheduler attr :logger # @reentrant Not thread safe. def block(blocker, timeout) fiber = Fiber.current if timeout timer = @timers.after(timeout) do if fiber.alive? fiber.resume(false) end end end begin @blocked += 1 Task.yield ensure @blocked -= 1 end ensure timer&.cancel end # @reentrant Thread safe. def unblock(blocker, fiber) @guard.synchronize do @unblocked << fiber @selector.wakeup end end def fiber(&block) if @scheduler Fiber.new(blocking: false, &block) else Fiber.new(&block) end end def to_s "\#<#{self.description} #{@children&.size || 0} children (#{stopped? ? 'stopped' : 'running'})>" end def stopped? @children.nil? end # Start an asynchronous task within the specified reactor. The task will be # executed until the first blocking call, at which point it will yield and # and this method will return. # # This is the main entry point for scheduling asynchronus tasks. # # @yield [Task] Executed within the task. # @return [Task] The task that was scheduled into the reactor. def async(*arguments, **options, &block) task = Task.new(self, **options, &block) # I want to take a moment to explain the logic of this. # When calling an async block, we deterministically execute it until the # first blocking operation. We don't *have* to do this - we could schedule # it for later execution, but it's useful to: # - Fail at the point of the method call where possible. # - Execute determinstically where possible. # - Avoid scheduler overhead if no blocking operation is performed. task.run(*arguments) # Console.logger.debug "Initial execution of task #{fiber} complete (#{result} -> #{fiber.alive?})..." return task end def register(io, interest, value = Fiber.current) monitor = @selector.register(io, interest) monitor.value = value return monitor end # Interrupt the reactor at the earliest convenience. Can be called from a different thread safely. def interrupt @guard.synchronize do unless @interrupted @interrupted = true @selector.wakeup end end end # Schedule a fiber (or equivalent object) to be resumed on the next loop through the reactor. # @param fiber [#resume] The object to be resumed on the next iteration of the run-loop. def << fiber @ready << fiber end # Yield the current fiber and resume it on the next iteration of the event loop. def yield(fiber = Fiber.current) @ready << fiber Task.yield end def finished? # TODO I'm not sure if checking `@running.empty?` is really required. super && @ready.empty? && @running.empty? && @blocked.zero? end # Run one iteration of the event loop. # @param timeout [Float | nil] the maximum timeout, or if nil, indefinite. # @return [Boolean] whether there is more work to do. def run_once(timeout = nil) # Console.logger.debug(self) {"@ready = #{@ready} @running = #{@running}"} if @ready.any? # running used to correctly answer on `finished?`, and to reuse Array object. @running, @ready = @ready, @running @running.each do |fiber| fiber.resume if fiber.alive? end @running.clear end if @unblocked.any? unblocked = Array.new @guard.synchronize do unblocked, @unblocked = @unblocked, unblocked end while fiber = unblocked.pop fiber.resume if fiber.alive? end end if @ready.empty? interval = @timers.wait_interval else # if there are tasks ready to execute, don't sleep: interval = 0 end # If we are finished, we stop the task tree and exit: if self.finished? return false end # If there is no interval to wait (thus no timers), and no tasks, we could be done: if interval.nil? # Allow the user to specify a maximum interval if we would otherwise be sleeping indefinitely: interval = timeout elsif interval < 0 # We have timers ready to fire, don't sleep in the selctor: interval = 0 elsif timeout and interval > timeout interval = timeout end # Console.logger.info(self) {"Selecting with #{@children&.size} children with interval = #{interval ? interval.round(2) : 'infinite'}..."} if monitors = @selector.select(interval) monitors.each do |monitor| monitor.value.resume end end @timers.fire # We check and clear the interrupted flag here: if @interrupted @guard.synchronize do @interrupted = false end return false end # The reactor still has work to do: return true end # Run the reactor until all tasks are finished. Proxies arguments to {#async} immediately before entering the loop, if a block is provided. def run(*arguments, **options, &block) raise RuntimeError, 'Reactor has been closed' if @selector.nil? @scheduler&.set! initial_task = self.async(*arguments, **options, &block) if block_given? while self.run_once # Round and round we go! end return initial_task ensure @scheduler&.clear! Console.logger.debug(self) {"Exiting run-loop because #{$! ? $! : 'finished'}."} end # Stop each of the children tasks and close the selector. def close # This is a critical step. Because tasks could be stored as instance variables, and since the reactor is (probably) going out of scope, we need to ensure they are stopped. Otherwise, the tasks will belong to a reactor that will never run again and are not stopped: self.terminate @selector.close @selector = nil end # Check if the selector has been closed. # @returns [Boolean] def closed? @selector.nil? end # Put the calling fiber to sleep for a given ammount of time. # @parameter duration [Numeric] The time in seconds, to sleep for. def sleep(duration) fiber = Fiber.current timer = @timers.after(duration) do if fiber.alive? fiber.resume end end Task.yield ensure timer.cancel if timer end # Invoke the block, but after the specified timeout, raise {TimeoutError} in any currenly blocking operation. If the block runs to completion before the timeout occurs or there are no non-blocking operations after the timeout expires, the code will complete without any exception. # @param duration [Numeric] The time in seconds, in which the task should # complete. def with_timeout(timeout, exception = TimeoutError) fiber = Fiber.current timer = @timers.after(timeout) do if fiber.alive? error = exception.new("execution expired") fiber.resume(error) end end yield timer ensure timer.cancel if timer end end end ruby-async-1.30.1/lib/async/scheduler.rb000066400000000000000000000052761410040740200200600ustar00rootroot00000000000000# Copyright, 2020, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require_relative 'clock' module Async class Scheduler if Fiber.respond_to?(:set_scheduler) def self.supported? true end else def self.supported? false end end def initialize(reactor) @reactor = reactor end attr :wrappers def set! Fiber.set_scheduler(self) end def clear! Fiber.set_scheduler(nil) end private def from_io(io) Wrapper.new(io, @reactor) end def io_wait(io, events, timeout = nil) wrapper = from_io(io) if events == ::IO::READABLE if wrapper.wait_readable(timeout) return ::IO::READABLE end elsif events == ::IO::WRITABLE if wrapper.wait_writable(timeout) return ::IO::WRITABLE end else if wrapper.wait_any(timeout) return events end end return false rescue TimeoutError return nil ensure wrapper&.reactor = nil end # Wait for the specified process ID to exit. # @parameter pid [Integer] The process ID to wait for. # @parameter flags [Integer] A bit-mask of flags suitable for `Process::Status.wait`. # @returns [Process::Status] A process status instance. def process_wait(pid, flags) Thread.new do ::Process::Status.wait(pid, flags) end.value end def kernel_sleep(duration) self.block(nil, duration) end def block(blocker, timeout) @reactor.block(blocker, timeout) end def unblock(blocker, fiber) @reactor.unblock(blocker, fiber) end def close end def fiber(&block) task = Task.new(@reactor, &block) fiber = task.fiber task.run return fiber end end end ruby-async-1.30.1/lib/async/semaphore.rb000066400000000000000000000064401410040740200200570ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. module Async # A semaphore is used to control access to a common resource in a concurrent system. A useful way to think of a semaphore as used in the real-world systems is as a record of how many units of a particular resource are available, coupled with operations to adjust that record safely (i.e. to avoid race conditions) as units are required or become free, and, if necessary, wait until a unit of the resource becomes available. class Semaphore def initialize(limit = 1, parent: nil) @count = 0 @limit = limit @waiting = [] @parent = parent end # The current number of tasks that have acquired the semaphore. attr :count # The maximum number of tasks that can acquire the semaphore. attr :limit # The tasks waiting on this semaphore. attr :waiting # Is the semaphore currently acquired? def empty? @count.zero? end # Whether trying to acquire this semaphore would block. def blocking? @count >= @limit end # Run an async task. Will wait until the semaphore is ready until spawning and running the task. def async(*arguments, parent: (@parent or Task.current), **options) wait parent.async(**options) do |task| @count += 1 begin yield task, *arguments ensure self.release end end end # Acquire the semaphore, block if we are at the limit. # If no block is provided, you must call release manually. # @yield when the semaphore can be acquired # @return the result of the block if invoked def acquire wait @count += 1 return unless block_given? begin return yield ensure self.release end end # Release the semaphore. Must match up with a corresponding call to `acquire`. Will release waiting fibers in FIFO order. def release @count -= 1 while (@limit - @count) > 0 and fiber = @waiting.shift if fiber.alive? fiber.resume end end end private # Wait until the semaphore becomes available. def wait fiber = Fiber.current if blocking? @waiting << fiber Task.yield while blocking? end rescue Exception @waiting.delete(fiber) raise end end end ruby-async-1.30.1/lib/async/task.rb000066400000000000000000000176561410040740200170510ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'fiber' require 'forwardable' require_relative 'node' require_relative 'condition' module Async # Raised when a task is explicitly stopped. class Stop < Exception class Later def initialize(task) @task = task end def alive? true end def resume @task.stop end end end # A task represents the state associated with the execution of an asynchronous # block. class Task < Node extend Forwardable # Yield the unerlying `result` for the task. If the result # is an Exception, then that result will be raised an its # exception. # @return [Object] result of the task # @raise [Exception] if the result is an exception # @yield [result] result of the task if a block if given. def self.yield if block_given? result = yield else result = Fiber.yield end if result.is_a? Exception raise result else return result end end # Create a new task. # @param reactor [Async::Reactor] the reactor this task will run within. # @param parent [Async::Task] the parent task. def initialize(reactor, parent = Task.current?, logger: nil, finished: nil, **options, &block) super(parent || reactor, **options) @reactor = reactor @status = :initialized @result = nil @finished = finished @logger = logger || @parent.logger @fiber = make_fiber(&block) end attr :logger if Fiber.current.respond_to?(:backtrace) def backtrace(*arguments) @fiber&.backtrace(*arguments) end end def to_s "\#<#{self.description} (#{@status})>" end # @attr ios [Reactor] The reactor the task was created within. attr :reactor def_delegators :@reactor, :with_timeout, :sleep # Yield back to the reactor and allow other fibers to execute. def yield Task.yield{reactor.yield} end # @attr fiber [Fiber] The fiber which is being used for the execution of this task. attr :fiber def alive? @fiber&.alive? end # @attr status [Symbol] The status of the execution of the fiber, one of `:initialized`, `:running`, `:complete`, `:stopped` or `:failed`. attr :status # Begin the execution of the task. def run(*arguments) if @status == :initialized @status = :running @fiber.resume(*arguments) else raise RuntimeError, "Task already running!" end end def async(*arguments, **options, &block) task = Task.new(@reactor, self, **options, &block) task.run(*arguments) return task end # Retrieve the current result of the task. Will cause the caller to wait until result is available. # @raise [RuntimeError] if the task's fiber is the current fiber. # @return [Object] the final expression/result of the task's block. def wait raise RuntimeError, "Cannot wait on own fiber" if Fiber.current.equal?(@fiber) if running? @finished ||= Condition.new @finished.wait else Task.yield{@result} end end # Deprecated. alias result wait # Soon to become attr :result # Stop the task and all of its children. def stop(later = false) if self.stopped? # If we already stopped this task... don't try to stop it again: return end if self.running? if self.current? if later @reactor << Stop::Later.new(self) else raise Stop, "Stopping current task!" end elsif @fiber&.alive? begin @fiber.resume(Stop.new) rescue FiberError @reactor << Stop::Later.new(self) end end else # We are not running, but children might be, so transition directly into stopped state: stop! end end # Lookup the {Task} for the current fiber. Raise `RuntimeError` if none is available. # @return [Async::Task] # @raise [RuntimeError] if task was not {set!} for the current fiber. def self.current Thread.current[:async_task] or raise RuntimeError, "No async task available!" end # Check if there is a task defined for the current fiber. # @return [Async::Task, nil] def self.current? Thread.current[:async_task] end def current? self.equal?(Thread.current[:async_task]) end # Check if the task is running. # @return [Boolean] def running? @status == :running end # Whether we can remove this node from the reactor graph. # @return [Boolean] def finished? super && @status != :running end def failed? @status == :failed end def stopping? @status == :stopping end def stopped? @status == :stopped end def complete? @status == :complete end private # This is a very tricky aspect of tasks to get right. I've modelled it after `Thread` but it's slightly different in that the exception can propagate back up through the reactor. If the user writes code which raises an exception, that exception should always be visible, i.e. cause a failure. If it's not visible, such code fails silently and can be very difficult to debug. # As an explcit choice, the user can start a task which doesn't propagate exceptions. This only applies to `StandardError` and derived tasks. This allows tasks to internally capture their error state which is raised when invoking `Task#result` similar to how `Thread#join` works. This mode makes {ruby Async::Task} behave more like a promise, and you would need to ensure that someone calls `Task#result` otherwise you might miss important errors. def fail!(exception = nil, propagate = true) @status = :failed @result = exception if propagate raise elsif @finished.nil? # If no one has called wait, we log this as an error: Console.logger.error(self) {$!} else Console.logger.debug(self) {$!} end end def stop! # logger.debug(self) {"Task was stopped with #{@children&.size.inspect} children!"} @status = :stopped stop_children(true) end def make_fiber(&block) Fiber.new do |*arguments| set! begin @result = yield(self, *arguments) @status = :complete # Console.logger.debug(self) {"Task was completed with #{@children.size} children!"} rescue Stop stop! rescue StandardError => error fail!(error, false) rescue Exception => exception fail!(exception, true) ensure # Console.logger.debug(self) {"Task ensure $!=#{$!} with #{@children.size} children!"} finish! end end end # Finish the current task, and all bound bound IO objects. def finish! # Allow the fiber to be recycled. @fiber = nil # Attempt to remove this node from the task tree. consume # If this task was being used as a future, signal completion here: if @finished @finished.signal(@result) end end # Set the current fiber's `:async_task` to this task. def set! # This is actually fiber-local: Thread.current[:async_task] = self Console.logger = @logger if @logger end end end ruby-async-1.30.1/lib/async/version.rb000066400000000000000000000022611410040740200175560ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. module Async VERSION = "1.30.1" end ruby-async-1.30.1/lib/async/wrapper.rb000066400000000000000000000124141410040740200175520ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'nio' module Async # Represents an asynchronous IO within a reactor. class Wrapper class Cancelled < StandardError class From def initialize @backtrace = caller[5..-1] end attr :backtrace def cause nil end def message "Cancelled" end end def initialize super "The operation has been cancelled!" @cause = From.new end attr :cause end # wait_readable, wait_writable and wait_any are not re-entrant, and will raise this failure. class WaitError < StandardError def initialize super "A fiber is already waiting!" end end # @param io the native object to wrap. # @param reactor [Reactor] the reactor that is managing this wrapper, or not specified, it's looked up by way of {Task.current}. def initialize(io, reactor = nil) @io = io @reactor = reactor @monitor = nil @readable = nil @writable = nil @any = nil end def dup self.class.new(@io.dup, @reactor) end def resume(*arguments) # It's possible that the monitor was closed before calling resume. return unless @monitor readiness = @monitor.readiness if @readable and (readiness == :r or readiness == :rw) @readable.resume(*arguments) end if @writable and (readiness == :w or readiness == :rw) @writable.resume(*arguments) end if @any @any.resume(*arguments) end end # The underlying native `io`. attr :io # The reactor this wrapper is associated with, if any. attr :reactor # The monitor for this wrapper, if any. attr :monitor # Bind this wrapper to a different reactor. Assign nil to convert to an unbound wrapper (can be used from any reactor/task but with slightly increased overhead.) # Binding to a reactor is purely a performance consideration. Generally, I don't like APIs that exist only due to optimisations. This is borderline, so consider this functionality semi-private. def reactor= reactor return if @reactor.equal?(reactor) cancel_monitor @reactor = reactor end # Wait for the io to become readable. def wait_readable(timeout = nil) raise WaitError if @readable self.reactor = Task.current.reactor begin @readable = Fiber.current wait_for(timeout) ensure @readable = nil @monitor.interests = interests if @monitor end end # Wait for the io to become writable. def wait_writable(timeout = nil) raise WaitError if @writable self.reactor = Task.current.reactor begin @writable = Fiber.current wait_for(timeout) ensure @writable = nil @monitor.interests = interests if @monitor end end # Wait fo the io to become either readable or writable. # @param duration [Float] timeout after the given duration if not `nil`. def wait_any(timeout = nil) raise WaitError if @any self.reactor = Task.current.reactor begin @any = Fiber.current wait_for(timeout) ensure @any = nil @monitor.interests = interests if @monitor end end # Close the io and monitor. def close cancel_monitor @io.close end def closed? @io.closed? end private # What an abomination. def interests if @any return :rw elsif @readable if @writable return :rw else return :r end elsif @writable return :w end return nil end def cancel_monitor if @readable readable = @readable @readable = nil readable.resume(Cancelled.new) end if @writable writable = @writable @writable = nil writable.resume(Cancelled.new) end if @any any = @any @any = nil any.resume(Cancelled.new) end if @monitor @monitor.close @monitor = nil end end def wait_for(timeout) if @monitor @monitor.interests = interests else @monitor = @reactor.register(@io, interests, self) end # If the user requested an explicit timeout for this operation: if timeout @reactor.with_timeout(timeout) do Task.yield end else Task.yield end return true end end end ruby-async-1.30.1/lib/kernel/000077500000000000000000000000001410040740200157065ustar00rootroot00000000000000ruby-async-1.30.1/lib/kernel/async.rb000066400000000000000000000026031410040740200173510ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require_relative "../async/reactor" module Kernel # Run the given block of code in a task, asynchronously, creating a reactor if necessary. def Async(*arguments, **options, &block) ::Async::Reactor.run(*arguments, **options, &block) end end ruby-async-1.30.1/lib/kernel/sync.rb000066400000000000000000000027061410040740200172140ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require_relative "../async/reactor" module Kernel # Run the given block of code synchronously, but within a reactor if not already in one. def Sync(&block) if task = ::Async::Task.current? yield task else ::Async::Reactor.run( finished: ::Async::Condition.new, &block ).wait end end end ruby-async-1.30.1/logo.png000066400000000000000000000372441410040740200153400ustar00rootroot00000000000000PNG  IHDRErb HiTXtXML:com.adobe.xmp JiCCPsRGB IEC61966-2.1(u+Q?h#ʒ M2F4jI3}'ɭr(qʵRDJʝkzm%{Ny>{tsNlN|m8錨6:=}=`۪Uܿ*GTM OrŻ.5 rA{Kd,á1+ V3r3Ut%. B08c`Xfnˊ*B+ʬ2IRuU%&DHno_ĠX/oiO Wr }]ֺy.Zt.I葂'~Mz Ş9}| @o^,DgťI pHYs   IDATxwx\IbՑ-`jH!i&a:&in%yn)$n*,a $$@!ݖdwizoqdc˚9g3cd`~!Q$)|8MevS9Dp:`@d[O:Ƞ `5X~&s!ՠ&cJ}$K !*S-NW9Yx8=b_D!?d#| h(BXSYNFf;x]E)9|TIOjj*\s&44Bss3ʹ@&!NNIRtL:C4evvYfffԯW׋( 9?p ꠌBŞvs]իWSWWW|Yi^ 933xԗ|O*Obu~s'p\++8{lyi8s#GsRKq"aygRB|׿Ju466st:q8qqt:q8'|^Mx"N&+Y}YR)Mbeg)'> ?S~p8Xj%Vpx*"Nɤdҧ~?3ӉUrQ+ ^)&Nkj+?̏~#ˤN$t+kZZZ̧?sJߡf5kP[ B!0Px<^p6 rD{{{^dDaNyg!,CbiD]GK`{{;>񳚚6(j3avv6߾vZ[[pwpS[WD^O/K~߿sCy_XX$I Zjjjw2y矷J$altɩIRTI.Ē%Kvؘs:E;ࡃz8f<h| /<}gg'/x ,}}=z`0{bK$|>y}Թꨯܶ6&&&HAFww7?[ \$Yԭr+M7vsyapeLNNhlj!8k=Nzz{кྡྷe7:Q21In|6DGJ4e߾}߷h4X. Ϯ],tGx\tk%Ir܄f 7u*7bQcl߶Ϭgc]4JF=ݛt:imm=g]x;r./.Jaf+͵qO|9p:H$޽!r࡜Sohh`ƍzN^Q SXїxH{ini& sN 6>>?)lذAϩ%)V0I:@FoPkgu&޵f7:2zbyZOoժUzN{vaU+k ]Dںuz8|p4IRLOMg}|ɒ%|g99{c $I2F%_@[['Tp$]7 Ulj.uj@8S%I\bD#٧7:nԤF#,C-*P__Ϻu팎NUB/[ί)z׉ԕ9HR>͉l62 D1J,:5wt&E)V(I֧xS-2L7u.+5 %#h֧&EQbSc$ u囎Z[[KK4Zlo  ^jM-*֪UR?כ׽---twusWrW60_H4kyh$&I4@\u`JQ~i~ >sa~qEW/J&Ei>W(Idk-Nm6mƫ^*kn0_9c+ 8vGKӟ?qFGGz^5!kRS/(D]_%\xKRw<KeN)}>}EQE;){ݝ3ٝ,W}n_E$)lݺ{ΐk:t_WٜIq^9ѕ߽K, I(O>tMd2Yk%ydy D(X(b箝>-#W @d2߶kVl:^pRvjRLuU%)V()=oDMv. FX2 .r B'yuś-VۼuN|PEoۮX_穧og ٳ@@{;ΦܩX$))yЍ{ iۧy5E)HRQI͍~Sk+zSs( xLN%_ B!"d G3'8>W7I ɄwK8EF$EQ{_U 'ldIRI$u%BUwHBIR|dMQF$EQ5bǑl-&IQFג&[s0KE19f(Mٚb*%5j&IQT}ҧX$)8m}nȬ~wMG*'IIYydIMQT~A=ŤX$)0>>x9/)IBIYHBIR0RSP(+$E!;D(D\$) ?IL &at'ѵ®&I B,:i> !I$)ïx'|6۽JT-IU'x'%I"Iҭd26J^|蠄yIRjW5ux}Sug|6ЄYIRxpǘ4 _ɤ_m#$IQTAr;m:?۴eN$EQilOWC2EQo79FabE&߼w内Vޡۀ6m (*mǿq׸W/Y` 8{(s:_6m~Mb&'IQT[ϞqAʪ(+ ?G56mnBV IQT|wot:ד|~~۴҆'D끟3|Mkמ}M'f2X<}ޏG|6/OKC?3Hq3oEG(,Gs?MX rdj۴L΅|tgyho3PX$Ea5m 1 `&OS;<xܷi|)KV¦ =7k955eWX$EaNWr}S< ]drmyoB/C@;NM/."G|6z a~| E-]kwlV0}(.۴QoQ$) $a=ߺ|} (w;ߦͺGuHRfN z _2VSWp8`REQ:^<۴k s(&^o_qP6EQ^_·i^A$) 3z9 %49jP{}S1o~۴YW/@ Њ:Yu޿88bs_u],};6KJ@&ǣO_)oM -j+,Rહ"{]'E3u7p#f? %e6{6qۆ+k3 򫆤:cxccC)[~x?jZx&wh^Ku x7[SU1B<6n}0<c6?8<ܖRr_ߎ~xw}e L'T*( x۞z]BESW>C[em t8t٦ };\K_&5z \|6l9C[[q8J7#008@<7uS|_Ϥ.z3zgٹחR29~r_.|#6aCs=WXK%&%t7ַFjjj:WNÉxӉL& Ltp(L pՙ7fn}Z!V+>z;yzDT>2|+*K>rO,ҜrMZ|ekZ3jarWip8}ptAKs uu;6Y\cKQ=舺~Y/{m}/vg>cеX4?3|nX:\&bЎG~{ӼP/Z/z/Z4w];,u &N391ٺu+?<Βɔ;ƺu쬺Y6pp`:>á=y-'\؏:@طH` N,}*1)j6;^o 16:}yf3ۻQbQHִDVƭPGӟ;t+l:x^UmTwNranUَٴd2x<<}@{{;:|M q?yw>KV7?HƓNl ں)ܪ2)"D˖Ecijh?p)vCw6 #ϕ$k%90'IH$ 02*0::ʶmc||%) anI:H'HȚB$E6;;˶mۈչa$)A*d03Ie G-BғXFrWfg)9gP55ozl۞Ε sգc%}GF[A\Mɶ,71:0WUʝ7Q$) !@$) Q^f`u̔]F 0i*EfJyQegJ$E!oۚ/ʚ5kp9˄aJQN'gavv 寣v킢m߾]O1I |QnzM\}EQ"O=7&)Z1SBso"I hqOBXʌ ,@/'HMp@:[x }BT ͔e4sسqջpt;in1mBT{x,c:w4M.2M zlٗEc 02Xd N7!   맱D Є0%i>Ĉjx8&388(s!f>Mυ3+'s!X4̅$TR4k5|\Hw.Z[[MuGbJ.fJITq%NHz]˺Xֵ 4 Qrj:?r1SRjH$刣db JWWr'E[))ƴ $J';>ΥtuubtXBFqdBW%P_.fJWS\H:fb| Yֵ.FXZ& ]Sxh4@Յ]Q#y:d2ٓb<:HMq)tV-zq:T2A\XE:ϭPg-Tqǩo?1z-kaf|^=Rh$J:zݦ^Ҽ擫s+hՕSNjtnn0IQjBR).y][[kthu~nb5D";F?nVJQ"WMQZ '5)rbfVZI3Z!Jg).J \.V\Awwl%]泾 5b; ]]\:LCj#Y YD̓JatdwU+Wya +)ܚm8U  #( SLOMʕ(J'W9WhK(\5`0AEPJ~Ǖ+vBd2(@*K2$㪓UK~UXD>xUpxΤ9'hXP0)Iq1ʕ+q(rDz'<M"H@2S~?~W+Vҽ[ ųϦZlY:_dQ"h>7펊8:kr2[Rhm6GH泾it)D9x MMy晴0X4'-" o>#*O1 IX5E}/(Ǯ8܆Y܋?KxRSfggYl)k֬iS |-~`Ť(M5RɉIfXDg%Eh1L&(\UVpʽՕ(3H4_YSMi9i3`2h=Ә*):>PP(TpD)$I=N $ȳϪL2b-)ƀBeEZ<H;vn 81+ cU|IOX9$ k.r.N*/WSh~͒IqbTmQEQexfu; USX*/[X@,c߾}ow!=DY".(GfgfٹcY^7s% f9)>U@/D4e]2ȴʊ(Nǎ#3`L#Gp2TX43F9v옞SIR,v4V=b vE,*4KIx爞91Ϸ)9)Ɓ IZd رcl[Bt}djVlR)٫ahMYI\IQߡdRVl٥il[@ L&s*'V d2Io_oVd8pSSFbYZh+BG̞B,*(:t1E4_0=5CQ?צe:ү(aX9b3X#)jv>|X y9vFaZMt:[S5 ħX,@ychp>S.b*tZsX\sNS?5_q# 622ب&U'sջQ$z:Z >kQ_SQ?Ǧg~Uh˖-9'NLNB"+EQ8x8'J16Jزe&xU"O Avʾ6m<,+%tx hPJNQO"{?￟;t-Ѓ:bZ"X7)f ===\vee-J*Ͻ)BR1zq4+UwwwW!rop1蝦,cդpoѝ(qcgv CE( hYG" NJL&I%S5M'sСut:]-yۘb]륡E9-\%$t:M(bjj&&&Gdjjj$IZT0"7nkς+7Ef+'E!9jmбTJM ̠.S4ڗph23w9i"1fvv`(xbB ,` M& C,{+xRl|fRiIY݀'}hhy瞧uuu\d*ih?c1PY, 084 `x<(.N M;9`P~+tx-0[DxSIу:5z ߿3Vtns)zf+bL&C435=ubzp8L2nK_eftam}n+9{P (AmF/Sxǎ\pyݺȊ+CMJ&a<^  ?~{^ DL&e$zC!˱ct/})?_\Lfe8W)\WWe^D_bƆ6_ hU%^bh3: q!m_[s<w%A->yf]|~tBm ZU'Eud7&n喂XHr̟uFyqF]]) pp-W~|^k&Io3sJ)8Z"[zǹcͅ<9 jNl6\. ԄiN n(.Ti&&'WCy%u+дU)oξ׭[r _}@04h}l6O$z]\.멭-v6kײb劢ΓH$LNMxt( _k??~,fmfaٱ_z={ jRExSI/p>Ub?{=g}6ׯTMRkvv^z{{h?HuFH$Ņb]םͲ.uF{[;mmmކ }b^ǃxdzsAsu*$uΙp}}(hkkSFJOAu:念\B*"J_IRɹ?;x$YmtGM7ONQPG,MQ51M-FEB:xܱX2 x~C ϑ:Pߟ;]x.F yYi>5^2(<khDDj.$y9)7z/$QX~ˁPcO2($^s_{9oQ&3'e'P7w(;{'B!B!B!B!B!B!B!B!B!B!B!BQB X DU?\-wW*# KtDx]k! xۀԝKe{̗IXY-+> 8j%,Kt_^^׺Z_(0Z/c{#}+=f0)(4)nDuI1/5/a $W{|XR/ZF+?EˊQe07(@pj cǻXRLR؏ ql+*yV n5:Ap>O*4)&TO~Ɇ<@J+B?l(Bg!~ tH0Z t IWZ*E 4:aIh3[| Jz mYVaf! u;OYLwN4*oR즰iDEo0:*=@&jvbC^QŜ-n;U*Wb~֔ a^W*3.IĪQX@_FnIkIz::ˬ h}Wm~BT6S(7=a90V/(Gu2eJFުES8/6"v+cWJtRn#k&gQo[RFRB~]s2%Ei> ag7(ֿ"IQ $EQ=fQY$E܁ک^GQb],ߍ$)ZofFYFPdI[QkԖĸс-@}$ʵY}'L16ZԚ'DB|lvSDы2; yiatHb~@Z;V|(y;Td^ZoStD籪FPL?5څJ[XZkhFP &9IR7PkV 1:*5:Q8Iۏ:Ϫ.4:*XFQ _sY_t.ϝ{1mosz umpmQ I#sKxu[LQzY딛׿ 4ȃ }7::v8 B>ڏ6:^d2E@K~n>duהz p/Pw %೘o+>kMΣIb~V[-/>@ev.+)^bt02)ދ{>ot%2ZK5Sb.8aN -|+u~.+(jT] Vk94wx |$I2'"_aAx7|,)Z_ up^SЬnB~|(Q 6=+W^^xLz2[)]"S-Vfxh,ѵJ%VvX )¼(Y"B!B!B!B!B!B!B!B!B!euoIENDB`ruby-async-1.30.1/logo.svg000066400000000000000000002421511410040740200153460ustar00rootroot00000000000000 ASYNC ruby-async-1.30.1/spec/000077500000000000000000000000001410040740200146125ustar00rootroot00000000000000ruby-async-1.30.1/spec/async/000077500000000000000000000000001410040740200157275ustar00rootroot00000000000000ruby-async-1.30.1/spec/async/barrier_spec.rb000066400000000000000000000076421410040740200207250ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2019, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'async/barrier' require 'async/clock' require 'async/rspec' require 'async/semaphore' require_relative 'chainable_async_examples' RSpec.describe Async::Barrier do include_context Async::RSpec::Reactor describe '#async' do let(:repeats) {40} let(:delay) {0.1} it 'should wait for all jobs to complete' do finished = 0 repeats.times.map do |i| subject.async do |task| task.sleep(delay) finished += 1 # This task is a child task but not part of the barrier. task.async do task.sleep(delay*3) end end end expect(subject).to_not be_empty expect(finished).to be < repeats duration = Async::Clock.measure{subject.wait} expect(duration).to be < (delay * 2 * Q) expect(finished).to be == repeats expect(subject).to be_empty end end describe '#wait' do it 'should wait for tasks even after exceptions' do task1 = subject.async do raise "Boom" end task2 = subject.async do end expect(task1).to be_failed expect(task2).to be_finished expect{subject.wait}.to raise_exception(/Boom/) subject.wait until subject.empty? expect(subject).to be_empty end it 'waits for tasks in order' do order = [] 5.times do |i| subject.async do order << i end end subject.wait expect(order).to be == [0, 1, 2, 3, 4] end end describe '#stop' do it "can stop several tasks" do task1 = subject.async do |task| task.sleep(10) end task2 = subject.async do |task| task.sleep(10) end subject.stop expect(task1).to be_stopped expect(task2).to be_stopped end it "can stop several tasks when waiting on barrier" do task1 = subject.async do |task| task.sleep(10) end task2 = subject.async do |task| task.sleep(10) end task3 = reactor.async do subject.wait end subject.stop task1.wait task2.wait expect(task1).to be_stopped expect(task2).to be_stopped task3.wait end it "several tasks can wait on the same barrier" do task1 = subject.async do |task| task.sleep(10) end task2 = reactor.async do |task| subject.wait end task3 = reactor.async do subject.wait end subject.stop task1.wait expect(task1).to be_stopped task2.wait task3.wait end end context 'with semaphore' do let(:capacity) {2} let(:semaphore) {Async::Semaphore.new(capacity)} let(:repeats) {capacity * 2} it 'should execute several tasks and wait using a barrier' do repeats.times do subject.async(parent: semaphore) do |task| task.sleep 0.1 end end expect(subject.size).to be == repeats subject.wait end end it_behaves_like 'chainable async' end ruby-async-1.30.1/spec/async/chainable_async_examples.rb000066400000000000000000000025561410040740200232650ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. RSpec.shared_examples 'chainable async' do let(:parent) {double} subject {described_class.new(parent: parent)} it 'should chain async to parent' do expect(parent).to receive(:async) subject.async do end end end ruby-async-1.30.1/spec/async/clock_spec.rb000066400000000000000000000033231410040740200203620ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2018, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'async/clock' RSpec.describe Async::Clock do it "can measure durations" do duration = Async::Clock.measure do sleep 0.1 end expect(duration).to be_within(0.01 * Q).of(0.1) end it "can get current offset" do expect(Async::Clock.now).to be_kind_of Float end it "can accumulate durations" do 2.times do subject.start! sleep(0.1) subject.stop! end expect(subject.total).to be_within(0.02 * Q).of(0.2) end context 'with given total' do subject {described_class.new(1.5)} it{is_expected.to have_attributes(total: 1.5)} end end ruby-async-1.30.1/spec/async/condition_examples.rb000066400000000000000000000045771410040740200221550ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2018, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. RSpec.shared_examples Async::Condition do it 'can signal waiting task' do state = nil reactor.async do state = :waiting subject.wait state = :resumed end expect(state).to be == :waiting subject.signal reactor.yield expect(state).to be == :resumed end it 'should be able to signal stopped task' do expect(subject.empty?).to be_truthy task = reactor.async do subject.wait end expect(subject.empty?).to be_falsey task.stop subject.signal end it 'resumes tasks in order' do order = [] 5.times do |i| task = reactor.async do subject.wait order << i end end subject.signal reactor.yield expect(order).to be == [0, 1, 2, 3, 4] end context "with timeout" do before do @state = nil end let!(:task) do reactor.async do |task| task.with_timeout(0.1) do begin @state = :waiting subject.wait @state = :signalled rescue Async::TimeoutError @state = :timeout end end end end it 'can timeout while waiting' do task.wait expect(@state).to be == :timeout end it 'can signal while waiting' do subject.signal task.wait expect(@state).to be == :signalled end end end ruby-async-1.30.1/spec/async/condition_spec.rb000066400000000000000000000040331410040740200212540ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'async/rspec' require 'async/condition' require_relative 'condition_examples' RSpec.describe Async::Condition do include_context Async::RSpec::Reactor it 'should continue after condition is signalled' do task = reactor.async do subject.wait end expect(task.status).to be :running # This will cause the task to exit: subject.signal expect(task.status).to be :complete task.stop end it 'can stop nested task' do producer = nil consumer = reactor.async do |task| condition = Async::Condition.new producer = task.async do |subtask| subtask.yield condition.signal subtask.sleep(10) end condition.wait expect do producer.stop end.to_not raise_error end consumer.wait producer.wait expect(producer.status).to be :stopped expect(consumer.status).to be :complete end it_behaves_like Async::Condition end ruby-async-1.30.1/spec/async/logger_spec.rb000066400000000000000000000045061410040740200205520ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'async' require 'async/logger' require 'console/capture' RSpec.describe 'Async.logger' do let(:name) {"nested"} let(:message) {"Talk is cheap. Show me the code."} let(:capture) {Console::Capture.new} let(:logger) {Console::Logger.new(capture, name: name)} it "can use nested logger" do Async(logger: logger) do |task| expect(task.logger).to be == logger logger.warn message end.wait expect(capture.last).to include({ severity: :warn, name: name, subject: message, }) end it "can change nested logger" do Async do |parent| parent.async(logger: logger) do |task| expect(task.logger).to be == logger expect(Async.logger).to be == logger expect(Console.logger).to be == logger end.wait end.wait end it "can use parent logger" do current_logger = Console.logger child = nil Async(logger: logger) do |parent| child = parent.async{|task| task.yield} expect(parent.logger).to be == logger expect(child.logger).to be == logger expect(Async.logger).to be == logger expect(Console.logger).to be == logger end.wait expect(child.logger).to be == logger expect(Console.logger).to be == current_logger end end ruby-async-1.30.1/spec/async/node_spec.rb000066400000000000000000000165521410040740200202240ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'async/node' RSpec.describe Async::Node do describe '#parent=' do let(:child) {Async::Node.new(subject)} it "should construct nested tree" do expect(child.parent).to be subject expect(subject.children).to include(child) end it "should break nested tree" do child.parent = nil expect(child.parent).to be_nil expect(subject.children).to be_nil end it "can consume bottom to top" do child.consume expect(child.parent).to be_nil expect(subject.children).to be_nil end end describe '#print_hierarchy' do let(:buffer) {StringIO.new} let(:output) {buffer.string} let(:lines) {output.lines} let!(:child) {Async::Node.new(subject)} it "can print hierarchy to bufffer" do subject.print_hierarchy(buffer) expect(lines.size).to be 2 expect(lines[0]).to be =~ /#\n/ expect(lines[1]).to be =~ /\t#\n/ end end describe '#consume' do it "can't consume middle node" do middle = Async::Node.new(subject) bottom = Async::Node.new(middle) expect(bottom.parent).to be middle middle.consume expect(bottom.parent).to be middle end it "can consume nodes while enumerating children" do 3.times do Async::Node.new(subject) end children = subject.children.each.to_a expect(children.size).to be == 3 enumerated = [] subject.children.each do |child| child.consume enumerated << child end expect(enumerated).to be == children end it "can consume multiple nodes while enumerating children" do 3.times do Async::Node.new(subject) end children = subject.children.each.to_a expect(children.size).to be == 3 enumerated = [] subject.children.each do |child| # On the first iteration, we consume the first two children: children[0].consume children[1].consume # This would end up appending the first child, and then the third child: enumerated << child end expect(enumerated).to be == [children[0], children[2]] end it "correctly enumerates finished children" do middle = Async::Node.new(subject) bottom = 2.times.map{Async::Node.new(middle)} allow(bottom[0]).to receive(:finished?).and_return(false) allow(bottom[1]).to receive(:finished?).and_return(false) allow(middle).to receive(:finished?).and_return(true) middle.consume expect(subject.children.size).to be == 2 expect(subject.children.each.to_a).to be == bottom end end describe '#annotate' do let(:annotation) {'reticulating splines'} it "should have no annotation by default" do expect(subject.annotation).to be_nil end it 'should output annotation when invoking #to_s' do subject.annotate(annotation) do expect(subject.to_s).to include(annotation) end end it 'can assign annotation' do subject.annotate(annotation) expect(subject.annotation).to be == annotation end end describe '#transient' do it 'can move transient child to parent' do # This example represents a persistent web connection (middle) with a background reader (child). We look at how when that connection goes out of scope, what happens to the child. # subject -> middle -> child (transient) middle = Async::Node.new(subject) child = Async::Node.new(middle, transient: true) expect(child).to be_transient expect(middle).to be_finished allow(child).to receive(:finished?).and_return(false) middle.consume # subject -> child (transient) expect(child.parent).to be subject expect(subject.children).to include(child) expect(subject.children).to_not include(middle) expect(child).to_not be_finished expect(subject).to be_finished expect(child).to receive(:stop) subject.terminate end it 'can move transient sibling to parent' do # This example represents a server task (middle) which has a single task listening on incoming connections (child2), and a transient task which is monitoring those connections/some shared resource (child1). We look at what happens when the server listener finishes. # subject -> middle -> child1 (transient) # -> child2 middle = Async::Node.new(subject, annotation: "middle") child1 = Async::Node.new(middle, transient: true, annotation: "child1") child2 = Async::Node.new(middle, annotation: "child2") allow(child1).to receive(:finished?).and_return(false) middle.consume # subject -> middle -> child1 (transient) # -> child2 expect(child1.parent).to be middle expect(child2.parent).to be middle expect(middle.parent).to be subject expect(subject.children).to include(middle) expect(middle.children).to include(child1) expect(middle.children).to include(child2) child2.consume # subject -> child1 (transient) expect(child1.parent).to be subject expect(child2.parent).to be_nil expect(middle.parent).to be_nil expect(subject.children).to include(child1) expect(middle.children).to be_nil end it 'ignores non-transient children of transient parent' do # subject -> middle (transient) -> child middle = Async::Node.new(subject, transient: true) child = Async::Node.new(middle) allow(middle).to receive(:finished?).and_return(false) child.consume # subject -> middle (transient) expect(child.parent).to be_nil expect(middle.parent).to be subject expect(subject.children).to include(middle) expect(middle.children).to be_nil end it 'does not stop child transient tasks' do middle = Async::Node.new(subject, annotation: "middle") child1 = Async::Node.new(middle, transient: true, annotation: "child1") child2 = Async::Node.new(middle, annotation: "child2") expect(child1).to_not receive(:stop) expect(child2).to receive(:stop) subject.stop end end describe '#terminate' do it 'stops all tasks' do middle = Async::Node.new(subject, annotation: "middle") child1 = Async::Node.new(middle, transient: true, annotation: "child1") child2 = Async::Node.new(middle, annotation: "child2") expect(child1).to receive(:stop).at_least(:once) expect(child2).to receive(:stop).at_least(:once) subject.terminate end end end ruby-async-1.30.1/spec/async/notification_spec.rb000066400000000000000000000036531410040740200217630ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'async/rspec' require 'async/notification' require_relative 'condition_examples' RSpec.describe Async::Notification do include_context Async::RSpec::Reactor it 'should continue after notification is signalled' do sequence = [] task = reactor.async do sequence << :waiting subject.wait sequence << :resumed end expect(task.status).to be :running sequence << :running # This will cause the task to exit: subject.signal sequence << :signalled expect(task.status).to be :running sequence << :yielding reactor.yield sequence << :finished expect(task.status).to be :complete expect(sequence).to be == [ :waiting, :running, :signalled, :yielding, :resumed, :finished ] end it_behaves_like Async::Condition end ruby-async-1.30.1/spec/async/performance_spec.rb000066400000000000000000000041051410040740200215670ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2019, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'benchmark/ips' require 'async' RSpec.describe Async::Wrapper do let(:pipe) {IO.pipe} after do pipe.each(&:close) end let(:input) {described_class.new(pipe.first)} let(:output) {described_class.new(pipe.last)} it "should be fast to wait until readable" do Benchmark.ips do |x| x.report('Wrapper#wait_readable') do |repeats| Async do |task| input = Async::Wrapper.new(pipe.first, task.reactor) output = pipe.last repeats.times do output.write(".") input.wait_readable input.io.read(1) end input.reactor = nil end end x.report('Reactor#register') do |repeats| Async do |task| input = pipe.first monitor = task.reactor.register(input, :r) output = pipe.last repeats.times do output.write(".") Async::Task.yield input.read(1) end monitor.close end end x.compare! end end end ruby-async-1.30.1/spec/async/queue_spec.rb000066400000000000000000000064141410040740200204170ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'async' require 'async/queue' require 'async/rspec' require 'async/semaphore' require_relative 'condition_examples' require_relative 'chainable_async_examples' RSpec.shared_context Async::Queue do it 'should process items in order' do reactor.async do |task| 10.times do |i| task.sleep(0.001) subject.enqueue(i) end end 10.times do |j| expect(subject.dequeue).to be == j end end it 'can dequeue items asynchronously' do reactor.async do |task| subject << 1 subject << nil end subject.async do |task, item| expect(item).to be 1 end end describe '#size' do it 'returns queue size' do reactor.async do |task| 10.times do |i| subject.enqueue(i) expect(subject.size).to be i + 1 end end end end context 'with an empty queue' do it {is_expected.to be_empty} end context 'with semaphore' do let(:capacity) {2} let(:semaphore) {Async::Semaphore.new(capacity)} let(:repeats) {capacity * 2} it 'should process several items limited by a semaphore' do count = 0 Async do repeats.times do subject.enqueue :item end subject.enqueue nil end subject.async(parent: semaphore) do |task| count += 1 end expect(count).to be == repeats end end it_behaves_like 'chainable async' do before do subject.enqueue(:item) # The limited queue may block. Async do subject.enqueue(nil) end end end end RSpec.describe Async::Queue do include_context Async::RSpec::Reactor it_behaves_like Async::Queue end RSpec.describe Async::LimitedQueue do include_context Async::RSpec::Reactor it_behaves_like Async::Queue it 'should become limited' do expect(subject).to_not be_limited subject.enqueue(10) expect(subject).to be_limited end it 'should resume waiting tasks in order' do total_resumed = 0 total_dequeued = 0 Async do |producer| 10.times do producer.async do subject.enqueue('foo') total_resumed += 1 end end end 10.times do item = subject.dequeue total_dequeued += 1 expect(total_resumed).to be == total_dequeued end end end ruby-async-1.30.1/spec/async/reactor/000077500000000000000000000000001410040740200173665ustar00rootroot00000000000000ruby-async-1.30.1/spec/async/reactor/nested_spec.rb000066400000000000000000000034731410040740200222160ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. RSpec.describe Async::Reactor do describe '::run (in existing reactor)' do include_context Async::RSpec::Reactor it "should nest reactor" do outer_reactor = Async::Task.current.reactor inner_reactor = nil described_class.run do |task| inner_reactor = task.reactor end expect(outer_reactor).to be_kind_of(described_class) expect(outer_reactor).to be_eql(inner_reactor) end end describe '::run' do it "should nest reactor" do expect(Async::Task.current?).to be_nil inner_reactor = nil described_class.run do |task| inner_reactor = task.reactor end expect(inner_reactor).to be_kind_of(described_class) end end end ruby-async-1.30.1/spec/async/reactor_spec.rb000066400000000000000000000132321410040740200207260ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'async' require 'async/rspec/reactor' require 'benchmark/ips' RSpec.describe Async::Reactor do describe '#run' do it "can run tasks on different fibers" do outer_fiber = Fiber.current inner_fiber = nil described_class.run do |task| task.sleep(0) inner_fiber = Fiber.current end expect(inner_fiber).to_not be nil expect(outer_fiber).to_not be == inner_fiber end end describe '#close' do it "can close empty reactor" do subject.close expect(subject).to be_closed end end describe '#run_once' do it "can run the reactor" do # Run the reactor for 1 second: task = subject.async do |task| task.yield end expect(task).to be_running # This will resume the task, and then the reactor will be finished. expect(subject.run_once).to be false expect(task).to be_finished end it "can run one iteration" do state = nil subject.async do |task| state = :started task.yield state = :finished end expect(state).to be :started subject.run_once expect(state).to be :finished end end describe '#print_hierarchy' do it "can print hierarchy" do subject.async do |parent| parent.async do |child| child.sleep 1 end parent.sleep 1 end output = StringIO.new subject.print_hierarchy(output, backtrace: false) lines = output.string.lines expect(lines[0]).to be =~ /# # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'async/scheduler' RSpec.describe Async::Scheduler, if: Async::Scheduler.supported? do include_context Async::RSpec::Reactor describe ::ConditionVariable do let(:mutex) {Mutex.new} let(:condition) {ConditionVariable.new} let(:timeout) {5.0} it "can signal between tasks" do time_taken = nil waiter = reactor.async do mutex.synchronize do time_taken = Async::Clock.measure do condition.wait(mutex, timeout) end end end signaller = reactor.async do mutex.synchronize do condition.signal end end signaller.wait waiter.wait expect(time_taken).to be < timeout end end end ruby-async-1.30.1/spec/async/scheduler/io_spec.rb000066400000000000000000000026561410040740200216640ustar00rootroot00000000000000# Copyright, 2021, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'async/scheduler' RSpec.describe Async::Scheduler, if: Async::Scheduler.supported? do include_context Async::RSpec::Reactor describe ::IO do it "can wait with timeout" do s1, s2 = Socket.pair :UNIX, :STREAM, 0 expect(s1.wait_readable(0)).to be nil ensure s1.close s2.close end end end ruby-async-1.30.1/spec/async/scheduler/thread_spec.rb000066400000000000000000000032761410040740200225230ustar00rootroot00000000000000# Copyright, 2021, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'async/scheduler' RSpec.describe Async::Scheduler, if: Async::Scheduler.supported? do include_context Async::RSpec::Reactor describe ::Thread do it "can wait for value" do value = Thread.new do sleep(0) :value end.value expect(value).to be == :value end it "can propagate exception" do thread = nil task = Async do begin thread = Thread.new do sleep end thread.join ensure thread.kill thread.join end end task.stop task.wait expect(thread).to_not be_alive end end end ruby-async-1.30.1/spec/async/scheduler_spec.rb000066400000000000000000000062611410040740200212510ustar00rootroot00000000000000# Copyright, 2020, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'async/scheduler' require 'async/barrier' require 'net/http' RSpec.describe Async::Scheduler, if: Async::Scheduler.supported? do include_context Async::RSpec::Reactor it "can intercept sleep" do expect(reactor.scheduler).to receive(:kernel_sleep).with(0.001) sleep(0.001) end describe 'Fiber.schedule' do it "can start child task" do fiber = Async do Fiber.schedule{} end.wait expect(fiber).to_not be_nil expect(fiber).to be_kind_of(Fiber) end end describe 'Process.wait' do it "can wait on child process" do expect(reactor.scheduler).to receive(:process_wait).and_call_original pid = ::Process.spawn("true") _, status = Process.wait2(pid) expect(status).to be_success end end describe 'IO.pipe' do let(:message) {"Helloooooo World!"} it "can send message via pipe" do input, output = IO.pipe reactor.async do sleep(0.001) message.each_char do |character| output.write(character) end output.close end expect(input.read).to be == message input.close end it "can fetch website using Net::HTTP" do barrier = Async::Barrier.new events = [] 3.times do |i| barrier.async do events << i response = Net::HTTP.get(URI "https://www.codeotaku.com/index") expect(response).to_not be_nil events << i end end barrier.wait # The requests all get started concurrently: expect(events.first(3)).to be == [0, 1, 2] end end context "with thread" do let(:queue) {Thread::Queue.new} subject {Thread.new{queue.pop}} it "can join thread" do waiting = 0 3.times do Async do waiting += 1 subject.join waiting -= 1 end end expect(waiting).to be == 3 queue.close end end context "with queue" do subject {Thread::Queue.new} let(:item) {"Hello World"} it "can pass items between thread and fiber" do Async do expect(subject.pop).to be == item end Thread.new do expect(Fiber).to be_blocking subject.push(item) end.join end end end ruby-async-1.30.1/spec/async/semaphore_spec.rb000066400000000000000000000100641410040740200212520ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2018, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'async/semaphore' require 'async/barrier' require 'async/rspec' require_relative 'chainable_async_examples' RSpec.describe Async::Semaphore do include_context Async::RSpec::Reactor context '#async' do let(:repeats) {40} let(:limit) {4} it 'should process work in batches' do semaphore = Async::Semaphore.new(limit) current, maximum = 0, 0 result = repeats.times.map do |i| semaphore.async do |task| current += 1 maximum = [current, maximum].max task.sleep(rand * 0.1) current -= 1 i end end.collect(&:result) # Verify that the maximum number of concurrent tasks was the specificed limit: expect(maximum).to be == limit # Verify that the results were in the correct order: expect(result).to be == (0...repeats).to_a end it 'only allows one task at a time' do semaphore = Async::Semaphore.new(1) order = [] 3.times.map do |i| semaphore.async do |task| order << i task.sleep(0.1) order << i end end.collect(&:result) expect(order).to be == [0, 0, 1, 1, 2, 2] end it 'allows tasks to execute concurrently' do semaphore = Async::Semaphore.new(3) order = [] 3.times.map do |i| semaphore.async do |task| order << i task.sleep(0.1) order << i end end.collect(&:result) expect(order).to be == [0, 1, 2, 0, 1, 2] end end context '#waiting' do subject {Async::Semaphore.new(0)} it 'handles exceptions thrown while waiting' do expect do reactor.with_timeout(0.1) do subject.acquire do end end end.to raise_error(Async::TimeoutError) expect(subject.waiting).to be_empty end end context '#count' do it 'should count number of current acquisitions' do expect(subject.count).to be == 0 subject.acquire do expect(subject.count).to be == 1 end end end context '#limit' do it 'should have a default limit' do expect(subject.limit).to be == 1 end end context '#empty?' do it 'should be empty unless acquired' do expect(subject).to be_empty subject.acquire do expect(subject).to_not be_empty end end end context '#blocking?' do it 'will be blocking when acquired' do expect(subject).to_not be_blocking subject.acquire do expect(subject).to be_blocking end end end context '#acquire/#release' do it 'works when called without block' do subject.acquire expect(subject.count).to be == 1 subject.release expect(subject.count).to be == 0 end end context 'with barrier' do let(:capacity) {2} let(:barrier) {Async::Barrier.new} let(:repeats) {capacity * 2} it 'should execute several tasks and wait using a barrier' do repeats.times do subject.async(parent: barrier) do |task| task.sleep 0.1 end end expect(barrier.size).to be == repeats barrier.wait end end it_behaves_like 'chainable async' end ruby-async-1.30.1/spec/async/task_spec.rb000066400000000000000000000243221410040740200202330ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'async' require 'async/clock' RSpec.describe Async::Task do let(:reactor) {Async::Reactor.new} describe '#run' do it "can't be invoked twice" do task = reactor.async do |task| end expect{task.run}.to raise_exception(RuntimeError, /already running/) end end describe '#current?' do it "can check if it is the currently running task" do task = reactor.async do |task| expect(task).to be_current task.sleep(0.1) end expect(task).to_not be_current end end describe '#async' do it "can start child async tasks" do child = nil parent = reactor.async do |task| child = task.async do task.sleep(1) end end expect(parent).to_not be_nil expect(child).to_not be_nil expect(child.parent).to_not be_nil end it "can pass in arguments" do reactor.async do |task| task.async(:arg) do |task, arg| expect(arg).to be == :arg end.wait end.wait end it "can set initial annotation" do reactor.async(annotation: "Hello World") do |task| expect(task.annotation).to be == "Hello World" end.wait end it "can raise exceptions" do expect do reactor.async do |task| raise "boom" end.wait end.to raise_exception RuntimeError, /boom/ end it "can raise exception after asynchronous operation" do task = nil expect do task = reactor.async do |task| task.sleep 0.1 raise "boom" end end.to_not raise_exception reactor.run do expect do task.wait end.to raise_exception RuntimeError, /boom/ end end it "can consume exceptions" do task = nil expect do task = reactor.async do |task| raise "boom" end end.to_not raise_exception expect do task.wait end.to raise_exception RuntimeError, /boom/ end it "won't consume non-StandardError exceptions" do expect do reactor.async do |task| raise SignalException.new(:TERM) end end.to raise_exception(SignalException, /TERM/) end end describe '#yield' do it "can yield back to reactor" do state = nil reactor.async do |task| state = :started task.yield state = :finished end reactor.run expect(state).to be == :finished end end describe '#stop' do it "can be stopped" do state = nil task = reactor.async do |task| state = :started task.sleep(10) state = :finished end task.stop expect(state).to be == :started expect(task).to be_stopped end it "can stop nested tasks with exception handling" do task = reactor.async do |task| child = task.async do |subtask| subtask.sleep(1) end begin child.wait ensure child.stop end end subtask = task.children.first task.stop expect(task.status).to be :stopped expect(subtask.status).to be :stopped end it "can stop current task" do state = nil task = reactor.async do |task| state = :started task.stop state = :finished end expect(state).to be == :started expect(task).to be_stopped end it "can stop current task using exception" do state = nil task = reactor.async do |task| state = :started raise Async::Stop, "I'm finished." state = :finished end expect(state).to be == :started expect(task).to be_stopped end it "should stop direct child" do parent_task = child_task = nil reactor.async do |task| parent_task = task reactor.async do |task| child_task = task task.sleep(10) end task.sleep(10) end expect(parent_task).to_not be_nil expect(child_task).to_not be_nil expect(parent_task.fiber).to be_alive expect(child_task.fiber).to be_alive parent_task.stop expect(parent_task).to_not be_alive expect(child_task).to_not be_alive end it "can stop nested parent" do parent_task = nil children_tasks = [] reactor.async do |task| parent_task = task reactor.async do |task| children_tasks << task task.sleep(2) end reactor.async do |task| children_tasks << task task.sleep(1) parent_task.stop end reactor.async do |task| children_tasks << task task.sleep(2) end end reactor.run expect(parent_task).to_not be_alive end it "should not remove running task" do top_task = middle_task = bottom_task = nil top_task = reactor.async do |task| middle_task = reactor.async do |task| bottom_task = reactor.async do |task| task.sleep(10) end task.sleep(10) end task.sleep(10) end bottom_task.stop expect(top_task.children).to include(middle_task) end it "can stop resumed task" do items = [1, 2, 3] Async do condition = Async::Condition.new producer = Async do |subtask| while item = items.pop subtask.yield # (1) Fiber.yield, (3) Reactor -> producer.resume condition.signal(item) # (4) consumer.resume(value) end end value = condition.wait # (2) value = Fiber.yield expect(value).to be == 3 producer.stop # (5) [producer is resumed already] producer.stop end expect(items).to be == [1] end end describe '#sleep' do let(:duration) {0.01} it "can sleep for the requested duration" do state = nil reactor.async do |task| task.sleep(duration) state = :finished end time = Async::Clock.measure do reactor.run end # This is too unstable on travis. # expect(time).to be_within(50).percent_of(duration) expect(time).to be >= duration expect(state).to be == :finished end end describe '#with_timeout' do it "can extend timeout" do reactor.async do |task| task.with_timeout(0.2) do |timer| task.sleep(0.1) expect(timer.fires_in).to be_within(10 * Q).percent_of(0.1) timer.reset expect(timer.fires_in).to be_within(10 * Q).percent_of(0.2) end end reactor.run end it "will timeout if execution takes too long" do state = nil reactor.async do |task| begin task.with_timeout(0.01) do state = :started task.sleep(10) state = :finished end rescue Async::TimeoutError state = :timeout end end reactor.run expect(state).to be == :timeout end it "won't timeout if execution completes in time" do state = nil reactor.async do |task| state = :started task.with_timeout(0.01) do task.sleep(0.001) state = :finished end end reactor.run expect(state).to be == :finished end def sleep_forever while true Async::Task.current.sleep(1) end end it "contains useful backtrace" do task = Async do |task| task.with_timeout(1.0) do sleep_forever end end expect{task.wait}.to raise_error(Async::TimeoutError) # TODO replace this with task.result task.wait rescue error = $! expect(error.backtrace).to include(/sleep_forever/) end end describe '#backtrace', if: Fiber.current.respond_to?(:backtrace) do it "has a backtrace" do Async do task = Async do |task| task.sleep(1) end expect(task.backtrace).to include(/sleep/) task.stop end end context "finished task" do it "has no backtrace" do task = Async{} expect(task.backtrace).to be_nil end end end describe '#wait' do it "will wait on another task to complete" do apples_task = reactor.async do |task| task.sleep(0.1) :apples end oranges_task = reactor.async do |task| task.sleep(0.01) :oranges end fruit_salad = reactor.async do |task| [apples_task.wait, oranges_task.wait] end reactor.run expect(fruit_salad.wait).to be == [:apples, :oranges] end it "will raise exceptions when checking result" do error_task = nil error_task = reactor.async do |task| raise RuntimeError, "brain not provided" end expect do error_task.wait end.to raise_exception(RuntimeError, /brain/) end it "will propagate exceptions after async operation" do error_task = nil error_task = reactor.async do |task| task.sleep(0.1) raise "boom" end innocent_task = reactor.async do |task| expect{error_task.wait}.to raise_exception RuntimeError, /boom/ end expect do reactor.run end.to_not raise_exception expect(error_task).to be_finished expect(innocent_task).to be_finished end end describe '#children' do it "enumerates children in same order they are created" do tasks = 10.times.map do |i| reactor.async(annotation: "Task #{i}") {|task| task.sleep(1)} end expect(reactor.children.each.to_a).to be == tasks end end describe '#to_s' do it "should show running" do apples_task = reactor.async do |task| task.sleep(0.1) end expect(apples_task.to_s).to include "running" end it "should show complete" do apples_task = reactor.async do |task| end expect(apples_task.to_s).to include "complete" end end end ruby-async-1.30.1/spec/async/wrapper_spec.rb000066400000000000000000000114341410040740200207510ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require "async/rspec" RSpec.describe Async::Wrapper do include_context Async::RSpec::Reactor let(:pipe) {IO.pipe} let(:input) {Async::Wrapper.new(pipe.last)} let(:output) {Async::Wrapper.new(pipe.first)} after(:each) do input.close unless input.closed? output.close unless output.closed? expect(input.monitor).to be_nil expect(output.monitor).to be_nil end describe '#wait_readable' do it "can wait to be readable" do reader = reactor.async do expect(output.wait_readable).to be_truthy end input.io.write('Hello World') reader.wait end it "can timeout if no event occurs" do expect do output.wait_readable(0.1) end.to raise_exception(Async::TimeoutError) end it "can wait for readability in sequential tasks" do reactor.async do input.wait_writable(1) input.io.write('Hello World') end 2.times do reactor.async do expect(output.wait_readable(1)).to be_truthy end.wait end end it "can be cancelled" do reactor.async do expect do output.wait_readable end.to raise_exception(Async::Wrapper::Cancelled) end expect(output.monitor).to_not be_nil end end describe '#wait_writable' do it "can wait to be writable" do expect(input.wait_writable).to be_truthy end it "can be cancelled while waiting to be readable" do reactor.async do expect do input.wait_readable end.to raise_exception(Async::Wrapper::Cancelled) end # This reproduces the race condition that can occur if two tasks are resumed in sequence. # Resume task 1 which closes IO: output.close # Resume task 2: expect do output.resume end.to_not raise_exception end it "can be cancelled" do reactor.async do expect do input.wait_readable end.to raise_exception(Async::Wrapper::Cancelled) end expect(input.monitor).to_not be_nil end end describe "#wait_any" do it "can wait for any events" do reactor.async do input.wait_any(1) input.io.write('Hello World') end expect(output.wait_readable(1)).to be_truthy end it "can wait for readability in one task and writability in another" do reactor.async do expect do input.wait_readable(1) end.to raise_exception(Async::Wrapper::Cancelled) end expect(input.monitor.interests).to be == :r reactor.async do input.wait_writable input.close output.close end.wait end it "fails if waiting on from multiple tasks" do input.reactor = reactor reactor.async do expect do input.wait_readable end.to raise_exception(Async::Wrapper::Cancelled) end expect(input.monitor.interests).to be == :r reactor.async do expect do input.wait_readable end.to raise_exception(Async::Wrapper::WaitError) end end end describe '#reactor=' do it 'can assign a wrapper to a reactor' do input.reactor = reactor expect(input.reactor).to be == reactor end it 'assigns current reactor when waiting for events' do input.wait_writable expect(input.reactor).to be == reactor end end describe '#dup' do let(:dup) {input.dup} it 'dups the underlying io' do expect(dup.io).to_not eq input.io dup.close expect(input).to_not be_closed end end describe '#close' do it "closes monitor when closing wrapper" do input.wait_writable expect(input.monitor).to_not be_nil input.close expect(input.monitor).to be_nil end it "can't wait on closed wrapper" do input.close output.close expect do output.wait_readable end.to raise_exception(IOError, /closed stream/) end end end ruby-async-1.30.1/spec/async_spec.rb000066400000000000000000000025021410040740200172650ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2019, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'async' RSpec.describe Async do describe '.run' do it "can run an asynchronous task" do Async.run do |task| expect(task).to be_a Async::Task end end end end ruby-async-1.30.1/spec/enumerator_spec.rb000066400000000000000000000050311410040740200203310ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2018, by Samuel G. D. Williams. # Copyright, 2018, by Sokolov Yura aka funny-falcon # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. RSpec.describe Enumerator do def some_yielder(task) yield 1 task.sleep(0.002) yield 2 end def enum(task) to_enum(:some_yielder, task) end it "should play well with Enumerator as internal iterator" do # no fiber really used in internal iterator, # but let this test be here for completness ar = nil Async do |task| ar = enum(task).to_a end expect(ar).to be == [1, 2] end it "should play well with Enumerator as external iterator", pending: "expected failure" do ar = [] Async do |task| en = enum(task) ar << en.next ar << en.next ar << begin en.next rescue $! end end expect(ar[0]).to be == 1 expect(ar[1]).to be == 2 expect(ar[2]).to be_a StopIteration end it "should play well with Enumerator.zip(Enumerator) method", pending: "expected failure" do Async do |task| ar = [:a, :b, :c, :d].each.zip(enum(task)) expect(ar).to be == [[:a, 1], [:b, 2], [:c, nil], [:d, nil]] end.wait end it "should play with explicit Fiber usage", pending: "expected failure" do ar = [] Async do |task| fib = Fiber.new { Fiber.yield 1 task.sleep(0.002) Fiber.yield 2 } ar << fib.resume ar << fib.resume ar << fib.resume ar << begin en.next rescue $! end end expect(ar[0]).to be == 1 expect(ar[1]).to be == 2 expect(ar[2]).to be nil expect(ar[3]).to be_a FiberError end endruby-async-1.30.1/spec/kernel/000077500000000000000000000000001410040740200160725ustar00rootroot00000000000000ruby-async-1.30.1/spec/kernel/async_spec.rb000066400000000000000000000027151410040740200205530ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2019, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'kernel/async' RSpec.describe Async do describe '#Async' do it "can run an asynchronous task" do Async do |task| expect(task).to be_a Async::Task end end it "passes options through to initial task" do Async(transient: true) do |task| expect(task).to be_transient end end end end ruby-async-1.30.1/spec/kernel/sync_spec.rb000066400000000000000000000037211410040740200204100ustar00rootroot00000000000000# frozen_string_literal: true # Copyright, 2019, by Samuel G. D. Williams. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'kernel/async' require 'kernel/sync' RSpec.describe Kernel do describe '#Sync' do let(:value) {10} it "can run a synchronous task" do result = Sync do |task| expect(Async::Task.current).to_not be nil expect(Async::Task.current).to be task next value end expect(result).to be == value end it "can run inside reactor" do Async do |task| result = Sync do |sync_task| expect(Async::Task.current).to be task expect(sync_task).to be task next value end expect(result).to be == value end end it "can propagate error without logging them" do expect(Console.logger).to_not receive(:error) expect do Sync do raise StandardError, "brain not provided" end end.to raise_exception(StandardError, /brain/) end end end ruby-async-1.30.1/spec/spec_helper.rb000066400000000000000000000005301410040740200174260ustar00rootroot00000000000000# frozen_string_literal: true require 'async/rspec' require 'covered/rspec' if RUBY_PLATFORM =~ /darwin/ Q = 20 else Q = 1 end RSpec.configure do |config| # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = ".rspec_status" config.expect_with :rspec do |c| c.syntax = :expect end end