├── .contributors.yaml ├── .editorconfig ├── .github └── workflows │ ├── benchmark.yaml │ ├── documentation-coverage.yaml │ ├── documentation.yaml │ ├── rubocop.yaml │ ├── test-coverage.yaml │ ├── test-debug.yaml │ ├── test-external.yaml │ └── test.yaml ├── .gitignore ├── .mailmap ├── .rubocop.yml ├── bake.rb ├── benchmark ├── instantiate.rb ├── readable.rb └── server │ ├── async.rb │ ├── bake.rb │ ├── buffer.rb │ ├── compiled.c │ ├── event.rb │ ├── fork.rb │ ├── gems.locked │ ├── gems.rb │ ├── loop.rb │ ├── scheduler.rb │ └── thread.rb ├── config ├── environment.rb ├── external.yaml └── sus.rb ├── design.md ├── examples ├── compare_interrupt.rb ├── handle_interrupt.rb ├── interrupt.rb ├── kqueue-bug-waitpid.c └── scheduler │ ├── scheduler.rb │ └── scheduler_spec.rb ├── ext ├── extconf.rb └── io │ └── event │ ├── array.h │ ├── event.c │ ├── event.h │ ├── fiber.c │ ├── fiber.h │ ├── interrupt.c │ ├── interrupt.h │ ├── list.h │ ├── selector │ ├── epoll.c │ ├── epoll.h │ ├── kqueue.c │ ├── kqueue.h │ ├── pidfd.c │ ├── selector.c │ ├── selector.h │ ├── uring.c │ └── uring.h │ ├── time.c │ └── time.h ├── fixtures └── unix_socket.rb ├── gems.rb ├── guides ├── getting-started │ └── readme.md └── links.yaml ├── io-event.gemspec ├── lib └── io │ ├── event.rb │ └── event │ ├── debug │ └── selector.rb │ ├── interrupt.rb │ ├── native.rb │ ├── priority_heap.rb │ ├── selector.rb │ ├── selector │ ├── nonblock.rb │ └── select.rb │ ├── support.rb │ ├── timers.rb │ └── version.rb ├── license.md ├── logo.svg ├── readme.md ├── release.cert ├── releases.md └── test └── io ├── event.rb └── event ├── priority_heap.rb ├── selector.rb ├── selector ├── buffered_io.rb ├── cancellable.rb ├── fifo_io.rb ├── file_io.rb ├── interruptable.rb ├── nonblock.rb ├── process_io.rb └── queue.rb └── timers.rb /.contributors.yaml: -------------------------------------------------------------------------------- 1 | - path: lib/io/event/priority_heap.rb 2 | time: 2021-02-12T12:19:44+01:00 3 | author: 4 | name: Wander Hillen 5 | email: wjw.hillen@gmail.com 6 | 7 | - path: lib/io/event/priority_heap.rb 8 | time: 2021-02-12T13:19:56+01:00 9 | author: 10 | name: Wander Hillen 11 | email: wjw.hillen@gmail.com 12 | 13 | - path: lib/io/event/priority_heap.rb 14 | time: 2021-02-12T13:28:58+01:00 15 | author: 16 | name: Wander Hillen 17 | email: wjw.hillen@gmail.com 18 | 19 | - path: lib/io/event/priority_heap.rb 20 | time: 2021-02-13T18:44:46+13:00 21 | author: 22 | name: Samuel Williams 23 | email: samuel.williams@oriontransfer.co.nz 24 | 25 | - path: lib/io/event/priority_heap.rb 26 | time: 2021-02-13T12:40:15+01:00 27 | author: 28 | name: Wander Hillen 29 | email: wjw.hillen@gmail.com 30 | 31 | - path: lib/io/event/priority_heap.rb 32 | time: 2021-02-13T12:47:51+01:00 33 | author: 34 | name: Wander Hillen 35 | email: wjw.hillen@gmail.com 36 | 37 | - path: lib/io/event/priority_heap.rb 38 | time: 2022-09-02T13:45:20+12:00 39 | author: 40 | name: Samuel Williams 41 | email: samuel.williams@oriontransfer.co.nz 42 | 43 | - path: lib/io/event/priority_heap.rb 44 | time: 2022-09-02T13:45:20+12:00 45 | author: 46 | name: Samuel Williams 47 | email: samuel.williams@oriontransfer.co.nz 48 | 49 | - path: lib/io/event/priority_heap.rb 50 | time: 2022-10-13T11:06:34+13:00 51 | author: 52 | name: Samuel Williams 53 | email: samuel.williams@oriontransfer.co.nz 54 | 55 | - path: lib/io/event/priority_heap.rb 56 | time: 2023-04-12T17:25:59+12:00 57 | author: 58 | name: Samuel Williams 59 | email: samuel.williams@oriontransfer.co.nz 60 | -------------------------------------------------------------------------------- /.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/benchmark.yaml: -------------------------------------------------------------------------------- 1 | name: Benchmark 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{matrix.os}}-latest 8 | 9 | strategy: 10 | matrix: 11 | os: 12 | - ubuntu 13 | 14 | ruby: 15 | - head 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: ${{matrix.ruby}} 22 | bundler-cache: true 23 | 24 | - name: Build extensions 25 | timeout-minutes: 5 26 | run: bundle exec bake build 27 | 28 | - name: Install packages 29 | timeout-minutes: 5 30 | run: | 31 | git clone https://github.com/ioquatix/wrk 32 | cd wrk 33 | make 34 | 35 | - name: Run benchmarks 36 | timeout-minutes: 5 37 | env: 38 | WRK: ./wrk/wrk 39 | run: bundle exec bake -b benchmark/server/bake.rb 40 | -------------------------------------------------------------------------------- /.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.1" 25 | - "3.2" 26 | - "3.3" 27 | - "3.4" 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: ruby/setup-ruby@v1 32 | with: 33 | ruby-version: ${{matrix.ruby}} 34 | bundler-cache: true 35 | 36 | - name: Run tests 37 | timeout-minutes: 5 38 | run: bundle exec bake build test 39 | 40 | - uses: actions/upload-artifact@v4 41 | with: 42 | include-hidden-files: true 43 | if-no-files-found: error 44 | name: coverage-${{matrix.os}}-${{matrix.ruby}} 45 | path: .covered.db 46 | 47 | validate: 48 | needs: test 49 | runs-on: ubuntu-latest 50 | 51 | steps: 52 | - uses: actions/checkout@v4 53 | - uses: ruby/setup-ruby@v1 54 | with: 55 | ruby-version: "3.4" 56 | bundler-cache: true 57 | 58 | - uses: actions/download-artifact@v4 59 | 60 | - name: Validate coverage 61 | timeout-minutes: 5 62 | run: bundle exec bake covered:validate --paths */.covered.db \; 63 | -------------------------------------------------------------------------------- /.github/workflows/test-debug.yaml: -------------------------------------------------------------------------------- 1 | name: Test Debug 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | RUBY_DEBUG: true 11 | RUBY_SANITIZE: true 12 | ASAN_OPTIONS: halt_on_error=0:use_sigaltstack=0:detect_leaks=0 13 | 14 | jobs: 15 | test: 16 | name: ${{matrix.ruby}} on ${{matrix.os}} 17 | runs-on: ${{matrix.os}} 18 | 19 | strategy: 20 | matrix: 21 | os: 22 | - ubuntu-24.04 23 | 24 | ruby: 25 | - asan 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: ${{matrix.ruby}} 32 | bundler-cache: true 33 | 34 | - name: Install packages (Ubuntu) 35 | if: matrix.os == 'ubuntu' 36 | run: sudo apt-get install -y liburing-dev 37 | 38 | - name: Run tests 39 | timeout-minutes: 10 40 | run: bundle exec bake build test 41 | -------------------------------------------------------------------------------- /.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 | RUBY_DEBUG: true 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.2" 25 | - "3.3" 26 | - "3.4" 27 | - "head" 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: ruby/setup-ruby@v1 32 | with: 33 | ruby-version: ${{matrix.ruby}} 34 | bundler-cache: true 35 | 36 | - name: Run tests 37 | timeout-minutes: 10 38 | run: bundle exec bake build test:external 39 | 40 | # - name: Run tests (pure Ruby) 41 | # env: 42 | # IO_EVENT_SELECTOR: Select 43 | # timeout-minutes: 10 44 | # run: bundle exec bake build test:external 45 | -------------------------------------------------------------------------------- /.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 | - windows 23 | 24 | ruby: 25 | - "3.1" 26 | - "3.2" 27 | - "3.3" 28 | - "3.4" 29 | - "head" 30 | 31 | experimental: [false] 32 | 33 | include: 34 | - os: ubuntu 35 | ruby: truffleruby 36 | experimental: true 37 | - os: ubuntu 38 | ruby: truffleruby-head 39 | experimental: true 40 | - os: ubuntu 41 | ruby: jruby 42 | experimental: true 43 | - os: ubuntu 44 | ruby: head 45 | experimental: true 46 | 47 | steps: 48 | - uses: actions/checkout@v4 49 | - uses: ruby/setup-ruby@v1 50 | with: 51 | ruby-version: ${{matrix.ruby}} 52 | bundler-cache: true 53 | 54 | - name: Install packages (Ubuntu) 55 | if: matrix.os == 'ubuntu' 56 | run: sudo apt-get install -y liburing-dev 57 | 58 | - name: Run tests 59 | timeout-minutes: 10 60 | run: bundle exec bake build test 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /pkg/ 3 | /gems.locked 4 | /.covered.db 5 | /external 6 | 7 | /ext/Makefile 8 | /ext/mkmf.log 9 | /ext/extconf.h 10 | /ext/**/*.o 11 | /ext/**/*.so 12 | /ext/**/*.bundle 13 | /ext/**/*.dSYM 14 | 15 | /benchmark/server/compiled 16 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Alex Matchneer 2 | Math Ieu 3 | Shizuo Fujita 4 | Jean Boussier 5 | -------------------------------------------------------------------------------- /.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 | Layout/EmptyLineAfterMagicComment: 49 | Enabled: true 50 | 51 | Style/FrozenStringLiteralComment: 52 | Enabled: true 53 | 54 | Style/StringLiterals: 55 | Enabled: true 56 | EnforcedStyle: double_quotes 57 | -------------------------------------------------------------------------------- /bake.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2024, by Samuel Williams. 5 | # Copyright, 2024, by Pavel Rosický. 6 | 7 | def build 8 | ext_path = File.expand_path("ext", __dir__) 9 | 10 | Dir.chdir(ext_path) do 11 | system("ruby ./extconf.rb") 12 | system("make") 13 | end 14 | end 15 | 16 | def clean 17 | ext_path = File.expand_path("ext", __dir__) 18 | 19 | Dir.chdir(ext_path) do 20 | system("make clean") 21 | end 22 | end 23 | 24 | # Update the project documentation with the new version number. 25 | # 26 | # @parameter version [String] The new version number. 27 | def after_gem_release_version_increment(version) 28 | context["releases:update"].call(version) 29 | context["utopia:project:readme:update"].call 30 | end 31 | -------------------------------------------------------------------------------- /benchmark/instantiate.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2021-2024, by Samuel Williams. 6 | 7 | require "benchmark/ips" 8 | require "fiber" 9 | 10 | require_relative "../lib/event" 11 | 12 | GC.disable 13 | 14 | Event::Selector.constants.each do |name| 15 | puts "Creating #{name}..." 16 | 1000.times.map do |i| 17 | puts i 18 | selector = Event::Selector.const_get(name).new(Fiber.current) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /benchmark/readable.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2021-2024, by Samuel Williams. 6 | 7 | require "benchmark/ips" 8 | require "fiber" 9 | require "console" 10 | 11 | require_relative "../lib/event" 12 | 13 | Event::Selector.constants.each do |name| 14 | selector = Event::Selector.const_get(name).new(Fiber.current) 15 | 16 | fibers = 256.times.map do |index| 17 | input, output = IO.pipe 18 | output.puts "Hello World" 19 | 20 | fiber = Fiber.new do 21 | while true 22 | selector.io_wait(fiber, input, IO::READABLE) 23 | end 24 | rescue RuntimeError 25 | # Ignore. 26 | ensure 27 | input.close 28 | output.close 29 | end 30 | end 31 | 32 | # Start initial wait: 33 | fibers.each(&:transfer) 34 | 35 | Console.logger.measure(selector) do 36 | i = 10_000 37 | while (i -= 1) > 0 38 | selector.select(0) 39 | end 40 | end 41 | 42 | fibers.each{|fiber| fiber.raise("Stop")} 43 | end 44 | -------------------------------------------------------------------------------- /benchmark/server/async.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2021-2025, by Samuel Williams. 6 | 7 | require "async" 8 | require "socket" 9 | 10 | RESPONSE = "HTTP/1.1 204 No Content\r\nXonnection: close\r\n\r\n" 11 | 12 | port = Integer(ARGV.pop || 9090) 13 | 14 | Async do |task| 15 | server = TCPServer.new("localhost", port) 16 | 17 | loop do 18 | peer, address = server.accept 19 | 20 | task.async do 21 | while (peer.recv(1024) rescue nil) 22 | sleep 0.02 23 | peer.send(RESPONSE, 0) 24 | end 25 | ensure 26 | peer.close 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /benchmark/server/bake.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2024, by Samuel Williams. 5 | 6 | SERVERS = [ 7 | "compiled", 8 | "event.rb", 9 | "buffer.rb", 10 | "loop.rb", 11 | "async.rb", 12 | "thread.rb", 13 | "fork.rb", 14 | ] 15 | 16 | def default 17 | build 18 | benchmark 19 | end 20 | 21 | def build 22 | compiler = ENV.fetch("CC", "clang") 23 | system(compiler, "compiled.c", "-o", "compiled", chdir: __dir__) 24 | end 25 | 26 | # @parameter connections [Integer] The number of simultaneous connections. 27 | # @parameter threads [Integer] The number of client threads to use. 28 | # @parameter duration [Integer] The duration of the test. 29 | def benchmark(connections: 8, threads: 1, duration: 1) 30 | port = 9095 31 | wrk = ENV.fetch("WRK", "wrk") 32 | 33 | SERVERS.each do |server| 34 | $stdout.puts [nil, "Benchmark #{server}..."] 35 | 36 | pid = Process.spawn(File.expand_path(server, __dir__), port.to_s) 37 | puts "Server running pid=#{pid}..." 38 | 39 | sleep 1 40 | 41 | system(wrk, "-d#{duration}", "-t#{threads}", "-c#{connections}", "http://localhost:#{port}") 42 | 43 | Process.kill(:TERM, pid) 44 | _, status = Process.wait2(pid) 45 | puts "Server exited status=#{status}..." 46 | 47 | port += 1 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /benchmark/server/buffer.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2021-2024, by Samuel Williams. 6 | 7 | require_relative "scheduler" 8 | 9 | scheduler = DirectScheduler.new 10 | Fiber.set_scheduler(scheduler) 11 | 12 | port = Integer(ARGV.pop || 9090) 13 | 14 | RESPONSE_STRING = "HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n" 15 | 16 | REQUEST = IO::Buffer.new(1024) 17 | RESPONSE = IO::Buffer.new(128) 18 | 19 | RESPONSE_SIZE = RESPONSE.set_string(RESPONSE_STRING) 20 | 21 | Fiber.schedule do 22 | server = TCPServer.new("localhost", port) 23 | 24 | loop do 25 | peer, address = server.accept 26 | 27 | Fiber.schedule do 28 | scheduler.io_read(peer, REQUEST, 1) 29 | scheduler.io_write(peer, RESPONSE, RESPONSE_SIZE) 30 | peer.close 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /benchmark/server/compiled.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define BUFFER_SIZE 1024 10 | #define on_error(...) { fprintf(stderr, __VA_ARGS__); fflush(stderr); exit(1); } 11 | 12 | int main (int argc, char *argv[]) { 13 | if (argc < 2) on_error("Usage: %s [port]\n", argv[0]); 14 | 15 | int port = atoi(argv[1]); 16 | 17 | int server_fd, client_fd, err; 18 | struct sockaddr_in server, client; 19 | char buf[BUFFER_SIZE]; 20 | 21 | server_fd = socket(AF_INET, SOCK_STREAM, 0); 22 | if (server_fd < 0) on_error("Could not create socket\n"); 23 | 24 | server.sin_family = AF_INET; 25 | server.sin_port = htons(port); 26 | server.sin_addr.s_addr = htonl(INADDR_ANY); 27 | 28 | int opt_val = 1; 29 | setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt_val, sizeof opt_val); 30 | 31 | err = bind(server_fd, (struct sockaddr *) &server, sizeof(server)); 32 | if (err < 0) on_error("Could not bind socket\n"); 33 | 34 | err = listen(server_fd, SOMAXCONN); 35 | if (err < 0) on_error("Could not listen on socket\n"); 36 | 37 | printf("Server is listening on %d\n", port); 38 | 39 | char* response = "HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n"; 40 | size_t response_size = strlen(response); 41 | 42 | while (1) { 43 | socklen_t client_len = sizeof(client); 44 | client_fd = accept(server_fd, (struct sockaddr *) &client, &client_len); 45 | 46 | if (client_fd < 0) on_error("Could not establish new connection\n"); 47 | 48 | recv(client_fd, buf, BUFFER_SIZE, 0); 49 | send(client_fd, response, response_size, 0); 50 | 51 | close(client_fd); 52 | } 53 | 54 | return 0; 55 | } 56 | -------------------------------------------------------------------------------- /benchmark/server/event.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2021-2025, by Samuel Williams. 6 | 7 | require_relative "scheduler" 8 | require "io/nonblock" 9 | 10 | RESPONSE = "HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n" 11 | 12 | #scheduler = DirectScheduler.new 13 | scheduler = Scheduler.new 14 | Fiber.set_scheduler(scheduler) 15 | 16 | port = Integer(ARGV.pop || 9090) 17 | 18 | Fiber.schedule do 19 | server = TCPServer.new("localhost", port) 20 | server.listen(Socket::SOMAXCONN) 21 | 22 | loop do 23 | peer, address = server.accept 24 | 25 | Fiber.schedule do 26 | peer.recv(1024) 27 | peer.send(RESPONSE, 0) 28 | peer.close 29 | end 30 | end 31 | end 32 | 33 | -------------------------------------------------------------------------------- /benchmark/server/fork.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2021-2024, by Samuel Williams. 6 | 7 | require "socket" 8 | 9 | port = Integer(ARGV.pop || 9090) 10 | server = TCPServer.new("localhost", port) 11 | 12 | loop do 13 | peer = server.accept 14 | 15 | fork do 16 | peer.recv(1024) 17 | peer.send("HTTP/1.1 200 Ok\r\nConnection: close\r\n\r\n", 0) 18 | peer.close 19 | end 20 | 21 | peer.close 22 | end 23 | -------------------------------------------------------------------------------- /benchmark/server/gems.locked: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | async (2.23.1) 5 | console (~> 1.29) 6 | fiber-annotation 7 | io-event (~> 1.9) 8 | metrics (~> 0.12) 9 | traces (~> 0.15) 10 | console (1.30.2) 11 | fiber-annotation 12 | fiber-local (~> 1.1) 13 | json 14 | fiber-annotation (0.2.0) 15 | fiber-local (1.1.0) 16 | fiber-storage 17 | fiber-storage (1.0.0) 18 | io-event (1.10.0) 19 | json (2.10.2) 20 | metrics (0.12.2) 21 | traces (0.15.2) 22 | 23 | PLATFORMS 24 | arm64-darwin-24 25 | ruby 26 | 27 | DEPENDENCIES 28 | async 29 | 30 | BUNDLED WITH 31 | 2.6.2 32 | -------------------------------------------------------------------------------- /benchmark/server/gems.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2025, by Samuel Williams. 5 | 6 | source "https://rubygems.org" 7 | 8 | gem "async" 9 | -------------------------------------------------------------------------------- /benchmark/server/loop.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2021-2025, by Samuel Williams. 6 | 7 | require "socket" 8 | 9 | RESPONSE = "HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n" 10 | 11 | port = Integer(ARGV.pop || 9090) 12 | server = TCPServer.new("localhost", port) 13 | 14 | loop do 15 | peer, address = server.accept 16 | 17 | while (peer.recv(1024) rescue nil) 18 | peer.send(RESPONSE, 0) 19 | end 20 | 21 | peer.close 22 | end 23 | 24 | -------------------------------------------------------------------------------- /benchmark/server/scheduler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2025, by Samuel Williams. 5 | 6 | $LOAD_PATH << File.expand_path("../../lib", __dir__) 7 | $LOAD_PATH << File.expand_path("../../ext", __dir__) 8 | 9 | require "io/event" 10 | 11 | require "socket" 12 | require "fiber" 13 | 14 | class Scheduler 15 | def initialize(selector = nil) 16 | @fiber = Fiber.current 17 | @selector = selector || IO::Event::Selector.new(@fiber) 18 | @waiting = 0 19 | 20 | unless @selector.respond_to?(:io_close) 21 | instance_eval{undef io_close} 22 | end 23 | 24 | @mutex = Mutex.new 25 | end 26 | 27 | def block(blocker, timeout) 28 | raise NotImplementedError 29 | end 30 | 31 | def unblock(blocker, fiber) 32 | raise NotImplementedError 33 | end 34 | 35 | def io_wait(io, events, timeout) 36 | fiber = Fiber.current 37 | @waiting += 1 38 | @selector.io_wait(fiber, io, events) 39 | ensure 40 | @waiting -= 1 41 | end 42 | 43 | def io_close(io) 44 | @selector.io_close(io) 45 | end 46 | 47 | def kernel_sleep(duration) 48 | @selector.defer 49 | end 50 | 51 | def close 52 | while @selector.ready? || @waiting > 0 53 | begin 54 | @selector.select(nil) 55 | rescue Errno::EINTR 56 | # Ignore. 57 | end 58 | end 59 | rescue Interrupt 60 | # Exit. 61 | end 62 | 63 | def fiber(&block) 64 | fiber = Fiber.new(&block) 65 | 66 | @selector.resume(fiber) 67 | 68 | return fiber 69 | end 70 | end 71 | 72 | class DirectScheduler < Scheduler 73 | def io_read(io, buffer, length) 74 | fiber = Fiber.current 75 | @waiting += 1 76 | result = @selector.io_read(fiber, io, buffer, length) 77 | ensure 78 | @waiting -= 1 79 | end 80 | 81 | def io_write(io, buffer, length) 82 | fiber = Fiber.current 83 | @waiting += 1 84 | @selector.io_write(fiber, io, buffer, length) 85 | ensure 86 | @waiting -= 1 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /benchmark/server/thread.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2021-2025, by Samuel Williams. 6 | 7 | require "socket" 8 | 9 | RESPONSE = "HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n" 10 | 11 | port = Integer(ARGV.pop || 9090) 12 | server = TCPServer.new("localhost", port) 13 | 14 | loop do 15 | peer = server.accept 16 | 17 | Thread.new do 18 | peer.recv(1024) 19 | peer.send(RESPONSE, 0) 20 | peer.close 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2025, by Samuel Williams. 5 | 6 | $LOAD_PATH << ::File.expand_path("../ext", __dir__) 7 | -------------------------------------------------------------------------------- /config/external.yaml: -------------------------------------------------------------------------------- 1 | # async-v2.0.3: 2 | # url: https://github.com/socketry/async 3 | # command: bundle exec rspec 4 | # branch: v2.0.3 5 | # async-v2.1.0: 6 | # url: https://github.com/socketry/async 7 | # command: bundle exec rspec 8 | # branch: v2.1.0 9 | async: 10 | url: https://github.com/socketry/async 11 | command: bundle exec sus 12 | extra: 13 | - $LOAD_PATH << ::File.expand_path("../../ext", __dir__) 14 | async-http: 15 | url: https://github.com/socketry/async-http 16 | command: bundle exec sus 17 | extra: 18 | - $LOAD_PATH << ::File.expand_path("../../ext", __dir__) 19 | -------------------------------------------------------------------------------- /config/sus.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2025, by Samuel Williams. 5 | 6 | require_relative "environment" 7 | 8 | Warning[:experimental] = false 9 | 10 | require "covered/sus" 11 | include Covered::Sus 12 | 13 | # Intensive GC checking: 14 | # 15 | # Thread.new do 16 | # while true 17 | # sleep 0.0001 18 | # $stderr.puts GC.verify_compaction_references 19 | # end 20 | # end 21 | -------------------------------------------------------------------------------- /design.md: -------------------------------------------------------------------------------- 1 | ## Dual `_select` without GVL: 2 | 3 | Always release GVL: 4 | 5 | ``` 6 | Warming up -------------------------------------- 7 | KQueue 55.896k i/100ms 8 | Select 17.023k i/100ms 9 | Calculating ------------------------------------- 10 | KQueue 532.515k (± 8.0%) i/s - 2.683M in 5.071193s 11 | Select 177.956k (± 3.4%) i/s - 902.219k in 5.075817s 12 | 13 | Comparison: 14 | KQueue: 532515.3 i/s 15 | Select: 177956.1 i/s - 2.99x (± 0.00) slower 16 | ``` 17 | 18 | Only release GVL with non-zero timeout, with selector.elect(1) (so always hitting slow path): 19 | 20 | ``` 21 | Warming up -------------------------------------- 22 | KQueue 39.628k i/100ms 23 | Select 18.330k i/100ms 24 | Calculating ------------------------------------- 25 | KQueue 381.868k (± 6.5%) i/s - 1.902M in 5.004267s 26 | Select 171.623k (± 3.0%) i/s - 861.510k in 5.024308s 27 | 28 | Comparison: 29 | KQueue: 381867.8 i/s 30 | Select: 171622.5 i/s - 2.23x (± 0.00) slower 31 | ``` 32 | 33 | Only release GVL with non-zero timeout, with selector.select(0) so always hitting fast path: 34 | 35 | ``` 36 | Warming up -------------------------------------- 37 | KQueue 56.240k i/100ms 38 | Select 17.888k i/100ms 39 | Calculating ------------------------------------- 40 | KQueue 543.042k (± 7.8%) i/s - 2.700M in 5.003790s 41 | Select 171.866k (± 4.3%) i/s - 858.624k in 5.005785s 42 | 43 | Comparison: 44 | KQueue: 543041.5 i/s 45 | Select: 171866.2 i/s - 3.16x (± 0.00) slower 46 | ``` 47 | 48 | Only release GVL when no events are ready and non-zero timeout, with selector.select(1): 49 | 50 | ``` 51 | Warming up -------------------------------------- 52 | KQueue 53.401k i/100ms 53 | Select 16.691k i/100ms 54 | Calculating ------------------------------------- 55 | KQueue 524.564k (± 6.1%) i/s - 2.617M in 5.006996s 56 | Select 179.329k (± 2.4%) i/s - 901.314k in 5.029136s 57 | 58 | Comparison: 59 | KQueue: 524564.0 i/s 60 | Select: 179329.1 i/s - 2.93x (± 0.00) slower 61 | ``` 62 | 63 | So this approach seems to be a net win of about 1.5x throughput. -------------------------------------------------------------------------------- /examples/compare_interrupt.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2021-2024, by Samuel Williams. 6 | 7 | require "io/event" 8 | require "fiber" 9 | require "benchmark" 10 | 11 | Thread.report_on_exception = true 12 | 13 | Benchmark.bmbm do |benchmark| 14 | benchmark.report("interrupt") do 15 | count = 0 16 | input, output = IO.pipe 17 | 18 | thread = Thread.new do 19 | selector = Event::Selector.new(Fiber.current) 20 | 21 | while true 22 | begin 23 | selector.select(10) 24 | rescue Errno::EINTR 25 | # Ignore 26 | # $stderr.puts "Errno::EINTR" 27 | end 28 | 29 | count += 1 30 | end 31 | end 32 | 33 | 100.times do 34 | thread.wakeup 35 | # Thread.pass 36 | end 37 | 38 | pp count: count 39 | end 40 | 41 | benchmark.report("io") do 42 | count = 0 43 | input, output = IO.pipe 44 | 45 | thread = Thread.new do 46 | selector = Event::Selector.new(Fiber.current) 47 | 48 | fiber = Fiber.new do 49 | while true 50 | selector.io_wait(Fiber.current, input, IO::READABLE) 51 | input.read(1) 52 | end 53 | end 54 | 55 | fiber.transfer 56 | 57 | while true 58 | selector.select(10) 59 | count += 1 60 | end 61 | end 62 | 63 | 100.times do 64 | output.write(".") 65 | output.flush 66 | end 67 | 68 | pp count: count 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /examples/handle_interrupt.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2021-2024, by Samuel Williams. 6 | 7 | require "io/event" 8 | require "fiber" 9 | require "benchmark" 10 | 11 | count = 0 12 | 13 | thread = Thread.new do 14 | input, output = IO.pipe 15 | 16 | selector = Event::Selector.new(Fiber.current) 17 | 18 | fiber = Fiber.new do 19 | while true 20 | selector.io_wait(Fiber.current, input, IO::READABLE) 21 | input.read(1) 22 | end 23 | end 24 | 25 | fiber.transfer 26 | 27 | Thread.handle_interrupt(Interrupt => :never) do 28 | while true 29 | $stderr.puts "Selecting" 30 | begin 31 | selector.select(1) 32 | rescue Errno::EINTR 33 | # Ignore. 34 | end 35 | 36 | Fiber.new do 37 | sleep 5 38 | end.resume 39 | 40 | # sleep 1 41 | count += 1 42 | 43 | begin 44 | Thread.handle_interrupt(Interrupt => :immediate) {} 45 | rescue Interrupt 46 | $stderr.puts "Interrupted" 47 | end 48 | end 49 | end 50 | end 51 | 52 | sleep 2 53 | 54 | 10.times do 55 | sleep 1 56 | $stderr.puts "Sending interrupt" 57 | thread.raise(Interrupt) 58 | end 59 | 60 | sleep 1000 61 | 62 | thread.join 63 | -------------------------------------------------------------------------------- /examples/interrupt.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2021-2024, by Samuel Williams. 6 | 7 | require "fiber" 8 | require "../lib/event" 9 | 10 | th = Thread.new do 11 | selector = Event::Selector.new(Fiber.current) 12 | $stderr.puts "select" 13 | selector.select(10) 14 | $stderr.puts "select done" 15 | ensure 16 | $stderr.puts "exiting: #{$!}" 17 | end 18 | 19 | sleep 1 20 | $stderr.puts "Sending interrupt" 21 | th.wakeup 22 | 23 | th.join 24 | 25 | # 26 | # c = Thread.new { Thread.stop; puts "hey!" } 27 | # sleep 0.1 while c.status!='sleep' 28 | # c.wakeup 29 | # c.join 30 | # #=> "hey!" 31 | -------------------------------------------------------------------------------- /examples/kqueue-bug-waitpid.c: -------------------------------------------------------------------------------- 1 | 2 | // This demonstrates a race condiiton between kqueue and waidpid. 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | int main(int argc, char ** argv) { 12 | int result; 13 | char * arguments[] = {"sleep", "0.01", NULL}; 14 | 15 | while (1) { 16 | int pid = 0; 17 | result = posix_spawn(&pid, "/bin/sleep", NULL, NULL, arguments, NULL); 18 | fprintf(stderr, "posix_spawn result=%d\n", result); 19 | if (result) { 20 | perror("posix_spawn"); 21 | exit(result); 22 | } 23 | 24 | int fd = kqueue(); 25 | struct kevent kev; 26 | EV_SET(&kev, pid, EVFILT_PROC, EV_ADD|EV_ENABLE, NOTE_EXIT, 0, NULL); 27 | kevent(fd, &kev, 1, NULL, 0, NULL); 28 | 29 | kevent(fd, NULL, 0, &kev, 1, NULL); // wait 30 | 31 | int status = -1; 32 | result = waitpid(pid, &status, WNOHANG); 33 | fprintf(stderr, "waitpid(%d) result=%d status=%d\n", pid, result, status); 34 | 35 | if (status) { 36 | exit(status); 37 | } 38 | 39 | close(fd); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/scheduler/scheduler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2024, by Samuel Williams. 5 | 6 | require "io/event" 7 | require "timers" 8 | require "resolv" 9 | 10 | class IO 11 | module Event 12 | class Scheduler 13 | def initialize(selector = nil) 14 | @timers = Timers::Group.new 15 | 16 | @selector = selector || Selector.new(Fiber.current) 17 | @thread = Thread.current 18 | 19 | @blocked = 0 20 | end 21 | 22 | def finished? 23 | @blocked.zero? 24 | end 25 | 26 | def close 27 | self.run 28 | 29 | Kernel.raise("Closing scheduler with blocked operations!") if @blocked > 0 30 | 31 | # We depend on GVL for consistency: 32 | @selector&.close 33 | @selector = nil 34 | end 35 | 36 | def closed? 37 | @selector.nil? 38 | end 39 | 40 | # Interrupt the event loop. 41 | def interrupt 42 | @thread.raise(Interrupt) 43 | end 44 | 45 | # Transfer from the calling fiber to the event loop. 46 | def transfer 47 | @selector.transfer 48 | end 49 | 50 | # Yield the current fiber and resume it on the next iteration of the event loop. 51 | def yield 52 | @selector.yield 53 | end 54 | 55 | # Schedule a fiber (or equivalent object) to be resumed on the next loop through the reactor. 56 | # @parameter fiber [Fiber | Object] The object to be resumed on the next iteration of the run-loop. 57 | def push(fiber) 58 | @selector.push(fiber) 59 | end 60 | 61 | def raise(*arguments) 62 | @selector.raise(*arguments) 63 | end 64 | 65 | def resume(fiber, *arguments) 66 | if Fiber.scheduler 67 | @selector.resume(fiber, *arguments) 68 | else 69 | @selector.push(fiber) 70 | end 71 | end 72 | 73 | # Invoked when a fiber tries to perform a blocking operation which cannot continue. A corresponding call {unblock} must be performed to allow this fiber to continue. 74 | # @asynchronous May only be called on same thread as fiber scheduler. 75 | def block(blocker, timeout) 76 | # $stderr.puts "block(#{blocker}, #{Fiber.current}, #{timeout})" 77 | fiber = Fiber.current 78 | 79 | if timeout 80 | timer = @timers.after(timeout) do 81 | if fiber.alive? 82 | fiber.transfer(false) 83 | end 84 | end 85 | end 86 | 87 | begin 88 | @blocked += 1 89 | @selector.transfer 90 | ensure 91 | @blocked -= 1 92 | end 93 | ensure 94 | timer&.cancel 95 | end 96 | 97 | # @asynchronous May be called from any thread. 98 | def unblock(blocker, fiber) 99 | # $stderr.puts "unblock(#{blocker}, #{fiber})" 100 | 101 | # This operation is protected by the GVL: 102 | @selector.push(fiber) 103 | @thread.raise(Errno::EINTR) 104 | end 105 | 106 | # @asynchronous May be non-blocking.. 107 | def kernel_sleep(duration = nil) 108 | if duration 109 | self.block(nil, duration) 110 | else 111 | self.transfer 112 | end 113 | end 114 | 115 | # @asynchronous May be non-blocking.. 116 | def address_resolve(hostname) 117 | ::Resolv.getaddresses(hostname) 118 | end 119 | 120 | # @asynchronous May be non-blocking.. 121 | def io_wait(io, events, timeout = nil) 122 | fiber = Fiber.current 123 | 124 | if timeout 125 | timer = @timers.after(timeout) do 126 | fiber.raise(TimeoutError) 127 | end 128 | end 129 | 130 | events = @selector.io_wait(fiber, io, events) 131 | 132 | return events 133 | rescue TimeoutError 134 | return false 135 | ensure 136 | timer&.cancel 137 | end 138 | 139 | # def io_read(io, buffer, length) 140 | # @selector.io_read(Fiber.current, io, buffer, length) 141 | # end 142 | # 143 | # def io_write(io, buffer, length) 144 | # @selector.io_write(Fiber.current, io, buffer, length) 145 | # end 146 | 147 | # Wait for the specified process ID to exit. 148 | # @parameter pid [Integer] The process ID to wait for. 149 | # @parameter flags [Integer] A bit-mask of flags suitable for `Process::Status.wait`. 150 | # @returns [Process::Status] A process status instance. 151 | # @asynchronous May be non-blocking.. 152 | def process_wait(pid, flags) 153 | return @selector.process_wait(Fiber.current, pid, flags) 154 | end 155 | 156 | # 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. 157 | # @parameter duration [Numeric] The time in seconds, in which the task should complete. 158 | def timeout_after(timeout, exception = TimeoutError, message = "execution expired", &block) 159 | fiber = Fiber.current 160 | 161 | timer = @timers.after(timeout) do 162 | if fiber.alive? 163 | fiber.raise(exception, message) 164 | end 165 | end 166 | 167 | yield timer 168 | ensure 169 | timer.cancel if timer 170 | end 171 | 172 | # Run one iteration of the event loop. 173 | # @parameter timeout [Float | Nil] The maximum timeout, or if nil, indefinite. 174 | # @returns [Boolean] Whether there is more work to do. 175 | def run_once(timeout = nil) 176 | Kernel.raise("Running scheduler on non-blocking fiber!") unless Fiber.blocking? 177 | 178 | # If we are finished, we stop the task tree and exit: 179 | if self.finished? 180 | return false 181 | end 182 | 183 | interval = @timers.wait_interval 184 | 185 | # If there is no interval to wait (thus no timers), and no tasks, we could be done: 186 | if interval.nil? 187 | # Allow the user to specify a maximum interval if we would otherwise be sleeping indefinitely: 188 | interval = timeout 189 | elsif interval < 0 190 | # We have timers ready to fire, don't sleep in the selctor: 191 | interval = 0 192 | elsif timeout and interval > timeout 193 | interval = timeout 194 | end 195 | 196 | begin 197 | Thread.handle_interrupt(Errno::EINTR => :on_blocking) do 198 | @selector.select(interval) 199 | end 200 | rescue Errno::EINTR 201 | # Ignore. 202 | end 203 | 204 | @timers.fire 205 | 206 | # The reactor still has work to do: 207 | return true 208 | end 209 | 210 | # Run the reactor until all tasks are finished. Proxies arguments to {#async} immediately before entering the loop, if a block is provided. 211 | def run 212 | Kernel.raise(RuntimeError, "Reactor has been closed") if @selector.nil? 213 | 214 | Thread.handle_interrupt(Errno::EINTR => :never, Interrupt => :never) do 215 | while self.run_once 216 | # Event loop. 217 | if Thread.pending_interrupt? 218 | break 219 | end 220 | end 221 | end 222 | end 223 | 224 | # Start an asynchronous task within the specified reactor. The task will be 225 | # executed until the first blocking call, at which point it will yield and 226 | # and this method will return. 227 | def fiber(&block) 228 | Fiber.new(blocking: false, &block).tap(&:transfer) 229 | end 230 | end 231 | end 232 | end 233 | -------------------------------------------------------------------------------- /examples/scheduler/scheduler_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2024, by Samuel Williams. 5 | 6 | require_relative "scheduler" 7 | 8 | RSpec.shared_examples_for IO::Event::Scheduler do 9 | subject(:scheduler) {IO::Event::Scheduler.new(selector)} 10 | 11 | around do |example| 12 | thread = Thread.new do 13 | Fiber.set_scheduler(scheduler) 14 | example.run 15 | end 16 | 17 | thread.join 18 | end 19 | 20 | it "can run several fibers" do 21 | sum = 0 22 | 23 | fibers = 3.times.map do |i| 24 | Fiber.schedule{sleep 0.001; sum += i} 25 | end 26 | 27 | subject.run 28 | 29 | expect(sum).to be == 3 30 | end 31 | 32 | it "can join threads" do 33 | Fiber.schedule do 34 | 1000.times do 35 | thread = ::Thread.new do 36 | sleep(0.001) 37 | end 38 | 39 | thread.join(0.001) 40 | ensure 41 | thread&.join 42 | end 43 | end 44 | end 45 | end 46 | 47 | IO::Event::Selector.constants.each do |name| 48 | klass = IO::Event::Selector.const_get(name) 49 | 50 | RSpec.describe(klass) do 51 | let(:loop) {Fiber.current} 52 | let(:selector){described_class.new(loop)} 53 | 54 | it_behaves_like IO::Event::Scheduler 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /ext/extconf.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Released under the MIT License. 5 | # Copyright, 2021-2025, by Samuel Williams. 6 | # Copyright, 2023, by Math Ieu. 7 | # Copyright, 2025, by Stanislav (Stas) Katkov. 8 | 9 | return if RUBY_DESCRIPTION =~ /jruby/ 10 | 11 | require "mkmf" 12 | 13 | gem_name = File.basename(__dir__) 14 | extension_name = "IO_Event" 15 | 16 | # dir_config(extension_name) 17 | 18 | append_cflags(["-Wall", "-Wno-unknown-pragmas", "-std=c99"]) 19 | 20 | if ENV.key?("RUBY_DEBUG") 21 | $stderr.puts "Enabling debug mode..." 22 | 23 | append_cflags(["-DRUBY_DEBUG", "-O0"]) 24 | end 25 | 26 | $srcs = ["io/event/event.c", "io/event/time.c", "io/event/fiber.c", "io/event/selector/selector.c"] 27 | $VPATH << "$(srcdir)/io/event" 28 | $VPATH << "$(srcdir)/io/event/selector" 29 | 30 | have_func("rb_ext_ractor_safe") 31 | have_func("&rb_fiber_transfer") 32 | 33 | if have_library("uring") and have_header("liburing.h") 34 | # We might want to consider using this in the future: 35 | # have_func("io_uring_submit_and_wait_timeout", "liburing.h") 36 | 37 | $srcs << "io/event/selector/uring.c" 38 | end 39 | 40 | if have_header("sys/epoll.h") 41 | $srcs << "io/event/selector/epoll.c" 42 | end 43 | 44 | if have_header("sys/event.h") 45 | $srcs << "io/event/selector/kqueue.c" 46 | end 47 | 48 | have_header("sys/wait.h") 49 | 50 | have_header("sys/eventfd.h") 51 | $srcs << "io/event/interrupt.c" 52 | 53 | have_func("rb_io_descriptor") 54 | have_func("&rb_process_status_wait") 55 | have_func("rb_fiber_current") 56 | have_func("&rb_fiber_raise") 57 | have_func("epoll_pwait2") 58 | 59 | have_header("ruby/io/buffer.h") 60 | 61 | if ENV.key?("RUBY_SANITIZE") 62 | $stderr.puts "Enabling sanitizers..." 63 | 64 | # Add address and undefined behaviour sanitizers: 65 | append_cflags(["-fsanitize=address", "-fsanitize=undefined", "-fno-omit-frame-pointer"]) 66 | $LDFLAGS << " -fsanitize=address -fsanitize=undefined" 67 | end 68 | 69 | create_header 70 | 71 | # Generate the makefile to compile the native binary into `lib`: 72 | create_makefile(extension_name) 73 | -------------------------------------------------------------------------------- /ext/io/event/array.h: -------------------------------------------------------------------------------- 1 | // Released under the MIT License. 2 | // Copyright, 2023, by Samuel Williams. 3 | 4 | // Provides a simple implementation of unique pointers to elements of the given size. 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | static const size_t IO_EVENT_ARRAY_MAXIMUM_COUNT = SIZE_MAX / sizeof(void*); 12 | static const size_t IO_EVENT_ARRAY_DEFAULT_COUNT = 128; 13 | 14 | struct IO_Event_Array { 15 | // The array of pointers to elements: 16 | void **base; 17 | 18 | // The allocated size of the array: 19 | size_t count; 20 | 21 | // The biggest item we've seen so far: 22 | size_t limit; 23 | 24 | // The size of each element that is allocated: 25 | size_t element_size; 26 | 27 | void (*element_initialize)(void*); 28 | void (*element_free)(void*); 29 | }; 30 | 31 | inline static int IO_Event_Array_initialize(struct IO_Event_Array *array, size_t count, size_t element_size) 32 | { 33 | array->limit = 0; 34 | array->element_size = element_size; 35 | 36 | if (count) { 37 | array->base = (void**)calloc(count, sizeof(void*)); 38 | 39 | if (array->base == NULL) { 40 | return -1; 41 | } 42 | 43 | array->count = count; 44 | 45 | return 1; 46 | } else { 47 | array->base = NULL; 48 | array->count = 0; 49 | 50 | return 0; 51 | } 52 | } 53 | 54 | inline static size_t IO_Event_Array_memory_size(const struct IO_Event_Array *array) 55 | { 56 | // Upper bound. 57 | return array->count * (sizeof(void*) + array->element_size); 58 | } 59 | 60 | inline static void IO_Event_Array_free(struct IO_Event_Array *array) 61 | { 62 | if (array->base) { 63 | void **base = array->base; 64 | size_t limit = array->limit; 65 | 66 | array->base = NULL; 67 | array->count = 0; 68 | array->limit = 0; 69 | 70 | for (size_t i = 0; i < limit; i += 1) { 71 | void *element = base[i]; 72 | if (element) { 73 | array->element_free(element); 74 | 75 | free(element); 76 | } 77 | } 78 | 79 | free(base); 80 | } 81 | } 82 | 83 | inline static int IO_Event_Array_resize(struct IO_Event_Array *array, size_t count) 84 | { 85 | if (count <= array->count) { 86 | // Already big enough: 87 | return 0; 88 | } 89 | 90 | if (count > IO_EVENT_ARRAY_MAXIMUM_COUNT) { 91 | errno = ENOMEM; 92 | return -1; 93 | } 94 | 95 | size_t new_count = array->count; 96 | 97 | // If the array is empty, we need to set the initial size: 98 | if (new_count == 0) new_count = IO_EVENT_ARRAY_DEFAULT_COUNT; 99 | else while (new_count < count) { 100 | // Ensure we don't overflow: 101 | if (new_count > (IO_EVENT_ARRAY_MAXIMUM_COUNT / 2)) { 102 | new_count = IO_EVENT_ARRAY_MAXIMUM_COUNT; 103 | break; 104 | } 105 | 106 | // Compute the next multiple (ideally a power of 2): 107 | new_count *= 2; 108 | } 109 | 110 | void **new_base = (void**)realloc(array->base, new_count * sizeof(void*)); 111 | 112 | if (new_base == NULL) { 113 | return -1; 114 | } 115 | 116 | // Zero out the new memory: 117 | memset(new_base + array->count, 0, (new_count - array->count) * sizeof(void*)); 118 | 119 | array->base = (void**)new_base; 120 | array->count = new_count; 121 | 122 | // Resizing sucessful: 123 | return 1; 124 | } 125 | 126 | inline static void* IO_Event_Array_lookup(struct IO_Event_Array *array, size_t index) 127 | { 128 | size_t count = index + 1; 129 | 130 | // Resize the array if necessary: 131 | if (count > array->count) { 132 | if (IO_Event_Array_resize(array, count) == -1) { 133 | return NULL; 134 | } 135 | } 136 | 137 | // Get the element: 138 | void **element = array->base + index; 139 | 140 | // Allocate the element if it doesn't exist: 141 | if (*element == NULL) { 142 | *element = malloc(array->element_size); 143 | assert(*element); 144 | 145 | if (array->element_initialize) { 146 | array->element_initialize(*element); 147 | } 148 | 149 | // Update the limit: 150 | if (count > array->limit) array->limit = count; 151 | } 152 | 153 | return *element; 154 | } 155 | 156 | inline static void* IO_Event_Array_last(struct IO_Event_Array *array) 157 | { 158 | if (array->limit == 0) return NULL; 159 | else return array->base[array->limit - 1]; 160 | } 161 | 162 | inline static void IO_Event_Array_truncate(struct IO_Event_Array *array, size_t limit) 163 | { 164 | if (limit < array->limit) { 165 | for (size_t i = limit; i < array->limit; i += 1) { 166 | void **element = array->base + i; 167 | if (*element) { 168 | array->element_free(*element); 169 | free(*element); 170 | *element = NULL; 171 | } 172 | } 173 | 174 | array->limit = limit; 175 | } 176 | } 177 | 178 | // Push a new element onto the end of the array. 179 | inline static void* IO_Event_Array_push(struct IO_Event_Array *array) 180 | { 181 | return IO_Event_Array_lookup(array, array->limit); 182 | } 183 | 184 | inline static void IO_Event_Array_each(struct IO_Event_Array *array, void (*callback)(void*)) 185 | { 186 | for (size_t i = 0; i < array->limit; i += 1) { 187 | void *element = array->base[i]; 188 | if (element) { 189 | callback(element); 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /ext/io/event/event.c: -------------------------------------------------------------------------------- 1 | // Released under the MIT License. 2 | // Copyright, 2021-2025, by Samuel Williams. 3 | 4 | #include "event.h" 5 | #include "fiber.h" 6 | #include "selector/selector.h" 7 | 8 | void Init_IO_Event(void) 9 | { 10 | #ifdef HAVE_RB_EXT_RACTOR_SAFE 11 | rb_ext_ractor_safe(true); 12 | #endif 13 | 14 | VALUE IO_Event = rb_define_module_under(rb_cIO, "Event"); 15 | 16 | Init_IO_Event_Fiber(IO_Event); 17 | 18 | VALUE IO_Event_Selector = rb_define_module_under(IO_Event, "Selector"); 19 | Init_IO_Event_Selector(IO_Event_Selector); 20 | 21 | #ifdef IO_EVENT_SELECTOR_URING 22 | Init_IO_Event_Selector_URing(IO_Event_Selector); 23 | #endif 24 | 25 | #ifdef IO_EVENT_SELECTOR_EPOLL 26 | Init_IO_Event_Selector_EPoll(IO_Event_Selector); 27 | #endif 28 | 29 | #ifdef IO_EVENT_SELECTOR_KQUEUE 30 | Init_IO_Event_Selector_KQueue(IO_Event_Selector); 31 | #endif 32 | } 33 | -------------------------------------------------------------------------------- /ext/io/event/event.h: -------------------------------------------------------------------------------- 1 | // Released under the MIT License. 2 | // Copyright, 2021-2025, by Samuel Williams. 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | void Init_IO_Event(void); 9 | 10 | #ifdef HAVE_LIBURING_H 11 | #include "selector/uring.h" 12 | #endif 13 | 14 | #ifdef HAVE_SYS_EPOLL_H 15 | #include "selector/epoll.h" 16 | #endif 17 | 18 | #ifdef HAVE_SYS_EVENT_H 19 | #include "selector/kqueue.h" 20 | #endif 21 | -------------------------------------------------------------------------------- /ext/io/event/fiber.c: -------------------------------------------------------------------------------- 1 | // Released under the MIT License. 2 | // Copyright, 2025, by Samuel Williams. 3 | 4 | #include "fiber.h" 5 | 6 | static ID id_transfer, id_alive_p; 7 | 8 | VALUE IO_Event_Fiber_transfer(VALUE fiber, int argc, VALUE *argv) { 9 | // TODO Consider introducing something like `rb_fiber_scheduler_transfer(...)`. 10 | #ifdef HAVE__RB_FIBER_TRANSFER 11 | if (RTEST(rb_obj_is_fiber(fiber))) { 12 | if (RTEST(rb_fiber_alive_p(fiber))) { 13 | return rb_fiber_transfer(fiber, argc, argv); 14 | } 15 | 16 | // If it's a fiber, but dead, we are done. 17 | return Qnil; 18 | } 19 | #endif 20 | if (RTEST(rb_funcall(fiber, id_alive_p, 0))) { 21 | return rb_funcallv(fiber, id_transfer, argc, argv); 22 | } 23 | 24 | return Qnil; 25 | } 26 | 27 | #ifndef HAVE__RB_FIBER_RAISE 28 | static ID id_raise; 29 | 30 | VALUE IO_Event_Fiber_raise(VALUE fiber, int argc, VALUE *argv) { 31 | return rb_funcallv(fiber, id_raise, argc, argv); 32 | } 33 | #endif 34 | 35 | #ifndef HAVE_RB_FIBER_CURRENT 36 | static ID id_current; 37 | 38 | static VALUE IO_Event_Fiber_current(void) { 39 | return rb_funcall(rb_cFiber, id_current, 0); 40 | } 41 | #endif 42 | 43 | // There is no public interface for this... yet. 44 | static ID id_blocking_p; 45 | 46 | int IO_Event_Fiber_blocking(VALUE fiber) { 47 | return RTEST(rb_funcall(fiber, id_blocking_p, 0)); 48 | } 49 | 50 | void Init_IO_Event_Fiber(VALUE IO_Event) { 51 | id_transfer = rb_intern("transfer"); 52 | id_alive_p = rb_intern("alive?"); 53 | 54 | #ifndef HAVE__RB_FIBER_RAISE 55 | id_raise = rb_intern("raise"); 56 | #endif 57 | 58 | #ifndef HAVE_RB_FIBER_CURRENT 59 | id_current = rb_intern("current"); 60 | #endif 61 | 62 | id_blocking_p = rb_intern("blocking?"); 63 | } 64 | -------------------------------------------------------------------------------- /ext/io/event/fiber.h: -------------------------------------------------------------------------------- 1 | // Released under the MIT License. 2 | // Copyright, 2025, by Samuel Williams. 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | VALUE IO_Event_Fiber_transfer(VALUE fiber, int argc, VALUE *argv); 9 | 10 | #ifdef HAVE__RB_FIBER_RAISE 11 | #define IO_Event_Fiber_raise(fiber, argc, argv) rb_fiber_raise(fiber, argc, argv) 12 | #else 13 | VALUE IO_Event_Fiber_raise(VALUE fiber, int argc, VALUE *argv); 14 | #endif 15 | 16 | #ifdef HAVE_RB_FIBER_CURRENT 17 | #define IO_Event_Fiber_current() rb_fiber_current() 18 | #else 19 | VALUE IO_Event_Fiber_current(void); 20 | #endif 21 | 22 | int IO_Event_Fiber_blocking(VALUE fiber); 23 | void Init_IO_Event_Fiber(VALUE IO_Event); 24 | -------------------------------------------------------------------------------- /ext/io/event/interrupt.c: -------------------------------------------------------------------------------- 1 | // Released under the MIT License. 2 | // Copyright, 2021-2025, by Samuel Williams. 3 | 4 | #include "interrupt.h" 5 | #include 6 | 7 | #include "selector/selector.h" 8 | 9 | #ifdef HAVE_RUBY_WIN32_H 10 | #include 11 | #if !defined(HAVE_PIPE) && !defined(pipe) 12 | #define pipe(p) rb_w32_pipe(p) 13 | #endif 14 | #endif 15 | 16 | #ifdef HAVE_SYS_EVENTFD_H 17 | #include 18 | 19 | void IO_Event_Interrupt_open(struct IO_Event_Interrupt *interrupt) 20 | { 21 | interrupt->descriptor = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); 22 | rb_update_max_fd(interrupt->descriptor); 23 | } 24 | 25 | void IO_Event_Interrupt_close(struct IO_Event_Interrupt *interrupt) 26 | { 27 | close(interrupt->descriptor); 28 | } 29 | 30 | void IO_Event_Interrupt_signal(struct IO_Event_Interrupt *interrupt) 31 | { 32 | uint64_t value = 1; 33 | ssize_t result = write(interrupt->descriptor, &value, sizeof(value)); 34 | 35 | if (result == -1) { 36 | if (errno == EAGAIN || errno == EWOULDBLOCK) return; 37 | 38 | rb_sys_fail("IO_Event_Interrupt_signal:write"); 39 | } 40 | } 41 | 42 | void IO_Event_Interrupt_clear(struct IO_Event_Interrupt *interrupt) 43 | { 44 | uint64_t value = 0; 45 | ssize_t result = read(interrupt->descriptor, &value, sizeof(value)); 46 | 47 | if (result == -1) { 48 | if (errno == EAGAIN || errno == EWOULDBLOCK) return; 49 | 50 | rb_sys_fail("IO_Event_Interrupt_clear:read"); 51 | } 52 | } 53 | #else 54 | void IO_Event_Interrupt_open(struct IO_Event_Interrupt *interrupt) 55 | { 56 | #ifdef __linux__ 57 | pipe2(interrupt->descriptor, O_CLOEXEC | O_NONBLOCK); 58 | #else 59 | pipe(interrupt->descriptor); 60 | IO_Event_Selector_nonblock_set(interrupt->descriptor[0]); 61 | IO_Event_Selector_nonblock_set(interrupt->descriptor[1]); 62 | #endif 63 | 64 | rb_update_max_fd(interrupt->descriptor[0]); 65 | rb_update_max_fd(interrupt->descriptor[1]); 66 | } 67 | 68 | void IO_Event_Interrupt_close(struct IO_Event_Interrupt *interrupt) 69 | { 70 | close(interrupt->descriptor[0]); 71 | close(interrupt->descriptor[1]); 72 | } 73 | 74 | void IO_Event_Interrupt_signal(struct IO_Event_Interrupt *interrupt) 75 | { 76 | ssize_t result = write(interrupt->descriptor[1], ".", 1); 77 | 78 | if (result == -1) { 79 | if (errno == EAGAIN || errno == EWOULDBLOCK) { 80 | // If we can't write to the pipe, it means the other end is full. In that case, we can be sure that the other end has already been woken up or is about to be woken up. 81 | } else { 82 | rb_sys_fail("IO_Event_Interrupt_signal:write"); 83 | } 84 | } 85 | } 86 | 87 | void IO_Event_Interrupt_clear(struct IO_Event_Interrupt *interrupt) 88 | { 89 | char buffer[128]; 90 | ssize_t result = read(interrupt->descriptor[0], buffer, sizeof(buffer)); 91 | 92 | if (result == -1) { 93 | if (errno == EAGAIN || errno == EWOULDBLOCK) { 94 | // If we can't read from the pipe, it means the other end is empty. In that case, we can be sure that the other end is already clear. 95 | } else { 96 | rb_sys_fail("IO_Event_Interrupt_clear:read"); 97 | } 98 | } 99 | } 100 | #endif 101 | -------------------------------------------------------------------------------- /ext/io/event/interrupt.h: -------------------------------------------------------------------------------- 1 | // Released under the MIT License. 2 | // Copyright, 2021-2025, by Samuel Williams. 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #ifdef HAVE_SYS_EVENTFD_H 9 | struct IO_Event_Interrupt { 10 | int descriptor; 11 | }; 12 | 13 | static inline int IO_Event_Interrupt_descriptor(struct IO_Event_Interrupt *interrupt) { 14 | return interrupt->descriptor; 15 | } 16 | #else 17 | struct IO_Event_Interrupt { 18 | int descriptor[2]; 19 | }; 20 | 21 | static inline int IO_Event_Interrupt_descriptor(struct IO_Event_Interrupt *interrupt) { 22 | return interrupt->descriptor[0]; 23 | } 24 | #endif 25 | 26 | void IO_Event_Interrupt_open(struct IO_Event_Interrupt *interrupt); 27 | void IO_Event_Interrupt_close(struct IO_Event_Interrupt *interrupt); 28 | 29 | void IO_Event_Interrupt_signal(struct IO_Event_Interrupt *interrupt); 30 | void IO_Event_Interrupt_clear(struct IO_Event_Interrupt *interrupt); 31 | -------------------------------------------------------------------------------- /ext/io/event/list.h: -------------------------------------------------------------------------------- 1 | // Released under the MIT License. 2 | // Copyright, 2023-2025, by Samuel Williams. 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | struct IO_Event_List_Type { 9 | }; 10 | 11 | struct IO_Event_List { 12 | struct IO_Event_List *head, *tail; 13 | struct IO_Event_List_Type *type; 14 | }; 15 | 16 | inline static void IO_Event_List_initialize(struct IO_Event_List *list) 17 | { 18 | list->head = list->tail = list; 19 | list->type = 0; 20 | } 21 | 22 | inline static void IO_Event_List_clear(struct IO_Event_List *list) 23 | { 24 | list->head = list->tail = NULL; 25 | list->type = 0; 26 | } 27 | 28 | // Append an item to the end of the list. 29 | inline static void IO_Event_List_append(struct IO_Event_List *list, struct IO_Event_List *node) 30 | { 31 | assert(node->head == NULL); 32 | assert(node->tail == NULL); 33 | 34 | struct IO_Event_List *head = list->head; 35 | node->tail = list; 36 | node->head = head; 37 | list->head = node; 38 | head->tail = node; 39 | } 40 | 41 | // Prepend an item to the beginning of the list. 42 | inline static void IO_Event_List_prepend(struct IO_Event_List *list, struct IO_Event_List *node) 43 | { 44 | assert(node->head == NULL); 45 | assert(node->tail == NULL); 46 | 47 | struct IO_Event_List *tail = list->tail; 48 | node->head = list; 49 | node->tail = tail; 50 | list->tail = node; 51 | tail->head = node; 52 | } 53 | 54 | // Pop an item from the list. 55 | inline static void IO_Event_List_pop(struct IO_Event_List *node) 56 | { 57 | assert(node->head != NULL); 58 | assert(node->tail != NULL); 59 | 60 | struct IO_Event_List *head = node->head; 61 | struct IO_Event_List *tail = node->tail; 62 | 63 | head->tail = tail; 64 | tail->head = head; 65 | node->head = node->tail = NULL; 66 | } 67 | 68 | // Remove an item from the list, if it is in a list. 69 | inline static void IO_Event_List_free(struct IO_Event_List *node) 70 | { 71 | if (node->head && node->tail) { 72 | IO_Event_List_pop(node); 73 | } 74 | } 75 | 76 | // Calculate the memory size of the list nodes. 77 | inline static size_t IO_Event_List_memory_size(const struct IO_Event_List *list) 78 | { 79 | size_t memsize = 0; 80 | 81 | const struct IO_Event_List *node = list->tail; 82 | while (node != list) { 83 | memsize += sizeof(struct IO_Event_List); 84 | node = node->tail; 85 | } 86 | 87 | return memsize; 88 | } 89 | 90 | // Return true if the list is empty. 91 | inline static int IO_Event_List_empty(const struct IO_Event_List *list) 92 | { 93 | return list->head == list->tail; 94 | } 95 | 96 | // Enumerate all items in the list, assuming the list will not be modified during iteration. 97 | inline static void IO_Event_List_immutable_each(struct IO_Event_List *list, void (*callback)(struct IO_Event_List *node)) 98 | { 99 | struct IO_Event_List *node = list->tail; 100 | 101 | while (node != list) { 102 | if (node->type) 103 | callback(node); 104 | 105 | node = node->tail; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /ext/io/event/selector/epoll.c: -------------------------------------------------------------------------------- 1 | // Released under the MIT License. 2 | // Copyright, 2021-2025, by Samuel Williams. 3 | 4 | #include "epoll.h" 5 | #include "selector.h" 6 | #include "../list.h" 7 | #include "../array.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "pidfd.c" 14 | #include "../interrupt.h" 15 | 16 | enum { 17 | DEBUG = 0, 18 | }; 19 | 20 | enum {EPOLL_MAX_EVENTS = 64}; 21 | 22 | // This represents an actual fiber waiting for a specific event. 23 | struct IO_Event_Selector_EPoll_Waiting 24 | { 25 | struct IO_Event_List list; 26 | 27 | // The events the fiber is waiting for. 28 | enum IO_Event events; 29 | 30 | // The events that are currently ready. 31 | enum IO_Event ready; 32 | 33 | // The fiber value itself. 34 | VALUE fiber; 35 | }; 36 | 37 | struct IO_Event_Selector_EPoll 38 | { 39 | struct IO_Event_Selector backend; 40 | int descriptor; 41 | int blocked; 42 | 43 | struct timespec idle_duration; 44 | 45 | struct IO_Event_Interrupt interrupt; 46 | struct IO_Event_Array descriptors; 47 | }; 48 | 49 | // This represents zero or more fibers waiting for a specific descriptor. 50 | struct IO_Event_Selector_EPoll_Descriptor 51 | { 52 | struct IO_Event_List list; 53 | 54 | // The last IO object that was used to register events. 55 | VALUE io; 56 | 57 | // The union of all events we are waiting for: 58 | enum IO_Event waiting_events; 59 | 60 | // The union of events we are registered for: 61 | enum IO_Event registered_events; 62 | }; 63 | 64 | static 65 | void IO_Event_Selector_EPoll_Waiting_mark(struct IO_Event_List *_waiting) 66 | { 67 | struct IO_Event_Selector_EPoll_Waiting *waiting = (void*)_waiting; 68 | 69 | if (waiting->fiber) { 70 | rb_gc_mark_movable(waiting->fiber); 71 | } 72 | } 73 | 74 | static 75 | void IO_Event_Selector_EPoll_Descriptor_mark(void *_descriptor) 76 | { 77 | struct IO_Event_Selector_EPoll_Descriptor *descriptor = _descriptor; 78 | 79 | IO_Event_List_immutable_each(&descriptor->list, IO_Event_Selector_EPoll_Waiting_mark); 80 | 81 | if (descriptor->io) { 82 | rb_gc_mark_movable(descriptor->io); 83 | } 84 | } 85 | 86 | static 87 | void IO_Event_Selector_EPoll_Type_mark(void *_selector) 88 | { 89 | struct IO_Event_Selector_EPoll *selector = _selector; 90 | 91 | IO_Event_Selector_mark(&selector->backend); 92 | IO_Event_Array_each(&selector->descriptors, IO_Event_Selector_EPoll_Descriptor_mark); 93 | } 94 | 95 | static 96 | void IO_Event_Selector_EPoll_Waiting_compact(struct IO_Event_List *_waiting) 97 | { 98 | struct IO_Event_Selector_EPoll_Waiting *waiting = (void*)_waiting; 99 | 100 | if (waiting->fiber) { 101 | waiting->fiber = rb_gc_location(waiting->fiber); 102 | } 103 | } 104 | 105 | static 106 | void IO_Event_Selector_EPoll_Descriptor_compact(void *_descriptor) 107 | { 108 | struct IO_Event_Selector_EPoll_Descriptor *descriptor = _descriptor; 109 | 110 | IO_Event_List_immutable_each(&descriptor->list, IO_Event_Selector_EPoll_Waiting_compact); 111 | 112 | if (descriptor->io) { 113 | descriptor->io = rb_gc_location(descriptor->io); 114 | } 115 | } 116 | 117 | static 118 | void IO_Event_Selector_EPoll_Type_compact(void *_selector) 119 | { 120 | struct IO_Event_Selector_EPoll *selector = _selector; 121 | 122 | IO_Event_Selector_compact(&selector->backend); 123 | IO_Event_Array_each(&selector->descriptors, IO_Event_Selector_EPoll_Descriptor_compact); 124 | } 125 | 126 | static 127 | void close_internal(struct IO_Event_Selector_EPoll *selector) 128 | { 129 | if (selector->descriptor >= 0) { 130 | close(selector->descriptor); 131 | selector->descriptor = -1; 132 | 133 | IO_Event_Interrupt_close(&selector->interrupt); 134 | } 135 | } 136 | 137 | static 138 | void IO_Event_Selector_EPoll_Type_free(void *_selector) 139 | { 140 | struct IO_Event_Selector_EPoll *selector = _selector; 141 | 142 | close_internal(selector); 143 | 144 | IO_Event_Array_free(&selector->descriptors); 145 | 146 | free(selector); 147 | } 148 | 149 | static 150 | size_t IO_Event_Selector_EPoll_Type_size(const void *_selector) 151 | { 152 | const struct IO_Event_Selector_EPoll *selector = _selector; 153 | 154 | return sizeof(struct IO_Event_Selector_EPoll) 155 | + IO_Event_Array_memory_size(&selector->descriptors) 156 | ; 157 | } 158 | 159 | static const rb_data_type_t IO_Event_Selector_EPoll_Type = { 160 | .wrap_struct_name = "IO::Event::Backend::EPoll", 161 | .function = { 162 | .dmark = IO_Event_Selector_EPoll_Type_mark, 163 | .dcompact = IO_Event_Selector_EPoll_Type_compact, 164 | .dfree = IO_Event_Selector_EPoll_Type_free, 165 | .dsize = IO_Event_Selector_EPoll_Type_size, 166 | }, 167 | .data = NULL, 168 | .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED, 169 | }; 170 | 171 | inline static 172 | struct IO_Event_Selector_EPoll_Descriptor * IO_Event_Selector_EPoll_Descriptor_lookup(struct IO_Event_Selector_EPoll *selector, int descriptor) 173 | { 174 | struct IO_Event_Selector_EPoll_Descriptor *epoll_descriptor = IO_Event_Array_lookup(&selector->descriptors, descriptor); 175 | 176 | if (!epoll_descriptor) { 177 | rb_sys_fail("IO_Event_Selector_EPoll_Descriptor_lookup:IO_Event_Array_lookup"); 178 | } 179 | 180 | return epoll_descriptor; 181 | } 182 | 183 | static inline 184 | uint32_t epoll_flags_from_events(int events) 185 | { 186 | uint32_t flags = 0; 187 | 188 | if (events & IO_EVENT_READABLE) flags |= EPOLLIN; 189 | if (events & IO_EVENT_PRIORITY) flags |= EPOLLPRI; 190 | if (events & IO_EVENT_WRITABLE) flags |= EPOLLOUT; 191 | 192 | flags |= EPOLLHUP; 193 | flags |= EPOLLERR; 194 | 195 | if (DEBUG) fprintf(stderr, "epoll_flags_from_events events=%d flags=%d\n", events, flags); 196 | 197 | return flags; 198 | } 199 | 200 | static inline 201 | int events_from_epoll_flags(uint32_t flags) 202 | { 203 | int events = 0; 204 | 205 | if (DEBUG) fprintf(stderr, "events_from_epoll_flags flags=%d\n", flags); 206 | 207 | // Occasionally, (and noted specifically when dealing with child processes stdout), flags will only be POLLHUP. In this case, we arm the file descriptor for reading so that the HUP will be noted, rather than potentially ignored, since there is no dedicated event for it. 208 | // if (flags & (EPOLLIN)) events |= IO_EVENT_READABLE; 209 | if (flags & (EPOLLIN|EPOLLHUP|EPOLLERR)) events |= IO_EVENT_READABLE; 210 | if (flags & EPOLLPRI) events |= IO_EVENT_PRIORITY; 211 | if (flags & EPOLLOUT) events |= IO_EVENT_WRITABLE; 212 | 213 | return events; 214 | } 215 | 216 | inline static 217 | int IO_Event_Selector_EPoll_Descriptor_update(struct IO_Event_Selector_EPoll *selector, VALUE io, int descriptor, struct IO_Event_Selector_EPoll_Descriptor *epoll_descriptor) 218 | { 219 | if (epoll_descriptor->io == io) { 220 | if (epoll_descriptor->registered_events == epoll_descriptor->waiting_events) { 221 | // All the events we are interested in are already registered. 222 | return 0; 223 | } 224 | } else { 225 | // The IO has changed, we need to reset the state: 226 | epoll_descriptor->registered_events = 0; 227 | RB_OBJ_WRITE(selector->backend.self, &epoll_descriptor->io, io); 228 | } 229 | 230 | if (epoll_descriptor->waiting_events == 0) { 231 | if (epoll_descriptor->registered_events) { 232 | // We are no longer interested in any events. 233 | epoll_ctl(selector->descriptor, EPOLL_CTL_DEL, descriptor, NULL); 234 | epoll_descriptor->registered_events = 0; 235 | } 236 | 237 | RB_OBJ_WRITE(selector->backend.self, &epoll_descriptor->io, 0); 238 | 239 | return 0; 240 | } 241 | 242 | // We need to register for additional events: 243 | struct epoll_event event = { 244 | .events = epoll_flags_from_events(epoll_descriptor->waiting_events), 245 | .data = {.fd = descriptor}, 246 | }; 247 | 248 | int operation; 249 | 250 | if (epoll_descriptor->registered_events) { 251 | operation = EPOLL_CTL_MOD; 252 | } else { 253 | operation = EPOLL_CTL_ADD; 254 | } 255 | 256 | int result = epoll_ctl(selector->descriptor, operation, descriptor, &event); 257 | if (result == -1) { 258 | if (errno == ENOENT) { 259 | result = epoll_ctl(selector->descriptor, EPOLL_CTL_ADD, descriptor, &event); 260 | } else if (errno == EEXIST) { 261 | result = epoll_ctl(selector->descriptor, EPOLL_CTL_MOD, descriptor, &event); 262 | } 263 | 264 | if (result == -1) { 265 | return -1; 266 | } 267 | } 268 | 269 | epoll_descriptor->registered_events = epoll_descriptor->waiting_events; 270 | 271 | return 1; 272 | } 273 | 274 | inline static 275 | int IO_Event_Selector_EPoll_Waiting_register(struct IO_Event_Selector_EPoll *selector, VALUE io, int descriptor, struct IO_Event_Selector_EPoll_Waiting *waiting) 276 | { 277 | struct IO_Event_Selector_EPoll_Descriptor *epoll_descriptor = IO_Event_Selector_EPoll_Descriptor_lookup(selector, descriptor); 278 | 279 | // We are waiting for these events: 280 | epoll_descriptor->waiting_events |= waiting->events; 281 | 282 | int result = IO_Event_Selector_EPoll_Descriptor_update(selector, io, descriptor, epoll_descriptor); 283 | if (result == -1) return -1; 284 | 285 | IO_Event_List_prepend(&epoll_descriptor->list, &waiting->list); 286 | 287 | return result; 288 | } 289 | 290 | inline static 291 | void IO_Event_Selector_EPoll_Waiting_cancel(struct IO_Event_Selector_EPoll_Waiting *waiting) 292 | { 293 | IO_Event_List_pop(&waiting->list); 294 | waiting->fiber = 0; 295 | } 296 | 297 | void IO_Event_Selector_EPoll_Descriptor_initialize(void *element) 298 | { 299 | struct IO_Event_Selector_EPoll_Descriptor *epoll_descriptor = element; 300 | IO_Event_List_initialize(&epoll_descriptor->list); 301 | epoll_descriptor->io = 0; 302 | epoll_descriptor->waiting_events = 0; 303 | epoll_descriptor->registered_events = 0; 304 | } 305 | 306 | void IO_Event_Selector_EPoll_Descriptor_free(void *element) 307 | { 308 | struct IO_Event_Selector_EPoll_Descriptor *epoll_descriptor = element; 309 | 310 | IO_Event_List_free(&epoll_descriptor->list); 311 | } 312 | 313 | VALUE IO_Event_Selector_EPoll_allocate(VALUE self) { 314 | struct IO_Event_Selector_EPoll *selector = NULL; 315 | VALUE instance = TypedData_Make_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); 316 | 317 | IO_Event_Selector_initialize(&selector->backend, self, Qnil); 318 | selector->descriptor = -1; 319 | selector->blocked = 0; 320 | 321 | selector->descriptors.element_initialize = IO_Event_Selector_EPoll_Descriptor_initialize; 322 | selector->descriptors.element_free = IO_Event_Selector_EPoll_Descriptor_free; 323 | int result = IO_Event_Array_initialize(&selector->descriptors, IO_EVENT_ARRAY_DEFAULT_COUNT, sizeof(struct IO_Event_Selector_EPoll_Descriptor)); 324 | if (result < 0) { 325 | rb_sys_fail("IO_Event_Selector_EPoll_allocate:IO_Event_Array_initialize"); 326 | } 327 | 328 | return instance; 329 | } 330 | 331 | void IO_Event_Interrupt_add(struct IO_Event_Interrupt *interrupt, struct IO_Event_Selector_EPoll *selector) { 332 | int descriptor = IO_Event_Interrupt_descriptor(interrupt); 333 | 334 | struct epoll_event event = { 335 | .events = EPOLLIN|EPOLLRDHUP, 336 | .data = {.fd = -1}, 337 | }; 338 | 339 | int result = epoll_ctl(selector->descriptor, EPOLL_CTL_ADD, descriptor, &event); 340 | 341 | if (result == -1) { 342 | rb_sys_fail("IO_Event_Interrupt_add:epoll_ctl"); 343 | } 344 | } 345 | 346 | VALUE IO_Event_Selector_EPoll_initialize(VALUE self, VALUE loop) { 347 | struct IO_Event_Selector_EPoll *selector = NULL; 348 | TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); 349 | 350 | IO_Event_Selector_initialize(&selector->backend, self, loop); 351 | int result = epoll_create1(EPOLL_CLOEXEC); 352 | 353 | if (result == -1) { 354 | rb_sys_fail("IO_Event_Selector_EPoll_initialize:epoll_create"); 355 | } else { 356 | selector->descriptor = result; 357 | 358 | rb_update_max_fd(selector->descriptor); 359 | } 360 | 361 | IO_Event_Interrupt_open(&selector->interrupt); 362 | IO_Event_Interrupt_add(&selector->interrupt, selector); 363 | 364 | return self; 365 | } 366 | 367 | VALUE IO_Event_Selector_EPoll_loop(VALUE self) { 368 | struct IO_Event_Selector_EPoll *selector = NULL; 369 | TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); 370 | 371 | return selector->backend.loop; 372 | } 373 | 374 | VALUE IO_Event_Selector_EPoll_idle_duration(VALUE self) { 375 | struct IO_Event_Selector_EPoll *selector = NULL; 376 | TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); 377 | 378 | double duration = selector->idle_duration.tv_sec + (selector->idle_duration.tv_nsec / 1000000000.0); 379 | 380 | return DBL2NUM(duration); 381 | } 382 | 383 | VALUE IO_Event_Selector_EPoll_close(VALUE self) { 384 | struct IO_Event_Selector_EPoll *selector = NULL; 385 | TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); 386 | 387 | close_internal(selector); 388 | 389 | return Qnil; 390 | } 391 | 392 | VALUE IO_Event_Selector_EPoll_transfer(VALUE self) 393 | { 394 | struct IO_Event_Selector_EPoll *selector = NULL; 395 | TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); 396 | 397 | return IO_Event_Selector_loop_yield(&selector->backend); 398 | } 399 | 400 | VALUE IO_Event_Selector_EPoll_resume(int argc, VALUE *argv, VALUE self) 401 | { 402 | struct IO_Event_Selector_EPoll *selector = NULL; 403 | TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); 404 | 405 | return IO_Event_Selector_resume(&selector->backend, argc, argv); 406 | } 407 | 408 | VALUE IO_Event_Selector_EPoll_yield(VALUE self) 409 | { 410 | struct IO_Event_Selector_EPoll *selector = NULL; 411 | TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); 412 | 413 | return IO_Event_Selector_yield(&selector->backend); 414 | } 415 | 416 | VALUE IO_Event_Selector_EPoll_push(VALUE self, VALUE fiber) 417 | { 418 | struct IO_Event_Selector_EPoll *selector = NULL; 419 | TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); 420 | 421 | IO_Event_Selector_ready_push(&selector->backend, fiber); 422 | 423 | return Qnil; 424 | } 425 | 426 | VALUE IO_Event_Selector_EPoll_raise(int argc, VALUE *argv, VALUE self) 427 | { 428 | struct IO_Event_Selector_EPoll *selector = NULL; 429 | TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); 430 | 431 | return IO_Event_Selector_raise(&selector->backend, argc, argv); 432 | } 433 | 434 | VALUE IO_Event_Selector_EPoll_ready_p(VALUE self) { 435 | struct IO_Event_Selector_EPoll *selector = NULL; 436 | TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); 437 | 438 | return selector->backend.ready ? Qtrue : Qfalse; 439 | } 440 | 441 | struct process_wait_arguments { 442 | struct IO_Event_Selector_EPoll *selector; 443 | struct IO_Event_Selector_EPoll_Waiting *waiting; 444 | int pid; 445 | int flags; 446 | int descriptor; 447 | }; 448 | 449 | static 450 | VALUE process_wait_transfer(VALUE _arguments) { 451 | struct process_wait_arguments *arguments = (struct process_wait_arguments *)_arguments; 452 | 453 | IO_Event_Selector_loop_yield(&arguments->selector->backend); 454 | 455 | if (arguments->waiting->ready) { 456 | return IO_Event_Selector_process_status_wait(arguments->pid, arguments->flags); 457 | } else { 458 | return Qfalse; 459 | } 460 | } 461 | 462 | static 463 | VALUE process_wait_ensure(VALUE _arguments) { 464 | struct process_wait_arguments *arguments = (struct process_wait_arguments *)_arguments; 465 | 466 | close(arguments->descriptor); 467 | 468 | IO_Event_Selector_EPoll_Waiting_cancel(arguments->waiting); 469 | 470 | return Qnil; 471 | } 472 | 473 | struct IO_Event_List_Type IO_Event_Selector_EPoll_process_wait_list_type = {}; 474 | 475 | VALUE IO_Event_Selector_EPoll_process_wait(VALUE self, VALUE fiber, VALUE _pid, VALUE _flags) { 476 | struct IO_Event_Selector_EPoll *selector = NULL; 477 | TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); 478 | 479 | pid_t pid = NUM2PIDT(_pid); 480 | int flags = NUM2INT(_flags); 481 | 482 | int descriptor = pidfd_open(pid, 0); 483 | 484 | if (descriptor == -1) { 485 | rb_sys_fail("IO_Event_Selector_EPoll_process_wait:pidfd_open"); 486 | } 487 | 488 | rb_update_max_fd(descriptor); 489 | 490 | // `pidfd_open` (above) may be edge triggered, so we need to check if the process is already exited, and if so, return immediately, otherwise we will block indefinitely. 491 | VALUE status = IO_Event_Selector_process_status_wait(pid, flags); 492 | if (status != Qnil) { 493 | close(descriptor); 494 | return status; 495 | } 496 | 497 | struct IO_Event_Selector_EPoll_Waiting waiting = { 498 | .list = {.type = &IO_Event_Selector_EPoll_process_wait_list_type}, 499 | .fiber = fiber, 500 | .events = IO_EVENT_READABLE, 501 | }; 502 | 503 | RB_OBJ_WRITTEN(self, Qundef, fiber); 504 | 505 | int result = IO_Event_Selector_EPoll_Waiting_register(selector, _pid, descriptor, &waiting); 506 | 507 | if (result == -1) { 508 | close(descriptor); 509 | rb_sys_fail("IO_Event_Selector_EPoll_process_wait:IO_Event_Selector_EPoll_Waiting_register"); 510 | } 511 | 512 | struct process_wait_arguments process_wait_arguments = { 513 | .selector = selector, 514 | .pid = pid, 515 | .flags = flags, 516 | .descriptor = descriptor, 517 | .waiting = &waiting, 518 | }; 519 | 520 | return rb_ensure(process_wait_transfer, (VALUE)&process_wait_arguments, process_wait_ensure, (VALUE)&process_wait_arguments); 521 | } 522 | 523 | struct io_wait_arguments { 524 | struct IO_Event_Selector_EPoll *selector; 525 | struct IO_Event_Selector_EPoll_Waiting *waiting; 526 | }; 527 | 528 | static 529 | VALUE io_wait_ensure(VALUE _arguments) { 530 | struct io_wait_arguments *arguments = (struct io_wait_arguments *)_arguments; 531 | 532 | IO_Event_Selector_EPoll_Waiting_cancel(arguments->waiting); 533 | 534 | return Qnil; 535 | }; 536 | 537 | static 538 | VALUE io_wait_transfer(VALUE _arguments) { 539 | struct io_wait_arguments *arguments = (struct io_wait_arguments *)_arguments; 540 | 541 | IO_Event_Selector_loop_yield(&arguments->selector->backend); 542 | 543 | if (arguments->waiting->ready) { 544 | return RB_INT2NUM(arguments->waiting->ready); 545 | } else { 546 | return Qfalse; 547 | } 548 | }; 549 | 550 | struct IO_Event_List_Type IO_Event_Selector_EPoll_io_wait_list_type = {}; 551 | 552 | VALUE IO_Event_Selector_EPoll_io_wait(VALUE self, VALUE fiber, VALUE io, VALUE events) { 553 | struct IO_Event_Selector_EPoll *selector = NULL; 554 | TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); 555 | 556 | int descriptor = IO_Event_Selector_io_descriptor(io); 557 | 558 | struct IO_Event_Selector_EPoll_Waiting waiting = { 559 | .list = {.type = &IO_Event_Selector_EPoll_io_wait_list_type}, 560 | .fiber = fiber, 561 | .events = RB_NUM2INT(events), 562 | }; 563 | 564 | RB_OBJ_WRITTEN(self, Qundef, fiber); 565 | 566 | int result = IO_Event_Selector_EPoll_Waiting_register(selector, io, descriptor, &waiting); 567 | 568 | if (result == -1) { 569 | if (errno == EPERM) { 570 | IO_Event_Selector_ready_push(&selector->backend, fiber); 571 | IO_Event_Selector_yield(&selector->backend); 572 | return events; 573 | } 574 | 575 | rb_sys_fail("IO_Event_Selector_EPoll_io_wait:IO_Event_Selector_EPoll_Waiting_register"); 576 | } 577 | 578 | struct io_wait_arguments io_wait_arguments = { 579 | .selector = selector, 580 | .waiting = &waiting, 581 | }; 582 | 583 | return rb_ensure(io_wait_transfer, (VALUE)&io_wait_arguments, io_wait_ensure, (VALUE)&io_wait_arguments); 584 | } 585 | 586 | #ifdef HAVE_RUBY_IO_BUFFER_H 587 | 588 | struct io_read_arguments { 589 | VALUE self; 590 | VALUE fiber; 591 | VALUE io; 592 | 593 | int flags; 594 | 595 | int descriptor; 596 | 597 | VALUE buffer; 598 | size_t length; 599 | size_t offset; 600 | }; 601 | 602 | static 603 | VALUE io_read_loop(VALUE _arguments) { 604 | struct io_read_arguments *arguments = (struct io_read_arguments *)_arguments; 605 | 606 | void *base; 607 | size_t size; 608 | rb_io_buffer_get_bytes_for_writing(arguments->buffer, &base, &size); 609 | 610 | size_t length = arguments->length; 611 | size_t offset = arguments->offset; 612 | size_t total = 0; 613 | 614 | size_t maximum_size = size - offset; 615 | while (maximum_size) { 616 | ssize_t result = read(arguments->descriptor, (char*)base+offset, maximum_size); 617 | 618 | if (result > 0) { 619 | total += result; 620 | offset += result; 621 | if ((size_t)result >= length) break; 622 | length -= result; 623 | } else if (result == 0) { 624 | break; 625 | } else if (length > 0 && IO_Event_try_again(errno)) { 626 | IO_Event_Selector_EPoll_io_wait(arguments->self, arguments->fiber, arguments->io, RB_INT2NUM(IO_EVENT_READABLE)); 627 | } else { 628 | return rb_fiber_scheduler_io_result(-1, errno); 629 | } 630 | 631 | maximum_size = size - offset; 632 | } 633 | 634 | return rb_fiber_scheduler_io_result(total, 0); 635 | } 636 | 637 | static 638 | VALUE io_read_ensure(VALUE _arguments) { 639 | struct io_read_arguments *arguments = (struct io_read_arguments *)_arguments; 640 | 641 | IO_Event_Selector_nonblock_restore(arguments->descriptor, arguments->flags); 642 | 643 | return Qnil; 644 | } 645 | 646 | VALUE IO_Event_Selector_EPoll_io_read(VALUE self, VALUE fiber, VALUE io, VALUE buffer, VALUE _length, VALUE _offset) { 647 | int descriptor = IO_Event_Selector_io_descriptor(io); 648 | 649 | size_t offset = NUM2SIZET(_offset); 650 | size_t length = NUM2SIZET(_length); 651 | 652 | struct io_read_arguments io_read_arguments = { 653 | .self = self, 654 | .fiber = fiber, 655 | .io = io, 656 | 657 | .flags = IO_Event_Selector_nonblock_set(descriptor), 658 | .descriptor = descriptor, 659 | .buffer = buffer, 660 | .length = length, 661 | .offset = offset, 662 | }; 663 | 664 | RB_OBJ_WRITTEN(self, Qundef, fiber); 665 | 666 | return rb_ensure(io_read_loop, (VALUE)&io_read_arguments, io_read_ensure, (VALUE)&io_read_arguments); 667 | } 668 | 669 | VALUE IO_Event_Selector_EPoll_io_read_compatible(int argc, VALUE *argv, VALUE self) 670 | { 671 | rb_check_arity(argc, 4, 5); 672 | 673 | VALUE _offset = SIZET2NUM(0); 674 | 675 | if (argc == 5) { 676 | _offset = argv[4]; 677 | } 678 | 679 | return IO_Event_Selector_EPoll_io_read(self, argv[0], argv[1], argv[2], argv[3], _offset); 680 | } 681 | 682 | struct io_write_arguments { 683 | VALUE self; 684 | VALUE fiber; 685 | VALUE io; 686 | 687 | int flags; 688 | 689 | int descriptor; 690 | 691 | VALUE buffer; 692 | size_t length; 693 | size_t offset; 694 | }; 695 | 696 | static 697 | VALUE io_write_loop(VALUE _arguments) { 698 | struct io_write_arguments *arguments = (struct io_write_arguments *)_arguments; 699 | 700 | const void *base; 701 | size_t size; 702 | rb_io_buffer_get_bytes_for_reading(arguments->buffer, &base, &size); 703 | 704 | size_t length = arguments->length; 705 | size_t offset = arguments->offset; 706 | size_t total = 0; 707 | 708 | if (length > size) { 709 | rb_raise(rb_eRuntimeError, "Length exceeds size of buffer!"); 710 | } 711 | 712 | size_t maximum_size = size - offset; 713 | while (maximum_size) { 714 | ssize_t result = write(arguments->descriptor, (char*)base+offset, maximum_size); 715 | 716 | if (result > 0) { 717 | total += result; 718 | offset += result; 719 | if ((size_t)result >= length) break; 720 | length -= result; 721 | } else if (result == 0) { 722 | break; 723 | } else if (length > 0 && IO_Event_try_again(errno)) { 724 | IO_Event_Selector_EPoll_io_wait(arguments->self, arguments->fiber, arguments->io, RB_INT2NUM(IO_EVENT_WRITABLE)); 725 | } else { 726 | return rb_fiber_scheduler_io_result(-1, errno); 727 | } 728 | 729 | maximum_size = size - offset; 730 | } 731 | 732 | return rb_fiber_scheduler_io_result(total, 0); 733 | }; 734 | 735 | static 736 | VALUE io_write_ensure(VALUE _arguments) { 737 | struct io_write_arguments *arguments = (struct io_write_arguments *)_arguments; 738 | 739 | IO_Event_Selector_nonblock_restore(arguments->descriptor, arguments->flags); 740 | 741 | return Qnil; 742 | }; 743 | 744 | VALUE IO_Event_Selector_EPoll_io_write(VALUE self, VALUE fiber, VALUE io, VALUE buffer, VALUE _length, VALUE _offset) { 745 | int descriptor = IO_Event_Selector_io_descriptor(io); 746 | 747 | size_t length = NUM2SIZET(_length); 748 | size_t offset = NUM2SIZET(_offset); 749 | 750 | struct io_write_arguments io_write_arguments = { 751 | .self = self, 752 | .fiber = fiber, 753 | .io = io, 754 | 755 | .flags = IO_Event_Selector_nonblock_set(descriptor), 756 | .descriptor = descriptor, 757 | .buffer = buffer, 758 | .length = length, 759 | .offset = offset, 760 | }; 761 | 762 | RB_OBJ_WRITTEN(self, Qundef, fiber); 763 | 764 | return rb_ensure(io_write_loop, (VALUE)&io_write_arguments, io_write_ensure, (VALUE)&io_write_arguments); 765 | } 766 | 767 | VALUE IO_Event_Selector_EPoll_io_write_compatible(int argc, VALUE *argv, VALUE self) 768 | { 769 | rb_check_arity(argc, 4, 5); 770 | 771 | VALUE _offset = SIZET2NUM(0); 772 | 773 | if (argc == 5) { 774 | _offset = argv[4]; 775 | } 776 | 777 | return IO_Event_Selector_EPoll_io_write(self, argv[0], argv[1], argv[2], argv[3], _offset); 778 | } 779 | 780 | #endif 781 | 782 | static 783 | struct timespec * make_timeout(VALUE duration, struct timespec * storage) { 784 | if (duration == Qnil) { 785 | return NULL; 786 | } 787 | 788 | if (RB_INTEGER_TYPE_P(duration)) { 789 | storage->tv_sec = NUM2TIMET(duration); 790 | storage->tv_nsec = 0; 791 | 792 | return storage; 793 | } 794 | 795 | duration = rb_to_float(duration); 796 | double value = RFLOAT_VALUE(duration); 797 | time_t seconds = value; 798 | 799 | storage->tv_sec = seconds; 800 | storage->tv_nsec = (value - seconds) * 1000000000L; 801 | 802 | return storage; 803 | } 804 | 805 | static 806 | int timeout_nonblocking(struct timespec * timespec) { 807 | return timespec && timespec->tv_sec == 0 && timespec->tv_nsec == 0; 808 | } 809 | 810 | struct select_arguments { 811 | struct IO_Event_Selector_EPoll *selector; 812 | 813 | int count; 814 | struct epoll_event events[EPOLL_MAX_EVENTS]; 815 | 816 | struct timespec * timeout; 817 | struct timespec storage; 818 | 819 | struct IO_Event_List saved; 820 | }; 821 | 822 | static int make_timeout_ms(struct timespec * timeout) { 823 | if (timeout == NULL) { 824 | return -1; 825 | } 826 | 827 | if (timeout_nonblocking(timeout)) { 828 | return 0; 829 | } 830 | 831 | return (timeout->tv_sec * 1000) + (timeout->tv_nsec / 1000000); 832 | } 833 | 834 | static 835 | int enosys_error(int result) { 836 | if (result == -1) { 837 | return errno == ENOSYS; 838 | } 839 | 840 | return 0; 841 | } 842 | 843 | static 844 | void * select_internal(void *_arguments) { 845 | struct select_arguments * arguments = (struct select_arguments *)_arguments; 846 | 847 | #if defined(HAVE_EPOLL_PWAIT2) 848 | arguments->count = epoll_pwait2(arguments->selector->descriptor, arguments->events, EPOLL_MAX_EVENTS, arguments->timeout, NULL); 849 | 850 | // Comment out the above line and enable the below lines to test ENOSYS code path. 851 | // arguments->count = -1; 852 | // errno = ENOSYS; 853 | 854 | if (!enosys_error(arguments->count)) { 855 | return NULL; 856 | } 857 | else { 858 | // Fall through and execute epoll_wait fallback. 859 | } 860 | #endif 861 | 862 | arguments->count = epoll_wait(arguments->selector->descriptor, arguments->events, EPOLL_MAX_EVENTS, make_timeout_ms(arguments->timeout)); 863 | 864 | return NULL; 865 | } 866 | 867 | static 868 | void select_internal_without_gvl(struct select_arguments *arguments) { 869 | arguments->selector->blocked = 1; 870 | rb_thread_call_without_gvl(select_internal, (void *)arguments, RUBY_UBF_IO, 0); 871 | arguments->selector->blocked = 0; 872 | 873 | if (arguments->count == -1) { 874 | if (errno != EINTR) { 875 | rb_sys_fail("select_internal_without_gvl:epoll_wait"); 876 | } else { 877 | arguments->count = 0; 878 | } 879 | } 880 | } 881 | 882 | static 883 | void select_internal_with_gvl(struct select_arguments *arguments) { 884 | select_internal((void *)arguments); 885 | 886 | if (arguments->count == -1) { 887 | if (errno != EINTR) { 888 | rb_sys_fail("select_internal_with_gvl:epoll_wait"); 889 | } else { 890 | arguments->count = 0; 891 | } 892 | } 893 | } 894 | 895 | static 896 | int IO_Event_Selector_EPoll_handle(struct IO_Event_Selector_EPoll *selector, const struct epoll_event *event, struct IO_Event_List *saved) 897 | { 898 | int descriptor = event->data.fd; 899 | 900 | // This is the mask of all events that occured for the given descriptor: 901 | enum IO_Event ready_events = events_from_epoll_flags(event->events); 902 | 903 | struct IO_Event_Selector_EPoll_Descriptor *epoll_descriptor = IO_Event_Selector_EPoll_Descriptor_lookup(selector, descriptor); 904 | struct IO_Event_List *list = &epoll_descriptor->list; 905 | struct IO_Event_List *node = list->tail; 906 | 907 | // Reset the events back to 0 so that we can re-arm if necessary: 908 | epoll_descriptor->waiting_events = 0; 909 | 910 | if (DEBUG) fprintf(stderr, "IO_Event_Selector_EPoll_handle: descriptor=%d, ready_events=%d epoll_descriptor=%p\n", descriptor, ready_events, epoll_descriptor); 911 | 912 | // It's possible (but unlikely) that the address of list will changing during iteration. 913 | while (node != list) { 914 | if (DEBUG) fprintf(stderr, "IO_Event_Selector_EPoll_handle: node=%p list=%p type=%p\n", node, list, node->type); 915 | 916 | struct IO_Event_Selector_EPoll_Waiting *waiting = (struct IO_Event_Selector_EPoll_Waiting *)node; 917 | 918 | // Compute the intersection of the events we are waiting for and the events that occured: 919 | enum IO_Event matching_events = waiting->events & ready_events; 920 | 921 | if (DEBUG) fprintf(stderr, "IO_Event_Selector_EPoll_handle: descriptor=%d, ready_events=%d, waiting_events=%d, matching_events=%d\n", descriptor, ready_events, waiting->events, matching_events); 922 | 923 | if (matching_events) { 924 | IO_Event_List_append(node, saved); 925 | 926 | // Resume the fiber: 927 | waiting->ready = matching_events; 928 | IO_Event_Selector_loop_resume(&selector->backend, waiting->fiber, 0, NULL); 929 | 930 | node = saved->tail; 931 | IO_Event_List_pop(saved); 932 | } else { 933 | // We are still waiting for the events: 934 | epoll_descriptor->waiting_events |= waiting->events; 935 | node = node->tail; 936 | } 937 | } 938 | 939 | return IO_Event_Selector_EPoll_Descriptor_update(selector, epoll_descriptor->io, descriptor, epoll_descriptor); 940 | } 941 | 942 | static 943 | VALUE select_handle_events(VALUE _arguments) 944 | { 945 | struct select_arguments *arguments = (struct select_arguments *)_arguments; 946 | struct IO_Event_Selector_EPoll *selector = arguments->selector; 947 | 948 | for (int i = 0; i < arguments->count; i += 1) { 949 | const struct epoll_event *event = &arguments->events[i]; 950 | if (DEBUG) fprintf(stderr, "-> fd=%d events=%d\n", event->data.fd, event->events); 951 | 952 | if (event->data.fd >= 0) { 953 | IO_Event_Selector_EPoll_handle(selector, event, &arguments->saved); 954 | } else { 955 | IO_Event_Interrupt_clear(&selector->interrupt); 956 | } 957 | } 958 | 959 | return INT2NUM(arguments->count); 960 | } 961 | 962 | static 963 | VALUE select_handle_events_ensure(VALUE _arguments) 964 | { 965 | struct select_arguments *arguments = (struct select_arguments *)_arguments; 966 | 967 | IO_Event_List_free(&arguments->saved); 968 | 969 | return Qnil; 970 | } 971 | 972 | // TODO This function is not re-entrant and we should document and assert as such. 973 | VALUE IO_Event_Selector_EPoll_select(VALUE self, VALUE duration) { 974 | struct IO_Event_Selector_EPoll *selector = NULL; 975 | TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); 976 | 977 | selector->idle_duration.tv_sec = 0; 978 | selector->idle_duration.tv_nsec = 0; 979 | 980 | int ready = IO_Event_Selector_ready_flush(&selector->backend); 981 | 982 | struct select_arguments arguments = { 983 | .selector = selector, 984 | .storage = { 985 | .tv_sec = 0, 986 | .tv_nsec = 0 987 | }, 988 | .saved = {}, 989 | }; 990 | 991 | arguments.timeout = &arguments.storage; 992 | 993 | // Process any currently pending events: 994 | select_internal_with_gvl(&arguments); 995 | 996 | // If we: 997 | // 1. Didn't process any ready fibers, and 998 | // 2. Didn't process any events from non-blocking select (above), and 999 | // 3. There are no items in the ready list, 1000 | // then we can perform a blocking select. 1001 | if (!ready && !arguments.count && !selector->backend.ready) { 1002 | arguments.timeout = make_timeout(duration, &arguments.storage); 1003 | 1004 | if (!timeout_nonblocking(arguments.timeout)) { 1005 | struct timespec start_time; 1006 | IO_Event_Time_current(&start_time); 1007 | 1008 | // Wait for events to occur: 1009 | select_internal_without_gvl(&arguments); 1010 | 1011 | struct timespec end_time; 1012 | IO_Event_Time_current(&end_time); 1013 | IO_Event_Time_elapsed(&start_time, &end_time, &selector->idle_duration); 1014 | } 1015 | } 1016 | 1017 | if (arguments.count) { 1018 | return rb_ensure(select_handle_events, (VALUE)&arguments, select_handle_events_ensure, (VALUE)&arguments); 1019 | } else { 1020 | return RB_INT2NUM(0); 1021 | } 1022 | } 1023 | 1024 | VALUE IO_Event_Selector_EPoll_wakeup(VALUE self) { 1025 | struct IO_Event_Selector_EPoll *selector = NULL; 1026 | TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); 1027 | 1028 | // If we are blocking, we can schedule a nop event to wake up the selector: 1029 | if (selector->blocked) { 1030 | IO_Event_Interrupt_signal(&selector->interrupt); 1031 | 1032 | return Qtrue; 1033 | } 1034 | 1035 | return Qfalse; 1036 | } 1037 | 1038 | static int IO_Event_Selector_EPoll_supported_p(void) { 1039 | int fd = epoll_create1(EPOLL_CLOEXEC); 1040 | 1041 | if (fd < 0) { 1042 | rb_warn("epoll_create1() was available at compile time but failed at run time: %s\n", strerror(errno)); 1043 | 1044 | return 0; 1045 | } 1046 | 1047 | close(fd); 1048 | 1049 | return 1; 1050 | } 1051 | 1052 | void Init_IO_Event_Selector_EPoll(VALUE IO_Event_Selector) { 1053 | if (!IO_Event_Selector_EPoll_supported_p()) { 1054 | return; 1055 | } 1056 | 1057 | VALUE IO_Event_Selector_EPoll = rb_define_class_under(IO_Event_Selector, "EPoll", rb_cObject); 1058 | 1059 | rb_define_alloc_func(IO_Event_Selector_EPoll, IO_Event_Selector_EPoll_allocate); 1060 | rb_define_method(IO_Event_Selector_EPoll, "initialize", IO_Event_Selector_EPoll_initialize, 1); 1061 | 1062 | rb_define_method(IO_Event_Selector_EPoll, "loop", IO_Event_Selector_EPoll_loop, 0); 1063 | rb_define_method(IO_Event_Selector_EPoll, "idle_duration", IO_Event_Selector_EPoll_idle_duration, 0); 1064 | 1065 | rb_define_method(IO_Event_Selector_EPoll, "transfer", IO_Event_Selector_EPoll_transfer, 0); 1066 | rb_define_method(IO_Event_Selector_EPoll, "resume", IO_Event_Selector_EPoll_resume, -1); 1067 | rb_define_method(IO_Event_Selector_EPoll, "yield", IO_Event_Selector_EPoll_yield, 0); 1068 | rb_define_method(IO_Event_Selector_EPoll, "push", IO_Event_Selector_EPoll_push, 1); 1069 | rb_define_method(IO_Event_Selector_EPoll, "raise", IO_Event_Selector_EPoll_raise, -1); 1070 | 1071 | rb_define_method(IO_Event_Selector_EPoll, "ready?", IO_Event_Selector_EPoll_ready_p, 0); 1072 | 1073 | rb_define_method(IO_Event_Selector_EPoll, "select", IO_Event_Selector_EPoll_select, 1); 1074 | rb_define_method(IO_Event_Selector_EPoll, "wakeup", IO_Event_Selector_EPoll_wakeup, 0); 1075 | rb_define_method(IO_Event_Selector_EPoll, "close", IO_Event_Selector_EPoll_close, 0); 1076 | 1077 | rb_define_method(IO_Event_Selector_EPoll, "io_wait", IO_Event_Selector_EPoll_io_wait, 3); 1078 | 1079 | #ifdef HAVE_RUBY_IO_BUFFER_H 1080 | rb_define_method(IO_Event_Selector_EPoll, "io_read", IO_Event_Selector_EPoll_io_read_compatible, -1); 1081 | rb_define_method(IO_Event_Selector_EPoll, "io_write", IO_Event_Selector_EPoll_io_write_compatible, -1); 1082 | #endif 1083 | 1084 | // Once compatibility isn't a concern, we can do this: 1085 | // rb_define_method(IO_Event_Selector_EPoll, "io_read", IO_Event_Selector_EPoll_io_read, 5); 1086 | // rb_define_method(IO_Event_Selector_EPoll, "io_write", IO_Event_Selector_EPoll_io_write, 5); 1087 | 1088 | rb_define_method(IO_Event_Selector_EPoll, "process_wait", IO_Event_Selector_EPoll_process_wait, 3); 1089 | } 1090 | -------------------------------------------------------------------------------- /ext/io/event/selector/epoll.h: -------------------------------------------------------------------------------- 1 | // Released under the MIT License. 2 | // Copyright, 2021-2025, by Samuel Williams. 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #define IO_EVENT_SELECTOR_EPOLL 9 | 10 | void Init_IO_Event_Selector_EPoll(VALUE IO_Event_Selector); 11 | -------------------------------------------------------------------------------- /ext/io/event/selector/kqueue.h: -------------------------------------------------------------------------------- 1 | // Released under the MIT License. 2 | // Copyright, 2021-2025, by Samuel Williams. 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #define IO_EVENT_SELECTOR_KQUEUE 9 | 10 | void Init_IO_Event_Selector_KQueue(VALUE IO_Event_Selector); 11 | -------------------------------------------------------------------------------- /ext/io/event/selector/pidfd.c: -------------------------------------------------------------------------------- 1 | // Released under the MIT License. 2 | // Copyright, 2021-2025, by Samuel Williams. 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #ifndef __NR_pidfd_open 14 | #define __NR_pidfd_open 434 /* System call # on most architectures */ 15 | #endif 16 | 17 | static int 18 | pidfd_open(pid_t pid, unsigned int flags) 19 | { 20 | return syscall(__NR_pidfd_open, pid, flags); 21 | } 22 | -------------------------------------------------------------------------------- /ext/io/event/selector/selector.c: -------------------------------------------------------------------------------- 1 | // Released under the MIT License. 2 | // Copyright, 2021-2025, by Samuel Williams. 3 | 4 | #include "selector.h" 5 | 6 | #include 7 | #include 8 | 9 | static const int DEBUG = 0; 10 | 11 | #ifndef HAVE_RB_IO_DESCRIPTOR 12 | static ID id_fileno; 13 | 14 | int IO_Event_Selector_io_descriptor(VALUE io) { 15 | return RB_NUM2INT(rb_funcall(io, id_fileno, 0)); 16 | } 17 | #endif 18 | 19 | #ifndef HAVE_RB_PROCESS_STATUS_WAIT 20 | static ID id_wait; 21 | static VALUE rb_Process_Status = Qnil; 22 | 23 | VALUE IO_Event_Selector_process_status_wait(rb_pid_t pid, int flags) 24 | { 25 | return rb_funcall(rb_Process_Status, id_wait, 2, PIDT2NUM(pid), INT2NUM(flags | WNOHANG)); 26 | } 27 | #endif 28 | 29 | int IO_Event_Selector_nonblock_set(int file_descriptor) 30 | { 31 | #ifdef _WIN32 32 | u_long nonblock = 1; 33 | ioctlsocket(file_descriptor, FIONBIO, &nonblock); 34 | // Windows does not provide any way to know this, so we always restore it back to unset: 35 | return 0; 36 | #else 37 | // Get the current mode: 38 | int flags = fcntl(file_descriptor, F_GETFL, 0); 39 | 40 | // Set the non-blocking flag if it isn't already: 41 | if (!(flags & O_NONBLOCK)) { 42 | fcntl(file_descriptor, F_SETFL, flags | O_NONBLOCK); 43 | } 44 | 45 | return flags; 46 | #endif 47 | } 48 | 49 | void IO_Event_Selector_nonblock_restore(int file_descriptor, int flags) 50 | { 51 | #ifdef _WIN32 52 | // Yolo... 53 | u_long nonblock = flags; 54 | ioctlsocket(file_descriptor, FIONBIO, &nonblock); 55 | #else 56 | // The flags didn't have O_NONBLOCK set, so it would have been set, so we need to restore it: 57 | if (!(flags & O_NONBLOCK)) { 58 | fcntl(file_descriptor, F_SETFL, flags); 59 | } 60 | #endif 61 | } 62 | 63 | struct IO_Event_Selector_nonblock_arguments { 64 | int file_descriptor; 65 | int flags; 66 | }; 67 | 68 | static VALUE IO_Event_Selector_nonblock_ensure(VALUE _arguments) { 69 | struct IO_Event_Selector_nonblock_arguments *arguments = (struct IO_Event_Selector_nonblock_arguments *)_arguments; 70 | 71 | IO_Event_Selector_nonblock_restore(arguments->file_descriptor, arguments->flags); 72 | 73 | return Qnil; 74 | } 75 | 76 | static VALUE IO_Event_Selector_nonblock(VALUE class, VALUE io) 77 | { 78 | struct IO_Event_Selector_nonblock_arguments arguments = { 79 | .file_descriptor = IO_Event_Selector_io_descriptor(io), 80 | .flags = IO_Event_Selector_nonblock_set(arguments.file_descriptor) 81 | }; 82 | 83 | return rb_ensure(rb_yield, io, IO_Event_Selector_nonblock_ensure, (VALUE)&arguments); 84 | } 85 | 86 | void Init_IO_Event_Selector(VALUE IO_Event_Selector) { 87 | #ifndef HAVE_RB_IO_DESCRIPTOR 88 | id_fileno = rb_intern("fileno"); 89 | #endif 90 | 91 | #ifndef HAVE_RB_PROCESS_STATUS_WAIT 92 | id_wait = rb_intern("wait"); 93 | rb_Process_Status = rb_const_get_at(rb_mProcess, rb_intern("Status")); 94 | rb_gc_register_mark_object(rb_Process_Status); 95 | #endif 96 | 97 | rb_define_singleton_method(IO_Event_Selector, "nonblock", IO_Event_Selector_nonblock, 1); 98 | } 99 | 100 | void IO_Event_Selector_initialize(struct IO_Event_Selector *backend, VALUE self, VALUE loop) { 101 | RB_OBJ_WRITE(self, &backend->self, self); 102 | RB_OBJ_WRITE(self, &backend->loop, loop); 103 | 104 | backend->waiting = NULL; 105 | backend->ready = NULL; 106 | } 107 | 108 | VALUE IO_Event_Selector_loop_resume(struct IO_Event_Selector *backend, VALUE fiber, int argc, VALUE *argv) { 109 | return IO_Event_Fiber_transfer(fiber, argc, argv); 110 | } 111 | 112 | VALUE IO_Event_Selector_loop_yield(struct IO_Event_Selector *backend) 113 | { 114 | // TODO Why is this assertion failing in async? 115 | // RUBY_ASSERT(backend->loop != IO_Event_Fiber_current()); 116 | return IO_Event_Fiber_transfer(backend->loop, 0, NULL); 117 | } 118 | 119 | struct wait_and_transfer_arguments { 120 | int argc; 121 | VALUE *argv; 122 | 123 | struct IO_Event_Selector *backend; 124 | struct IO_Event_Selector_Queue *waiting; 125 | }; 126 | 127 | static void queue_pop(struct IO_Event_Selector *backend, struct IO_Event_Selector_Queue *waiting) { 128 | if (waiting->head) { 129 | waiting->head->tail = waiting->tail; 130 | } else { 131 | // We must have been at the head of the queue: 132 | backend->waiting = waiting->tail; 133 | } 134 | 135 | if (waiting->tail) { 136 | waiting->tail->head = waiting->head; 137 | } else { 138 | // We must have been at the tail of the queue: 139 | backend->ready = waiting->head; 140 | } 141 | 142 | waiting->head = NULL; 143 | waiting->tail = NULL; 144 | } 145 | 146 | static void queue_push(struct IO_Event_Selector *backend, struct IO_Event_Selector_Queue *waiting) { 147 | assert(waiting->head == NULL); 148 | assert(waiting->tail == NULL); 149 | 150 | if (backend->waiting) { 151 | // If there was an item in the queue already, we shift it along: 152 | backend->waiting->head = waiting; 153 | waiting->tail = backend->waiting; 154 | } else { 155 | // If the queue was empty, we update the tail too: 156 | backend->ready = waiting; 157 | } 158 | 159 | // We always push to the front/head: 160 | backend->waiting = waiting; 161 | } 162 | 163 | static VALUE wait_and_transfer(VALUE _arguments) { 164 | struct wait_and_transfer_arguments *arguments = (struct wait_and_transfer_arguments *)_arguments; 165 | 166 | VALUE fiber = arguments->argv[0]; 167 | int argc = arguments->argc - 1; 168 | VALUE *argv = arguments->argv + 1; 169 | 170 | return IO_Event_Selector_loop_resume(arguments->backend, fiber, argc, argv); 171 | } 172 | 173 | static VALUE wait_and_transfer_ensure(VALUE _arguments) { 174 | struct wait_and_transfer_arguments *arguments = (struct wait_and_transfer_arguments *)_arguments; 175 | 176 | queue_pop(arguments->backend, arguments->waiting); 177 | 178 | return Qnil; 179 | } 180 | 181 | VALUE IO_Event_Selector_resume(struct IO_Event_Selector *backend, int argc, VALUE *argv) 182 | { 183 | rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS); 184 | 185 | struct IO_Event_Selector_Queue waiting = { 186 | .head = NULL, 187 | .tail = NULL, 188 | .flags = IO_EVENT_SELECTOR_QUEUE_FIBER, 189 | .fiber = IO_Event_Fiber_current() 190 | }; 191 | 192 | RB_OBJ_WRITTEN(backend->self, Qundef, waiting.fiber); 193 | 194 | queue_push(backend, &waiting); 195 | 196 | struct wait_and_transfer_arguments arguments = { 197 | .argc = argc, 198 | .argv = argv, 199 | .backend = backend, 200 | .waiting = &waiting, 201 | }; 202 | 203 | return rb_ensure(wait_and_transfer, (VALUE)&arguments, wait_and_transfer_ensure, (VALUE)&arguments); 204 | } 205 | 206 | static VALUE wait_and_raise(VALUE _arguments) { 207 | struct wait_and_transfer_arguments *arguments = (struct wait_and_transfer_arguments *)_arguments; 208 | 209 | VALUE fiber = arguments->argv[0]; 210 | int argc = arguments->argc - 1; 211 | VALUE *argv = arguments->argv + 1; 212 | 213 | return IO_Event_Fiber_raise(fiber, argc, argv); 214 | } 215 | 216 | VALUE IO_Event_Selector_raise(struct IO_Event_Selector *backend, int argc, VALUE *argv) 217 | { 218 | rb_check_arity(argc, 2, UNLIMITED_ARGUMENTS); 219 | 220 | struct IO_Event_Selector_Queue waiting = { 221 | .head = NULL, 222 | .tail = NULL, 223 | .flags = IO_EVENT_SELECTOR_QUEUE_FIBER, 224 | .fiber = IO_Event_Fiber_current() 225 | }; 226 | 227 | RB_OBJ_WRITTEN(backend->self, Qundef, waiting.fiber); 228 | 229 | queue_push(backend, &waiting); 230 | 231 | struct wait_and_transfer_arguments arguments = { 232 | .argc = argc, 233 | .argv = argv, 234 | .backend = backend, 235 | .waiting = &waiting, 236 | }; 237 | 238 | return rb_ensure(wait_and_raise, (VALUE)&arguments, wait_and_transfer_ensure, (VALUE)&arguments); 239 | } 240 | 241 | void IO_Event_Selector_ready_push(struct IO_Event_Selector *backend, VALUE fiber) 242 | { 243 | struct IO_Event_Selector_Queue *waiting = malloc(sizeof(struct IO_Event_Selector_Queue)); 244 | assert(waiting); 245 | 246 | waiting->head = NULL; 247 | waiting->tail = NULL; 248 | waiting->flags = IO_EVENT_SELECTOR_QUEUE_INTERNAL; 249 | 250 | RB_OBJ_WRITE(backend->self, &waiting->fiber, fiber); 251 | 252 | queue_push(backend, waiting); 253 | } 254 | 255 | static inline 256 | void IO_Event_Selector_ready_pop(struct IO_Event_Selector *backend, struct IO_Event_Selector_Queue *ready) 257 | { 258 | if (DEBUG) fprintf(stderr, "IO_Event_Selector_ready_pop -> %p\n", (void*)ready->fiber); 259 | 260 | VALUE fiber = ready->fiber; 261 | 262 | if (ready->flags & IO_EVENT_SELECTOR_QUEUE_INTERNAL) { 263 | // This means that the fiber was added to the ready queue by the selector itself, and we need to transfer control to it, but before we do that, we need to remove it from the queue, as there is no expectation that returning from `transfer` will remove it. 264 | queue_pop(backend, ready); 265 | free(ready); 266 | } else if (ready->flags & IO_EVENT_SELECTOR_QUEUE_FIBER) { 267 | // This means the fiber added itself to the ready queue, and we need to transfer control back to it. Transferring control back to the fiber will call `queue_pop` and remove it from the queue. 268 | } else { 269 | rb_raise(rb_eRuntimeError, "Unknown queue type!"); 270 | } 271 | 272 | IO_Event_Selector_loop_resume(backend, fiber, 0, NULL); 273 | } 274 | 275 | int IO_Event_Selector_ready_flush(struct IO_Event_Selector *backend) 276 | { 277 | int count = 0; 278 | 279 | // During iteration of the queue, the same item may be re-queued. If we don't handle this correctly, we may end up in an infinite loop. So, to avoid this situation, we keep note of the current head of the queue and break the loop if we reach the same item again. 280 | 281 | // Get the current tail and head of the queue: 282 | struct IO_Event_Selector_Queue *waiting = backend->waiting; 283 | if (DEBUG) fprintf(stderr, "IO_Event_Selector_ready_flush waiting = %p\n", waiting); 284 | 285 | // Process from head to tail in order: 286 | // During this, more items may be appended to tail. 287 | while (backend->ready) { 288 | if (DEBUG) fprintf(stderr, "backend->ready = %p\n", backend->ready); 289 | struct IO_Event_Selector_Queue *ready = backend->ready; 290 | 291 | count += 1; 292 | IO_Event_Selector_ready_pop(backend, ready); 293 | 294 | if (ready == waiting) break; 295 | } 296 | 297 | return count; 298 | } 299 | -------------------------------------------------------------------------------- /ext/io/event/selector/selector.h: -------------------------------------------------------------------------------- 1 | // Released under the MIT License. 2 | // Copyright, 2021-2025, by Samuel Williams. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "../time.h" 11 | #include "../fiber.h" 12 | 13 | #ifdef HAVE_RUBY_IO_BUFFER_H 14 | #include 15 | #include 16 | #endif 17 | 18 | #ifndef RUBY_FIBER_SCHEDULER_VERSION 19 | #define RUBY_FIBER_SCHEDULER_VERSION 1 20 | #endif 21 | 22 | #ifdef HAVE_SYS_WAIT_H 23 | #include 24 | #endif 25 | 26 | enum IO_Event { 27 | IO_EVENT_READABLE = 1, 28 | IO_EVENT_PRIORITY = 2, 29 | IO_EVENT_WRITABLE = 4, 30 | IO_EVENT_ERROR = 8, 31 | IO_EVENT_HANGUP = 16, 32 | 33 | // Used by kqueue to differentiate between process exit and file descriptor events: 34 | IO_EVENT_EXIT = 32, 35 | }; 36 | 37 | void Init_IO_Event_Selector(VALUE IO_Event_Selector); 38 | 39 | static inline int IO_Event_try_again(int error) { 40 | return error == EAGAIN || error == EWOULDBLOCK; 41 | } 42 | 43 | #ifdef HAVE_RB_IO_DESCRIPTOR 44 | #define IO_Event_Selector_io_descriptor(io) rb_io_descriptor(io) 45 | #else 46 | int IO_Event_Selector_io_descriptor(VALUE io); 47 | #endif 48 | 49 | // Reap a process without hanging. 50 | #ifdef HAVE_RB_PROCESS_STATUS_WAIT 51 | #define IO_Event_Selector_process_status_wait(pid, flags) rb_process_status_wait(pid, flags | WNOHANG) 52 | #else 53 | VALUE IO_Event_Selector_process_status_wait(rb_pid_t pid, int flags); 54 | #endif 55 | 56 | int IO_Event_Selector_nonblock_set(int file_descriptor); 57 | void IO_Event_Selector_nonblock_restore(int file_descriptor, int flags); 58 | 59 | enum IO_Event_Selector_Queue_Flags { 60 | IO_EVENT_SELECTOR_QUEUE_FIBER = 1, 61 | IO_EVENT_SELECTOR_QUEUE_INTERNAL = 2, 62 | }; 63 | 64 | struct IO_Event_Selector_Queue { 65 | struct IO_Event_Selector_Queue *head; 66 | struct IO_Event_Selector_Queue *tail; 67 | 68 | enum IO_Event_Selector_Queue_Flags flags; 69 | 70 | VALUE fiber; 71 | }; 72 | 73 | // The internal state of the event selector. 74 | // The event selector is responsible for managing the scheduling of fibers, as well as selecting for events. 75 | struct IO_Event_Selector { 76 | VALUE self; 77 | VALUE loop; 78 | 79 | // The ready queue is a list of fibers that are ready to be resumed from the event loop fiber. 80 | // Append to waiting (front/head of queue). 81 | struct IO_Event_Selector_Queue *waiting; 82 | // Process from ready (back/tail of queue). 83 | struct IO_Event_Selector_Queue *ready; 84 | }; 85 | 86 | void IO_Event_Selector_initialize(struct IO_Event_Selector *backend, VALUE self, VALUE loop); 87 | 88 | static inline 89 | void IO_Event_Selector_mark(struct IO_Event_Selector *backend) { 90 | rb_gc_mark_movable(backend->self); 91 | rb_gc_mark_movable(backend->loop); 92 | 93 | // Walk backwards through the ready queue: 94 | struct IO_Event_Selector_Queue *ready = backend->ready; 95 | while (ready) { 96 | rb_gc_mark_movable(ready->fiber); 97 | ready = ready->head; 98 | } 99 | } 100 | 101 | static inline 102 | void IO_Event_Selector_compact(struct IO_Event_Selector *backend) { 103 | backend->self = rb_gc_location(backend->self); 104 | backend->loop = rb_gc_location(backend->loop); 105 | 106 | struct IO_Event_Selector_Queue *ready = backend->ready; 107 | while (ready) { 108 | ready->fiber = rb_gc_location(ready->fiber); 109 | ready = ready->head; 110 | } 111 | } 112 | 113 | // Transfer control from the event loop to a user fiber. 114 | // This is used to transfer control to a user fiber when it may proceed. 115 | // Strictly speaking, it's not a scheduling operation (does not schedule the current fiber). 116 | VALUE IO_Event_Selector_loop_resume(struct IO_Event_Selector *backend, VALUE fiber, int argc, VALUE *argv); 117 | 118 | // Transfer from a user fiber back to the event loop. 119 | // This is used to transfer control back to the event loop in order to wait for events. 120 | // Strictly speaking, it's not a scheduling operation (does not schedule the current fiber). 121 | VALUE IO_Event_Selector_loop_yield(struct IO_Event_Selector *backend); 122 | 123 | // Resume a specific fiber. This is a scheduling operation. 124 | // The first argument is the fiber, the rest are the arguments to the resume. 125 | // 126 | // The implementation has two possible strategies: 127 | // 1. Add the current fiber to the ready queue and transfer control to the target fiber. 128 | // 2. Schedule the target fiber to be resumed by the event loop later on. 129 | // 130 | // We currently only implement the first strategy. 131 | VALUE IO_Event_Selector_resume(struct IO_Event_Selector *backend, int argc, VALUE *argv); 132 | 133 | // Raise an exception on a specific fiber. 134 | // The first argument is the fiber, the rest are the arguments to the exception. 135 | // 136 | // The implementation has two possible strategies: 137 | // 1. Add the current fiber to the ready queue and transfer control to the target fiber. 138 | // 2. Schedule the target fiber to be resumed by the event loop with an exception later on. 139 | // 140 | // We currently only implement the first strategy. 141 | VALUE IO_Event_Selector_raise(struct IO_Event_Selector *backend, int argc, VALUE *argv); 142 | 143 | // Yield control to the event loop. This is a scheduling operation. 144 | // 145 | // The implementation adds the current fiber to the ready queue and transfers control to the event loop. 146 | static inline 147 | VALUE IO_Event_Selector_yield(struct IO_Event_Selector *backend) 148 | { 149 | return IO_Event_Selector_resume(backend, 1, &backend->loop); 150 | } 151 | 152 | // Append a specific fiber to the ready queue. 153 | // The fiber can be an actual fiber or an object that responds to `alive?` and `transfer`. 154 | // The implementation will transfer control to the fiber later on. 155 | void IO_Event_Selector_ready_push(struct IO_Event_Selector *backend, VALUE fiber); 156 | 157 | // Flush the ready queue by transferring control one at a time. 158 | int IO_Event_Selector_ready_flush(struct IO_Event_Selector *backend); 159 | -------------------------------------------------------------------------------- /ext/io/event/selector/uring.h: -------------------------------------------------------------------------------- 1 | // Released under the MIT License. 2 | // Copyright, 2021-2025, by Samuel Williams. 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | #define IO_EVENT_SELECTOR_URING 9 | 10 | void Init_IO_Event_Selector_URing(VALUE IO_Event_Selector); 11 | -------------------------------------------------------------------------------- /ext/io/event/time.c: -------------------------------------------------------------------------------- 1 | // Released under the MIT License. 2 | // Copyright, 2025, by Samuel Williams. 3 | 4 | #include "time.h" 5 | 6 | void IO_Event_Time_elapsed(const struct timespec* start, const struct timespec* stop, struct timespec *duration) 7 | { 8 | if ((stop->tv_nsec - start->tv_nsec) < 0) { 9 | duration->tv_sec = stop->tv_sec - start->tv_sec - 1; 10 | duration->tv_nsec = stop->tv_nsec - start->tv_nsec + 1000000000; 11 | } else { 12 | duration->tv_sec = stop->tv_sec - start->tv_sec; 13 | duration->tv_nsec = stop->tv_nsec - start->tv_nsec; 14 | } 15 | } 16 | 17 | float IO_Event_Time_duration(const struct timespec *duration) 18 | { 19 | return duration->tv_sec + duration->tv_nsec / 1000000000.0; 20 | } 21 | 22 | void IO_Event_Time_current(struct timespec *time) { 23 | clock_gettime(CLOCK_MONOTONIC, time); 24 | } 25 | 26 | float IO_Event_Time_proportion(const struct timespec *duration, const struct timespec *total_duration) { 27 | return IO_Event_Time_duration(duration) / IO_Event_Time_duration(total_duration); 28 | } 29 | 30 | float IO_Event_Time_delta(const struct timespec *start, const struct timespec *stop) { 31 | struct timespec duration; 32 | IO_Event_Time_elapsed(start, stop, &duration); 33 | 34 | return IO_Event_Time_duration(&duration); 35 | } 36 | -------------------------------------------------------------------------------- /ext/io/event/time.h: -------------------------------------------------------------------------------- 1 | // Released under the MIT License. 2 | // Copyright, 2025, by Samuel Williams. 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | void IO_Event_Time_elapsed(const struct timespec* start, const struct timespec* stop, struct timespec *duration); 10 | float IO_Event_Time_duration(const struct timespec *duration); 11 | void IO_Event_Time_current(struct timespec *time); 12 | 13 | float IO_Event_Time_delta(const struct timespec *start, const struct timespec *stop); 14 | float IO_Event_Time_proportion(const struct timespec *duration, const struct timespec *total_duration); 15 | 16 | #define IO_EVENT_TIME_PRINTF_TIMESPEC "%.3g" 17 | #define IO_EVENT_TIME_PRINTF_TIMESPEC_ARGUMENTS(ts) ((double)(ts).tv_sec + (ts).tv_nsec / 1e9) 18 | -------------------------------------------------------------------------------- /fixtures/unix_socket.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2023, by Samuel Williams. 5 | 6 | unless Object.const_defined?(:UNIXSocket) 7 | class UNIXSocket 8 | def self.pair(&block) 9 | Socket.pair(:INET, :STREAM, 0, &block) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /gems.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2024, by Samuel Williams. 5 | 6 | source "https://rubygems.org" 7 | 8 | gemspec 9 | 10 | group :maintenance, optional: true do 11 | gem "bake-gem" 12 | gem "bake-modernize" 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 | gem "async" 27 | end 28 | -------------------------------------------------------------------------------- /guides/getting-started/readme.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | This guide explains how to use `io-event` for non-blocking IO. 4 | 5 | ## Installation 6 | 7 | Add the gem to your project: 8 | 9 | ~~~ bash 10 | $ bundle add io-event 11 | ~~~ 12 | 13 | ## Core Concepts 14 | 15 | `io-event` has several core concepts: 16 | 17 | - A {ruby IO::Event::Selector} implementation which provides the primitive operations for implementation an event loop. 18 | - A {ruby IO::Event::Debug::Selector} which adds extra validations and checks at the expense of performance. You should generally use this during tests. 19 | 20 | ## Basic Event Loop 21 | 22 | This example shows how to perform a blocking operation 23 | 24 | ```ruby 25 | require 'fiber' 26 | require 'io/event' 27 | 28 | selector = IO::Event::Selector.new(Fiber.current) 29 | input, output = IO.pipe 30 | 31 | writer = Fiber.new do 32 | output.write("Hello World") 33 | output.close 34 | end 35 | 36 | reader = Fiber.new do 37 | selector.io_wait(Fiber.current, input, IO::READABLE) 38 | pp read: input.read 39 | end 40 | 41 | # The reader will be blocked until the IO has data available: 42 | reader.transfer 43 | 44 | # Write some data to the pipe and close the writing end: 45 | writer.transfer 46 | 47 | selector.select(1) 48 | 49 | # Results in: 50 | # {:read=>"Hello World"} 51 | ``` 52 | 53 | ## Debugging 54 | 55 | The {ruby IO::Event::Debug::Selector} class adds extra validations and checks at the expense of performance. It can also log all operations. You can use this by setting the following environment variables: 56 | 57 | ```shell 58 | $ IO_EVENT_SELECTOR_DEBUG=y IO_EVENT_SELECTOR_DEBUG_LOG=/dev/stderr bundle exec ./my_script.rb 59 | ``` 60 | 61 | The format of the log is subject to change, but it may be useful for debugging. 62 | -------------------------------------------------------------------------------- /guides/links.yaml: -------------------------------------------------------------------------------- 1 | getting-started: 2 | order: 1 3 | -------------------------------------------------------------------------------- /io-event.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/io/event/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "io-event" 7 | spec.version = IO::Event::VERSION 8 | 9 | spec.summary = "An event loop." 10 | spec.authors = ["Samuel Williams", "Math Ieu", "Wander Hillen", "Jean Boussier", "Benoit Daloze", "Bruno Sutic", "Alex Matchneer", "Anthony Ross", "Delton Ding", "Pavel Rosický", "Shizuo Fujita", "Stanislav (Stas) Katkov"] 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/io-event" 17 | 18 | spec.metadata = { 19 | "documentation_uri" => "https://socketry.github.io/io-event/", 20 | "source_code_uri" => "https://github.com/socketry/io-event.git", 21 | } 22 | 23 | spec.files = Dir["ext/extconf.rb", "ext/io/**/*.{c,h}", "{lib}/**/*", "*.md", base: __dir__] 24 | spec.require_paths = ["lib"] 25 | 26 | spec.extensions = ["ext/extconf.rb"] 27 | 28 | spec.required_ruby_version = ">= 3.1" 29 | end 30 | -------------------------------------------------------------------------------- /lib/io/event.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2025, by Samuel Williams. 5 | 6 | require_relative "event/version" 7 | require_relative "event/selector" 8 | require_relative "event/timers" 9 | require_relative "event/native" 10 | -------------------------------------------------------------------------------- /lib/io/event/debug/selector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2024, by Samuel Williams. 5 | 6 | require_relative "../support" 7 | 8 | module IO::Event 9 | # @namespace 10 | module Debug 11 | # Enforces the selector interface and delegates operations to a wrapped selector instance. 12 | # 13 | # You can enable this in the default selector by setting the `IO_EVENT_DEBUG_SELECTOR` environment variable. In addition, you can log all selector operations to a file by setting the `IO_EVENT_DEBUG_SELECTOR_LOG` environment variable. This is useful for debugging and understanding the behavior of the event loop. 14 | class Selector 15 | # Wrap the given selector with debugging. 16 | # 17 | # @parameter selector [Selector] The selector to wrap. 18 | # @parameter env [Hash] The environment to read configuration from. 19 | def self.wrap(selector, env = ENV) 20 | log = nil 21 | 22 | if log_path = env["IO_EVENT_DEBUG_SELECTOR_LOG"] 23 | log = File.open(log_path, "w") 24 | end 25 | 26 | return self.new(selector, log: log) 27 | end 28 | 29 | # Initialize the debug selector with the given selector and optional log. 30 | # 31 | # @parameter selector [Selector] The selector to wrap. 32 | # @parameter log [IO] The log to write debug messages to. 33 | def initialize(selector, log: nil) 34 | @selector = selector 35 | 36 | @readable = {} 37 | @writable = {} 38 | @priority = {} 39 | 40 | unless Fiber.current == selector.loop 41 | Kernel::raise "Selector must be initialized on event loop fiber!" 42 | end 43 | 44 | @log = log 45 | end 46 | 47 | # The idle duration of the underlying selector. 48 | # 49 | # @returns [Numeric] The idle duration. 50 | def idle_duration 51 | @selector.idle_duration 52 | end 53 | 54 | # The current time. 55 | # 56 | # @returns [Numeric] The current time. 57 | def now 58 | Process.clock_gettime(Process::CLOCK_MONOTONIC) 59 | end 60 | 61 | # Log the given message. 62 | # 63 | # @asynchronous Will block the calling fiber and the entire event loop. 64 | def log(message) 65 | return unless @log 66 | 67 | Fiber.blocking do 68 | @log.puts("T+%10.1f; %s" % [now, message]) 69 | end 70 | end 71 | 72 | # Wakeup the the selector. 73 | def wakeup 74 | @selector.wakeup 75 | end 76 | 77 | # Close the selector. 78 | def close 79 | log("Closing selector") 80 | 81 | if @selector.nil? 82 | Kernel::raise "Selector already closed!" 83 | end 84 | 85 | @selector.close 86 | @selector = nil 87 | end 88 | 89 | # Transfer from the calling fiber to the selector. 90 | def transfer 91 | log("Transfering to event loop") 92 | @selector.transfer 93 | end 94 | 95 | # Resume the given fiber with the given arguments. 96 | def resume(*arguments) 97 | log("Resuming fiber with #{arguments.inspect}") 98 | @selector.resume(*arguments) 99 | end 100 | 101 | # Yield to the selector. 102 | def yield 103 | log("Yielding to event loop") 104 | @selector.yield 105 | end 106 | 107 | # Push the given fiber to the selector ready list, such that it will be resumed on the next call to {select}. 108 | # 109 | # @parameter fiber [Fiber] The fiber that is ready. 110 | def push(fiber) 111 | log("Pushing fiber #{fiber.inspect} to ready list") 112 | @selector.push(fiber) 113 | end 114 | 115 | # Raise the given exception on the given fiber. 116 | # 117 | # @parameter fiber [Fiber] The fiber to raise the exception on. 118 | # @parameter arguments [Array] The arguments to use when raising the exception. 119 | def raise(fiber, *arguments) 120 | log("Raising exception on fiber #{fiber.inspect} with #{arguments.inspect}") 121 | @selector.raise(fiber, *arguments) 122 | end 123 | 124 | # Check if the selector is ready. 125 | # 126 | # @returns [Boolean] Whether the selector is ready. 127 | def ready? 128 | @selector.ready? 129 | end 130 | 131 | # Wait for the given process, forwarded to the underlying selector. 132 | def process_wait(*arguments) 133 | log("Waiting for process with #{arguments.inspect}") 134 | @selector.process_wait(*arguments) 135 | end 136 | 137 | # Wait for the given IO, forwarded to the underlying selector. 138 | def io_wait(fiber, io, events) 139 | log("Waiting for IO #{io.inspect} for events #{events.inspect}") 140 | @selector.io_wait(fiber, io, events) 141 | end 142 | 143 | # Read from the given IO, forwarded to the underlying selector. 144 | def io_read(fiber, io, buffer, length, offset = 0) 145 | log("Reading from IO #{io.inspect} with buffer #{buffer}; length #{length} offset #{offset}") 146 | @selector.io_read(fiber, io, buffer, length, offset) 147 | end 148 | 149 | # Write to the given IO, forwarded to the underlying selector. 150 | def io_write(fiber, io, buffer, length, offset = 0) 151 | log("Writing to IO #{io.inspect} with buffer #{buffer}; length #{length} offset #{offset}") 152 | @selector.io_write(fiber, io, buffer, length, offset) 153 | end 154 | 155 | # Forward the given method to the underlying selector. 156 | def respond_to?(name, include_private = false) 157 | @selector.respond_to?(name, include_private) 158 | end 159 | 160 | # Select for the given duration, forwarded to the underlying selector. 161 | def select(duration = nil) 162 | log("Selecting for #{duration.inspect}") 163 | unless Fiber.current == @selector.loop 164 | Kernel::raise "Selector must be run on event loop fiber!" 165 | end 166 | 167 | @selector.select(duration) 168 | end 169 | end 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /lib/io/event/interrupt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2024, by Samuel Williams. 5 | 6 | module IO::Event 7 | # A thread safe synchronisation primative. 8 | class Interrupt 9 | def self.attach(selector) 10 | self.new(selector) 11 | end 12 | 13 | def initialize(selector) 14 | @selector = selector 15 | @input, @output = ::IO.pipe 16 | 17 | @fiber = Fiber.new do 18 | while true 19 | if @selector.io_wait(@fiber, @input, IO::READABLE) 20 | @input.read_nonblock(1) 21 | end 22 | end 23 | end 24 | 25 | @fiber.transfer 26 | end 27 | 28 | # Send a sigle byte interrupt. 29 | def signal 30 | @output.write(".") 31 | @output.flush 32 | rescue IOError 33 | # Ignore. 34 | end 35 | 36 | def close 37 | @input.close 38 | @output.close 39 | # @fiber.raise(::Interrupt) 40 | end 41 | end 42 | 43 | private_constant :Interrupt 44 | end 45 | -------------------------------------------------------------------------------- /lib/io/event/native.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2025, by Samuel Williams. 5 | 6 | begin 7 | require "IO_Event" 8 | rescue LoadError => error 9 | warn "Could not load native event selector: #{error}" 10 | require_relative "selector/nonblock" 11 | end 12 | -------------------------------------------------------------------------------- /lib/io/event/priority_heap.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021, by Wander Hillen. 5 | # Copyright, 2021-2024, by Samuel Williams. 6 | 7 | class IO 8 | module Event 9 | # A priority queue implementation using a standard binary minheap. It uses straight comparison 10 | # of its contents to determine priority. 11 | # See for explanations of the main methods. 12 | class PriorityHeap 13 | # Initializes the heap. 14 | def initialize 15 | # The heap is represented with an array containing a binary tree. See 16 | # https://en.wikipedia.org/wiki/Binary_heap#Heap_implementation for how this array 17 | # is built up. 18 | @contents = [] 19 | end 20 | 21 | # @returns [Object | Nil] the smallest element in the heap without removing it, or nil if the heap is empty. 22 | def peek 23 | @contents[0] 24 | end 25 | 26 | # @returns [Integer] the number of elements in the heap. 27 | def size 28 | @contents.size 29 | end 30 | 31 | # Removes and returns the smallest element in the heap, or nil if the heap is empty. 32 | # 33 | # @returns [Object | Nil] The smallest element in the heap, or nil if the heap is empty. 34 | def pop 35 | # If the heap is empty: 36 | if @contents.empty? 37 | return nil 38 | end 39 | 40 | # If we have only one item, no swapping is required: 41 | if @contents.size == 1 42 | return @contents.pop 43 | end 44 | 45 | # Take the root of the tree: 46 | value = @contents[0] 47 | 48 | # Remove the last item in the tree: 49 | last = @contents.pop 50 | 51 | # Overwrite the root of the tree with the item: 52 | @contents[0] = last 53 | 54 | # Bubble it down into place: 55 | bubble_down(0) 56 | 57 | # validate! 58 | 59 | return value 60 | end 61 | 62 | # Add a new element to the heap, then rearrange elements until the heap invariant is true again. 63 | # 64 | # @parameter element [Object] The element to add to the heap. 65 | def push(element) 66 | # Insert the item at the end of the heap: 67 | @contents.push(element) 68 | 69 | # Bubble it up into position: 70 | bubble_up(@contents.size - 1) 71 | 72 | # validate! 73 | 74 | return self 75 | end 76 | 77 | # Empties out the heap, discarding all elements 78 | def clear! 79 | @contents = [] 80 | end 81 | 82 | # Validate the heap invariant. Every element except the root must not be smaller than its parent element. Note that it MAY be equal. 83 | def valid? 84 | # Notice we skip index 0 on purpose, because it has no parent 85 | (1..(@contents.size - 1)).all? { |e| @contents[e] >= @contents[(e - 1) / 2] } 86 | end 87 | 88 | private 89 | 90 | # Left here for reference, but unused. 91 | # def swap(i, j) 92 | # @contents[i], @contents[j] = @contents[j], @contents[i] 93 | # end 94 | 95 | def bubble_up(index) 96 | parent_index = (index - 1) / 2 # watch out, integer division! 97 | 98 | while index > 0 && @contents[index] < @contents[parent_index] 99 | # If the node has a smaller value than its parent, swap these nodes to uphold the minheap invariant and update the index of the 'current' node. If the node is already at index 0, we can also stop because that is the root of the heap. 100 | # swap(index, parent_index) 101 | @contents[index], @contents[parent_index] = @contents[parent_index], @contents[index] 102 | 103 | index = parent_index 104 | parent_index = (index - 1) / 2 # watch out, integer division! 105 | end 106 | end 107 | 108 | def bubble_down(index) 109 | swap_value = 0 110 | swap_index = nil 111 | 112 | while true 113 | left_index = (2 * index) + 1 114 | left_value = @contents[left_index] 115 | 116 | if left_value.nil? 117 | # This node has no children so it can't bubble down any further. We're done here! 118 | return 119 | end 120 | 121 | # Determine which of the child nodes has the smallest value: 122 | right_index = left_index + 1 123 | right_value = @contents[right_index] 124 | 125 | if right_value.nil? or right_value > left_value 126 | swap_value = left_value 127 | swap_index = left_index 128 | else 129 | swap_value = right_value 130 | swap_index = right_index 131 | end 132 | 133 | if @contents[index] < swap_value 134 | # No need to swap, the minheap invariant is already satisfied: 135 | return 136 | else 137 | # At least one of the child node has a smaller value than the current node, swap current node with that child and update current node for if it might need to bubble down even further: 138 | # swap(index, swap_index) 139 | @contents[index], @contents[swap_index] = @contents[swap_index], @contents[index] 140 | 141 | index = swap_index 142 | end 143 | end 144 | end 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /lib/io/event/selector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2024, by Samuel Williams. 5 | 6 | require_relative "selector/select" 7 | require_relative "debug/selector" 8 | require_relative "support" 9 | 10 | module IO::Event 11 | # @namespace 12 | module Selector 13 | # The default selector implementation, which is chosen based on the environment and available implementations. 14 | # 15 | # @parameter env [Hash] The environment to read configuration from. 16 | # @returns [Class] The default selector implementation. 17 | def self.default(env = ENV) 18 | if name = env["IO_EVENT_SELECTOR"]&.to_sym 19 | return const_get(name) 20 | end 21 | 22 | if self.const_defined?(:URing) 23 | URing 24 | elsif self.const_defined?(:EPoll) 25 | EPoll 26 | elsif self.const_defined?(:KQueue) 27 | KQueue 28 | else 29 | Select 30 | end 31 | end 32 | 33 | # Create a new selector instance, according to the best available implementation. 34 | # 35 | # @parameter loop [Fiber] The event loop fiber. 36 | # @parameter env [Hash] The environment to read configuration from. 37 | # @returns [Selector] The new selector instance. 38 | def self.new(loop, env = ENV) 39 | selector = default(env).new(loop) 40 | 41 | if debug = env["IO_EVENT_DEBUG_SELECTOR"] 42 | selector = Debug::Selector.wrap(selector, env) 43 | end 44 | 45 | return selector 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/io/event/selector/nonblock.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2024, by Samuel Williams. 5 | 6 | require "io/nonblock" 7 | 8 | module IO::Event 9 | module Selector 10 | # Execute the given block in non-blocking mode. 11 | # 12 | # @parameter io [IO] The IO object to operate on. 13 | # @yields {...} The block to execute. 14 | def self.nonblock(io, &block) 15 | io.nonblock(&block) 16 | rescue Errno::EBADF 17 | # Windows. 18 | yield 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/io/event/selector/select.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2024, by Samuel Williams. 5 | # Copyright, 2023, by Math Ieu. 6 | 7 | require_relative "../interrupt" 8 | require_relative "../support" 9 | 10 | module IO::Event 11 | module Selector 12 | # A pure-Ruby implementation of the event selector. 13 | class Select 14 | # Initialize the selector with the given event loop fiber. 15 | def initialize(loop) 16 | @loop = loop 17 | 18 | @waiting = Hash.new.compare_by_identity 19 | 20 | @blocked = false 21 | 22 | @ready = Queue.new 23 | @interrupt = Interrupt.attach(self) 24 | 25 | @idle_duration = 0.0 26 | end 27 | 28 | # @attribute [Fiber] The event loop fiber. 29 | attr :loop 30 | 31 | # @attribute [Float] This is the amount of time the event loop was idle during the last select call. 32 | attr :idle_duration 33 | 34 | # Wake up the event loop if it is currently sleeping. 35 | def wakeup 36 | if @blocked 37 | @interrupt.signal 38 | 39 | return true 40 | end 41 | 42 | return false 43 | end 44 | 45 | # Close the selector and release any resources. 46 | def close 47 | @interrupt.close 48 | 49 | @loop = nil 50 | @waiting = nil 51 | end 52 | 53 | Optional = Struct.new(:fiber) do 54 | def transfer(*arguments) 55 | fiber&.transfer(*arguments) 56 | end 57 | 58 | def alive? 59 | fiber&.alive? 60 | end 61 | 62 | def nullify 63 | self.fiber = nil 64 | end 65 | end 66 | 67 | # Transfer from the current fiber to the event loop. 68 | def transfer 69 | @loop.transfer 70 | end 71 | 72 | # Transfer from the current fiber to the specified fiber. Put the current fiber into the ready list. 73 | def resume(fiber, *arguments) 74 | optional = Optional.new(Fiber.current) 75 | @ready.push(optional) 76 | 77 | fiber.transfer(*arguments) 78 | ensure 79 | optional.nullify 80 | end 81 | 82 | # Yield from the current fiber back to the event loop. Put the current fiber into the ready list. 83 | def yield 84 | optional = Optional.new(Fiber.current) 85 | @ready.push(optional) 86 | 87 | @loop.transfer 88 | ensure 89 | optional.nullify 90 | end 91 | 92 | # Append the given fiber into the ready list. 93 | def push(fiber) 94 | @ready.push(fiber) 95 | end 96 | 97 | # Transfer to the given fiber and raise an exception. Put the current fiber into the ready list. 98 | def raise(fiber, *arguments) 99 | optional = Optional.new(Fiber.current) 100 | @ready.push(optional) 101 | 102 | fiber.raise(*arguments) 103 | ensure 104 | optional.nullify 105 | end 106 | 107 | # @returns [Boolean] Whether the ready list is not empty, i.e. there are fibers ready to be resumed. 108 | def ready? 109 | !@ready.empty? 110 | end 111 | 112 | Waiter = Struct.new(:fiber, :events, :tail) do 113 | def alive? 114 | self.fiber&.alive? 115 | end 116 | 117 | # Dispatch the given events to the list of waiting fibers. If the fiber was not waiting for the given events, it is reactivated by calling the given block. 118 | def dispatch(events, &reactivate) 119 | # We capture the tail here, because calling reactivate might modify it: 120 | tail = self.tail 121 | 122 | if fiber = self.fiber 123 | if fiber.alive? 124 | revents = events & self.events 125 | if revents.zero? 126 | reactivate.call(self) 127 | else 128 | self.fiber = nil 129 | fiber.transfer(revents) 130 | end 131 | else 132 | self.fiber = nil 133 | end 134 | end 135 | 136 | tail&.dispatch(events, &reactivate) 137 | end 138 | 139 | def invalidate 140 | self.fiber = nil 141 | end 142 | 143 | def each(&block) 144 | if fiber = self.fiber 145 | yield fiber, self.events 146 | end 147 | 148 | self.tail&.each(&block) 149 | end 150 | end 151 | 152 | # Wait for the given IO to become readable or writable. 153 | # 154 | # @parameter fiber [Fiber] The fiber that is waiting. 155 | # @parameter io [IO] The IO object to wait on. 156 | # @parameter events [Integer] The events to wait for. 157 | def io_wait(fiber, io, events) 158 | waiter = @waiting[io] = Waiter.new(fiber, events, @waiting[io]) 159 | 160 | @loop.transfer 161 | ensure 162 | waiter&.invalidate 163 | end 164 | 165 | # Wait for multiple IO objects to become readable or writable. 166 | # 167 | # @parameter readable [Array(IO)] The list of IO objects to wait for readability. 168 | # @parameter writable [Array(IO)] The list of IO objects to wait for writability. 169 | # @parameter priority [Array(IO)] The list of IO objects to wait for priority events. 170 | def io_select(readable, writable, priority, timeout) 171 | Thread.new do 172 | IO.select(readable, writable, priority, timeout) 173 | end.value 174 | end 175 | 176 | EAGAIN = -Errno::EAGAIN::Errno 177 | EWOULDBLOCK = -Errno::EWOULDBLOCK::Errno 178 | 179 | # Whether the given error code indicates that the operation should be retried. 180 | protected def again?(errno) 181 | errno == EAGAIN or errno == EWOULDBLOCK 182 | end 183 | 184 | if Support.fiber_scheduler_v3? 185 | # Ruby 3.3+, full IO::Buffer support. 186 | 187 | # Read from the given IO to the buffer. 188 | # 189 | # @parameter length [Integer] The minimum number of bytes to read. 190 | # @parameter offset [Integer] The offset into the buffer to read to. 191 | def io_read(fiber, io, buffer, length, offset = 0) 192 | total = 0 193 | 194 | Selector.nonblock(io) do 195 | while true 196 | result = Fiber.blocking{buffer.read(io, 0, offset)} 197 | 198 | if result < 0 199 | if again?(result) 200 | self.io_wait(fiber, io, IO::READABLE) 201 | else 202 | return result 203 | end 204 | elsif result == 0 205 | break 206 | else 207 | total += result 208 | break if total >= length 209 | offset += result 210 | end 211 | end 212 | end 213 | 214 | return total 215 | end 216 | 217 | # Write to the given IO from the buffer. 218 | # 219 | # @parameter length [Integer] The minimum number of bytes to write. 220 | # @parameter offset [Integer] The offset into the buffer to write from. 221 | def io_write(fiber, io, buffer, length, offset = 0) 222 | total = 0 223 | 224 | Selector.nonblock(io) do 225 | while true 226 | result = Fiber.blocking{buffer.write(io, 0, offset)} 227 | 228 | if result < 0 229 | if again?(result) 230 | self.io_wait(fiber, io, IO::READABLE) 231 | else 232 | return result 233 | end 234 | elsif result == 0 235 | break result 236 | else 237 | total += result 238 | break if total >= length 239 | offset += result 240 | end 241 | end 242 | end 243 | 244 | return total 245 | end 246 | elsif Support.fiber_scheduler_v2? 247 | # Ruby 3.2, most IO::Buffer support, but slightly clunky read/write methods. 248 | def io_read(fiber, io, buffer, length, offset = 0) 249 | total = 0 250 | 251 | Selector.nonblock(io) do 252 | maximum_size = buffer.size - offset 253 | while maximum_size > 0 254 | result = Fiber.blocking{buffer.read(io, maximum_size, offset)} 255 | 256 | if again?(result) 257 | if length > 0 258 | self.io_wait(fiber, io, IO::READABLE) 259 | else 260 | return result 261 | end 262 | elsif result < 0 263 | return result 264 | else 265 | total += result 266 | offset += result 267 | break if total >= length 268 | end 269 | 270 | maximum_size = buffer.size - offset 271 | end 272 | end 273 | 274 | return total 275 | end 276 | 277 | def io_write(fiber, io, buffer, length, offset = 0) 278 | total = 0 279 | 280 | Selector.nonblock(io) do 281 | maximum_size = buffer.size - offset 282 | while maximum_size > 0 283 | result = Fiber.blocking{buffer.write(io, maximum_size, offset)} 284 | 285 | if again?(result) 286 | if length > 0 287 | self.io_wait(fiber, io, IO::READABLE) 288 | else 289 | return result 290 | end 291 | elsif result < 0 292 | return result 293 | else 294 | total += result 295 | offset += result 296 | break if total >= length 297 | end 298 | 299 | maximum_size = buffer.size - offset 300 | end 301 | end 302 | 303 | return total 304 | end 305 | elsif Support.fiber_scheduler_v1? 306 | # Ruby <= 3.1, limited IO::Buffer support. 307 | def io_read(fiber, _io, buffer, length, offset = 0) 308 | # We need to avoid any internal buffering, so we use a duplicated IO object: 309 | io = IO.for_fd(_io.fileno, autoclose: false) 310 | 311 | total = 0 312 | 313 | maximum_size = buffer.size - offset 314 | while maximum_size > 0 315 | case result = blocking{io.read_nonblock(maximum_size, exception: false)} 316 | when :wait_readable 317 | if length > 0 318 | self.io_wait(fiber, io, IO::READABLE) 319 | else 320 | return EWOULDBLOCK 321 | end 322 | when :wait_writable 323 | if length > 0 324 | self.io_wait(fiber, io, IO::WRITABLE) 325 | else 326 | return EWOULDBLOCK 327 | end 328 | when nil 329 | break 330 | else 331 | buffer.set_string(result, offset) 332 | 333 | size = result.bytesize 334 | total += size 335 | offset += size 336 | break if size >= length 337 | length -= size 338 | end 339 | 340 | maximum_size = buffer.size - offset 341 | end 342 | 343 | return total 344 | rescue IOError => error 345 | return -Errno::EBADF::Errno 346 | rescue SystemCallError => error 347 | return -error.errno 348 | end 349 | 350 | def io_write(fiber, _io, buffer, length, offset = 0) 351 | # We need to avoid any internal buffering, so we use a duplicated IO object: 352 | io = IO.for_fd(_io.fileno, autoclose: false) 353 | 354 | total = 0 355 | 356 | maximum_size = buffer.size - offset 357 | while maximum_size > 0 358 | chunk = buffer.get_string(offset, maximum_size) 359 | case result = blocking{io.write_nonblock(chunk, exception: false)} 360 | when :wait_readable 361 | if length > 0 362 | self.io_wait(fiber, io, IO::READABLE) 363 | else 364 | return EWOULDBLOCK 365 | end 366 | when :wait_writable 367 | if length > 0 368 | self.io_wait(fiber, io, IO::WRITABLE) 369 | else 370 | return EWOULDBLOCK 371 | end 372 | else 373 | total += result 374 | offset += result 375 | break if result >= length 376 | length -= result 377 | end 378 | 379 | maximum_size = buffer.size - offset 380 | end 381 | 382 | return total 383 | rescue IOError => error 384 | return -Errno::EBADF::Errno 385 | rescue SystemCallError => error 386 | return -error.errno 387 | end 388 | 389 | def blocking(&block) 390 | fiber = Fiber.new(blocking: true, &block) 391 | return fiber.resume(fiber) 392 | end 393 | end 394 | 395 | def process_wait(fiber, pid, flags) 396 | Thread.new do 397 | Process::Status.wait(pid, flags) 398 | end.value 399 | end 400 | 401 | private def pop_ready 402 | unless @ready.empty? 403 | count = @ready.size 404 | 405 | count.times do 406 | fiber = @ready.pop 407 | fiber.transfer if fiber.alive? 408 | end 409 | 410 | return true 411 | end 412 | end 413 | 414 | def select(duration = nil) 415 | if pop_ready 416 | # If we have popped items from the ready list, they may influence the duration calculation, so we don't delay the event loop: 417 | duration = 0 418 | end 419 | 420 | readable = Array.new 421 | writable = Array.new 422 | priority = Array.new 423 | 424 | @waiting.each do |io, waiter| 425 | waiter.each do |fiber, events| 426 | if (events & IO::READABLE) > 0 427 | readable << io 428 | end 429 | 430 | if (events & IO::WRITABLE) > 0 431 | writable << io 432 | end 433 | 434 | if (events & IO::PRIORITY) > 0 435 | priority << io 436 | end 437 | end 438 | end 439 | 440 | duration = 0 unless @ready.empty? 441 | error = nil 442 | 443 | if duration&.>(0) 444 | start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) 445 | else 446 | @idle_duration = 0.0 447 | end 448 | 449 | # We need to handle interrupts on blocking IO. Every other implementation uses EINTR, but that doesn't work with `::IO.select` as it will retry the call on EINTR. 450 | Thread.handle_interrupt(::Exception => :on_blocking) do 451 | @blocked = true 452 | readable, writable, priority = ::IO.select(readable, writable, priority, duration) 453 | rescue ::Exception => error 454 | # Requeue below... 455 | ensure 456 | @blocked = false 457 | if start_time 458 | end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) 459 | @idle_duration = end_time - start_time 460 | end 461 | end 462 | 463 | if error 464 | # Requeue the error into the pending exception queue: 465 | Thread.current.raise(error) 466 | return 0 467 | end 468 | 469 | ready = Hash.new(0).compare_by_identity 470 | 471 | readable&.each do |io| 472 | ready[io] |= IO::READABLE 473 | end 474 | 475 | writable&.each do |io| 476 | ready[io] |= IO::WRITABLE 477 | end 478 | 479 | priority&.each do |io| 480 | ready[io] |= IO::PRIORITY 481 | end 482 | 483 | ready.each do |io, events| 484 | @waiting.delete(io).dispatch(events) do |waiter| 485 | # Re-schedule the waiting IO: 486 | waiter.tail = @waiting[io] 487 | @waiting[io] = waiter 488 | end 489 | end 490 | 491 | return ready.size 492 | end 493 | end 494 | end 495 | end 496 | -------------------------------------------------------------------------------- /lib/io/event/support.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2024, by Samuel Williams. 5 | 6 | class IO 7 | module Event 8 | # Helper methods for detecting support for various features. 9 | module Support 10 | # Some features are only availble if the IO::Buffer class is available. 11 | # 12 | # @returns [Boolean] Whether the IO::Buffer class is available. 13 | def self.buffer? 14 | IO.const_defined?(:Buffer) 15 | end 16 | 17 | # The basic fiber scheduler was introduced along side the IO::Buffer class. 18 | # 19 | # @returns [Boolean] Whether the IO::Buffer class is available. 20 | # 21 | # To be removed on 31 Mar 2025. 22 | def self.fiber_scheduler_v1? 23 | IO.const_defined?(:Buffer) 24 | end 25 | 26 | # More advanced read/write methods and blocking controls were introduced in Ruby 3.2. 27 | # 28 | # To be removed on 31 Mar 2026. 29 | def self.fiber_scheduler_v2? 30 | # Some interface changes were back-ported incorrectly: 31 | # https://github.com/ruby/ruby/pull/10778 32 | # Specifically "Improvements to IO::Buffer read/write/pread/pwrite." 33 | # Missing correct size calculation. 34 | return false if RUBY_VERSION >= "3.2.5" 35 | 36 | IO.const_defined?(:Buffer) and Fiber.respond_to?(:blocking) and IO::Buffer.instance_method(:read).arity == -1 37 | end 38 | 39 | # Updated inferfaces for read/write and IO::Buffer were introduced in Ruby 3.3, including pread/pwrite. 40 | # 41 | # To become the default 31 Mar 2026. 42 | def self.fiber_scheduler_v3? 43 | if fiber_scheduler_v2? 44 | return true if RUBY_VERSION >= "3.3" 45 | 46 | # Feature detection if required: 47 | begin 48 | IO::Buffer.new.slice(0, 0).write(STDOUT) 49 | return true 50 | rescue 51 | return false 52 | end 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/io/event/timers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024-2025, by Samuel Williams. 5 | 6 | require_relative "priority_heap" 7 | 8 | class IO 9 | module Event 10 | # An efficient sorted set of timers. 11 | class Timers 12 | # A handle to a scheduled timer. 13 | class Handle 14 | # Initialize the handle with the given time and block. 15 | # 16 | # @parameter time [Float] The time at which the block should be called. 17 | # @parameter block [Proc] The block to call. 18 | def initialize(time, block) 19 | @time = time 20 | @block = block 21 | end 22 | 23 | # @attribute [Float] The time at which the block should be called. 24 | attr :time 25 | 26 | # @attribute [Proc | Nil] The block to call when the timer fires. 27 | attr :block 28 | 29 | # Compare the handle with another handle. 30 | # 31 | # @parameter other [Handle] The other handle to compare with. 32 | # @returns [Boolean] Whether the handle is less than the other handle. 33 | def < other 34 | @time < other.time 35 | end 36 | 37 | # Compare the handle with another handle. 38 | # 39 | # @parameter other [Handle] The other handle to compare with. 40 | # @returns [Boolean] Whether the handle is greater than the other handle. 41 | def > other 42 | @time > other.time 43 | end 44 | 45 | # Invoke the block. 46 | def call(...) 47 | @block.call(...) 48 | end 49 | 50 | # Cancel the timer. 51 | def cancel! 52 | @block = nil 53 | end 54 | 55 | # @returns [Boolean] Whether the timer has been cancelled. 56 | def cancelled? 57 | @block.nil? 58 | end 59 | end 60 | 61 | # Initialize the timers. 62 | def initialize 63 | @heap = PriorityHeap.new 64 | @scheduled = [] 65 | end 66 | 67 | # @returns [Integer] The number of timers in the heap. 68 | def size 69 | flush! 70 | 71 | return @heap.size 72 | end 73 | 74 | # Schedule a block to be called at a specific time in the future. 75 | # 76 | # @parameter time [Float] The time at which the block should be called, relative to {#now}. 77 | # @parameter block [Proc] The block to call. 78 | def schedule(time, block) 79 | handle = Handle.new(time, block) 80 | 81 | @scheduled << handle 82 | 83 | return handle 84 | end 85 | 86 | # Schedule a block to be called after a specific time offset, relative to the current time as returned by {#now}. 87 | # 88 | # @parameter offset [#to_f] The time offset from the current time at which the block should be called. 89 | # @yields {|now| ...} When the timer fires. 90 | def after(offset, &block) 91 | schedule(self.now + offset.to_f, block) 92 | end 93 | 94 | # Compute the time interval until the next timer fires. 95 | # 96 | # @parameter now [Float] The current time. 97 | # @returns [Float | Nil] The time interval until the next timer fires, if any. 98 | def wait_interval(now = self.now) 99 | flush! 100 | 101 | while handle = @heap.peek 102 | if handle.cancelled? 103 | @heap.pop 104 | else 105 | return handle.time - now 106 | end 107 | end 108 | end 109 | 110 | # @returns [Float] The current time. 111 | def now 112 | ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) 113 | end 114 | 115 | # Fire all timers that are ready to fire. 116 | # 117 | # @parameter now [Float] The current time. 118 | def fire(now = self.now) 119 | # Flush scheduled timers into the heap: 120 | flush! 121 | 122 | # Get the earliest timer: 123 | while handle = @heap.peek 124 | if handle.cancelled? 125 | @heap.pop 126 | elsif handle.time <= now 127 | # Remove the earliest timer from the heap: 128 | @heap.pop 129 | 130 | # Call the block: 131 | handle.call(now) 132 | else 133 | break 134 | end 135 | end 136 | end 137 | 138 | # Flush all scheduled timers into the heap. 139 | # 140 | # This is a small optimization which assumes that most timers (timeouts) will be cancelled. 141 | protected def flush! 142 | while handle = @scheduled.pop 143 | @heap.push(handle) unless handle.cancelled? 144 | end 145 | end 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /lib/io/event/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2025, by Samuel Williams. 5 | 6 | # @namespace 7 | class IO 8 | # @namespace 9 | module Event 10 | VERSION = "1.10.1" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright, 2021, by Wander Hillen. 4 | Copyright, 2021-2025, by Samuel Williams. 5 | Copyright, 2021, by Delton Ding. 6 | Copyright, 2021-2024, by Benoit Daloze. 7 | Copyright, 2022, by Alex Matchneer. 8 | Copyright, 2022, by Bruno Sutic. 9 | Copyright, 2023, by Math Ieu. 10 | Copyright, 2024, by Pavel Rosický. 11 | Copyright, 2024, by Anthony Ross. 12 | Copyright, 2024, by Shizuo Fujita. 13 | Copyright, 2024, by Jean Boussier. 14 | Copyright, 2025, by Stanislav (Stas) Katkov. 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy 17 | of this software and associated documentation files (the "Software"), to deal 18 | in the Software without restriction, including without limitation the rights 19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | copies of the Software, and to permit persons to whom the Software is 21 | furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all 24 | copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | SOFTWARE. 33 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | E 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | V 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | E 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | N 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | T 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ![IO::Event](logo.svg) 2 | 3 | Provides low level cross-platform primitives for constructing event loops, with support for `select`, `kqueue`, `epoll` and `io_uring`. 4 | 5 | [![Development Status](https://github.com/socketry/io-event/workflows/Test/badge.svg)](https://github.com/socketry/io-event/actions?workflow=Test) 6 | 7 | ## Motivation 8 | 9 | The initial proof-of-concept [Async](https://github.com/socketry/async) was built on [NIO4r](https://github.com/socketry/nio4r). It was perfectly acceptable and well tested in production, however being built on `libev` was a little bit limiting. I wanted to directly build my fiber scheduler into the fabric of the event loop, which is what this gem exposes - it is specifically implemented to support building event loops beneath the fiber scheduler interface, providing an efficient C implementation of all the core operations. 10 | 11 | ## Usage 12 | 13 | Please see the [project documentation](https://socketry.github.io/io-event/) for more details. 14 | 15 | - [Getting Started](https://socketry.github.io/io-event/guides/getting-started/index) - This guide explains how to use `io-event` for non-blocking IO. 16 | 17 | ## Releases 18 | 19 | Please see the [project releases](https://socketry.github.io/io-event/releases/index) for all releases. 20 | 21 | ### v1.10.0 22 | 23 | - `IO::Event::Profiler` is moved to dedicated gem: [fiber-profiler](https://github.com/socketry/fiber-profiler). 24 | - Perform runtime checks for native selectors to ensure they are supported in the current environment. While compile-time checks determine availability, restrictions like seccomp and SELinux may still prevent them from working. 25 | 26 | ### v1.9.0 27 | 28 | - Improved `IO::Event::Profiler` for detecting stalls. 29 | 30 | ### v1.8.0 31 | 32 | - Detecting fibers that are stalling the event loop. 33 | 34 | ### v1.7.5 35 | 36 | - Fix `process_wait` race condition on EPoll that could cause a hang. 37 | 38 | ## Contributing 39 | 40 | We welcome contributions to this project. 41 | 42 | 1. Fork it. 43 | 2. Create your feature branch (`git checkout -b my-new-feature`). 44 | 3. Commit your changes (`git commit -am 'Add some feature'`). 45 | 4. Push to the branch (`git push origin my-new-feature`). 46 | 5. Create new Pull Request. 47 | 48 | ### Developer Certificate of Origin 49 | 50 | 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. 51 | 52 | ### Community Guidelines 53 | 54 | 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. 55 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /releases.md: -------------------------------------------------------------------------------- 1 | # Releases 2 | 3 | ## v1.10.0 4 | 5 | - `IO::Event::Profiler` is moved to dedicated gem: [fiber-profiler](https://github.com/socketry/fiber-profiler). 6 | - Perform runtime checks for native selectors to ensure they are supported in the current environment. While compile-time checks determine availability, restrictions like seccomp and SELinux may still prevent them from working. 7 | 8 | ## v1.9.0 9 | 10 | - Improved `IO::Event::Profiler` for detecting stalls. 11 | 12 | ## v1.8.0 13 | 14 | - Detecting fibers that are stalling the event loop. 15 | 16 | ## v1.7.5 17 | 18 | - Fix `process_wait` race condition on EPoll that could cause a hang. 19 | -------------------------------------------------------------------------------- /test/io/event.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2024, by Samuel Williams. 5 | 6 | require "io/event" 7 | 8 | describe IO::Event::VERSION do 9 | it "has a version number" do 10 | expect(subject).to be =~ /\d+\.\d+\.\d+/ 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/io/event/priority_heap.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024, by Samuel Williams. 5 | 6 | require "io/event/priority_heap" 7 | 8 | describe IO::Event::PriorityHeap do 9 | let(:priority_heap) {subject.new} 10 | 11 | with "empty heap" do 12 | it "should return nil when the first element is requested" do 13 | expect(priority_heap.peek).to be_nil 14 | end 15 | 16 | it "should return nil when the first element is extracted" do 17 | expect(priority_heap.pop).to be_nil 18 | end 19 | 20 | it "should report its size as zero" do 21 | expect(priority_heap.size).to be(:zero?) 22 | end 23 | end 24 | 25 | it "returns the same element after inserting a single element" do 26 | priority_heap.push(1) 27 | expect(priority_heap.size).to be == 1 28 | expect(priority_heap.pop).to be == 1 29 | expect(priority_heap.size).to be(:zero?) 30 | end 31 | 32 | it "should return inserted elements in ascending order no matter the insertion order" do 33 | (1..10).to_a.shuffle.each do |e| 34 | priority_heap.push(e) 35 | end 36 | 37 | expect(priority_heap.size).to be == 10 38 | expect(priority_heap.peek).to be == 1 39 | 40 | result = [] 41 | 10.times do 42 | result << priority_heap.pop 43 | end 44 | 45 | expect(result.size).to be == 10 46 | expect(priority_heap.size).to be(:zero?) 47 | expect(result.sort).to be == result 48 | end 49 | 50 | with "maintaining the heap invariant" do 51 | it "for empty heaps" do 52 | expect(priority_heap).to be(:valid?) 53 | end 54 | 55 | it "for heap of size 1" do 56 | priority_heap.push(123) 57 | expect(priority_heap).to be(:valid?) 58 | end 59 | # Exhaustive testing of all permutations of [1..6] 60 | it "for all permutations of size 6" do 61 | [1,2,3,4,5,6].permutation do |arr| 62 | priority_heap.clear! 63 | arr.each { |e| priority_heap.push(e) } 64 | expect(priority_heap).to be(:valid?) 65 | end 66 | end 67 | 68 | # A few examples with more elements (but not ALL permutations) 69 | it "for larger amounts of values" do 70 | 5.times do 71 | priority_heap.clear! 72 | (1..1000).to_a.shuffle.each { |e| priority_heap.push(e) } 73 | expect(priority_heap).to be(:valid?) 74 | end 75 | end 76 | 77 | # What if we insert several of the same item along with others? 78 | it "with several elements of the same value" do 79 | test_values = (1..10).to_a + [4] * 5 80 | test_values.each { |e| priority_heap.push(e) } 81 | expect(priority_heap).to be(:valid?) 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/io/event/selector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2024, by Samuel Williams. 5 | # Copyright, 2023, by Math Ieu. 6 | 7 | require "io/event" 8 | require "io/event/selector" 9 | require "io/event/debug/selector" 10 | 11 | require "socket" 12 | require "fiber" 13 | 14 | require "unix_socket" 15 | 16 | class FakeFiber 17 | def initialize(alive = true) 18 | @alive = alive 19 | @count = 0 20 | end 21 | 22 | attr :count 23 | 24 | def alive? 25 | @alive 26 | end 27 | 28 | def transfer 29 | @count += 1 30 | end 31 | end 32 | 33 | Selector = Sus::Shared("a selector") do 34 | with "#select" do 35 | let(:quantum) {0.2} 36 | 37 | it "can select with 0s timeout" do 38 | expect do 39 | selector.select(0) 40 | end.to have_duration(be < quantum) 41 | end 42 | 43 | it "can select with a short timeout" do 44 | expect do 45 | selector.select(0.01) 46 | end.to have_duration(be <= (0.01 + quantum)) 47 | end 48 | 49 | it "raises an error when given an invalid duration" do 50 | expect do 51 | selector.select("invalid") 52 | end.to raise_exception 53 | end 54 | end 55 | 56 | with "#idle_duration" do 57 | it "can report idle duration" do 58 | 10.times do 59 | selector.select(0.001) 60 | expect(selector.idle_duration).to be > 0.0 61 | 62 | selector.select(0) 63 | expect(selector.idle_duration).to be == 0.0 64 | end 65 | end 66 | end 67 | 68 | with "#wakeup" do 69 | it "can wakeup selector from different thread" do 70 | thread = Thread.new do 71 | sleep 0.001 72 | selector.wakeup 73 | end 74 | 75 | expect do 76 | selector.select(1) 77 | end.to have_duration(be < 1) 78 | ensure 79 | thread.join 80 | end 81 | 82 | it "can wakeup selector from different thread twice in a row" do 83 | 2.times do 84 | thread = Thread.new do 85 | sleep 0.001 86 | selector.wakeup 87 | end 88 | 89 | expect do 90 | selector.select(1) 91 | end.to have_duration(be < 1) 92 | ensure 93 | thread.join 94 | end 95 | end 96 | 97 | it "ignores wakeup if not selecting" do 98 | expect(selector.wakeup).to be == false 99 | end 100 | 101 | it "doesn't block when readying another fiber" do 102 | fiber = FakeFiber.new 103 | 104 | 10.times do |i| 105 | thread = Thread.new do 106 | sleep(i / 10000.0) 107 | selector.push(fiber) 108 | selector.wakeup 109 | end 110 | 111 | expect do 112 | selector.select(1.0) 113 | end.to have_duration(be < 1.0) 114 | ensure 115 | thread.join 116 | end 117 | end 118 | end 119 | 120 | with "#io_wait" do 121 | let(:events) {Array.new} 122 | let(:sockets) {UNIXSocket.pair} 123 | let(:local) {sockets.first} 124 | let(:remote) {sockets.last} 125 | 126 | it "can wait for an io to become readable" do 127 | fiber = Fiber.new do 128 | events << :wait_readable 129 | 130 | expect( 131 | selector.io_wait(Fiber.current, local, IO::READABLE) 132 | ).to be == IO::READABLE 133 | 134 | events << :readable 135 | end 136 | 137 | events << :transfer 138 | fiber.transfer 139 | 140 | remote.puts "Hello World" 141 | 142 | events << :select 143 | 144 | selector.select(1) 145 | 146 | expect(events).to be == [ 147 | :transfer, :wait_readable, 148 | :select, :readable 149 | ] 150 | end 151 | 152 | it "can wait for an io to become writable" do 153 | fiber = Fiber.new do 154 | events << :wait_writable 155 | 156 | expect( 157 | selector.io_wait(Fiber.current, local, IO::WRITABLE) 158 | ).to be == IO::WRITABLE 159 | 160 | events << :writable 161 | end 162 | 163 | events << :transfer 164 | fiber.transfer 165 | 166 | events << :select 167 | selector.select(1) 168 | 169 | expect(events).to be == [ 170 | :transfer, :wait_writable, 171 | :select, :writable 172 | ] 173 | end 174 | 175 | it "can read and write from two different fibers" do 176 | readable = writable = false 177 | 178 | read_fiber = Fiber.new do 179 | events << :wait_readable 180 | 181 | expect( 182 | selector.io_wait(Fiber.current, local, IO::READABLE) 183 | ).to be == IO::READABLE 184 | 185 | readable = true 186 | end 187 | 188 | write_fiber = Fiber.new do 189 | events << :wait_writable 190 | 191 | expect( 192 | selector.io_wait(Fiber.current, local, IO::WRITABLE) 193 | ).to be == IO::WRITABLE 194 | 195 | writable = true 196 | end 197 | 198 | events << :transfer 199 | read_fiber.transfer 200 | write_fiber.transfer 201 | 202 | remote.puts "Hello World" 203 | events << :select 204 | selector.select(1) 205 | 206 | expect(events).to be == [ 207 | :transfer, :wait_readable, :wait_writable, 208 | :select 209 | ] 210 | 211 | expect(readable).to be == true 212 | expect(writable).to be == true 213 | end 214 | 215 | it "can read and write from two different fibers (alternate)" do 216 | read_fiber = Fiber.new do 217 | events << :wait_readable 218 | 219 | expect( 220 | selector.io_wait(Fiber.current, local, IO::READABLE) 221 | ).to be == IO::READABLE 222 | 223 | events << :readable 224 | end 225 | 226 | write_fiber = Fiber.new do 227 | events << :wait_writable 228 | 229 | expect( 230 | selector.io_wait(Fiber.current, local, IO::WRITABLE) 231 | ).to be == IO::WRITABLE 232 | 233 | events << :writable 234 | end 235 | 236 | events << :transfer 237 | read_fiber.transfer 238 | write_fiber.transfer 239 | 240 | events << :select1 241 | selector.select(1) 242 | remote.puts "Hello World" 243 | events << :select2 244 | selector.select(1) 245 | 246 | expect(events).to be == [ 247 | :transfer, 248 | :wait_readable, 249 | :wait_writable, 250 | :select1, 251 | :writable, 252 | :select2, 253 | :readable, 254 | ] 255 | end 256 | 257 | it "can wait consecutively on two different io objects that share the same file descriptor" do 258 | fiber = Fiber.new do 259 | events << :write1 260 | remote.puts "Hello World" 261 | 262 | events << :wait_readable1 263 | 264 | expect( 265 | selector.io_wait(Fiber.current, local, IO::READABLE) 266 | ).to be == IO::READABLE 267 | 268 | events << :readable1 269 | 270 | events << :new_io 271 | fileno = local.fileno 272 | local.close 273 | 274 | new_local, new_remote = UNIXSocket.pair 275 | 276 | # Make sure we attempt to wait on the same file descriptor: 277 | if new_remote.fileno == fileno 278 | new_local, new_remote = new_remote, new_local 279 | end 280 | 281 | if new_local.fileno != fileno 282 | warn "Could not create new IO object with same FD, test ineffective!" 283 | end 284 | 285 | events << :write2 286 | new_remote.puts "Hello World" 287 | 288 | events << :wait_readable2 289 | 290 | expect( 291 | selector.io_wait(Fiber.current, new_local, IO::READABLE) 292 | ).to be == IO::READABLE 293 | 294 | events << :readable2 295 | end 296 | 297 | events << :transfer 298 | fiber.transfer 299 | 300 | events << :select1 301 | 302 | selector.select(1) 303 | 304 | events << :select2 305 | 306 | selector.select(1) 307 | 308 | expect(events).to be == [ 309 | :transfer, 310 | :write1, 311 | :wait_readable1, 312 | :select1, 313 | :readable1, 314 | :new_io, 315 | :write2, 316 | :wait_readable2, 317 | :select2, 318 | :readable2, 319 | ] 320 | end 321 | 322 | it "can handle exception during wait" do 323 | fiber = Fiber.new do 324 | events << :wait_readable 325 | 326 | expect do 327 | while true 328 | selector.io_wait(Fiber.current, local, IO::READABLE) 329 | events << :readable 330 | end 331 | end.to raise_exception(RuntimeError, message: be =~ /Boom/) 332 | 333 | events << :error 334 | end 335 | 336 | events << :transfer 337 | fiber.transfer 338 | 339 | events << :select 340 | selector.select(0) 341 | fiber.raise(RuntimeError.new("Boom")) 342 | 343 | events << :puts 344 | remote.puts "Hello World" 345 | selector.select(0) 346 | 347 | expect(events).to be == [ 348 | :transfer, :wait_readable, 349 | :select, :error, :puts 350 | ] 351 | end 352 | 353 | 354 | it "can have two fibers reading from the same io" do 355 | fiber1 = Fiber.new do 356 | events << :wait_readable1 357 | selector.io_wait(Fiber.current, local, IO::READABLE) 358 | events << :readable 359 | rescue 360 | events << :error1 361 | end 362 | 363 | fiber2 = Fiber.new do 364 | events << :wait_readable2 365 | selector.io_wait(Fiber.current, local, IO::READABLE) 366 | events << :readable 367 | rescue 368 | events << :error2 369 | end 370 | 371 | events << :transfer 372 | fiber1.transfer 373 | fiber2.transfer 374 | 375 | remote.puts "Hello World" 376 | events << :select 377 | selector.select(1) 378 | 379 | expect(events).to be == [ 380 | :transfer, :wait_readable1, :wait_readable2, 381 | :select, :readable, :readable 382 | ] 383 | end 384 | 385 | it "can handle exception raised during wait from another fiber that was waiting on the same io" do 386 | [false, true].each do |swapped| # Try both orderings. 387 | writable1 = writable2 = false 388 | error1 = false 389 | raised1 = false 390 | 391 | boom = Class.new(RuntimeError) 392 | 393 | fiber1 = fiber2 = nil 394 | 395 | fiber1 = Fiber.new do 396 | begin 397 | selector.io_wait(Fiber.current, local, IO::WRITABLE) 398 | rescue boom 399 | error1 = true 400 | # Transfer back to the signaling fiber to simulate doing something similar to raising an exception in an asynchronous task or thread. 401 | fiber2.transfer 402 | end 403 | writable1 = true 404 | end 405 | 406 | fiber2 = Fiber.new do 407 | selector.io_wait(Fiber.current, local, IO::WRITABLE) 408 | # Don't do anything if the other fiber was resumed before we were by the selector. 409 | unless writable1 410 | raised1 = true 411 | fiber1.raise(boom) # Will return here. 412 | end 413 | writable2 = true 414 | end 415 | 416 | fiber1.transfer unless swapped 417 | fiber2.transfer 418 | fiber1.transfer if swapped 419 | selector.select(0) 420 | 421 | # If fiber2 did manage to be resumed by the selector before fiber1, it should have raised an exception in fiber1, and fiber1 should not have been resumed by the selector since its #io_wait call should have been cancelled. 422 | expect(error1).to be == raised1 423 | expect(writable1).to be == !raised1 424 | expect(writable2).to be == true 425 | end 426 | end 427 | end 428 | 429 | with "#io_read" do 430 | let(:message) {"Hello World"} 431 | let(:events) {Array.new} 432 | let(:sockets) {UNIXSocket.pair} 433 | let(:local) {sockets.first} 434 | let(:remote) {sockets.last} 435 | 436 | let(:buffer) {IO::Buffer.new(1024, IO::Buffer::MAPPED)} 437 | 438 | it "can read a single message" do 439 | return unless selector.respond_to?(:io_read) 440 | 441 | fiber = Fiber.new do 442 | events << :io_read 443 | offset = selector.io_read(Fiber.current, local, buffer, message.bytesize) 444 | expect(buffer.get_string(0, offset)).to be == message 445 | end 446 | 447 | fiber.transfer 448 | 449 | events << :write 450 | remote.write(message) 451 | 452 | selector.select(1) 453 | 454 | expect(events).to be == [ 455 | :io_read, :write 456 | ] 457 | end 458 | 459 | it "can handle partial reads" do 460 | return unless selector.respond_to?(:io_read) 461 | 462 | fiber = Fiber.new do 463 | events << :io_read 464 | offset = selector.io_read(Fiber.current, local, buffer, message.bytesize) 465 | expect(buffer.get_string(0, offset)).to be == message 466 | end 467 | 468 | fiber.transfer 469 | 470 | events << :write 471 | remote.write(message[0...5]) 472 | selector.select(1) 473 | remote.write(message[5...message.bytesize]) 474 | selector.select(1) 475 | 476 | expect(events).to be == [ 477 | :io_read, :write 478 | ] 479 | end 480 | 481 | it "can stop reading when reads are ready" do 482 | # This could trigger a busy-loop in the KQueue selector. 483 | return unless selector.respond_to?(:io_read) 484 | 485 | fiber = Fiber.new do 486 | offset = selector.io_read(Fiber.current, local, buffer, message.bytesize) 487 | expect(buffer.get_string(0, offset)).to be == message 488 | sleep(0.001) 489 | end 490 | 491 | fiber.transfer 492 | 493 | remote.write(message) 494 | 495 | expect(selector.select(0)).to be == 1 496 | 497 | remote.write(message) 498 | 499 | result = nil 500 | 3.times do 501 | result = selector.select(0) 502 | break if result == 0 503 | end 504 | 505 | expect(result).to be == 0 506 | end 507 | end 508 | 509 | with "#io_write" do 510 | let(:message) {"Hello World"} 511 | let(:events) {Array.new} 512 | let(:sockets) {UNIXSocket.pair} 513 | let(:local) {sockets.first} 514 | let(:remote) {sockets.last} 515 | 516 | it "can write a single message" do 517 | skip_if_ruby_platform(/mswin|mingw|cygwin/) 518 | 519 | return unless selector.respond_to?(:io_write) 520 | 521 | fiber = Fiber.new do 522 | events << :io_write 523 | buffer = IO::Buffer.for(message.dup) 524 | result = selector.io_write(Fiber.current, local, buffer, buffer.size) 525 | expect(result).to be == message.bytesize 526 | local.close 527 | end 528 | 529 | fiber.transfer 530 | 531 | selector.select(0) 532 | 533 | events << :read 534 | expect(remote.read).to be == message 535 | 536 | expect(events).to be == [ 537 | :io_write, :read 538 | ] 539 | end 540 | end 541 | 542 | with "#process_wait" do 543 | it "can wait for a process which has terminated already" do 544 | result = nil 545 | events = [] 546 | 547 | fiber = Fiber.new do 548 | pid = Process.spawn("true") 549 | result = selector.process_wait(Fiber.current, pid, 0) 550 | expect(result).to be(:success?) 551 | events << :process_finished 552 | end 553 | 554 | fiber.transfer 555 | 556 | while fiber.alive? 557 | selector.select(1) 558 | end 559 | 560 | expect(events).to be == [:process_finished] 561 | expect(result.success?).to be == true 562 | end 563 | 564 | it "can wait for a process to terminate" do 565 | result = nil 566 | events = [] 567 | 568 | fiber = Fiber.new do 569 | pid = Process.spawn("sleep 0.001") 570 | result = selector.process_wait(Fiber.current, pid, 0) 571 | expect(result).to be(:success?) 572 | events << :process_finished 573 | end 574 | 575 | fiber.transfer 576 | 577 | while fiber.alive? 578 | selector.select(0) 579 | end 580 | 581 | expect(events).to be == [:process_finished] 582 | expect(result).to be(:success?) 583 | end 584 | 585 | it "can wait for two processes sequentially" do 586 | result1 = result2 = nil 587 | events = [] 588 | 589 | fiber = Fiber.new do 590 | pid1 = Process.spawn("sleep 0") 591 | pid2 = Process.spawn("sleep 0") 592 | 593 | result1 = selector.process_wait(Fiber.current, pid1, 0) 594 | events << :process_finished1 595 | 596 | result2 = selector.process_wait(Fiber.current, pid2, 0) 597 | events << :process_finished2 598 | end 599 | 600 | fiber.transfer 601 | 602 | while fiber.alive? 603 | selector.select(0) 604 | end 605 | 606 | expect(events).to be == [:process_finished1, :process_finished2] 607 | expect(result1).to be(:success?) 608 | expect(result2).to be(:success?) 609 | end 610 | end 611 | 612 | with "#resume" do 613 | it "can resume a fiber" do 614 | other_fiber_count = 0 615 | 616 | 5.times do 617 | fiber = Fiber.new do 618 | other_fiber_count += 1 619 | end 620 | 621 | selector.resume(fiber) 622 | end 623 | 624 | expect(other_fiber_count).to be == 5 625 | end 626 | end 627 | end 628 | 629 | describe IO::Event::Selector do 630 | with ".default" do 631 | it "can get the default selector" do 632 | expect(subject.default).to be_a(Module) 633 | end 634 | 635 | it "returns the default if an invalid name is provided" do 636 | env = {"IO_EVENT_SELECTOR" => "invalid"} 637 | expect{subject.default(env)}.to raise_exception(NameError) 638 | end 639 | end 640 | end 641 | 642 | IO::Event::Selector.constants.each do |name| 643 | klass = IO::Event::Selector.const_get(name) 644 | 645 | describe(klass, unique: name) do 646 | with ".default" do 647 | it "can get the specified selector" do 648 | env = {"IO_EVENT_SELECTOR" => name} 649 | expect(IO::Event::Selector.default(env)).to be == klass 650 | end 651 | end 652 | 653 | with ".new" do 654 | let(:count) {8} 655 | let(:loop) {Fiber.current} 656 | 657 | it "can create multiple selectors" do 658 | selectors = count.times.map do |i| 659 | subject.new(loop) 660 | end 661 | 662 | expect(selectors.size).to be == count 663 | 664 | selectors.each(&:close) 665 | end 666 | end 667 | 668 | with "an instance" do 669 | before do 670 | @loop = Fiber.current 671 | @selector = subject.new(@loop) 672 | end 673 | 674 | after do 675 | @selector&.close 676 | end 677 | 678 | attr :loop 679 | attr :selector 680 | 681 | it_behaves_like Selector 682 | end 683 | end 684 | end 685 | 686 | describe IO::Event::Debug::Selector do 687 | before do 688 | @loop = Fiber.current 689 | @selector = subject.new(IO::Event::Selector.new(loop)) 690 | end 691 | 692 | after do 693 | @selector&.close 694 | end 695 | 696 | attr :loop 697 | attr :selector 698 | 699 | it_behaves_like Selector 700 | end 701 | -------------------------------------------------------------------------------- /test/io/event/selector/buffered_io.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2024, by Samuel Williams. 5 | # Copyright, 2023, by Math Ieu. 6 | 7 | require "io/event" 8 | require "io/event/selector" 9 | require "socket" 10 | 11 | require "unix_socket" 12 | 13 | BufferedIO = Sus::Shared("buffered io") do 14 | with "a pipe" do 15 | let(:pipe) {IO.pipe} 16 | let(:input) {pipe.first} 17 | let(:output) {pipe.last} 18 | 19 | it "can read using a buffer" do 20 | skip_if_ruby_platform(/mswin|mingw|cygwin/) 21 | 22 | writer = Fiber.new do 23 | buffer = IO::Buffer.new(128) 24 | expect(selector.io_write(Fiber.current, output, buffer, 128)).to be == 128 25 | end 26 | 27 | reader = Fiber.new do 28 | buffer = IO::Buffer.new(64) 29 | expect(selector.io_read(Fiber.current, input, buffer, 1)).to be == 64 30 | end 31 | 32 | reader.transfer 33 | writer.transfer 34 | 35 | expect(selector.select(1)).to be >= 1 36 | end 37 | 38 | it "can write zero length buffers" do 39 | skip_if_ruby_platform(/mswin|mingw|cygwin/) 40 | 41 | buffer = IO::Buffer.new(1).slice(0, 0) 42 | expect(selector.io_write(Fiber.current, output, buffer, 0)).to be == 0 43 | end 44 | 45 | it "can read and write at the specified offset" do 46 | skip_if_ruby_platform(/mswin|mingw|cygwin/) 47 | 48 | writer = Fiber.new do 49 | buffer = IO::Buffer.new(128) 50 | # We can't write 128 bytes because there are only +64 bytes from offset 64. 51 | expect(selector.io_write(Fiber.current, output, buffer, 128, 64)).to be == 64 52 | end 53 | 54 | reader = Fiber.new do 55 | buffer = IO::Buffer.new(128) 56 | # Only 64 bytes are available to read. 57 | expect(selector.io_read(Fiber.current, input, buffer, 1, 64)).to be == 64 58 | end 59 | 60 | reader.transfer 61 | writer.transfer 62 | 63 | expect(selector.select(1)).to be >= 1 64 | end 65 | 66 | it "can't write to the read end of a pipe" do 67 | skip_if_ruby_platform(/mswin|mingw|cygwin/) 68 | 69 | output.close 70 | 71 | writer = Fiber.new do 72 | buffer = IO::Buffer.new(64) 73 | result = selector.io_write(Fiber.current, input, buffer, 64) 74 | expect(result).to be < 0 75 | end 76 | 77 | writer.transfer 78 | selector.select(0) 79 | end 80 | end 81 | end 82 | 83 | IO::Event::Selector.constants.each do |name| 84 | klass = IO::Event::Selector.const_get(name) 85 | 86 | # Don't run the test if the selector doesn't support `io_read`/`io_write`: 87 | next unless klass.instance_methods.include?(:io_read) 88 | 89 | describe(klass, unique: name) do 90 | before do 91 | @loop = Fiber.current 92 | @selector = subject.new(@loop) 93 | end 94 | 95 | after do 96 | @selector&.close 97 | end 98 | 99 | attr :loop 100 | attr :selector 101 | 102 | it_behaves_like BufferedIO 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /test/io/event/selector/cancellable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2023-2024, by Samuel Williams. 5 | 6 | require "io/event" 7 | require "io/event/selector" 8 | require "socket" 9 | 10 | require "unix_socket" 11 | 12 | Cancellable = Sus::Shared("cancellable") do 13 | with "a pipe" do 14 | let(:pipe) {IO.pipe} 15 | let(:input) {pipe.first} 16 | let(:output) {pipe.last} 17 | 18 | after do 19 | input.close 20 | output.close 21 | end 22 | 23 | it "can cancel reads" do 24 | reader = Fiber.new do 25 | buffer = IO::Buffer.new(64) 26 | 27 | 10.times do 28 | expect{selector.io_read(Fiber.current, input, buffer, 1)}.to raise_exception(Interrupt) 29 | end 30 | end 31 | 32 | # Enter the `io_read` operation: 33 | reader.transfer 34 | 35 | while reader.alive? 36 | reader.raise(Interrupt) 37 | selector.select(0) 38 | end 39 | end 40 | 41 | it "can cancel waits" do 42 | reader = Fiber.new do 43 | buffer = IO::Buffer.new(64) 44 | 45 | 10.times do 46 | expect{selector.io_wait(Fiber.current, input, IO::READABLE)}.to raise_exception(Interrupt) 47 | selector.io_read(Fiber.current, input, buffer, 1) 48 | end 49 | end 50 | 51 | # Enter the `io_read` operation: 52 | reader.transfer 53 | 54 | while reader.alive? 55 | reader.raise(Interrupt) 56 | output.write(".") 57 | selector.select(0.1) 58 | end 59 | end 60 | end 61 | end 62 | 63 | IO::Event::Selector.constants.each do |name| 64 | klass = IO::Event::Selector.const_get(name) 65 | 66 | # Don't run the test if the selector doesn't support `io_read`/`io_write`: 67 | next unless klass.instance_methods.include?(:io_read) 68 | 69 | describe(klass, unique: name) do 70 | before do 71 | @loop = Fiber.current 72 | @selector = subject.new(@loop) 73 | end 74 | 75 | after do 76 | @selector&.close 77 | end 78 | 79 | attr :loop 80 | attr :selector 81 | 82 | it_behaves_like Cancellable 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/io/event/selector/fifo_io.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024, by Samuel Williams. 5 | 6 | require "io/event" 7 | require "io/event/selector" 8 | require "fileutils" 9 | require "tmpdir" 10 | 11 | FifoIO = Sus::Shared("fifo io") do 12 | with "a fifo" do 13 | def around(&block) 14 | @root = Dir.mktmpdir 15 | super 16 | ensure 17 | FileUtils.rm_rf(@root) if @root 18 | end 19 | 20 | let(:path) {File.join(@root, "fifo")} 21 | 22 | it "can read and write" do 23 | skip_if_ruby_platform(/mswin|mingw|cygwin/) 24 | 25 | File.mkfifo(path) 26 | 27 | output = File.open(path, "w+") 28 | input = File.open(path, "r") 29 | 30 | buffer = IO::Buffer.new(128) 31 | 32 | reader = Fiber.new do 33 | @selector.io_wait(Fiber.current, input, IO::READABLE) 34 | result = buffer.read(input, 0) 35 | buffer.resize(result) 36 | end 37 | 38 | writer = Fiber.new do 39 | output.puts("Hello World\n") 40 | output.close 41 | end 42 | 43 | reader.transfer 44 | writer.transfer 45 | 46 | 2.times do 47 | @selector.select(0) 48 | end 49 | 50 | expect(buffer.get_string).to be == "Hello World\n" 51 | end 52 | end 53 | end 54 | 55 | IO::Event::Selector.constants.each do |name| 56 | klass = IO::Event::Selector.const_get(name) 57 | 58 | describe(klass, unique: name) do 59 | def before 60 | @loop = Fiber.current 61 | @selector = subject.new(@loop) 62 | end 63 | 64 | def after(error = nil) 65 | @selector&.close 66 | end 67 | 68 | attr :loop 69 | attr :selector 70 | 71 | it_behaves_like FifoIO 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/io/event/selector/file_io.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2024, by Samuel Williams. 5 | 6 | require "io/event" 7 | require "io/event/selector" 8 | require "tempfile" 9 | 10 | FileIO = Sus::Shared("file io") do 11 | with "a file" do 12 | let(:file) {Tempfile.new} 13 | 14 | it "can read using a buffer" do 15 | skip_if_ruby_platform(/mswin|mingw|cygwin/) 16 | 17 | write_result = nil 18 | read_result = nil 19 | 20 | writer = Fiber.new do 21 | buffer = IO::Buffer.new(128) 22 | file.seek(0) 23 | write_result = selector.io_write(Fiber.current, file, buffer, 128) 24 | end 25 | 26 | reader = Fiber.new do 27 | buffer = IO::Buffer.new(64) 28 | file.seek(0) 29 | 30 | # The read will return 0 if the data is not written yet: 31 | read_result = selector.io_read(Fiber.current, file, buffer, 0) 32 | end 33 | 34 | writer.transfer 35 | 36 | while write_result.nil? 37 | selector.select(0) 38 | end 39 | 40 | reader.transfer 41 | 42 | while read_result.nil? 43 | selector.select(0) 44 | end 45 | 46 | expect(write_result).to be == 128 47 | expect(read_result).to be == 64 48 | end 49 | 50 | it "can pread using a buffer" do 51 | skip "io_pread is not implemented" unless selector.respond_to?(:io_pread) 52 | 53 | write_result = nil 54 | read_result = nil 55 | 56 | writer = Fiber.new do 57 | buffer = IO::Buffer.new(128) 58 | write_result = selector.io_pwrite(Fiber.current, file, buffer, 0, 128, 0) 59 | end 60 | 61 | reader = Fiber.new do 62 | buffer = IO::Buffer.new(64) 63 | read_result = selector.io_pread(Fiber.current, file, buffer, 0, 64, 0) 64 | end 65 | 66 | writer.transfer 67 | 68 | while write_result.nil? 69 | selector.select(0) 70 | end 71 | 72 | reader.transfer 73 | 74 | while read_result.nil? 75 | selector.select(0) 76 | end 77 | 78 | expect(write_result).to be == 128 79 | expect(read_result).to be == 64 80 | end 81 | 82 | it "can wait for the file to become writable" do 83 | wait_result = nil 84 | 85 | writer = Fiber.new do 86 | wait_result = selector.io_wait(Fiber.current, file, IO::WRITABLE) 87 | end 88 | 89 | writer.transfer 90 | 91 | selector.select(0) 92 | 93 | expect(wait_result).to be == IO::WRITABLE 94 | end 95 | end 96 | end 97 | 98 | IO::Event::Selector.constants.each do |name| 99 | klass = IO::Event::Selector.const_get(name) 100 | 101 | # Don't run the test if the selector doesn't support `io_read`/`io_write`: 102 | next unless klass.instance_methods.include?(:io_read) 103 | 104 | describe(klass, unique: name) do 105 | before do 106 | @loop = Fiber.current 107 | @selector = subject.new(@loop) 108 | end 109 | 110 | after do 111 | @selector&.close 112 | end 113 | 114 | attr :loop 115 | attr :selector 116 | 117 | it_behaves_like FileIO 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /test/io/event/selector/interruptable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2023-2024, by Samuel Williams. 5 | 6 | require "io/event" 7 | require "io/event/selector" 8 | require "socket" 9 | 10 | Interruptable = Sus::Shared("interruptable") do 11 | it "can interrupt sleeping selector" do 12 | result = nil 13 | 14 | thread = Thread.new do 15 | Thread.current.report_on_exception = false 16 | selector = subject.new(Fiber.current) 17 | 18 | Thread.handle_interrupt(::SignalException => :never) do 19 | result = selector.select(nil) 20 | end 21 | end 22 | 23 | # Wait for thread to enter the selector: 24 | sleep(0.001) until thread.status == "sleep" 25 | 26 | thread.raise(::Interrupt) 27 | 28 | expect{thread.join}.to raise_exception(::Interrupt) 29 | expect(result).to be == 0 30 | end 31 | 32 | with "pipe" do 33 | let(:pipe) {IO.pipe} 34 | let(:input) {pipe.first} 35 | let(:output) {pipe.last} 36 | 37 | it "can interrupt waiting selector" do 38 | thread = Thread.new do 39 | Thread.current.report_on_exception = false 40 | selector = subject.new(Fiber.current) 41 | 42 | Fiber.new do 43 | selector.io_wait(Fiber.current, input, IO::READABLE) 44 | end 45 | 46 | Thread.handle_interrupt(::SignalException => :never) do 47 | selector.select(nil) 48 | end 49 | end 50 | 51 | # Wait for thread to enter the selector: 52 | sleep(0.001) until thread.status == "sleep" 53 | 54 | thread.raise(::Interrupt) 55 | 56 | expect{thread.join}.to raise_exception(::Interrupt) 57 | end 58 | end 59 | end 60 | 61 | IO::Event::Selector.constants.each do |name| 62 | klass = IO::Event::Selector.const_get(name) 63 | 64 | describe(klass, unique: name) do 65 | it_behaves_like Interruptable 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/io/event/selector/nonblock.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2024, by Samuel Williams. 5 | 6 | require "io/event" 7 | require "io/nonblock" 8 | require "io/event/selector" 9 | 10 | describe IO::Event::Selector do 11 | with ".nonblock" do 12 | it "makes non-blocking IO" do 13 | executed = false 14 | 15 | UNIXSocket.pair do |input, output| 16 | input.nonblock = false 17 | output.nonblock = false 18 | 19 | IO::Event::Selector.nonblock(input) do 20 | executed = true 21 | 22 | # This does not work on Windows... 23 | unless RUBY_PLATFORM =~ /mswin|mingw|cygwin/ 24 | expect(input).to be(:nonblock?) 25 | expect(output).not.to be(:nonblock?) 26 | end 27 | end 28 | end 29 | 30 | expect(executed).to be == true 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/io/event/selector/process_io.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2022-2024, by Samuel Williams. 5 | # Copyright, 2023, by Math Ieu. 6 | 7 | require "io/event" 8 | require "io/event/selector" 9 | require "io/event/debug/selector" 10 | 11 | require "socket" 12 | require "fiber" 13 | 14 | ProcessIO = Sus::Shared("process io") do 15 | it "can wait for a process which has terminated already" do 16 | result = nil 17 | 18 | fiber = Fiber.new do 19 | input, output = IO.pipe 20 | 21 | # For some reason, sleep 0.1 here is very unreliable...? 22 | pid = Process.spawn("true", out: output) 23 | output.close 24 | 25 | # Internally, this should generate POLLHUP, which is what we want to test: 26 | expect(selector.io_wait(Fiber.current, input, IO::READABLE)).to be == IO::READABLE 27 | input.close 28 | 29 | _, result = Process.wait2(pid) 30 | end 31 | 32 | fiber.transfer 33 | 34 | # Wait until the result is collected: 35 | until result 36 | selector.select(1) 37 | end 38 | 39 | expect(result.success?).to be == true 40 | end 41 | end 42 | 43 | IO::Event::Selector.constants.each do |name| 44 | klass = IO::Event::Selector.const_get(name) 45 | 46 | describe(klass, unique: name) do 47 | before do 48 | @loop = Fiber.current 49 | @selector = subject.new(@loop) 50 | end 51 | 52 | after do 53 | @selector&.close 54 | end 55 | 56 | attr :loop 57 | attr :selector 58 | 59 | it_behaves_like ProcessIO 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/io/event/selector/queue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2024, by Samuel Williams. 5 | 6 | require "io/event" 7 | require "io/event/selector" 8 | require "socket" 9 | 10 | Queue = Sus::Shared("queue") do 11 | with "#transfer" do 12 | it "can transfer back to event loop" do 13 | sequence = [] 14 | 15 | fiber = Fiber.new do 16 | while true 17 | sequence << :transfer 18 | selector.transfer 19 | end 20 | end 21 | 22 | selector.push(fiber) 23 | sequence << :select 24 | selector.select(0) 25 | sequence << :select 26 | selector.select(0) 27 | 28 | expect(sequence).to be == [:select, :transfer, :select] 29 | end 30 | end 31 | 32 | with "#push" do 33 | it "can push fiber into queue" do 34 | sequence = [] 35 | 36 | fiber = Fiber.new do 37 | sequence << :executed 38 | end 39 | 40 | selector.push(fiber) 41 | selector.select(0) 42 | 43 | expect(sequence).to be == [:executed] 44 | end 45 | 46 | it "can push non-fiber object into queue" do 47 | object = Object.new 48 | 49 | def object.alive? 50 | true 51 | end 52 | 53 | def object.transfer 54 | end 55 | 56 | selector.push(object) 57 | selector.select(0) 58 | end 59 | 60 | it "defers push during push to next iteration" do 61 | sequence = [] 62 | 63 | fiber = Fiber.new do 64 | sequence << :yield 65 | selector.yield 66 | sequence << :resume 67 | end 68 | 69 | selector.push(fiber) 70 | sequence << :select 71 | selector.select(0) 72 | sequence << :select 73 | selector.select(0) 74 | 75 | expect(sequence).to be == [:select, :yield, :select, :resume] 76 | end 77 | 78 | it "can push a fiber into the queue while processing queue" do 79 | sequence = [] 80 | 81 | second = Fiber.new do 82 | sequence << :second 83 | end 84 | 85 | first = Fiber.new do 86 | sequence << :first 87 | selector.push(second) 88 | end 89 | 90 | selector.push(first) 91 | 92 | selector.select(0) 93 | expect(sequence).to be == [:first] 94 | 95 | selector.select(0) 96 | expect(sequence).to be == [:first, :second] 97 | end 98 | end 99 | 100 | with "#raise" do 101 | it "can raise exception on fiber" do 102 | sequence = [] 103 | 104 | fiber = Fiber.new do 105 | begin 106 | selector.yield 107 | rescue 108 | sequence << :rescue 109 | end 110 | end 111 | 112 | selector.push(fiber) 113 | selector.select(0) 114 | 115 | sequence << :raise 116 | selector.raise(fiber, "Boom") 117 | 118 | expect(sequence).to be == [:raise, :rescue] 119 | end 120 | end 121 | 122 | with "#resume" do 123 | it "can resume a fiber for execution from the main fiber" do 124 | sequence = [] 125 | 126 | fiber = Fiber.new do |argument| 127 | sequence << argument 128 | end 129 | 130 | selector.resume(fiber, :resumed) 131 | sequence << :select 132 | selector.select(0) 133 | 134 | expect(sequence).to be == [:resumed, :select] 135 | end 136 | 137 | it "can resume a fiber for execution from a nested fiber" do 138 | sequence = [] 139 | 140 | child = Fiber.new do |argument| 141 | sequence << argument 142 | end 143 | 144 | parent = Fiber.new do |argument| 145 | sequence << argument 146 | selector.resume(child, :child) 147 | sequence << :parent 148 | end 149 | 150 | selector.resume(parent, :resumed) 151 | sequence << :select 152 | selector.select(0) 153 | 154 | expect(sequence).to be == [:resumed, :child, :select, :parent] 155 | end 156 | end 157 | 158 | with "#yield" do 159 | it "can yield to the scheduler and later resume execution" do 160 | sequence = [] 161 | 162 | fiber = Fiber.new do |argument| 163 | sequence << :yield 164 | selector.yield 165 | sequence << :resumed 166 | end 167 | 168 | selector.resume(fiber) 169 | sequence << :select 170 | selector.select(0) 171 | 172 | expect(sequence).to be == [:yield, :select, :resumed] 173 | end 174 | 175 | it "can yield from resumed fiber" do 176 | sequence = [] 177 | 178 | child = Fiber.new do |argument| 179 | sequence << :yield 180 | selector.yield 181 | sequence << :resumed 182 | end 183 | 184 | parent = Fiber.new do 185 | child.resume 186 | end 187 | 188 | selector.resume(parent) 189 | sequence << :select 190 | selector.select(0) 191 | 192 | expect(sequence).to be == [:yield, :select, :resumed] 193 | end 194 | end 195 | end 196 | 197 | IO::Event::Selector.constants.each do |name| 198 | klass = IO::Event::Selector.const_get(name) 199 | 200 | describe(klass, unique: name) do 201 | before do 202 | @loop = Fiber.current 203 | @selector = subject.new(@loop) 204 | end 205 | 206 | after do 207 | @selector&.close 208 | end 209 | 210 | attr :loop 211 | attr :selector 212 | 213 | it_behaves_like Queue 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /test/io/event/timers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2024, by Samuel Williams. 5 | 6 | require "io/event/timers" 7 | 8 | class FloatWrapper 9 | def initialize(value) 10 | @value = value 11 | end 12 | 13 | def to_f 14 | @value 15 | end 16 | end 17 | 18 | describe IO::Event::Timers do 19 | let(:timers) {subject.new} 20 | 21 | it "should register an event" do 22 | fired = false 23 | 24 | callback = proc do |_time| 25 | fired = true 26 | end 27 | 28 | timers.after(0.1, &callback) 29 | 30 | expect(timers.size).to be == 1 31 | 32 | timers.fire(timers.now + 0.15) 33 | 34 | expect(timers.size).to be == 0 35 | 36 | expect(fired).to be == true 37 | end 38 | 39 | it "should register timers in order" do 40 | fired = [] 41 | 42 | offsets = [0.95, 0.1, 0.3, 0.5, 0.4, 0.2, 0.01, 0.9] 43 | 44 | offsets.each do |offset| 45 | timers.after(offset) do 46 | fired << offset 47 | end 48 | end 49 | 50 | timers.fire(timers.now + 0.5) 51 | expect(fired).to be == offsets.sort.first(6) 52 | 53 | timers.fire(timers.now + 1.0) 54 | expect(fired).to be == offsets.sort 55 | end 56 | 57 | it "should fire timers with the time they were fired at" do 58 | fired_at = :not_fired 59 | 60 | timers.after(0.5) do |time| 61 | # The time we actually were fired at: 62 | fired_at = time 63 | end 64 | 65 | now = timers.now + 1.0 66 | timers.fire(now) 67 | 68 | expect(fired_at).to be == now 69 | end 70 | 71 | it "should flush cancelled timers" do 72 | 10.times do 73 | handle = timers.after(0.1) {} 74 | handle.cancel! 75 | end 76 | 77 | expect(timers.size).to be == 0 78 | end 79 | 80 | with "#schedule" do 81 | it "raises an error if given an invalid time" do 82 | expect do 83 | timers.after(Object.new) {} 84 | end.to raise_exception(NoMethodError, message: be =~ /to_f/) 85 | end 86 | 87 | it "converts the offset to a float" do 88 | fired = false 89 | 90 | timers.after(FloatWrapper.new(0.1)) do 91 | fired = true 92 | end 93 | 94 | timers.fire(timers.now + 0.15) 95 | 96 | expect(fired).to be == true 97 | end 98 | end 99 | 100 | with "#wait_interval" do 101 | it "should return nil if no timers are scheduled" do 102 | expect(timers.wait_interval).to be_nil 103 | end 104 | 105 | it "should return nil if all timers are cancelled" do 106 | handle = timers.after(0.1) {} 107 | handle.cancel! 108 | 109 | expect(timers.wait_interval).to be_nil 110 | end 111 | 112 | it "should return the time until the next timer" do 113 | timers.after(0.1) {} 114 | timers.after(0.2) {} 115 | 116 | expect(timers.wait_interval).to be_within(0.01).of(0.1) 117 | end 118 | end 119 | end 120 | --------------------------------------------------------------------------------