├── .editorconfig ├── .github └── workflows │ ├── documentation-coverage.yaml │ ├── documentation.yaml │ ├── rubocop.yaml │ ├── test-coverage.yaml │ ├── test-external.yaml │ └── test.yaml ├── .gitignore ├── .rubocop.yml ├── async-await.gemspec ├── bake.rb ├── config └── sus.rb ├── examples ├── chickens.rb ├── echo.rb ├── port_scanner │ ├── README.md │ ├── port_scanner.go │ ├── port_scanner.py │ └── port_scanner.rb └── sleep_sort.rb ├── gems.rb ├── guides └── getting-started │ └── readme.md ├── lib └── async │ ├── await.rb │ └── await │ ├── enumerable.rb │ └── version.rb ├── license.md ├── readme.md ├── release.cert └── test └── async ├── await.rb └── await └── enumerable.rb /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | 7 | [*.{yml,yaml}] 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.github/workflows/documentation-coverage.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation Coverage 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | COVERAGE: PartialSummary 11 | 12 | jobs: 13 | validate: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: "3.4" 21 | bundler-cache: true 22 | 23 | - name: Validate coverage 24 | timeout-minutes: 5 25 | run: bundle exec bake decode:index:coverage lib 26 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages: 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | # Allow one concurrent deployment: 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: true 18 | 19 | env: 20 | CONSOLE_OUTPUT: XTerm 21 | BUNDLE_WITH: maintenance 22 | 23 | jobs: 24 | generate: 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - uses: ruby/setup-ruby@v1 31 | with: 32 | ruby-version: "3.4" 33 | bundler-cache: true 34 | 35 | - name: Installing packages 36 | run: sudo apt-get install wget 37 | 38 | - name: Generate documentation 39 | timeout-minutes: 5 40 | run: bundle exec bake utopia:project:static --force no 41 | 42 | - name: Upload documentation artifact 43 | uses: actions/upload-pages-artifact@v3 44 | with: 45 | path: docs 46 | 47 | deploy: 48 | runs-on: ubuntu-latest 49 | 50 | environment: 51 | name: github-pages 52 | url: ${{steps.deployment.outputs.page_url}} 53 | 54 | needs: generate 55 | steps: 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v4 59 | -------------------------------------------------------------------------------- /.github/workflows/rubocop.yaml: -------------------------------------------------------------------------------- 1 | name: RuboCop 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | 11 | jobs: 12 | check: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: ruby/setup-ruby@v1 18 | with: 19 | ruby-version: ruby 20 | bundler-cache: true 21 | 22 | - name: Run RuboCop 23 | timeout-minutes: 10 24 | run: bundle exec rubocop 25 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | name: Test Coverage 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | COVERAGE: PartialSummary 11 | 12 | jobs: 13 | test: 14 | name: ${{matrix.ruby}} on ${{matrix.os}} 15 | runs-on: ${{matrix.os}}-latest 16 | 17 | strategy: 18 | matrix: 19 | os: 20 | - ubuntu 21 | - macos 22 | 23 | ruby: 24 | - "3.4" 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{matrix.ruby}} 31 | bundler-cache: true 32 | 33 | - name: Run tests 34 | timeout-minutes: 5 35 | run: bundle exec bake test 36 | 37 | - uses: actions/upload-artifact@v4 38 | with: 39 | include-hidden-files: true 40 | if-no-files-found: error 41 | name: coverage-${{matrix.os}}-${{matrix.ruby}} 42 | path: .covered.db 43 | 44 | validate: 45 | needs: test 46 | runs-on: ubuntu-latest 47 | 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: ruby/setup-ruby@v1 51 | with: 52 | ruby-version: "3.4" 53 | bundler-cache: true 54 | 55 | - uses: actions/download-artifact@v4 56 | 57 | - name: Validate coverage 58 | timeout-minutes: 5 59 | run: bundle exec bake covered:validate --paths */.covered.db \; 60 | -------------------------------------------------------------------------------- /.github/workflows/test-external.yaml: -------------------------------------------------------------------------------- 1 | name: Test External 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | 11 | jobs: 12 | test: 13 | name: ${{matrix.ruby}} on ${{matrix.os}} 14 | runs-on: ${{matrix.os}}-latest 15 | 16 | strategy: 17 | matrix: 18 | os: 19 | - ubuntu 20 | - macos 21 | 22 | ruby: 23 | - "3.1" 24 | - "3.2" 25 | - "3.3" 26 | - "3.4" 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: ruby/setup-ruby@v1 31 | with: 32 | ruby-version: ${{matrix.ruby}} 33 | bundler-cache: true 34 | 35 | - name: Run tests 36 | timeout-minutes: 10 37 | run: bundle exec bake test:external 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | 11 | jobs: 12 | test: 13 | name: ${{matrix.ruby}} on ${{matrix.os}} 14 | runs-on: ${{matrix.os}}-latest 15 | continue-on-error: ${{matrix.experimental}} 16 | 17 | strategy: 18 | matrix: 19 | os: 20 | - ubuntu 21 | - macos 22 | 23 | ruby: 24 | - "3.1" 25 | - "3.2" 26 | - "3.3" 27 | - "3.4" 28 | 29 | experimental: [false] 30 | 31 | include: 32 | - os: ubuntu 33 | ruby: truffleruby 34 | experimental: true 35 | - os: ubuntu 36 | ruby: jruby 37 | experimental: true 38 | - os: ubuntu 39 | ruby: head 40 | experimental: true 41 | 42 | steps: 43 | - uses: actions/checkout@v4 44 | - uses: ruby/setup-ruby@v1 45 | with: 46 | ruby-version: ${{matrix.ruby}} 47 | bundler-cache: true 48 | 49 | - name: Run tests 50 | timeout-minutes: 10 51 | run: bundle exec bake test 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /pkg/ 3 | /gems.locked 4 | /.covered.db 5 | /external 6 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | DisabledByDefault: true 3 | 4 | Layout/IndentationStyle: 5 | Enabled: true 6 | EnforcedStyle: tabs 7 | 8 | Layout/InitialIndentation: 9 | Enabled: true 10 | 11 | Layout/IndentationWidth: 12 | Enabled: true 13 | Width: 1 14 | 15 | Layout/IndentationConsistency: 16 | Enabled: true 17 | EnforcedStyle: normal 18 | 19 | Layout/BlockAlignment: 20 | Enabled: true 21 | 22 | Layout/EndAlignment: 23 | Enabled: true 24 | EnforcedStyleAlignWith: start_of_line 25 | 26 | Layout/BeginEndAlignment: 27 | Enabled: true 28 | EnforcedStyleAlignWith: start_of_line 29 | 30 | Layout/ElseAlignment: 31 | Enabled: true 32 | 33 | Layout/DefEndAlignment: 34 | Enabled: true 35 | 36 | Layout/CaseIndentation: 37 | Enabled: true 38 | 39 | Layout/CommentIndentation: 40 | Enabled: true 41 | 42 | Layout/EmptyLinesAroundClassBody: 43 | Enabled: true 44 | 45 | Layout/EmptyLinesAroundModuleBody: 46 | Enabled: true 47 | 48 | Style/FrozenStringLiteralComment: 49 | Enabled: true 50 | 51 | Style/StringLiterals: 52 | Enabled: true 53 | EnforcedStyle: double_quotes 54 | -------------------------------------------------------------------------------- /async-await.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/async/await/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "async-await" 7 | spec.version = Async::Await::VERSION 8 | 9 | spec.summary = "Implements the async/await pattern on top of async :)" 10 | spec.authors = ["Samuel Williams", "Kent 'picat' Gruber", "Olle Jonsson"] 11 | spec.license = "MIT" 12 | 13 | spec.cert_chain = ["release.cert"] 14 | spec.signing_key = File.expand_path("~/.gem/release.pem") 15 | 16 | spec.homepage = "https://github.com/socketry/async-await" 17 | 18 | spec.metadata = { 19 | "documentation_uri" => "https://socketry.github.io/async-await/", 20 | "source_code_uri" => "https://github.com/socketry/async-await.git", 21 | } 22 | 23 | spec.files = Dir.glob(["{lib}/**/*", "*.md"], File::FNM_DOTMATCH, base: __dir__) 24 | 25 | spec.required_ruby_version = ">= 3.1" 26 | 27 | spec.add_dependency "async" 28 | end 29 | -------------------------------------------------------------------------------- /bake.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2025, by Samuel Williams. 5 | 6 | # Update the project documentation with the new version number. 7 | # 8 | # @parameter version [String] The new version number. 9 | def after_gem_release_version_increment(version) 10 | context["releases:update"].call(version) 11 | context["utopia:project:readme:update"].call 12 | end 13 | -------------------------------------------------------------------------------- /config/sus.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024-2025, by Samuel Williams. 5 | 6 | require "covered/sus" 7 | include Covered::Sus 8 | -------------------------------------------------------------------------------- /examples/chickens.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2025, by Samuel Williams. 5 | 6 | require_relative "../lib/async/await" 7 | 8 | class Coop 9 | include Async::Await 10 | 11 | async def count_chickens(area_name) 12 | 3.times do |i| 13 | sleep rand 14 | 15 | puts "Found a chicken in the #{area_name}!" 16 | end 17 | end 18 | 19 | async def find_chicken(areas) 20 | puts "Searching for chicken..." 21 | 22 | sleep rand * 5 23 | 24 | return areas.sample 25 | end 26 | 27 | async def count_all_chickens 28 | # These methods all run at the same time. 29 | count_chickens("garden") 30 | count_chickens("house") 31 | count_chickens("tree") 32 | 33 | # Wait for all previous async work to complete... 34 | barrier! 35 | 36 | puts "There was a chicken in the #{find_chicken(["garden", "house", "tree"]).wait}" 37 | end 38 | end 39 | 40 | coop = Coop.new 41 | coop.count_all_chickens 42 | -------------------------------------------------------------------------------- /examples/echo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2025, by Samuel Williams. 5 | 6 | require_relative "../lib/async/await" 7 | 8 | require "async/io" 9 | require "async/io/tcp_socket" 10 | 11 | require "pry" 12 | 13 | class Echo 14 | include Async::Await 15 | include Async::IO 16 | 17 | async def handle(peer, address) 18 | data = peer.gets 19 | peer.puts("#{data} #{Time.now}") 20 | ensure 21 | peer.close 22 | end 23 | 24 | async def server 25 | puts "Binding server..." 26 | server = TCPServer.new("127.0.0.1", 9009) 27 | 28 | handle(*server.accept) 29 | ensure 30 | server.close rescue nil 31 | end 32 | 33 | async def client 34 | puts "Client connecting..." 35 | client = TCPSocket.new("127.0.0.1", 9009) 36 | 37 | client.puts("Hello World!") 38 | response = client.gets 39 | 40 | puts "Server said: #{response}" 41 | ensure 42 | client.close rescue nil 43 | end 44 | 45 | async def run 46 | puts "Creating server..." 47 | server 48 | 49 | puts "Creating client..." 50 | client 51 | 52 | puts "Run returning..." 53 | end 54 | end 55 | 56 | puts "Starting echo..." 57 | echo = Echo.new 58 | echo.run 59 | puts "Echo finished :)" 60 | -------------------------------------------------------------------------------- /examples/port_scanner/README.md: -------------------------------------------------------------------------------- 1 | # Port Scanner 2 | 3 | A simple `connect`-based port scanner. It scans locahost for all open ports. 4 | 5 | ## Usage 6 | 7 | ### Go 8 | 9 | Go is pretty awesome, because when the operation would not block, it runs sequentially in the same thread. Go spins up threads and delegates work across available CPU cores. 10 | 11 | $ go get golang.org/x/sync/semaphore 12 | $ go build port_scanner.go 13 | $ time ./port_scanner 14 | 22 open 15 | 139 open 16 | 445 open 17 | 3306 open 18 | 5355 open 19 | 5432 open 20 | 6379 open 21 | 9293 open 22 | 9292 open 23 | 9516 open 24 | 9515 open 25 | 12046 open 26 | 12813 open 27 | ./port_scanner 1.70s user 1.18s system 503% cpu 0.572 total 28 | 29 | ### Python 30 | 31 | Python was the slowest. This is possibly due to the implementation of semaphore. It creates all 65,535 tasks, and then most of them block on the semaphore. 32 | 33 | $ ./port_scanner.py 34 | 5355 open 35 | 5432 open 36 | 3306 open 37 | 39610 open 38 | 58260 open 39 | 12813 open 40 | 139 open 41 | 445 open 42 | 12046 open 43 | 22 open 44 | 9292 open 45 | 9293 open 46 | 9515 open 47 | 9516 open 48 | 6379 open 49 | ./port_scanner.py 11.41s user 0.88s system 98% cpu 12.485 total 50 | 51 | ### Ruby 52 | 53 | Ruby performance isn't that bad. It's only about half as fast as Go, considering that Go runs across all cores, while the Ruby implementation is limited to one core. 54 | 55 | $ ./port_scanner.rb 56 | 22 open 57 | 139 open 58 | 445 open 59 | 3306 open 60 | 5432 open 61 | 5355 open 62 | 6379 open 63 | 9516 open 64 | 9515 open 65 | 9293 open 66 | 9292 open 67 | 12046 open 68 | 12813 open 69 | ./port_scanner.rb 5.99s user 1.18s system 95% cpu 7.543 total 70 | 71 | ## Notes 72 | 73 | ### Why do I sometimes see high ports? 74 | 75 | Believe it or not, you can connect to your own sockets. 76 | 77 | ```ruby 78 | require 'socket' 79 | a = Addrinfo.tcp("127.0.0.1", 50000) 80 | s = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) 81 | s.bind(a) 82 | s.connect(a) 83 | 84 | s.write("Hello World") 85 | => 11 86 | [8] pry(main)> s.read(11) 87 | => "Hello World" 88 | ``` 89 | 90 | What's happening is that your socket is implicitly binding to a high port, and at the same time it's trying to connect to it. 91 | -------------------------------------------------------------------------------- /examples/port_scanner/port_scanner.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "golang.org/x/sync/semaphore" 7 | "net" 8 | "syscall" 9 | "strings" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | // The star of the show, of modest means, 15 | // used to manage the port scan for a single host. 16 | type PortScanner struct { 17 | ip string 18 | lock *semaphore.Weighted 19 | } 20 | 21 | // Provides a simple wrapper to initializing a PortScanner. 22 | func NewPortScanner(ip string, limit uint64) *PortScanner { 23 | return &PortScanner{ 24 | ip: ip, 25 | lock: semaphore.NewWeighted(int64(limit)), 26 | } 27 | } 28 | 29 | // Compute the maximum number of files we can open. 30 | func FileLimit(max uint64) uint64 { 31 | var rlimit syscall.Rlimit 32 | err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlimit) 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | if (max < rlimit.Cur) { 38 | return max 39 | } 40 | 41 | return rlimit.Cur 42 | } 43 | 44 | // As the name might suggest, this function checks if a given port 45 | // is open to TCP communication. Used in conjunction with the Start function 46 | // to sweep over a range of ports concurrently. 47 | func checkPortOpen(ip string, port int, timeout time.Duration) { 48 | conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ip, port), timeout) 49 | 50 | if err != nil { 51 | if strings.Contains(err.Error(), "timeout") { 52 | fmt.Println(port, "timeout", err.Error()) 53 | } else if strings.Contains(err.Error(), "deadline exceeded") { 54 | fmt.Println(port, "timeout", err.Error()) 55 | } else if strings.Contains(err.Error(), "refused") { 56 | // fmt.Println(port, "closed", err.Error()) 57 | } else { 58 | panic(err) 59 | } 60 | return 61 | } 62 | 63 | fmt.Println(port, "open") 64 | conn.Close() 65 | } 66 | 67 | // This function is the bread and butter of this script. It manages the 68 | // port scanning for a given range of ports with the given timeout value 69 | // to deal with filtered ports by a firewall typically. 70 | func (ps *PortScanner) Start(start, stop int, timeout time.Duration) { 71 | wg := sync.WaitGroup{} 72 | defer wg.Wait() 73 | 74 | for port := start; port <= stop; port++ { 75 | 76 | ctx := context.TODO() 77 | 78 | for { 79 | err := ps.lock.Acquire(ctx, 1) 80 | if err == nil { 81 | break 82 | } 83 | } 84 | 85 | wg.Add(1) 86 | 87 | go func(ip string, port int) { 88 | defer ps.lock.Release(1) 89 | defer wg.Done() 90 | 91 | checkPortOpen(ps.ip, port, timeout) 92 | }(ps.ip, port) 93 | 94 | } 95 | } 96 | 97 | // This function kicks off the whole shindig' and provides a 98 | // basic example of the internal API usage. 99 | func main() { 100 | batch_size := FileLimit(512) 101 | 102 | // Create a new PortScanner for localhost. 103 | ps := NewPortScanner("127.0.0.1", batch_size) 104 | 105 | // Start scanning all the ports on localhost. 106 | ps.Start(1, 65535, 1000*time.Millisecond) 107 | } 108 | -------------------------------------------------------------------------------- /examples/port_scanner/port_scanner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os, resource 4 | import asyncio 5 | 6 | class PortScanner: 7 | def __init__(self, host="0.0.0.0", ports=range(1, 1024+1), batch_size=1024): 8 | self.host = host 9 | self.ports = ports 10 | self.semaphore = asyncio.Semaphore(value=batch_size) 11 | self.loop = asyncio.get_event_loop() 12 | 13 | async def scan_port(self, port, timeout): 14 | async with self.semaphore: 15 | try: 16 | future = asyncio.open_connection(self.host, port, loop=self.loop) 17 | reader, writer = await asyncio.wait_for(future, timeout=timeout) 18 | print("{} open".format(port)) 19 | writer.close() 20 | except ConnectionRefusedError: 21 | pass 22 | # print("{} closed".format(port)) 23 | except asyncio.TimeoutError: 24 | print("{} timeout".format(port)) 25 | 26 | def start(self, timeout=1.0): 27 | self.loop.run_until_complete(asyncio.gather( 28 | *[self.scan_port(port, timeout) for port in self.ports] 29 | )) 30 | 31 | limits = resource.getrlimit(resource.RLIMIT_NOFILE) 32 | batch_size = min(512, limits[0]) 33 | 34 | scanner = PortScanner(host="127.0.0.1", ports=range(1, 65535+1), batch_size=batch_size) 35 | 36 | scanner.start() 37 | -------------------------------------------------------------------------------- /examples/port_scanner/port_scanner.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2018, by Kent 'picat' Gruber. 6 | # Copyright, 2018-2025, by Samuel Williams. 7 | 8 | require "async/io" 9 | require "async/semaphore" 10 | require_relative "../../lib/async/await" 11 | 12 | class PortScanner 13 | include Async::Await 14 | include Async::IO 15 | 16 | def initialize(host: "0.0.0.0", ports:, batch_size: 1024) 17 | @host = host 18 | @ports = ports 19 | @semaphore = Async::Semaphore.new(batch_size) 20 | end 21 | 22 | def scan_port(port, timeout) 23 | with_timeout(timeout) do 24 | address = Async::IO::Address.tcp(@host, port) 25 | peer = Socket.connect(address) 26 | puts "#{port} open" 27 | peer.close 28 | end 29 | rescue Errno::ECONNREFUSED 30 | # puts "#{port} closed" 31 | rescue Async::TimeoutError 32 | puts "#{port} timeout" 33 | end 34 | 35 | async def start(timeout = 1.0) 36 | @ports.map do |port| 37 | @semaphore.async do 38 | scan_port(port, timeout) 39 | end 40 | end.collect(&:result) 41 | end 42 | end 43 | 44 | limits = Process.getrlimit(Process::RLIMIT_NOFILE) 45 | batch_size = [512, (limits.first * 0.9).ceil].min 46 | 47 | scanner = PortScanner.new(host: "127.0.0.1", ports: Range.new(1, 65535), batch_size: batch_size) 48 | 49 | scanner.start 50 | -------------------------------------------------------------------------------- /examples/sleep_sort.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2025, by Samuel Williams. 5 | 6 | require_relative "../lib/async/await" 7 | 8 | class << self 9 | include Async::Await 10 | 11 | async def sort_one(item, into) 12 | sleep(item.to_f) 13 | into << item 14 | 15 | puts "I've sorted #{item} for you." 16 | end 17 | 18 | async def sort(items) 19 | result = [] 20 | 21 | items.each do |item| 22 | sort_one(item, result) 23 | end 24 | 25 | # Wait until all previous async method calls have finished executing. 26 | barrier! 27 | 28 | return result 29 | end 30 | end 31 | 32 | puts "Hold on, sorting..." 33 | puts sort([5, 2, 3, 4, 9, 2, 5, 7, 8]).result.inspect 34 | -------------------------------------------------------------------------------- /gems.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2025, by Samuel Williams. 5 | 6 | source "https://rubygems.org" 7 | 8 | gemspec 9 | 10 | group :maintenance, optional: true do 11 | gem "bake-modernize" 12 | gem "bake-gem" 13 | gem "bake-releases" 14 | 15 | gem "utopia-project" 16 | end 17 | 18 | group :test do 19 | gem "sus" 20 | gem "covered" 21 | gem "decode" 22 | gem "rubocop" 23 | 24 | gem "bake-test" 25 | gem "bake-test-external" 26 | end 27 | -------------------------------------------------------------------------------- /guides/getting-started/readme.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | This guide explains how to use `async-await` for implementing some common concurrency patterns. 4 | 5 | ## Installation 6 | 7 | Add the gem to your project: 8 | 9 | ~~~ bash 10 | $ bundle add async-await 11 | ~~~ 12 | 13 | ## Usage 14 | 15 | ### "async" Keyword 16 | 17 | This gem provides {ruby Async::Await} which introduces the `async` keyword. This keyword is used to define asynchronous methods. The method will return an `Async::Task` object, which can be waited on to get the result. 18 | 19 | ``` ruby 20 | require 'async/await' 21 | 22 | class Coop 23 | include Async::Await 24 | 25 | async def count_chickens(area_name) 26 | 3.times do |i| 27 | sleep rand 28 | 29 | puts "Found a chicken in the #{area_name}!" 30 | end 31 | end 32 | 33 | async def count_all_chickens 34 | # These methods all run at the same time. 35 | count_chickens("garden") 36 | count_chickens("house") 37 | 38 | # We wait for the result 39 | count_chickens("tree").wait 40 | end 41 | end 42 | 43 | coop = Coop.new 44 | coop.count_all_chickens 45 | ``` 46 | 47 | This interface was originally designed as a joke, but may be useful in some limited contexts. It is not recommended for general use. 48 | 49 | ### Enumerable 50 | 51 | This gem provides {ruby Async::Await::Enumerable} which adds async support to the `Enumerable` module. This allows you to use concurrency in a more functional style. 52 | 53 | ``` ruby 54 | require "async/await/enumerable" 55 | 56 | [1, 2, 3].async_each do |i| 57 | sleep rand 58 | puts i 59 | end 60 | ``` 61 | 62 | This will run the block for each element in the array concurrently. 63 | 64 | #### Using a Semaphore 65 | 66 | In order to prevent unlimited concurrency, you can use a semaphore to limit the number of concurrent tasks. This is useful when you want to limit the number of concurrent tasks to a specific number. 67 | 68 | ``` ruby 69 | require "async/await/enumerable" 70 | require "async/semaphore" 71 | 72 | semaphore = Async::Semaphore.new(2) 73 | 74 | [1, 2, 3].async_each(parent: semaphore) do |i| 75 | sleep rand 76 | puts i 77 | end 78 | ``` 79 | -------------------------------------------------------------------------------- /lib/async/await.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2025, by Samuel Williams. 5 | 6 | require_relative "await/version" 7 | 8 | # @namespace 9 | module Async 10 | # Provide a way to wrap methods so that they can be run synchronously or asynchronously in an event loop. 11 | module Await 12 | # A hook that is called when the module is included in a class in order to provide the class with the methods defined in this module. 13 | def self.included(klass) 14 | klass.extend(self) 15 | end 16 | 17 | # Wrap the method with the given name in a block that will run the method synchronously in an event loop. 18 | # 19 | # @parameter name [Symbol] The name of the method to wrap. 20 | def sync(name) 21 | original_method = instance_method(name) 22 | 23 | remove_method(name) 24 | 25 | define_method(name) do |*arguments, &block| 26 | if task = Task.current? 27 | original_method.bind(self).call(*arguments, &block) 28 | else 29 | Async::Reactor.run do 30 | original_method.bind(self).call(*arguments, &block) 31 | end.wait 32 | end 33 | end 34 | 35 | return name 36 | end 37 | 38 | # Wrap the method with the given name in a block that will run the method asynchronously in an event loop. 39 | # 40 | # @parameter name [Symbol] The name of the method to wrap. 41 | def async(name) 42 | original_method = instance_method(name) 43 | 44 | remove_method(name) 45 | 46 | define_method(name) do |*arguments, &block| 47 | Async::Reactor.run do |task| 48 | original_method.bind(self).call(*arguments, &block) 49 | end 50 | end 51 | 52 | return name 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/async/await/enumerable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2020-2025, by Samuel Williams. 5 | 6 | require "async" 7 | 8 | module Async 9 | module Await 10 | # Provide asynchronous methods for enumerables. 11 | module Enumerable 12 | # This method is used to map the elements of an enumerable collection asynchronously. 13 | # 14 | # @parameter parent [Interface(:async)] The parent to use for creating new tasks. 15 | # @yields {|item| ...} The block to execute for each element in the collection. 16 | def async_map(parent: nil, &block) 17 | Sync do |task| 18 | parent ||= task 19 | 20 | self.map do |*arguments| 21 | parent.async(finished: false) do 22 | yield(*arguments) 23 | end 24 | end.map(&:wait) 25 | end 26 | end 27 | 28 | # This method is used to iterate over the elements of an enumerable collection asynchronously. 29 | # 30 | # @parameter parent [Interface(:async)] The parent to use for creating new tasks. 31 | # @yields {|item| ...} The block to execute for each element in the collection. 32 | # @return [self] The original enumerable collection. 33 | def async_each(parent: nil, &block) 34 | Sync do |task| 35 | parent ||= task 36 | 37 | self.each do |*arguments| 38 | parent.async do 39 | yield(*arguments) 40 | end 41 | end 42 | end 43 | 44 | return self 45 | end 46 | end 47 | end 48 | end 49 | 50 | ::Enumerable.include(Async::Await::Enumerable) 51 | -------------------------------------------------------------------------------- /lib/async/await/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2024, by Samuel Williams. 5 | 6 | module Async 7 | module Await 8 | VERSION = "0.8.0" 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright, 2017-2025, by Samuel Williams. 4 | Copyright, 2018, by Kent 'picat' Gruber. 5 | Copyright, 2020, by Olle Jonsson. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Async::Await 2 | 3 | Implements the async/await pattern for Ruby using [async](https://github.com/socketry/async). 4 | 5 | [![Development Status](https://github.com/socketry/async-await/workflows/Test/badge.svg)](https://github.com/socketry/async-await/actions?workflow=Test) 6 | 7 | ## Usage 8 | 9 | Please see the [project documentation](https://socketry.github.io/async-await/) for more details. 10 | 11 | - [Getting Started](https://socketry.github.io/async-await/guides/getting-started/index) - This guide explains how to use `async-await` for implementing some common concurrency patterns. 12 | 13 | ## Contributing 14 | 15 | We welcome contributions to this project. 16 | 17 | 1. Fork it. 18 | 2. Create your feature branch (`git checkout -b my-new-feature`). 19 | 3. Commit your changes (`git commit -am 'Add some feature'`). 20 | 4. Push to the branch (`git push origin my-new-feature`). 21 | 5. Create new Pull Request. 22 | 23 | ### Developer Certificate of Origin 24 | 25 | In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed. 26 | 27 | ### Community Guidelines 28 | 29 | This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers. 30 | -------------------------------------------------------------------------------- /release.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11 3 | ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK 4 | CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz 5 | MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd 6 | MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj 7 | bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB 8 | igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2 9 | 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW 10 | sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE 11 | e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN 12 | XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss 13 | RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn 14 | tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM 15 | zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW 16 | xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O 17 | BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs 18 | aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs 19 | aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE 20 | cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl 21 | xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/ 22 | c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp 23 | 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws 24 | JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP 25 | eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt 26 | Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8 27 | voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg= 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /test/async/await.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024-2025, by Samuel Williams. 5 | 6 | require "async/await" 7 | 8 | class Coop 9 | include Async::Await 10 | 11 | async def find_chicken(areas) 12 | sleep(rand) 13 | return areas.sample 14 | end 15 | 16 | sync def find_chickens(count, areas) 17 | count.times.map do 18 | find_chicken(areas) 19 | end 20 | end 21 | end 22 | 23 | describe Async::Await do 24 | let(:coop) {Coop.new} 25 | 26 | it "can find async chicken" do 27 | result = coop.find_chicken(["house"]).wait 28 | 29 | expect(result).to be == "house" 30 | end 31 | 32 | it "can find several chickens" do 33 | result = coop.find_chickens(3, ["house", "yard"]) 34 | 35 | expect(result).to have_attributes(size: be == 3) 36 | end 37 | 38 | it "can find several chickens in nested async" do 39 | Async do 40 | result = coop.find_chickens(3, ["house", "yard"]) 41 | 42 | expect(result).to have_attributes(size: be == 3) 43 | end 44 | end 45 | 46 | with "#await" do 47 | 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/async/await/enumerable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2020-2025, by Samuel Williams. 5 | 6 | require "async/await/enumerable" 7 | 8 | describe Async::Await::Enumerable do 9 | with "#async_map" do 10 | it "should map values" do 11 | result = [1, 2, 3].async_map do |value| 12 | value * 2 13 | end 14 | 15 | expect(result).to be == [2, 4, 6] 16 | end 17 | 18 | it "should fail if the block fails" do 19 | expect do 20 | [1, 2, 3].async_map do |value| 21 | raise "Fake error!" 22 | end 23 | end.to raise_exception(RuntimeError, message: be == "Fake error!") 24 | end 25 | end 26 | 27 | with "#async_each" do 28 | it "should iterate over values" do 29 | result = [] 30 | 31 | [1, 2, 3].async_each do |value| 32 | result << value * 2 33 | end 34 | 35 | expect(result).to be == [2, 4, 6] 36 | end 37 | end 38 | end 39 | --------------------------------------------------------------------------------