pax_global_header00006660000000000000000000000064151644421020014511gustar00rootroot0000000000000052 comment=ccd095308e734305940c6778446646f2ab612d09 socketry-io-event-ccd0953/000077500000000000000000000000001516444210200155145ustar00rootroot00000000000000socketry-io-event-ccd0953/.context/000077500000000000000000000000001516444210200172565ustar00rootroot00000000000000socketry-io-event-ccd0953/.context/agent-context/000077500000000000000000000000001516444210200220365ustar00rootroot00000000000000socketry-io-event-ccd0953/.context/agent-context/index.yaml000066400000000000000000000010371516444210200240320ustar00rootroot00000000000000--- description: Install and manage context files from Ruby gems. version: 0.1.3 metadata: documentation_uri: https://ioquatix.github.io/agent-context/ funding_uri: https://github.com/sponsors/ioquatix/ source_code_uri: https://github.com/ioquatix/agent-context.git files: - path: usage.md title: Usage Guide description: "`agent-context` is a tool that helps you discover and install contextual information from Ruby gems for AI agents. Gems can provide additional documentation, examples, and guidance in a `context/` ..." socketry-io-event-ccd0953/.context/agent-context/usage.md000066400000000000000000000127421516444210200234720ustar00rootroot00000000000000# Usage Guide ## What is agent-context? `agent-context` is a tool that helps you discover and install contextual information from Ruby gems for AI agents. Gems can provide additional documentation, examples, and guidance in a `context/` directory. ## Quick Commands ```bash # See what context is available bake agent:context:list # Install all available context bake agent:context:install # Install context from a specific gem bake agent:context:install --gem async # See what context files a gem provides bake agent:context:list --gem async # View a specific context file bake agent:context:show --gem async --file thread-safety ``` ## Understanding context/ vs .context/ **Important distinction:** - **`context/`** (no dot) = Directory in gems that contains context files to share. - **`.context/`** (with dot) = Directory in your project where context gets installed. ### What happens when you install context? When you run `bake agent:context:install`, the tool: 1. Scans all installed gems for `context/` directories (in the gem's root). 2. Creates a `.context/` directory in your current project. 3. Copies context files organized by gem name. For example: ``` your-project/ ├── .context/ # ← Installed context (with dot) │ ├── async/ # ← From the 'async' gem's context/ directory │ │ ├── thread-safety.md │ │ └── performance.md │ └── rack/ # ← From the 'rack' gem's context/ directory │ └── middleware.md ├── lib/ └── Gemfile ``` Meanwhile, in the gems themselves: ``` async-gem/ ├── context/ # ← Source context (no dot) │ ├── thread-safety.md │ └── performance.md ├── lib/ └── async.gemspec ``` ## Using Context (For Gem Users) ### Why use this? - **Discover hidden documentation** that gems provide. - **Get practical examples** and guidance. - **Understand best practices** from gem authors. - **Access migration guides** and troubleshooting tips. ### Key Points for Users - Run `bake agent:context:install` to copy context to `.context/` (with dot). - The `.context/` directory is where installed context lives in your project. - Don't edit files in `.context/` - they get completely replaced when you reinstall. ## Providing Context (For Gem Authors) ### How to provide context in your gem #### 1. Create a `context/` directory In your gem's root directory, create a `context/` folder (no dot): ``` your-gem/ ├── context/ # ← Source context (no dot) - this is what you create │ ├── getting-started.md │ ├── configuration.md │ └── troubleshooting.md ├── lib/ └── your-gem.gemspec ``` **Important:** This is different from `.context/` (with dot) which is where context gets installed in user projects. #### 2. Add context files Create files with helpful information for users of your gem. Common types include: - **getting-started.md** - Quick start guide for using your gem. - **configuration.md** - Configuration options and examples. - **troubleshooting.md** - Common issues and solutions. - **migration.md** - Migration guides between versions. - **performance.md** - Performance tips and best practices. - **security.md** - Security considerations. **Focus on the agent experience:** These files should help AI agents understand how to use your gem effectively, not document your gem's internal APIs. #### 3. Document your context Add a section to your gem's README: ```markdown ## Context This gem provides additional context files that can be installed using `bake agent:context:install`. Available context files: - `getting-started.md` - Quick start guide. - `configuration.md` - Configuration options. - `troubleshooting.md` - Common issues and solutions. ``` #### 4. File format and content guidelines Context files can be in any format, but `.md` is commonly used for documentation. The content should be: - **Practical** - Include real examples and working code. - **Focused** - One topic per file. - **Clear** - Easy to understand and follow. - **Actionable** - Provide specific guidance and next steps. - **Agent-focused** - Help AI agents understand how to use your gem effectively. ### Key Points for Gem Authors - Create a `context/` directory (no dot) in your gem's root. - Put helpful guides for users of your gem there. - Focus on practical usage, not API documentation. ## Example Context Files For examples of well-structured context files, see the existing files in this directory: - `usage.md` - Shows how to use the tool (this file). - `examples.md` - Demonstrates practical usage scenarios. ## Key Differences from API Documentation Context files are NOT the same as API documentation: - **Context files**: Help agents accomplish tasks ("How do I configure authentication?"). - **API documentation**: Document methods and classes ("Method `authenticate` returns Boolean"). Context files should answer questions like: - "How do I get started?". - "How do I configure this for production?". - "What do I do when X goes wrong?". - "How do I migrate from version Y to Z?". ## Testing Your Context Before publishing, test your context files: 1. Have an AI agent try to follow your getting-started guide. 2. Check that all code examples actually work. 3. Ensure the files are focused and don't try to cover too much. 4. Verify that they complement rather than duplicate your main documentation. ## Summary - **`context/`** = source (in gems). - **`.context/`** = destination (in your project). socketry-io-event-ccd0953/.context/decode/000077500000000000000000000000001516444210200205015ustar00rootroot00000000000000socketry-io-event-ccd0953/.context/decode/coverage.md000066400000000000000000000164611516444210200226260ustar00rootroot00000000000000# Documentation Coverage This guide explains how to test and monitor documentation coverage in your Ruby projects using the Decode gem's built-in bake tasks. ## Available Bake Tasks The Decode gem provides several bake tasks for analyzing your codebase: - `bake decode:index:coverage` - Check documentation coverage. - `bake decode:index:symbols` - List all symbols in the codebase. - `bake decode:index:documentation` - Extract all documentation. ## Checking Documentation Coverage ### Basic Coverage Check ```bash # Check coverage for the lib directory: bake decode:index:coverage lib # Check coverage for a specific directory: bake decode:index:coverage app/models ``` ### Understanding Coverage Output When you run the coverage command, you'll see output like: ``` 15 definitions have documentation, out of 20 public definitions. Missing documentation for: - MyGem::SomeClass#method_name - MyGem::AnotherClass - MyGem::Utility.helper_method ``` The coverage check: - **Counts only public definitions** (public methods, classes, modules). - **Reports the ratio** of documented vs total public definitions. - **Lists missing documentation** by qualified name. - **Fails with an error** if coverage is incomplete. ### What Counts as "Documented" A definition is considered documented if it has: - Any comment preceding it. - Documentation pragmas (like `@parameter`, `@returns`). - A `@namespace` pragma (for organizational modules). ```ruby # Represents a user in the system. class MyClass end # @namespace module OrganizationalModule # Contains helper functionality. end # Process user data and return formatted results. # @parameter name [String] The user's name. # @returns [Boolean] Success status. def process(name) # Validation logic here: return false if name.empty? # Processing logic: true end class UndocumentedClass end ``` ## Analyzing Symbols ### List All Symbols ```bash # See the structure of your codebase bake decode:index:symbols lib ``` This shows the hierarchical structure of your code: ``` [] -> [] ["MyGem"] -> [#] MyGem ["MyGem", "User"] -> [#] MyGem::User ["MyGem", "User", "initialize"] -> [#] MyGem::User#initialize ``` ### Extract Documentation ```bash # Extract all documentation from your codebase bake decode:index:documentation lib ``` This outputs formatted documentation for all documented definitions: ~~~markdown ## `MyGem::User#initialize` Initialize a new user with the given email address. ## `MyGem::User#authenticate` Authenticate the user with a password. Returns true if authentication is successful. ~~~ ## Achieving 100% Coverage ### Strategy for Complete Coverage 1. **Document all public APIs** ```ruby # Represents a user management system. class User # @attribute [String] The user's email address. attr_reader :email # Initialize a new user. # @parameter email [String] The user's email address. def initialize(email) # Store the email address: @email = email end end ``` 2. **Use @namespace for organizational modules** ```ruby # @namespace module MyGem # Contains the main functionality. end ``` 3. **Document edge cases** ```ruby # @private def internal_helper # Internal implementation details. end ``` ### Common Coverage Issues **Issue: Missing namespace documentation** ```ruby # This module has no documentation and will show as missing coverage: module MyGem end # Solution: Add @namespace pragma: # @namespace module MyGem # Provides core functionality. end ``` **Issue: Undocumented methods** Problem: Methods without any comments will show as missing coverage: ```ruby def process_data # Implementation here end ``` Solution: Add description and pragmas: ```ruby # Process the input data and return results. # @parameter data [Hash] Input data to process. # @returns [Array] Processed results. def process_data(data) # Process the input: results = data.map { |item| transform(item) } # Return processed results: results end ``` **Issue: Missing attr documentation** Problem: Attributes without documentation will show as missing coverage: ```ruby attr_reader :name ``` Solution: Document with @attribute pragma: ```ruby # @attribute [String] The user's full name. attr_reader :name ``` ## Integrating into CI/CD ### GitHub Actions Example ```yaml name: Documentation Coverage on: [push, pull_request] jobs: docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: bundler-cache: true - name: Check documentation coverage run: bake decode:index:coverage lib ``` ### Rake Task Integration Add to your `Rakefile`: ```ruby require 'decode' desc "Check documentation coverage" task :doc_coverage do system("bake decode:index:coverage lib") || exit(1) end task default: [:test, :doc_coverage] ``` ## Monitoring Coverage Over Time ### Generate Coverage Reports ```ruby # Generate a coverage percentage for the specified directory. # @parameter root [String] The root directory to analyze. # @returns [Float] The coverage percentage. def coverage_percentage(root) index = Decode::Index.new index.update(Dir.glob(File.join(root, "**/*.rb"))) documented = 0 total = 0 index.trie.traverse do |path, node, descend| node.values&.each do |definition| if definition.public? total += 1 documented += 1 if definition.comments&.any? end end descend.call if node.values.nil? end (documented.to_f / total * 100).round(2) end puts "Coverage: #{coverage_percentage('lib')}%" ``` ### Exclude Patterns If you need to exclude certain files from coverage: ```ruby # Custom coverage script with exclusions. paths = Dir.glob("lib/**/*.rb").reject do |path| # Exclude vendor files and test files: path.include?('vendor/') || path.end_with?('_test.rb') end index = Decode::Index.new index.update(paths) # ... continue with coverage analysis ``` ## Best Practices 1. **Run coverage checks regularly** - Include in your CI pipeline 2. **Set coverage targets** - Aim for 100% coverage of public APIs 3. **Document incrementally** - Add documentation as you write code 4. **Use meaningful descriptions** - Don't just add empty comments 5. **Leverage @namespace** - For modules that only serve as containers 6. **Review coverage reports** - Use the missing documentation list to prioritize ## Troubleshooting ### Common Error Messages **"Insufficient documentation!"** - Some public definitions are missing documentation - Check the list of missing items and add appropriate comments **No output from coverage command** - Verify the path exists: `bake decode:index:coverage lib` - Check that Ruby files exist in the specified directory **Coverage shows 0/0 definitions** - The directory might not contain any Ruby files - Try a different path or check your file extensions ### Debug Coverage Issues ```bash # First, see what symbols are being detected bake decode:index:symbols lib # Then check what documentation exists bake decode:index:documentation lib # Finally, run coverage to see what's missing bake decode:index:coverage lib ``` This workflow helps you understand what the tool is detecting and why certain items might be missing documentation. socketry-io-event-ccd0953/.context/decode/getting-started.md000066400000000000000000000151041516444210200241310ustar00rootroot00000000000000# Getting Started with Decode The Decode gem provides programmatic access to Ruby code structure and metadata. It can parse Ruby files and extract definitions, comments, and documentation pragmas, enabling code analysis, documentation generation, and other programmatic manipulations of Ruby codebases. ## Installation Add to your Gemfile: ```ruby gem 'decode' ``` Or install directly: ```bash bundle add decode ``` ## Basic Usage ### Analyzing a Ruby File ```ruby require 'decode' # Create a source object: source = Decode::Source.new('lib/my_class.rb', Decode::Language::Ruby.new) # Extract definitions (classes, methods, etc.): definitions = source.definitions.to_a definitions.each do |definition| puts "#{definition.name}: #{definition.short_form}" end ``` ### Extracting Documentation ```ruby # Get segments (blocks of comments + code): segments = source.segments.to_a segments.each do |segment| puts "Comments: #{segment.comments.join(' ')}" puts "Code: #{segment.code}" end ``` ### Checking Documentation Coverage ```ruby # Check which definitions have documentation: definitions.each do |definition| status = definition.comments.any? ? 'documented' : 'missing docs' puts "#{definition.name}: #{status}" end ``` ## Working with Documentation Pragmas The Decode gem understands structured documentation pragmas: ```ruby # This will be parsed and structured: source_code = <<~RUBY # Represents a user in the system. class User # @attribute [String] The user's email address. attr_reader :email # Initialize a new user. # @parameter email [String] The user's email address. # @parameter options [Hash] Additional options. # @option :active [Boolean] Whether the account is active. # @raises [ArgumentError] If email is invalid. def initialize(email, options = {}) # Validate the email format: raise ArgumentError, "Invalid email" if email.empty? # Set instance variables: @email = email @active = options.fetch(:active, true) end end RUBY # Parse and analyze: result = Decode::Language::Ruby.new.parser.parse_source(source_code) definitions = Decode::Language::Ruby.new.parser.definitions_for(source_code).to_a definitions.each do |definition| puts "#{definition.name}: #{definition.comments.join(' ')}" end ``` ## Common Use Cases ### 1. Code Analysis and Metrics ```ruby # Analyze a codebase and return metrics. # @parameter file_path [String] Path to the Ruby file to analyze. def analyze_codebase(file_path) source = Decode::Source.new(file_path, Decode::Language::Ruby.new) definitions = source.definitions.to_a # Count different definition types: classes = definitions.count { |d| d.is_a?(Decode::Language::Ruby::Class) } methods = definitions.count { |d| d.is_a?(Decode::Language::Ruby::Method) } modules = definitions.count { |d| d.is_a?(Decode::Language::Ruby::Module) } puts "Classes: #{classes}, Methods: #{methods}, Modules: #{modules}" end ``` ### 2. Documentation Coverage Reports ```ruby # Calculate documentation coverage for a file. # @parameter file_path [String] Path to the Ruby file to analyze. def documentation_coverage(file_path) source = Decode::Source.new(file_path, Decode::Language::Ruby.new) definitions = source.definitions.to_a # Calculate coverage statistics: total = definitions.count documented = definitions.count { |d| d.comments.any? } puts "Coverage: #{documented}/#{total} (#{(documented.to_f / total * 100).round(1)}%)" end ``` ### 3. Extracting API Information ```ruby # Extract API information from a Ruby file. # @parameter file_path [String] Path to the Ruby file to analyze. def extract_api_info(file_path) source = Decode::Source.new(file_path, Decode::Language::Ruby.new) definitions = source.definitions.to_a # Get public methods only: public_methods = definitions.select do |definition| definition.is_a?(Decode::Language::Ruby::Method) && definition.visibility == :public end public_methods.each do |method| puts "#{method.long_form}" puts " Comments: #{method.comments.join(' ')}" if method.comments.any? end end ``` ### 4. Code Structure Analysis ```ruby # Analyze the structure of Ruby files in a directory. # @parameter directory [String] Path to the directory to analyze. def analyze_structure(directory) Dir.glob("#{directory}/**/*.rb").each do |file| source = Decode::Source.new(file, Decode::Language::Ruby.new) definitions = source.definitions.to_a # Find nested classes and modules: nested = definitions.select { |d| d.parent } if nested.any? puts "#{file}:" nested.each do |definition| puts " #{definition.qualified_name}" end end end end ``` ### 5. Finding Undocumented Code ```ruby # Find undocumented code in a directory. # @parameter directory [String] Path to the directory to analyze. def find_undocumented(directory) Dir.glob("#{directory}/**/*.rb").each do |file| source = Decode::Source.new(file, Decode::Language::Ruby.new) definitions = source.definitions.to_a # Filter for undocumented public definitions: undocumented = definitions.select { |d| d.comments.empty? && d.visibility == :public } if undocumented.any? puts "#{file}:" undocumented.each do |definition| puts " - #{definition.short_form}" end end end end ``` ## Advanced Features ### Using the Index ```ruby # Create an index for multiple files: index = Decode::Index.new index.update(Dir.glob("lib/**/*.rb")) # Search through the index: index.trie.traverse do |path, node, descend| if node.values node.values.each do |definition| puts "#{path.join('::')} - #{definition.short_form}" end end descend.call end ``` ### Custom Language Support ```ruby # The decode gem is extensible: language = Decode::Language::Generic.new("custom") # You can implement your own parser for other languages: ``` ## Tips for Effective Usage 1. **Use structured pragmas** - They help tools understand your code better. 2. **Leverage programmatic access** - Build tools that analyze and manipulate code. 3. **Use @namespace** - For organizational modules to achieve complete coverage. 4. **Analyze code patterns** - Use decode to understand codebase structure. 5. **Build automation** - Use decode in CI/CD pipelines for code quality checks. ## Next Steps - See [Ruby Documentation](ruby-documentation.md) for complete pragma reference. - Check out [Documentation Coverage](coverage.md) for coverage monitoring. - Use decode to build code analysis tools for your projects. - Integrate decode into your development workflow and CI/CD pipelines. socketry-io-event-ccd0953/.context/decode/index.yaml000066400000000000000000000020131516444210200224700ustar00rootroot00000000000000--- description: Code analysis for documentation generation. version: 0.23.3 metadata: documentation_uri: https://ioquatix.github.io/decode/ funding_uri: https://github.com/sponsors/ioquatix/ source_code_uri: https://github.com/ioquatix/decode.git files: - path: getting-started.md title: Getting Started with Decode description: The Decode gem provides programmatic access to Ruby code structure and metadata. It can parse Ruby files and extract definitions, comments, and documentation pragmas, enabling code analysis, docume... - path: coverage.md title: Documentation Coverage description: This guide explains how to test and monitor documentation coverage in your Ruby projects using the Decode gem's built-in bake tasks. - path: ruby-documentation.md title: Ruby Documentation description: This guide covers documentation practices and pragmas supported by the Decode gem for documenting Ruby code. These pragmas provide structured documentation that can be parsed and used to generate A... socketry-io-event-ccd0953/.context/decode/ruby-documentation.md000066400000000000000000000252651516444210200246650ustar00rootroot00000000000000# Ruby Documentation This guide covers documentation practices and pragmas supported by the Decode gem for documenting Ruby code. These pragmas provide structured documentation that can be parsed and used to generate API documentation and achieve complete documentation coverage. ## Documentation Guidelines ### Writing Style and Format #### Definition Documentation - **Full sentences**: All documentation for definitions (classes, modules, methods) should be written as complete sentences with proper grammar and punctuation. - **Class documentation**: Documentation for classes should generally start with "Represents a ..." to clearly indicate what the class models or encapsulates. - **Method documentation**: Should clearly describe what the method does, not how it does it. - **Markdown format**: All documentation comments are written in Markdown format, allowing for rich formatting including lists, emphasis, code blocks, and links. #### Inline Code Comments - **Explanatory comments**: Comments within methods that explain specific lines or sections of code should end with a colon `:` to distinguish them from definition documentation. - **Purpose**: These comments explain the reasoning behind specific implementation details. #### Links and Code Formatting - **Curly braces `{}`**: Use curly braces to create links to other methods, classes, or modules. The Decode gem uses `@index.lookup(text)` to resolve these references. - **Absolute references**: `{Decode::Index#lookup}` - Links to a specific method in a specific class - **Relative references**: `{lookup}` - Links to a method in the current scope or class - **Class references**: `{User}` - Links to a class or module - **Backticks**: Use backticks for code formatting of symbols, values, method names, and technical terms that should appear in monospace font. - **Symbols**: `:admin`, `:user`, `:guest` - **Values**: `true`, `false`, `nil` - **Technical terms**: `attr_*`, `catch`/`throw` - **Code expressions**: `**options` #### Examples ```ruby # Represents a user account in the system. class User # @attribute [String] The user's email address. attr_reader :email # Initialize a new user account. # @parameter email [String] The user's email address. # @raises [ArgumentError] If email is invalid. def initialize(email) # Validate email format before assignment: raise ArgumentError, "Invalid email format" unless email.match?(/\A[^@\s]+@[^@\s]+\z/) # Store the normalized email: @email = email.downcase.strip end # Authenticate the user with the provided password. # @parameter password [String] The password to verify. # @returns [Boolean] True if authentication succeeds. def authenticate(password) # Hash the password for comparison: hashed = hash_password(password) # Compare against stored hash: hashed == @password_hash end # Deactivate the user account. # This method sets the user's status to inactive. Use this instead of # the deprecated {disable!} method. The account status can be checked # using `active?` or by examining the `:active` attribute. # @returns [Boolean] Returns `true` if deactivation was successful. def deactivate! @active = false true end end # Represents a collection of users with search capabilities. class UserCollection # Find users matching the given criteria. # @parameter criteria [Hash] Search parameters. # @returns [Array(User)] Matching users. def find(**criteria) # Start with all users: results = @users.dup # Apply each filter criterion: criteria.each do |key, value| results = filter_by(results, key, value) end results end end ``` **Key formatting examples from above:** - `{disable!}` - Creates a link to the `disable!` method (relative reference) - `active?` - Formats the method name in monospace (backticks for code formatting) - `:active` - Formats the symbol in monospace (backticks for code formatting) - `true` - Formats the boolean value in monospace (backticks for code formatting) ### Best Practices 1. **Be Consistent**: Use the same format for similar types of documentation. 2. **Include Types**: Always specify types for parameters, returns, and attributes. 3. **Be Descriptive**: Provide clear, actionable descriptions. 4. **Document Exceptions**: Always document what exceptions might be raised. 5. **Use Examples**: Include usage examples when the behavior isn't obvious. 6. **Keep Updated**: Update documentation when you change the code. 7. **Use @namespace wisely**: Apply to organizational modules to achieve 100% coverage. 8. **Avoid redundancy**: For simple attributes and methods, attach descriptions directly to pragmas rather than repeating obvious information. #### Simple Attributes and Methods For extremely simple attributes and methods where the name clearly indicates the purpose, avoid redundant descriptions. Instead, attach the description directly to the `@attribute` pragma: ```ruby # Good - concise and clear: # @attribute [String] The name of the parameter. attr :name # @attribute [String] The type of the parameter. attr :type # Avoid - redundant descriptions: # The name of the parameter. # @attribute [String] The parameter name. attr :name ``` This approach keeps documentation concise while still providing essential type information. ## Type Signatures Type signatures are used to specify the expected types of parameters, return values, and attributes in Ruby code. They help clarify the intended use of methods and improve code readability. ### Primitive Types - `String`: Represents a sequence of characters. - `Integer`: Represents whole numbers. - `Float`: Represents decimal numbers. - `Boolean`: Represents true or false values. - `Symbol`: Represents a name or identifier. ### Composite Types - `Array(Type)`: Represents an ordered collection of items. - `Hash(KeyType, ValueType)`: Represents a collection of key-value pairs. - `Interface(:method1, :method2)`: Represents a contract that a class must implement, specifying required methods. - `Type | Nil`: Represents an optional type. ## Supported Pragmas ### Type and Return Value Documentation #### `@attribute [Type] Description.` Documents class attributes, instance variables, and `attr_*` declarations. Prefer to have one attribute per line for clarity. ```ruby # Represents a person with basic attributes. class Person # @attribute [String] The person's full name. attr_reader :name # @attribute [Integer] The person's age in years. attr_accessor :age # @attribute [Hash] Configuration settings. attr_writer :config end ``` #### `@parameter name [Type] Description.` Documents method parameters with their types and descriptions. ```ruby # @parameter x [Integer] The x coordinate. # @parameter y [Integer] The y coordinate. # @parameter options [Hash] Optional configuration. def move(x, y, **options) # ... end ``` #### `@option :key [Type] Description.` Documents hash options (keyword arguments). ```ruby # @parameter user [User] The user object. # @option :cached [Boolean] Whether to cache the result. # @option :timeout [Integer] Request timeout in seconds. def fetch_user_data(user, **options) # ... end ``` #### `@returns [Type] Description.` Documents return values. ```ruby # @returns [String] The formatted user name. def full_name "#{first_name} #{last_name}" end # @returns [Array(User)] All active users. def active_users users.select(&:active?) end ``` #### `@raises [Exception] Description.` Documents exceptions that may be raised. ```ruby # @parameter age [Integer] The person's age. # @raises [ArgumentError] If age is negative. # @raises [TypeError] If age is not an integer. def set_age(age) raise ArgumentError, "Age cannot be negative" if age < 0 raise TypeError, "Age must be an integer" unless age.is_a?(Integer) @age = age end ``` #### `@throws [:symbol] Description.` Documents symbols that may be thrown (used with `catch`/`throw`). ```ruby # @throws [:skip] To skip processing this item. # @throws [:retry] To retry the operation. def process_item(item) throw :skip if item.nil? throw :retry if item.invalid? # ... end ``` ### Block Documentation #### `@yields {|param| ...} Description.` Documents block parameters and behavior. ```ruby # @yields {|item| ...} Each item in the collection. # @parameter item [Object] The current item being processed. def each_item(&block) items.each(&block) end # @yields {|user, index| ...} User and their index. # @parameter user [User] The current user. # @parameter index [Integer] The user's position in the list. def each_user_with_index(&block) users.each_with_index(&block) end ``` ### Visibility and Access Control #### `@public` Explicitly marks a method as public (useful for documentation clarity). ```ruby # @public def public_method # This method is part of the public API. end ``` #### `@private` Marks a method as private (for documentation purposes). ```ruby # @private def internal_helper # This method is for internal use only. end ``` ### Behavioral Documentation #### `@deprecated Description.` Marks methods as deprecated with migration guidance. ```ruby # @deprecated Use {new_method} instead. def old_method # Legacy implementation end ``` #### `@asynchronous` Indicates that a method may yield control. ```ruby # @asynchronous def fetch_data # This method may yield control during execution. end ``` ### Namespace Documentation #### `@namespace` Marks a module as serving only as a namespace, achieving 100% documentation coverage without requiring detailed documentation of empty container modules. ```ruby # @namespace module MyGem # This module serves only as a namespace for organizing classes. class ActualImplementation # This class contains the real functionality. end end ``` **Why use `@namespace`?** - Achieves 100% documentation coverage without redundant documentation. - Clearly indicates that a module is purely organizational. - Avoids documenting modules that exist only to group related classes. - Maintains clean, focused documentation on actual functionality. **When to use `@namespace`:** - Root gem modules that only contain other classes/modules. - Organizational modules with no methods or meaningful state. - Modules that exist purely for constant scoping. - Any module where documentation would add no value. **Note:** This pragma is treated as a form of documentation by the Decode gem, satisfying coverage requirements while keeping the codebase clean. ## Special Pragmas for Code Structure ### `@name custom_name` Overrides the default name extraction for attributes or methods. ```ruby # @name hostname # @attribute [String] The server hostname. attr_reader :server_name ``` ### `@scope Module::Name` Defines scope for definitions that should be associated with a specific module. ```ruby # @scope Database def connect # This method belongs to the Database scope. end ``` socketry-io-event-ccd0953/.context/sus/000077500000000000000000000000001516444210200200705ustar00rootroot00000000000000socketry-io-event-ccd0953/.context/sus/index.yaml000066400000000000000000000016631516444210200220710ustar00rootroot00000000000000--- description: A fast and scalable test runner. version: 0.33.1 metadata: documentation_uri: https://socketry.github.io/sus/ funding_uri: https://github.com/sponsors/ioquatix/ source_code_uri: https://github.com/socketry/sus.git files: - path: usage.md title: Using Sus Testing Framework description: Sus is a modern Ruby testing framework that provides a clean, BDD-style syntax for writing tests. It's designed to be fast, simple, and expressive. - path: mocking.md title: Mocking description: 'There are two types of mocking in sus: `receive` and `mock`. The `receive` matcher is a subset of full mocking and is used to set expectations on method calls, while `mock` can be used to replace m...' - path: shared.md title: Shared Test Behaviors and Fixtures description: Sus provides shared test contexts which can be used to define common behaviours or tests that can be reused across one or more test files. socketry-io-event-ccd0953/.context/sus/mocking.md000066400000000000000000000056721516444210200220530ustar00rootroot00000000000000# Mocking There are two types of mocking in sus: `receive` and `mock`. The `receive` matcher is a subset of full mocking and is used to set expectations on method calls, while `mock` can be used to replace method implementations or set up more complex behavior. Mocking non-local objects permanently changes the object's ancestors, so it should be used with care. For local objects, you can use `let` to define the object and then mock it. Sus does not support the concept of test doubles, but you can use `receive` and `mock` to achieve similar functionality. ## Method Call Expectations The `receive(:method)` expectation is used to set up an expectation that a method will be called on an object. You can also specify arguments and return values. However, `receive` is not sequenced, meaning it does not enforce the order of method calls. If you need to enforce the order, use `mock` instead. ```ruby describe MyThing do let(:my_thing) {subject.new} it "calls the expected method" do expect(my_thing).to receive(:my_method) expect(my_thing.my_method).to be == 42 end end ``` ### With Arguments ```ruby it "calls the method with arguments" do expect(object).to receive(:method_name).with(arg1, arg2) # or .with_arguments(be == [arg1, arg2]) # or .with_options(be == {option1: value1, option2: value2}) # or .with_block object.method_name(arg1, arg2) end ``` ### Returning Values ```ruby it "returns a value" do expect(object).to receive(:method_name).and_return("expected value") result = object.method_name expect(result).to be == "expected value" end ``` ### Raising Exceptions ```ruby it "raises an exception" do expect(object).to receive(:method_name).and_raise(StandardError, "error message") expect{object.method_name}.to raise_exception(StandardError, message: "error message") end ``` ### Multiple Calls ```ruby it "calls the method multiple times" do expect(object).to receive(:method_name).twice.and_return("result") # or .with_call_count(be == 2) expect(object.method_name).to be == "result" expect(object.method_name).to be == "result" end ``` ## Mock Objects Mock objects are used to replace method implementations or set up complex behavior. They can be used to intercept method calls, modify arguments, and control the flow of execution. They are thread-local, meaning they only affect the current thread, therefore are not suitable for use in tests that have multiple threads. ```ruby describe ApiClient do let(:http_client) {Object.new} let(:client) {ApiClient.new(http_client)} let(:users) {["Alice", "Bob"]} it "makes GET requests" do mock(http_client) do |mock| mock.replace(:get) do |url, headers: {}| expect(url).to be == "/api/users" expect(headers).to be == {"accept" => "application/json"} users.to_json end # or mock.before {|...| ...} # or mock.after {|...| ...} # or mock.wrap(:new) {|original, ...| original.call(...)} end expect(client.fetch_users).to be == users end end ``` socketry-io-event-ccd0953/.context/sus/shared.md000066400000000000000000000101731516444210200216620ustar00rootroot00000000000000# Shared Test Behaviors and Fixtures ## Overview Sus provides shared test contexts which can be used to define common behaviours or tests that can be reused across one or more test files. When you have common test behaviors that you want to apply to multiple test files, add them to the `fixtures/` directory. When you have common test behaviors that you want to apply to multiple implementations of the same interface, within a single test file, you can define them as shared contexts within that file. ## Shared Fixtures ### Directory Structure ``` my-gem/ ├── lib/ │ ├── my_gem.rb │ └── my_gem/ │ └── my_thing.rb ├── fixtures/ │ └── my_gem/ │ └── a_thing.rb # Provides MyGem::AThing shared context └── test/ ├── my_gem.rb └── my_gem/ └── my_thing.rb ``` ### Creating Shared Fixtures Create shared behaviors in the `fixtures/` directory using `Sus::Shared`: ```ruby # fixtures/my_gem/a_user.rb require "sus/shared" module MyGem AUser = Sus::Shared("a user") do |role| let(:user) do { name: "Test User", email: "test@example.com", role: role } end it "has a name" do expect(user[:name]).not.to be_nil end it "has a valid email" do expect(user[:email]).to be(:include?, "@") end it "has a role" do expect(user[:role]).to be_a(String) end end end ``` ### Using Shared Fixtures Require and use shared fixtures in your test files: ```ruby # test/my_gem/user_manager.rb require 'my_gem/a_user' describe MyGem::UserManager do it_behaves_like MyGem::AUser, "manager" # or include_context MyGem::AUser, "manager" end ``` ### Multiple Shared Fixtures You can create multiple shared fixtures for different scenarios: ```ruby # fixtures/my_gem/users.rb module MyGem module Users AStandardUser = Sus::Shared("a standard user") do let(:user) do { name: "John Doe", role: "user", active: true } end it "is active" do expect(user[:active]).to be_truthy end end AnAdminUser = Sus::Shared("an admin user") do let(:user) do { name: "Admin User", role: "admin", active: true } end it "has admin role" do expect(user[:role]).to be == "admin" end end end end ``` Use specific shared fixtures: ```ruby # test/my_gem/authorization.rb require 'my_gem/users' describe MyGem::Authorization do with "standard user" do # If there are no arguments, you can use `include` directly: include MyGem::Users::AStandardUser it "denies admin access" do auth = subject.new expect(auth.can_admin?(user)).to be_falsey end end with "admin user" do include MyGem::Users::AnAdminUser it "allows admin access" do auth = subject.new expect(auth.can_admin?(user)).to be_truthy end end end ``` ### Modules You can also define shared behaviors in modules and include them in your test files: ```ruby # fixtures/my_gem/shared_behaviors.rb module MyGem module SharedBehaviors def self.included(base) base.it "uses shared data" do expect(shared_data).to be == "some shared data" end end def shared_data "some shared data" end end end ``` ### Enumerating Tests Some tests will be run multiple times with different arguments (for example, multiple database adapters). You can use `Sus::Shared` to define these tests and then enumerate them: ```ruby # test/my_gem/database_adapter.rb require "sus/shared" ADatabaseAdapter = Sus::Shared("a database adapter") do |adapter| let(:database) {adapter.new} it "connects to the database" do expect(database.connect).to be_truthy end it "can execute queries" do expect(database.execute("SELECT 1")).to be == [[1]] end end # Enumerate the tests with different adapters MyGem::DatabaseAdapters.each do |adapter| describe "with #{adapter}", unique: adapter.name do it_behaves_like ADatabaseAdapter, adapter end end ``` Note the use of `unique: adapter.name` to ensure each test is uniquely identified, which is useful for reporting and debugging - otherwise the same test line number would be used for all iterations, which can make it hard to identify which specific test failed. socketry-io-event-ccd0953/.context/sus/usage.md000066400000000000000000000201751516444210200215230ustar00rootroot00000000000000# Using Sus Testing Framework ## Overview Sus is a modern Ruby testing framework that provides a clean, BDD-style syntax for writing tests. It's designed to be fast, simple, and expressive. ## Basic Structure Here is an example structure for testing with Sus - the actual structure may vary based on your gem's organization, but aside from the `lib/` directory, sus expects the following structure: ``` my-gem/ ├── config/ │ └── sus.rb # Sus configuration file ├── lib/ │ ├── my_gem.rb │ └── my_gem/ │ └── my_thing.rb ├── fixtures/ │ └── my_gem/ │ └── a_thing.rb # Provides MyGem::AThing shared context └── test/ ├── my_gem.rb # Tests MyGem └── my_gem/ └── my_thing.rb # Tests MyGem::MyThing ``` ### Configuration File Create `config/sus.rb`: ```ruby # frozen_string_literal: true # Use the covered gem for test coverage reporting: require 'covered/sus' include Covered::Sus def before_tests(assertions, output: self.output) # Starts the clock and sets up the test environment: super end def after_tests(assertions, output: self.output) # Stops the clock and prints the test results: super end ``` ### Fixtures Files `fixtures/` gets added to the `$LOAD_PATH` automatically, so you can require files from there without needing to specify the full path. ### Test Files Sus runs all Ruby files in the `test/` directory by default. But you can also create tests in any file, and run them with the `sus my_tests.rb` command. ## Basic Syntax ```ruby # frozen_string_literal: true describe MyThing do let(:my_thing) {subject.new} with "#my_method" do it "does something" do expect(my_thing.my_method).to be == 42 end end end ``` ### `describe` - Test Groups Use `describe` to group related tests: ```ruby describe MyThing do # The subject will be whatever is described: let(:my_thing) {subject.new} end ``` ### `it` - Individual Tests Use `it` to define individual test cases: ```ruby it "returns the expected value" do expect(result).to be == "expected" end ``` You can use `it` blocks at the top level or within `describe` or `with` blocks. ### `with` - Context Blocks Use `with` to create context-specific test groups: ```ruby with "valid input" do let(:input) {"valid input"} it "succeeds" do expect{my_thing.process(input)}.not.to raise_exception end end # Non-lazy state can be provided as keyword arguments: with "invalid input", input: nil do it "raises an error" do expect{my_thing.process(input)}.to raise_exception(ArgumentError) end end ``` When testing methods, use `with` to specify the method being tested: ```ruby with "#my_method" do it "results a value" do expect(my_thing.method).to be == 42 end end with ".my_class_method" do it "returns a value" do expect(MyThing.class_method).to be == "class value" end end ``` ### `let` - Lazy Variables Use `let` to define variables that are evaluated when first accessed: ```ruby let(:helper) {subject.new} let(:test_data) {"test value"} it "uses the helper" do expect(helper.process(test_data)).to be_truthy end ``` ### `before` and `after` - Setup/Teardown Use `before` and `after` for setup and teardown logic: ```ruby before do # Setup logic. end after do # Cleanup logic. end ``` Error handling in `after` allows you to perform cleanup even if the test fails with an exception (not a test failure). ```ruby after do |error = nil| if error # The state of the test is unknown, so you may want to forcefully kill processes or clean up resources. Process.kill(:KILL, @child_pid) else # Normal cleanup logic. Process.kill(:TERM, @child_pid) end Process.waitpid(@child_pid) end ``` ### `around` - Setup/Teardown Use `around` for setup and teardown logic: ```ruby around do |&block| # Setup logic. super() do # Run the test. block.call end ensure # Cleanup logic. end ``` Invoking `super()` calls any parent `around` block, allowing you to chain setup and teardown logic. ## Assertions ### Basic Assertions ```ruby expect(value).to be == expected exepct(value).to be >= 10 expect(value).to be <= 100 expect(value).to be > 0 expect(value).to be < 1000 expect(value).to be_truthy expect(value).to be_falsey expect(value).to be_nil expect(value).to be_equal(another_value) expect(value).to be_a(Class) ``` ### Strings ```ruby expect(string).to be(:start_with?, "prefix") expect(string).to be(:end_with?, "suffix") expect(string).to be(:match?, /pattern/) expect(string).to be(:include?, "substring") ``` ### Ranges and Tolerance ```ruby expect(value).to be_within(0.1).of(5.0) expect(value).to be_within(5).percent_of(100) ``` ### Method Calls To call methods on the expected object: ```ruby expect(array).to be(:include?, "value") expect(string).to be(:start_with?, "prefix") expect(object).to be(:respond_to?, :method_name) ``` ### Collection Assertions ```ruby expect(array).to have_attributes(length: be == 1) expect(array).to have_value(be > 1) expect(hash).to have_keys(:key1, "key2") expect(hash).to have_keys(key1: be == 1, "key2" => be == 2) ``` ### Attribute Testing ```ruby expect(user).to have_attributes( name: be == "John", age: be >= 18, email: be(:include?, "@") ) ``` ### Exception Assertions ```ruby expect do risky_operation end.to raise_exception(RuntimeError, message: be =~ /expected error message/) ``` ## Combining Predicates Predicates can be nested. ```ruby expect(user).to have_attributes( name: have_attributes( first: be == "John", last: be == "Doe" ), comments: have_value(be =~ /test comment/), created_at: be_within(1.minute).of(Time.now) ) ``` ### Logical Combinations ```ruby expect(value).to (be > 10).and(be < 20) expect(value).to be_a(String).or(be_a(Symbol), be_a(Integer)) ``` ### Custom Predicates You can create custom predicates for more complex assertions: ```ruby def be_small_prime (be == 2).or(be == 3, be == 5, be == 7) end ``` ## Block Expectations ### Testing Blocks ```ruby expect{operation}.to raise_exception(Error) expect{operation}.to have_duration(be < 1.0) ``` ### Performance Testing You should generally avoid testing performance in unit tests, as it will be highly unstable and dependent on the environment. However, if you need to test performance, you can use: ```ruby expect{slow_operation}.to have_duration(be < 2.0) expect{fast_operation}.to have_duration(be < 0.1) ``` - For less unsable performance tests, you can use the `sus-fixtures-time` gem which tries to compensate for the environment by measuring execution time. - For benchmarking, you can use the `sus-fixtures-benchmark` gem which measures a block of code multiple times and reports the execution time. ## File Operations ### Temporary Directories Use `Dir.mktmpdir` for isolated test environments: ```ruby around do |block| Dir.mktmpdir do |root| @root = root block.call end end let(:test_path) {File.join(@root, "test.txt")} it "can create a file" do File.write(test_path, "content") expect(File).to be(:exist?, test_path) end ``` ## Test Output In general, tests should not produce output unless there is an error or failure. ### Informational Output You can use `inform` to print informational messages during tests: ```ruby it "logs an informational message" do rate = copy_data(source, destination) inform "Copied data at #{rate}MB/s" expect(rate).to be > 0 end ``` This can be useful for debugging or providing context during test runs. ### Console Output The `sus-fixtures-console` gem provides a way to surpress and capture console output during tests. If you are using code which generates console output, you can use this gem to capture it and assert on it. ## Running Tests ```bash # Run all tests bundle exec sus # Run specific test file bundle exec sus test/specific_test.rb ``` ## Best Practices 1. **Use real objects** instead of mocks when possible. 2. **Dependency injection** for testability. 3. **Isolatae mutable state** using temporary directories. 4. **Clear test descriptions** that explain the behavior. 5. **Group tests** with `describe` (classes) and `with` for better organization. 6. **Keep tests simple** and focused on one behavior. socketry-io-event-ccd0953/.contributors.yaml000066400000000000000000000027021516444210200212140ustar00rootroot00000000000000- path: lib/io/event/priority_heap.rb time: 2021-02-12T12:19:44+01:00 author: name: Wander Hillen email: wjw.hillen@gmail.com - path: lib/io/event/priority_heap.rb time: 2021-02-12T13:19:56+01:00 author: name: Wander Hillen email: wjw.hillen@gmail.com - path: lib/io/event/priority_heap.rb time: 2021-02-12T13:28:58+01:00 author: name: Wander Hillen email: wjw.hillen@gmail.com - path: lib/io/event/priority_heap.rb time: 2021-02-13T18:44:46+13:00 author: name: Samuel Williams email: samuel.williams@oriontransfer.co.nz - path: lib/io/event/priority_heap.rb time: 2021-02-13T12:40:15+01:00 author: name: Wander Hillen email: wjw.hillen@gmail.com - path: lib/io/event/priority_heap.rb time: 2021-02-13T12:47:51+01:00 author: name: Wander Hillen email: wjw.hillen@gmail.com - path: lib/io/event/priority_heap.rb time: 2022-09-02T13:45:20+12:00 author: name: Samuel Williams email: samuel.williams@oriontransfer.co.nz - path: lib/io/event/priority_heap.rb time: 2022-09-02T13:45:20+12:00 author: name: Samuel Williams email: samuel.williams@oriontransfer.co.nz - path: lib/io/event/priority_heap.rb time: 2022-10-13T11:06:34+13:00 author: name: Samuel Williams email: samuel.williams@oriontransfer.co.nz - path: lib/io/event/priority_heap.rb time: 2023-04-12T17:25:59+12:00 author: name: Samuel Williams email: samuel.williams@oriontransfer.co.nz socketry-io-event-ccd0953/.editorconfig000066400000000000000000000001511516444210200201660ustar00rootroot00000000000000root = true [*] indent_style = tab indent_size = 2 [*.{yml,yaml}] indent_style = space indent_size = 2 socketry-io-event-ccd0953/.github/000077500000000000000000000000001516444210200170545ustar00rootroot00000000000000socketry-io-event-ccd0953/.github/copilot-instructions.md000066400000000000000000000016431516444210200236150ustar00rootroot00000000000000# GitHub Copilot Instructions ## Use Agent Context When working on this project, consult the `agents.md` file for project-specific guidelines, architecture decisions, and development patterns. This file contains curated information that will help you make better decisions aligned with the project's goals and standards. If the file does not exist, you will need to install it, by running the following command: ```bash $ bundle install $ bundle exec bake agent:context:install ``` This command will set up the necessary context files that help you understand the project structure, dependencies, and conventions. ## Ignoring Files The `.gitignore` file is split into two sections, separated by a blank line. The first section is automatically generated, while the second section is user controlled. While working on pull requests, you should not add unrelated changes to the `.gitignore` file as part of the pull request. socketry-io-event-ccd0953/.github/workflows/000077500000000000000000000000001516444210200211115ustar00rootroot00000000000000socketry-io-event-ccd0953/.github/workflows/benchmark.yaml000066400000000000000000000013501516444210200237260ustar00rootroot00000000000000name: Benchmark on: [push, pull_request] jobs: test: runs-on: ${{matrix.os}}-latest strategy: matrix: os: - ubuntu ruby: - head steps: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Build extensions timeout-minutes: 5 run: bundle exec bake build - name: Install packages timeout-minutes: 5 run: | git clone https://github.com/ioquatix/wrk cd wrk make - name: Run benchmarks timeout-minutes: 5 env: WRK: ./wrk/wrk run: bundle exec bake -b benchmark/server/bake.rb socketry-io-event-ccd0953/.github/workflows/documentation-coverage.yaml000066400000000000000000000006511516444210200264410ustar00rootroot00000000000000name: Documentation Coverage on: [push, pull_request] permissions: contents: read env: COVERAGE: PartialSummary jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: ruby-version: ruby bundler-cache: true - name: Validate coverage timeout-minutes: 5 run: bundle exec bake decode:index:coverage lib socketry-io-event-ccd0953/.github/workflows/documentation.yaml000066400000000000000000000021311516444210200246430ustar00rootroot00000000000000name: Documentation on: push: branches: - main # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages: permissions: contents: read pages: write id-token: write # Allow one concurrent deployment: concurrency: group: "pages" cancel-in-progress: true env: BUNDLE_WITH: maintenance jobs: generate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: ruby-version: ruby bundler-cache: true - name: Installing packages run: sudo apt-get install wget - name: Generate documentation timeout-minutes: 5 run: bundle exec bake utopia:project:static --force no - name: Upload documentation artifact uses: actions/upload-pages-artifact@v4 with: path: docs deploy: runs-on: ubuntu-latest environment: name: github-pages url: ${{steps.deployment.outputs.page_url}} needs: generate steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 socketry-io-event-ccd0953/.github/workflows/rubocop.yaml000066400000000000000000000005321516444210200234460ustar00rootroot00000000000000name: RuboCop on: [push, pull_request] permissions: contents: read jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: ruby-version: ruby bundler-cache: true - name: Run RuboCop timeout-minutes: 10 run: bundle exec rubocop socketry-io-event-ccd0953/.github/workflows/test-coverage.yaml000066400000000000000000000022501516444210200245440ustar00rootroot00000000000000name: Test Coverage on: [push, pull_request] permissions: contents: read env: COVERAGE: PartialSummary jobs: test: name: ${{matrix.ruby}} on ${{matrix.os}} runs-on: ${{matrix.os}}-latest strategy: matrix: os: - ubuntu - macos ruby: - "3.3" - "3.4" - "4.0" steps: - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Run tests timeout-minutes: 5 run: bundle exec bake test - uses: actions/upload-artifact@v5 with: include-hidden-files: true if-no-files-found: error name: coverage-${{matrix.os}}-${{matrix.ruby}} path: .covered.db validate: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: ruby-version: ruby bundler-cache: true - uses: actions/download-artifact@v6 - name: Validate coverage timeout-minutes: 5 run: bundle exec bake covered:validate --paths */.covered.db \; socketry-io-event-ccd0953/.github/workflows/test-debug.yaml000066400000000000000000000014151516444210200240410ustar00rootroot00000000000000name: Test Debug on: [push, pull_request] permissions: contents: read env: CONSOLE_OUTPUT: XTerm RUBY_DEBUG: true RUBY_SANITIZE: true ASAN_OPTIONS: halt_on_error=0:use_sigaltstack=0:detect_leaks=0 jobs: test: name: ${{matrix.ruby}} on ${{matrix.os}} runs-on: ${{matrix.os}} strategy: matrix: os: - ubuntu-24.04 ruby: - asan steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Install packages (Ubuntu) if: matrix.os == 'ubuntu' run: sudo apt-get install -y liburing-dev - name: Run tests timeout-minutes: 10 run: bundle exec bake build test socketry-io-event-ccd0953/.github/workflows/test-external.yaml000066400000000000000000000021051516444210200245720ustar00rootroot00000000000000name: Test External on: [push, pull_request] permissions: contents: read env: RUBY_DEBUG: true jobs: test: name: ${{matrix.ruby}} on ${{matrix.os}} runs-on: ${{matrix.os}}-latest strategy: matrix: os: - ubuntu - macos ruby: - "3.3" - "3.4" - "4.0" steps: - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Build extensions timeout-minutes: 10 run: bundle exec bake build - name: Run tests timeout-minutes: 10 run: bundle exec bake test:external - name: Run tests (worker pool) if: ${{matrix.os == 'ubuntu' && matrix.ruby == '4.0'}} env: ASYNC_SCHEDULER_WORKER_POOL: true timeout-minutes: 10 run: bundle exec bake test:external # - name: Run tests (pure Ruby) # env: # IO_EVENT_SELECTOR: Select # timeout-minutes: 10 # run: bundle exec bake build test:externalsocketry-io-event-ccd0953/.github/workflows/test.yaml000066400000000000000000000020451516444210200227550ustar00rootroot00000000000000name: Test on: [push, pull_request] permissions: contents: read jobs: test: name: ${{matrix.ruby}} on ${{matrix.os}} runs-on: ${{matrix.os}}-latest continue-on-error: ${{matrix.experimental}} strategy: matrix: os: - ubuntu - macos - windows ruby: - "3.3" - "3.4" - "4.0" experimental: [false] include: - os: ubuntu ruby: truffleruby experimental: true - os: ubuntu ruby: jruby experimental: true - os: ubuntu ruby: head experimental: true steps: - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Install packages (Ubuntu) if: matrix.os == 'ubuntu' run: sudo apt-get install -y liburing-dev - name: Run tests timeout-minutes: 10 run: bundle exec bake test socketry-io-event-ccd0953/.gitignore000066400000000000000000000003101516444210200174760ustar00rootroot00000000000000/agents.md /.context /.bundle /pkg /gems.locked /.covered.db /external /ext/Makefile /ext/mkmf.log /ext/extconf.h /ext/**/*.o /ext/**/*.so /ext/**/*.bundle /ext/**/*.dSYM /benchmark/server/compiled socketry-io-event-ccd0953/.mailmap000066400000000000000000000002241516444210200171330ustar00rootroot00000000000000Alex Matchneer Math Ieu Shizuo Fujita Jean Boussier socketry-io-event-ccd0953/.rubocop.yml000066400000000000000000000035401516444210200177700ustar00rootroot00000000000000plugins: - rubocop-md - rubocop-socketry AllCops: DisabledByDefault: true # Socketry specific rules: Layout/ConsistentBlankLineIndentation: Enabled: true Layout/BlockDelimiterSpacing: Enabled: true Style/GlobalExceptionVariables: Enabled: true # General Layout rules: Layout/IndentationStyle: Enabled: true EnforcedStyle: tabs Layout/InitialIndentation: Enabled: true Layout/IndentationWidth: Enabled: true Width: 1 Layout/IndentationConsistency: Enabled: true EnforcedStyle: normal Layout/BlockAlignment: Enabled: true Layout/EndAlignment: Enabled: true EnforcedStyleAlignWith: start_of_line Layout/BeginEndAlignment: Enabled: true EnforcedStyleAlignWith: start_of_line Layout/RescueEnsureAlignment: Enabled: true Layout/ElseAlignment: Enabled: true Layout/DefEndAlignment: Enabled: true Layout/CaseIndentation: Enabled: true EnforcedStyle: end Layout/CommentIndentation: Enabled: true Layout/FirstHashElementIndentation: Enabled: true EnforcedStyle: consistent Layout/EmptyLinesAroundClassBody: Enabled: true Layout/EmptyLinesAroundModuleBody: Enabled: true Layout/EmptyLineAfterMagicComment: Enabled: true Layout/SpaceInsideBlockBraces: Enabled: true EnforcedStyle: no_space SpaceBeforeBlockParameters: false Layout/SpaceAroundBlockParameters: Enabled: true EnforcedStyleInsidePipes: no_space Layout/FirstArrayElementIndentation: Enabled: true EnforcedStyle: consistent Layout/ArrayAlignment: Enabled: true EnforcedStyle: with_fixed_indentation Layout/FirstArgumentIndentation: Enabled: true EnforcedStyle: consistent Layout/ArgumentAlignment: Enabled: true EnforcedStyle: with_fixed_indentation Layout/ClosingParenthesisIndentation: Enabled: true Style/FrozenStringLiteralComment: Enabled: true Style/StringLiterals: Enabled: true EnforcedStyle: double_quotes socketry-io-event-ccd0953/agent.md000066400000000000000000000046011516444210200171350ustar00rootroot00000000000000# Agent ## Context This section provides links to documentation from installed packages. It is automatically generated and may be updated by running `bake agent:context:install`. **Important:** Before performing any code, documentation, or analysis tasks, always read and apply the full content of any relevant documentation referenced in the following sections. These context files contain authoritative standards and best practices for documentation, code style, and project-specific workflows. **Do not proceed with any actions until you have read and incorporated the guidance from relevant context files.** ### agent-context Install and manage context files from Ruby gems. #### [Usage Guide](.context/agent-context/usage.md) `agent-context` is a tool that helps you discover and install contextual information from Ruby gems for AI agents. Gems can provide additional documentation, examples, and guidance in a `context/` ... ### decode Code analysis for documentation generation. #### [Getting Started with Decode](.context/decode/getting-started.md) The Decode gem provides programmatic access to Ruby code structure and metadata. It can parse Ruby files and extract definitions, comments, and documentation pragmas, enabling code analysis, docume... #### [Documentation Coverage](.context/decode/coverage.md) This guide explains how to test and monitor documentation coverage in your Ruby projects using the Decode gem's built-in bake tasks. #### [Ruby Documentation](.context/decode/ruby-documentation.md) This guide covers documentation practices and pragmas supported by the Decode gem for documenting Ruby code. These pragmas provide structured documentation that can be parsed and used to generate A... ### sus A fast and scalable test runner. #### [Using Sus Testing Framework](.context/sus/usage.md) Sus is a modern Ruby testing framework that provides a clean, BDD-style syntax for writing tests. It's designed to be fast, simple, and expressive. #### [Mocking](.context/sus/mocking.md) There are two types of mocking in sus: `receive` and `mock`. The `receive` matcher is a subset of full mocking and is used to set expectations on method calls, while `mock` can be used to replace m... #### [Shared Test Behaviors and Fixtures](.context/sus/shared.md) Sus provides shared test contexts which can be used to define common behaviours or tests that can be reused across one or more test files. socketry-io-event-ccd0953/bake.rb000066400000000000000000000015631516444210200167500ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2026, by Samuel Williams. # Copyright, 2024, by Pavel Rosický. def build ext_path = File.expand_path("ext", __dir__) Dir.chdir(ext_path) do system("ruby ./extconf.rb") system("make") end end def clean ext_path = File.expand_path("ext", __dir__) Dir.chdir(ext_path) do system("make clean") end end def before_test self.build end # Update the project documentation with the new version number. # # @parameter version [String] The new version number. def after_gem_release_version_increment(version) context["releases:update"].call(version) context["utopia:project:update"].call end # Create a GitHub release for the given tag. # # @parameter tag [String] The tag to create a release for. def after_gem_release(tag:, **options) context["releases:github:release"].call(tag) end socketry-io-event-ccd0953/benchmark/000077500000000000000000000000001516444210200174465ustar00rootroot00000000000000socketry-io-event-ccd0953/benchmark/instantiate.rb000077500000000000000000000006061516444210200223230ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2024, by Samuel Williams. require "benchmark/ips" require "fiber" require_relative "../lib/event" GC.disable Event::Selector.constants.each do |name| puts "Creating #{name}..." 1000.times.map do |i| puts i selector = Event::Selector.const_get(name).new(Fiber.current) end end socketry-io-event-ccd0953/benchmark/readable.rb000077500000000000000000000014431516444210200215370ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2024, by Samuel Williams. require "benchmark/ips" require "fiber" require "console" require_relative "../lib/event" Event::Selector.constants.each do |name| selector = Event::Selector.const_get(name).new(Fiber.current) fibers = 256.times.map do |index| input, output = IO.pipe output.puts "Hello World" fiber = Fiber.new do while true selector.io_wait(fiber, input, IO::READABLE) end rescue RuntimeError # Ignore. ensure input.close output.close end end # Start initial wait: fibers.each(&:transfer) Console.logger.measure(selector) do i = 10_000 while (i -= 1) > 0 selector.select(0) end end fibers.each{|fiber| fiber.raise("Stop")} end socketry-io-event-ccd0953/benchmark/server/000077500000000000000000000000001516444210200207545ustar00rootroot00000000000000socketry-io-event-ccd0953/benchmark/server/async.rb000077500000000000000000000007771516444210200224340ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2025, by Samuel Williams. require "async" require "socket" RESPONSE = "HTTP/1.1 204 No Content\r\nXonnection: close\r\n\r\n" port = Integer(ARGV.pop || 9090) Async do |task| server = TCPServer.new("localhost", port) loop do peer, address = server.accept task.async do while (peer.recv(1024) rescue nil) sleep 0.02 peer.send(RESPONSE, 0) end ensure peer.close end end end socketry-io-event-ccd0953/benchmark/server/bake.rb000066400000000000000000000021061516444210200222020ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2024, by Samuel Williams. SERVERS = [ "compiled", "event.rb", "buffer.rb", "loop.rb", "async.rb", "thread.rb", "fork.rb", ] def default build benchmark end def build compiler = ENV.fetch("CC", "clang") system(compiler, "compiled.c", "-o", "compiled", chdir: __dir__) end # @parameter connections [Integer] The number of simultaneous connections. # @parameter threads [Integer] The number of client threads to use. # @parameter duration [Integer] The duration of the test. def benchmark(connections: 8, threads: 1, duration: 1) port = 9095 wrk = ENV.fetch("WRK", "wrk") SERVERS.each do |server| $stdout.puts [nil, "Benchmark #{server}..."] pid = Process.spawn(File.expand_path(server, __dir__), port.to_s) puts "Server running pid=#{pid}..." sleep 1 system(wrk, "-d#{duration}", "-t#{threads}", "-c#{connections}", "http://localhost:#{port}") Process.kill(:TERM, pid) _, status = Process.wait2(pid) puts "Server exited status=#{status}..." port += 1 end end socketry-io-event-ccd0953/benchmark/server/buffer.rb000077500000000000000000000012711516444210200225560ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2026, by Samuel Williams. require_relative "scheduler" scheduler = DirectScheduler.new Fiber.set_scheduler(scheduler) port = Integer(ARGV.pop || 9090) RESPONSE_STRING = "HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n" REQUEST = IO::Buffer.new(1024) RESPONSE = IO::Buffer.new(128) RESPONSE_SIZE = RESPONSE.set_string(RESPONSE_STRING) Fiber.schedule do server = TCPServer.new("localhost", port) loop do peer, address = server.accept Fiber.schedule do scheduler.io_read(peer, REQUEST, 1) scheduler.io_write(peer, RESPONSE, RESPONSE_SIZE) peer.close end end end socketry-io-event-ccd0953/benchmark/server/compiled.c000066400000000000000000000036061516444210200227210ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #define BUFFER_SIZE 1024 #define on_error(...) {fprintf(stderr, __VA_ARGS__); fflush(stderr); exit(1);} int main (int argc, char *argv[]) { if (argc < 2) on_error("Usage: %s [port]\n", argv[0]); int port = atoi(argv[1]); int server_fd, client_fd, err; struct sockaddr_in server, client; char buf[BUFFER_SIZE]; server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd < 0) on_error("Could not create socket\n"); server.sin_family = AF_INET; server.sin_port = htons(port); // server.sin_addr.s_addr = htonl(INADDR_ANY); if (inet_pton(AF_INET, "127.0.0.1", &server.sin_addr) <= 0) { perror("inet_pton"); exit(EXIT_FAILURE); } int opt_val = 1; setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt_val, sizeof opt_val); err = bind(server_fd, (struct sockaddr *) &server, sizeof(server)); if (err < 0) on_error("Could not bind socket\n"); err = listen(server_fd, SOMAXCONN); if (err < 0) on_error("Could not listen on socket\n"); printf("Server is listening on %d\n", port); char* response = "HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n"; size_t response_size = strlen(response); while (1) { socklen_t client_len = sizeof(client); client_fd = accept(server_fd, (struct sockaddr *) &client, &client_len); if (client_fd < 0) on_error("Could not establish new connection\n"); // Removing this line entirely gives us the best performance, but obviously we are just blasting out a response without considering the request. Setting it to MSG_DONTWAIT gives us the second best performance, but we are still reading the request but probably not all of it. recv(client_fd, buf, BUFFER_SIZE, MSG_DONTWAIT); send(client_fd, response, response_size, 0); close(client_fd); } return 0; } socketry-io-event-ccd0953/benchmark/server/config.ru000066400000000000000000000000761516444210200225740ustar00rootroot00000000000000# frozen_string_literal: true run lambda{|env| [204, {}, []]}socketry-io-event-ccd0953/benchmark/server/event.rb000077500000000000000000000011421516444210200224230ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2025, by Samuel Williams. require_relative "scheduler" require "io/nonblock" RESPONSE = "HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n" #scheduler = DirectScheduler.new scheduler = Scheduler.new Fiber.set_scheduler(scheduler) port = Integer(ARGV.pop || 9090) Fiber.schedule do server = TCPServer.new("localhost", port) server.listen(Socket::SOMAXCONN) loop do peer, address = server.accept Fiber.schedule do peer.recv(1024) peer.send(RESPONSE, 0) peer.close end end end socketry-io-event-ccd0953/benchmark/server/fork.rb000077500000000000000000000005761516444210200222550ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2024, by Samuel Williams. require "socket" port = Integer(ARGV.pop || 9090) server = TCPServer.new("localhost", port) loop do peer = server.accept fork do peer.recv(1024) peer.send("HTTP/1.1 200 Ok\r\nConnection: close\r\n\r\n", 0) peer.close end peer.close end socketry-io-event-ccd0953/benchmark/server/gems.locked000066400000000000000000000010151516444210200230670ustar00rootroot00000000000000GEM remote: https://rubygems.org/ specs: async (2.23.1) console (~> 1.29) fiber-annotation io-event (~> 1.9) metrics (~> 0.12) traces (~> 0.15) console (1.30.2) fiber-annotation fiber-local (~> 1.1) json fiber-annotation (0.2.0) fiber-local (1.1.0) fiber-storage fiber-storage (1.0.0) io-event (1.10.0) json (2.10.2) metrics (0.12.2) traces (0.15.2) PLATFORMS arm64-darwin-24 ruby DEPENDENCIES async BUNDLED WITH 2.6.2 socketry-io-event-ccd0953/benchmark/server/gems.rb000066400000000000000000000002311516444210200222300ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2025, by Samuel Williams. source "https://rubygems.org" gem "async" socketry-io-event-ccd0953/benchmark/server/loop.rb000077500000000000000000000013221516444210200222530ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2025, by Samuel Williams. require "socket" require "time" # Assuming request per connection: RESPONSE = "HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n" port = Integer(ARGV.pop || 9090) server = TCPServer.new("localhost", port) loop do peer, address = server.accept # This is by far the fastest path, clocking in at around 80,000 requests per second in a single thread. peer.recv(1024) rescue nil peer.send(RESPONSE, 0) # This drops us to about 1/4 the performance due to the overhead of blocking operations. # while (peer.recv(1024) rescue nil) # peer.send(RESPONSE, 0) # end peer.close end socketry-io-event-ccd0953/benchmark/server/scheduler.rb000066400000000000000000000027741516444210200232710ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2025, by Samuel Williams. $LOAD_PATH << File.expand_path("../../lib", __dir__) $LOAD_PATH << File.expand_path("../../ext", __dir__) require "io/event" require "socket" require "fiber" class Scheduler def initialize(selector = nil) @fiber = Fiber.current @selector = selector || IO::Event::Selector.new(@fiber) @waiting = 0 unless @selector.respond_to?(:io_close) instance_eval{undef io_close} end @mutex = Mutex.new end def block(blocker, timeout) raise NotImplementedError end def unblock(blocker, fiber) raise NotImplementedError end def io_wait(io, events, timeout) fiber = Fiber.current @waiting += 1 @selector.io_wait(fiber, io, events) ensure @waiting -= 1 end def io_close(io) @selector.io_close(io) end def kernel_sleep(duration) @selector.defer end def close while @selector.ready? || @waiting > 0 begin @selector.select(nil) rescue Errno::EINTR # Ignore. end end rescue Interrupt # Exit. end def fiber(&block) fiber = Fiber.new(&block) @selector.resume(fiber) return fiber end end class DirectScheduler < Scheduler def io_read(io, buffer, length) fiber = Fiber.current @waiting += 1 result = @selector.io_read(fiber, io, buffer, length) ensure @waiting -= 1 end def io_write(io, buffer, length) fiber = Fiber.current @waiting += 1 @selector.io_write(fiber, io, buffer, length) ensure @waiting -= 1 end end socketry-io-event-ccd0953/benchmark/server/thread.rb000077500000000000000000000006231516444210200225540ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2025, by Samuel Williams. require "socket" RESPONSE = "HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n" port = Integer(ARGV.pop || 9090) server = TCPServer.new("localhost", port) loop do peer = server.accept Thread.new do peer.recv(1024) peer.send(RESPONSE, 0) peer.close end end socketry-io-event-ccd0953/config/000077500000000000000000000000001516444210200167615ustar00rootroot00000000000000socketry-io-event-ccd0953/config/environment.rb000066400000000000000000000002351516444210200216520ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2025, by Samuel Williams. $LOAD_PATH << ::File.expand_path("../ext", __dir__) socketry-io-event-ccd0953/config/external.yaml000066400000000000000000000010061516444210200214640ustar00rootroot00000000000000# async-v2.0.3: # url: https://github.com/socketry/async # command: bundle exec rspec # branch: v2.0.3 # async-v2.1.0: # url: https://github.com/socketry/async # command: bundle exec rspec # branch: v2.1.0 async: url: https://github.com/socketry/async command: bundle exec sus extra: - $LOAD_PATH << ::File.expand_path("../../ext", __dir__) async-http: url: https://github.com/socketry/async-http command: bundle exec sus extra: - $LOAD_PATH << ::File.expand_path("../../ext", __dir__) socketry-io-event-ccd0953/config/sus.rb000066400000000000000000000005421516444210200201210ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2025, by Samuel Williams. require_relative "environment" Warning[:experimental] = false require "covered/sus" include Covered::Sus # Intensive GC checking: # # Thread.new do # while true # sleep 0.0001 # $stderr.puts GC.verify_compaction_references # end # end socketry-io-event-ccd0953/context/000077500000000000000000000000001516444210200172005ustar00rootroot00000000000000socketry-io-event-ccd0953/context/getting-started.md000066400000000000000000000027251516444210200226350ustar00rootroot00000000000000# Getting Started This guide explains how to use `io-event` for non-blocking IO. ## Installation Add the gem to your project: ~~~ bash $ bundle add io-event ~~~ ## Core Concepts `io-event` has several core concepts: - A {ruby IO::Event::Selector} implementation which provides the primitive operations for implementation an event loop. - A {ruby IO::Event::Debug::Selector} which adds extra validations and checks at the expense of performance. You should generally use this during tests. ## Basic Event Loop This example shows how to perform a blocking operation ```ruby require "fiber" require "io/event" selector = IO::Event::Selector.new(Fiber.current) input, output = IO.pipe writer = Fiber.new do output.write("Hello World") output.close end reader = Fiber.new do selector.io_wait(Fiber.current, input, IO::READABLE) pp read: input.read end # The reader will be blocked until the IO has data available: reader.transfer # Write some data to the pipe and close the writing end: writer.transfer selector.select(1) # Results in: # {:read=>"Hello World"} ``` ## Debugging 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: ```shell $ IO_EVENT_SELECTOR_DEBUG=y IO_EVENT_SELECTOR_DEBUG_LOG=/dev/stderr bundle exec ./my_script.rb ``` The format of the log is subject to change, but it may be useful for debugging. socketry-io-event-ccd0953/context/index.yaml000066400000000000000000000007631516444210200212010ustar00rootroot00000000000000# Automatically generated context index for Utopia::Project guides. # Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`. --- description: An event loop. metadata: documentation_uri: https://socketry.github.io/io-event/ source_code_uri: https://github.com/socketry/io-event.git files: - path: getting-started.md title: Getting Started description: This guide explains how to use `io-event` for non-blocking IO. socketry-io-event-ccd0953/design.md000066400000000000000000000042441516444210200173130ustar00rootroot00000000000000## Dual `_select` without GVL: Always release GVL: ``` Warming up -------------------------------------- KQueue 55.896k i/100ms Select 17.023k i/100ms Calculating ------------------------------------- KQueue 532.515k (± 8.0%) i/s - 2.683M in 5.071193s Select 177.956k (± 3.4%) i/s - 902.219k in 5.075817s Comparison: KQueue: 532515.3 i/s Select: 177956.1 i/s - 2.99x (± 0.00) slower ``` Only release GVL with non-zero timeout, with selector.elect(1) (so always hitting slow path): ``` Warming up -------------------------------------- KQueue 39.628k i/100ms Select 18.330k i/100ms Calculating ------------------------------------- KQueue 381.868k (± 6.5%) i/s - 1.902M in 5.004267s Select 171.623k (± 3.0%) i/s - 861.510k in 5.024308s Comparison: KQueue: 381867.8 i/s Select: 171622.5 i/s - 2.23x (± 0.00) slower ``` Only release GVL with non-zero timeout, with selector.select(0) so always hitting fast path: ``` Warming up -------------------------------------- KQueue 56.240k i/100ms Select 17.888k i/100ms Calculating ------------------------------------- KQueue 543.042k (± 7.8%) i/s - 2.700M in 5.003790s Select 171.866k (± 4.3%) i/s - 858.624k in 5.005785s Comparison: KQueue: 543041.5 i/s Select: 171866.2 i/s - 3.16x (± 0.00) slower ``` Only release GVL when no events are ready and non-zero timeout, with selector.select(1): ``` Warming up -------------------------------------- KQueue 53.401k i/100ms Select 16.691k i/100ms Calculating ------------------------------------- KQueue 524.564k (± 6.1%) i/s - 2.617M in 5.006996s Select 179.329k (± 2.4%) i/s - 901.314k in 5.029136s Comparison: KQueue: 524564.0 i/s Select: 179329.1 i/s - 2.93x (± 0.00) slower ``` So this approach seems to be a net win of about 1.5x throughput.socketry-io-event-ccd0953/ext/000077500000000000000000000000001516444210200163145ustar00rootroot00000000000000socketry-io-event-ccd0953/ext/extconf.rb000077500000000000000000000042351516444210200203160ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2026, by Samuel Williams. # Copyright, 2023, by Math Ieu. # Copyright, 2025, by Stanislav (Stas) Katkov. # Copyright, 2026, by Stan Hu. return if RUBY_DESCRIPTION =~ /jruby/ require "mkmf" gem_name = File.basename(__dir__) extension_name = "IO_Event" # dir_config(extension_name) append_cflags(["-Wall", "-Wno-unknown-pragmas", "-std=c99"]) if ENV.key?("RUBY_DEBUG") $stderr.puts "Enabling debug mode..." append_cflags(["-DRUBY_DEBUG", "-O0"]) end $srcs = ["io/event/event.c", "io/event/time.c", "io/event/fiber.c", "io/event/selector/selector.c"] $VPATH << "$(srcdir)/io/event" $VPATH << "$(srcdir)/io/event/selector" have_func("rb_ext_ractor_safe") have_func("&rb_fiber_transfer") if have_library("uring") and have_header("liburing.h") # We might want to consider using this in the future: # have_func("io_uring_submit_and_wait_timeout", "liburing.h") $srcs << "io/event/selector/uring.c" end if have_header("sys/epoll.h") $srcs << "io/event/selector/epoll.c" end if have_header("sys/event.h") $srcs << "io/event/selector/kqueue.c" end have_header("sys/wait.h") have_header("sys/eventfd.h") $srcs << "io/event/interrupt.c" have_func("rb_io_descriptor") have_func("&rb_process_status_wait") have_func("rb_fiber_current") have_func("&rb_fiber_raise") have_func("epoll_pwait2") if enable_config("epoll_pwait2", true) have_header("ruby/io/buffer.h") # Feature detection for blocking operation support if have_func("rb_fiber_scheduler_blocking_operation_extract") # Feature detection for pthread support (needed for WorkerPool) if have_header("pthread.h") append_cflags(["-DHAVE_IO_EVENT_WORKER_POOL"]) $srcs << "io/event/worker_pool.c" $srcs << "io/event/worker_pool_test.c" end end if ENV.key?("RUBY_SANITIZE") $stderr.puts "Enabling sanitizers..." # Add address and undefined behaviour sanitizers: append_cflags(["-fsanitize=address", "-fsanitize=undefined", "-fno-omit-frame-pointer"]) $LDFLAGS << " -fsanitize=address -fsanitize=undefined" end create_header # Generate the makefile to compile the native binary into `lib`: create_makefile(extension_name) socketry-io-event-ccd0953/ext/io/000077500000000000000000000000001516444210200167235ustar00rootroot00000000000000socketry-io-event-ccd0953/ext/io/event/000077500000000000000000000000001516444210200200445ustar00rootroot00000000000000socketry-io-event-ccd0953/ext/io/event/array.h000066400000000000000000000101551516444210200213350ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2023, by Samuel Williams. // Provides a simple implementation of unique pointers to elements of the given size. #include #include #include #include static const size_t IO_EVENT_ARRAY_MAXIMUM_COUNT = SIZE_MAX / sizeof(void*); static const size_t IO_EVENT_ARRAY_DEFAULT_COUNT = 128; struct IO_Event_Array { // The array of pointers to elements: void **base; // The allocated size of the array: size_t count; // The biggest item we've seen so far: size_t limit; // The size of each element that is allocated: size_t element_size; void (*element_initialize)(void*); void (*element_free)(void*); }; inline static int IO_Event_Array_initialize(struct IO_Event_Array *array, size_t count, size_t element_size) { array->limit = 0; array->element_size = element_size; if (count) { array->base = (void**)calloc(count, sizeof(void*)); if (array->base == NULL) { return -1; } array->count = count; return 1; } else { array->base = NULL; array->count = 0; return 0; } } inline static size_t IO_Event_Array_memory_size(const struct IO_Event_Array *array) { // Upper bound. return array->count * (sizeof(void*) + array->element_size); } inline static void IO_Event_Array_free(struct IO_Event_Array *array) { if (array->base) { void **base = array->base; size_t limit = array->limit; array->base = NULL; array->count = 0; array->limit = 0; for (size_t i = 0; i < limit; i += 1) { void *element = base[i]; if (element) { array->element_free(element); free(element); } } free(base); } } inline static int IO_Event_Array_resize(struct IO_Event_Array *array, size_t count) { if (count <= array->count) { // Already big enough: return 0; } if (count > IO_EVENT_ARRAY_MAXIMUM_COUNT) { errno = ENOMEM; return -1; } size_t new_count = array->count; // If the array is empty, we need to set the initial size: if (new_count == 0) new_count = IO_EVENT_ARRAY_DEFAULT_COUNT; else while (new_count < count) { // Ensure we don't overflow: if (new_count > (IO_EVENT_ARRAY_MAXIMUM_COUNT / 2)) { new_count = IO_EVENT_ARRAY_MAXIMUM_COUNT; break; } // Compute the next multiple (ideally a power of 2): new_count *= 2; } void **new_base = (void**)realloc(array->base, new_count * sizeof(void*)); if (new_base == NULL) { return -1; } // Zero out the new memory: memset(new_base + array->count, 0, (new_count - array->count) * sizeof(void*)); array->base = (void**)new_base; array->count = new_count; // Resizing sucessful: return 1; } inline static void* IO_Event_Array_lookup(struct IO_Event_Array *array, size_t index) { size_t count = index + 1; // Resize the array if necessary: if (count > array->count) { if (IO_Event_Array_resize(array, count) == -1) { return NULL; } } // Get the element: void **element = array->base + index; // Allocate the element if it doesn't exist: if (*element == NULL) { *element = malloc(array->element_size); assert(*element); if (array->element_initialize) { array->element_initialize(*element); } // Update the limit: if (count > array->limit) array->limit = count; } return *element; } inline static void* IO_Event_Array_last(struct IO_Event_Array *array) { if (array->limit == 0) return NULL; else return array->base[array->limit - 1]; } inline static void IO_Event_Array_truncate(struct IO_Event_Array *array, size_t limit) { if (limit < array->limit) { for (size_t i = limit; i < array->limit; i += 1) { void **element = array->base + i; if (*element) { array->element_free(*element); free(*element); *element = NULL; } } array->limit = limit; } } // Push a new element onto the end of the array. inline static void* IO_Event_Array_push(struct IO_Event_Array *array) { return IO_Event_Array_lookup(array, array->limit); } inline static void IO_Event_Array_each(struct IO_Event_Array *array, void (*callback)(void*)) { for (size_t i = 0; i < array->limit; i += 1) { void *element = array->base[i]; if (element) { callback(element); } } } socketry-io-event-ccd0953/ext/io/event/event.c000066400000000000000000000014601516444210200213320ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2021-2025, by Samuel Williams. #include "event.h" #include "fiber.h" #include "selector/selector.h" void Init_IO_Event(void) { #ifdef HAVE_RB_EXT_RACTOR_SAFE rb_ext_ractor_safe(true); #endif VALUE IO_Event = rb_define_module_under(rb_cIO, "Event"); Init_IO_Event_Fiber(IO_Event); #ifdef HAVE_IO_EVENT_WORKER_POOL Init_IO_Event_WorkerPool(IO_Event); #endif VALUE IO_Event_Selector = rb_define_module_under(IO_Event, "Selector"); Init_IO_Event_Selector(IO_Event_Selector); #ifdef IO_EVENT_SELECTOR_URING Init_IO_Event_Selector_URing(IO_Event_Selector); #endif #ifdef IO_EVENT_SELECTOR_EPOLL Init_IO_Event_Selector_EPoll(IO_Event_Selector); #endif #ifdef IO_EVENT_SELECTOR_KQUEUE Init_IO_Event_Selector_KQueue(IO_Event_Selector); #endif } socketry-io-event-ccd0953/ext/io/event/event.h000066400000000000000000000006021516444210200213340ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2021-2025, by Samuel Williams. #pragma once #include void Init_IO_Event(void); #ifdef HAVE_LIBURING_H #include "selector/uring.h" #endif #ifdef HAVE_SYS_EPOLL_H #include "selector/epoll.h" #endif #ifdef HAVE_SYS_EVENT_H #include "selector/kqueue.h" #endif #ifdef HAVE_IO_EVENT_WORKER_POOL #include "worker_pool.h" #endif socketry-io-event-ccd0953/ext/io/event/fiber.c000066400000000000000000000026371516444210200213070ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2025, by Samuel Williams. #include "fiber.h" static ID id_transfer, id_alive_p; VALUE IO_Event_Fiber_transfer(VALUE fiber, int argc, VALUE *argv) { // TODO Consider introducing something like `rb_fiber_scheduler_transfer(...)`. #ifdef HAVE__RB_FIBER_TRANSFER if (RTEST(rb_obj_is_fiber(fiber))) { if (RTEST(rb_fiber_alive_p(fiber))) { return rb_fiber_transfer(fiber, argc, argv); } // If it's a fiber, but dead, we are done. return Qnil; } #endif if (RTEST(rb_funcall(fiber, id_alive_p, 0))) { return rb_funcallv(fiber, id_transfer, argc, argv); } return Qnil; } #ifndef HAVE__RB_FIBER_RAISE static ID id_raise; VALUE IO_Event_Fiber_raise(VALUE fiber, int argc, VALUE *argv) { return rb_funcallv(fiber, id_raise, argc, argv); } #endif #ifndef HAVE_RB_FIBER_CURRENT static ID id_current; VALUE IO_Event_Fiber_current(void) { return rb_funcall(rb_cFiber, id_current, 0); } #endif // There is no public interface for this... yet. static ID id_blocking_p; int IO_Event_Fiber_blocking(VALUE fiber) { return RTEST(rb_funcall(fiber, id_blocking_p, 0)); } void Init_IO_Event_Fiber(VALUE IO_Event) { id_transfer = rb_intern("transfer"); id_alive_p = rb_intern("alive?"); #ifndef HAVE__RB_FIBER_RAISE id_raise = rb_intern("raise"); #endif #ifndef HAVE_RB_FIBER_CURRENT id_current = rb_intern("current"); #endif id_blocking_p = rb_intern("blocking?"); } socketry-io-event-ccd0953/ext/io/event/fiber.h000066400000000000000000000011041516444210200213000ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2025, by Samuel Williams. #pragma once #include VALUE IO_Event_Fiber_transfer(VALUE fiber, int argc, VALUE *argv); #ifdef HAVE__RB_FIBER_RAISE #define IO_Event_Fiber_raise(fiber, argc, argv) rb_fiber_raise(fiber, argc, argv) #else VALUE IO_Event_Fiber_raise(VALUE fiber, int argc, VALUE *argv); #endif #ifdef HAVE_RB_FIBER_CURRENT #define IO_Event_Fiber_current() rb_fiber_current() #else VALUE IO_Event_Fiber_current(void); #endif int IO_Event_Fiber_blocking(VALUE fiber); void Init_IO_Event_Fiber(VALUE IO_Event); socketry-io-event-ccd0953/ext/io/event/interrupt.c000066400000000000000000000051451516444210200222510ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2021-2025, by Samuel Williams. #include "interrupt.h" #include #include "selector/selector.h" #ifdef HAVE_RUBY_WIN32_H #include #if !defined(HAVE_PIPE) && !defined(pipe) #define pipe(p) rb_w32_pipe(p) #endif #endif #ifdef HAVE_SYS_EVENTFD_H #include void IO_Event_Interrupt_open(struct IO_Event_Interrupt *interrupt) { interrupt->descriptor = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); rb_update_max_fd(interrupt->descriptor); } void IO_Event_Interrupt_close(struct IO_Event_Interrupt *interrupt) { close(interrupt->descriptor); } void IO_Event_Interrupt_signal(struct IO_Event_Interrupt *interrupt) { uint64_t value = 1; ssize_t result = write(interrupt->descriptor, &value, sizeof(value)); if (result == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) return; rb_sys_fail("IO_Event_Interrupt_signal:write"); } } void IO_Event_Interrupt_clear(struct IO_Event_Interrupt *interrupt) { uint64_t value = 0; ssize_t result = read(interrupt->descriptor, &value, sizeof(value)); if (result == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) return; rb_sys_fail("IO_Event_Interrupt_clear:read"); } } #else void IO_Event_Interrupt_open(struct IO_Event_Interrupt *interrupt) { #ifdef __linux__ pipe2(interrupt->descriptor, O_CLOEXEC | O_NONBLOCK); #else pipe(interrupt->descriptor); IO_Event_Selector_nonblock_set(interrupt->descriptor[0]); IO_Event_Selector_nonblock_set(interrupt->descriptor[1]); #endif rb_update_max_fd(interrupt->descriptor[0]); rb_update_max_fd(interrupt->descriptor[1]); } void IO_Event_Interrupt_close(struct IO_Event_Interrupt *interrupt) { close(interrupt->descriptor[0]); close(interrupt->descriptor[1]); } void IO_Event_Interrupt_signal(struct IO_Event_Interrupt *interrupt) { ssize_t result = write(interrupt->descriptor[1], ".", 1); if (result == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { // 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. } else { rb_sys_fail("IO_Event_Interrupt_signal:write"); } } } void IO_Event_Interrupt_clear(struct IO_Event_Interrupt *interrupt) { char buffer[128]; ssize_t result = read(interrupt->descriptor[0], buffer, sizeof(buffer)); if (result == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { // 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. } else { rb_sys_fail("IO_Event_Interrupt_clear:read"); } } } #endif socketry-io-event-ccd0953/ext/io/event/interrupt.h000066400000000000000000000014111516444210200222460ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2021-2025, by Samuel Williams. #pragma once #include #ifdef HAVE_SYS_EVENTFD_H struct IO_Event_Interrupt { int descriptor; }; static inline int IO_Event_Interrupt_descriptor(struct IO_Event_Interrupt *interrupt) { return interrupt->descriptor; } #else struct IO_Event_Interrupt { int descriptor[2]; }; static inline int IO_Event_Interrupt_descriptor(struct IO_Event_Interrupt *interrupt) { return interrupt->descriptor[0]; } #endif void IO_Event_Interrupt_open(struct IO_Event_Interrupt *interrupt); void IO_Event_Interrupt_close(struct IO_Event_Interrupt *interrupt); void IO_Event_Interrupt_signal(struct IO_Event_Interrupt *interrupt); void IO_Event_Interrupt_clear(struct IO_Event_Interrupt *interrupt); socketry-io-event-ccd0953/ext/io/event/list.h000066400000000000000000000046751516444210200212040ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2023-2025, by Samuel Williams. #include #include #include struct IO_Event_List_Type { }; struct IO_Event_List { struct IO_Event_List *head, *tail; struct IO_Event_List_Type *type; }; inline static void IO_Event_List_initialize(struct IO_Event_List *list) { list->head = list->tail = list; list->type = 0; } inline static void IO_Event_List_clear(struct IO_Event_List *list) { list->head = list->tail = NULL; list->type = 0; } // Append an item to the end of the list. inline static void IO_Event_List_append(struct IO_Event_List *list, struct IO_Event_List *node) { assert(node->head == NULL); assert(node->tail == NULL); struct IO_Event_List *head = list->head; node->tail = list; node->head = head; list->head = node; head->tail = node; } // Prepend an item to the beginning of the list. inline static void IO_Event_List_prepend(struct IO_Event_List *list, struct IO_Event_List *node) { assert(node->head == NULL); assert(node->tail == NULL); struct IO_Event_List *tail = list->tail; node->head = list; node->tail = tail; list->tail = node; tail->head = node; } // Pop an item from the list. inline static void IO_Event_List_pop(struct IO_Event_List *node) { assert(node->head != NULL); assert(node->tail != NULL); struct IO_Event_List *head = node->head; struct IO_Event_List *tail = node->tail; head->tail = tail; tail->head = head; node->head = node->tail = NULL; } // Remove an item from the list, if it is in a list. inline static void IO_Event_List_free(struct IO_Event_List *node) { if (node->head && node->tail) { IO_Event_List_pop(node); } } // Calculate the memory size of the list nodes. inline static size_t IO_Event_List_memory_size(const struct IO_Event_List *list) { size_t memsize = 0; const struct IO_Event_List *node = list->tail; while (node != list) { memsize += sizeof(struct IO_Event_List); node = node->tail; } return memsize; } // Return true if the list is empty. inline static int IO_Event_List_empty(const struct IO_Event_List *list) { return list->head == list->tail; } // Enumerate all items in the list, assuming the list will not be modified during iteration. inline static void IO_Event_List_immutable_each(struct IO_Event_List *list, void (*callback)(struct IO_Event_List *node)) { struct IO_Event_List *node = list->tail; while (node != list) { if (node->type) callback(node); node = node->tail; } } socketry-io-event-ccd0953/ext/io/event/selector/000077500000000000000000000000001516444210200216645ustar00rootroot00000000000000socketry-io-event-ccd0953/ext/io/event/selector/epoll.c000066400000000000000000000763701516444210200231600ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2021-2025, by Samuel Williams. #include "epoll.h" #include "selector.h" #include "../list.h" #include "../array.h" #include #include #include #include "pidfd.c" #include "../interrupt.h" enum { DEBUG = 0, }; enum {EPOLL_MAX_EVENTS = 64}; // This represents an actual fiber waiting for a specific event. struct IO_Event_Selector_EPoll_Waiting { struct IO_Event_List list; // The events the fiber is waiting for. enum IO_Event events; // The events that are currently ready. enum IO_Event ready; // The fiber value itself. VALUE fiber; }; struct IO_Event_Selector_EPoll { struct IO_Event_Selector backend; int descriptor; // Flag indicating whether the selector is currently blocked in a system call. // Set to 1 when blocked in epoll_wait() without GVL, 0 otherwise. // Used by wakeup() to determine if an interrupt signal is needed. int blocked; struct timespec idle_duration; struct IO_Event_Interrupt interrupt; struct IO_Event_Array descriptors; }; // This represents zero or more fibers waiting for a specific descriptor. struct IO_Event_Selector_EPoll_Descriptor { struct IO_Event_List list; // The last IO object that was used to register events. VALUE io; // The union of all events we are waiting for: enum IO_Event waiting_events; // The union of events we are registered for: enum IO_Event registered_events; }; static void IO_Event_Selector_EPoll_Waiting_mark(struct IO_Event_List *_waiting) { struct IO_Event_Selector_EPoll_Waiting *waiting = (void*)_waiting; if (waiting->fiber) { rb_gc_mark_movable(waiting->fiber); } } static void IO_Event_Selector_EPoll_Descriptor_mark(void *_descriptor) { struct IO_Event_Selector_EPoll_Descriptor *descriptor = _descriptor; IO_Event_List_immutable_each(&descriptor->list, IO_Event_Selector_EPoll_Waiting_mark); if (descriptor->io) { rb_gc_mark_movable(descriptor->io); } } static void IO_Event_Selector_EPoll_Type_mark(void *_selector) { struct IO_Event_Selector_EPoll *selector = _selector; IO_Event_Selector_mark(&selector->backend); IO_Event_Array_each(&selector->descriptors, IO_Event_Selector_EPoll_Descriptor_mark); } static void IO_Event_Selector_EPoll_Waiting_compact(struct IO_Event_List *_waiting) { struct IO_Event_Selector_EPoll_Waiting *waiting = (void*)_waiting; if (waiting->fiber) { waiting->fiber = rb_gc_location(waiting->fiber); } } static void IO_Event_Selector_EPoll_Descriptor_compact(void *_descriptor) { struct IO_Event_Selector_EPoll_Descriptor *descriptor = _descriptor; IO_Event_List_immutable_each(&descriptor->list, IO_Event_Selector_EPoll_Waiting_compact); if (descriptor->io) { descriptor->io = rb_gc_location(descriptor->io); } } static void IO_Event_Selector_EPoll_Type_compact(void *_selector) { struct IO_Event_Selector_EPoll *selector = _selector; IO_Event_Selector_compact(&selector->backend); IO_Event_Array_each(&selector->descriptors, IO_Event_Selector_EPoll_Descriptor_compact); } static void close_internal(struct IO_Event_Selector_EPoll *selector) { if (selector->descriptor >= 0) { close(selector->descriptor); selector->descriptor = -1; IO_Event_Interrupt_close(&selector->interrupt); } } static void IO_Event_Selector_EPoll_Type_free(void *_selector) { struct IO_Event_Selector_EPoll *selector = _selector; close_internal(selector); IO_Event_Array_free(&selector->descriptors); xfree(selector); } static size_t IO_Event_Selector_EPoll_Type_size(const void *_selector) { const struct IO_Event_Selector_EPoll *selector = _selector; return sizeof(struct IO_Event_Selector_EPoll) + IO_Event_Array_memory_size(&selector->descriptors) ; } static const rb_data_type_t IO_Event_Selector_EPoll_Type = { .wrap_struct_name = "IO::Event::Backend::EPoll", .function = { .dmark = IO_Event_Selector_EPoll_Type_mark, .dcompact = IO_Event_Selector_EPoll_Type_compact, .dfree = IO_Event_Selector_EPoll_Type_free, .dsize = IO_Event_Selector_EPoll_Type_size, }, .data = NULL, .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED, }; inline static struct IO_Event_Selector_EPoll_Descriptor * IO_Event_Selector_EPoll_Descriptor_lookup(struct IO_Event_Selector_EPoll *selector, int descriptor) { struct IO_Event_Selector_EPoll_Descriptor *epoll_descriptor = IO_Event_Array_lookup(&selector->descriptors, descriptor); if (!epoll_descriptor) { rb_sys_fail("IO_Event_Selector_EPoll_Descriptor_lookup:IO_Event_Array_lookup"); } return epoll_descriptor; } static inline uint32_t epoll_flags_from_events(int events) { uint32_t flags = 0; if (events & IO_EVENT_READABLE) flags |= EPOLLIN; if (events & IO_EVENT_PRIORITY) flags |= EPOLLPRI; if (events & IO_EVENT_WRITABLE) flags |= EPOLLOUT; flags |= EPOLLHUP; flags |= EPOLLERR; if (DEBUG) fprintf(stderr, "epoll_flags_from_events events=%d flags=%d\n", events, flags); return flags; } static inline int events_from_epoll_flags(uint32_t flags) { int events = 0; if (DEBUG) fprintf(stderr, "events_from_epoll_flags flags=%d\n", flags); // 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. // if (flags & (EPOLLIN)) events |= IO_EVENT_READABLE; if (flags & (EPOLLIN|EPOLLHUP|EPOLLERR)) events |= IO_EVENT_READABLE; if (flags & EPOLLPRI) events |= IO_EVENT_PRIORITY; if (flags & EPOLLOUT) events |= IO_EVENT_WRITABLE; return events; } inline static 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) { if (epoll_descriptor->io == io) { if (epoll_descriptor->registered_events == epoll_descriptor->waiting_events) { // All the events we are interested in are already registered. return 0; } } else { // The IO has changed, we need to reset the state: epoll_descriptor->registered_events = 0; RB_OBJ_WRITE(selector->backend.self, &epoll_descriptor->io, io); } if (epoll_descriptor->waiting_events == 0) { if (epoll_descriptor->registered_events) { // We are no longer interested in any events. epoll_ctl(selector->descriptor, EPOLL_CTL_DEL, descriptor, NULL); epoll_descriptor->registered_events = 0; } RB_OBJ_WRITE(selector->backend.self, &epoll_descriptor->io, 0); return 0; } // We need to register for additional events: struct epoll_event event = { .events = epoll_flags_from_events(epoll_descriptor->waiting_events), .data = {.fd = descriptor}, }; int operation; if (epoll_descriptor->registered_events) { operation = EPOLL_CTL_MOD; } else { operation = EPOLL_CTL_ADD; } int result = epoll_ctl(selector->descriptor, operation, descriptor, &event); if (result == -1) { if (errno == ENOENT) { result = epoll_ctl(selector->descriptor, EPOLL_CTL_ADD, descriptor, &event); } else if (errno == EEXIST) { result = epoll_ctl(selector->descriptor, EPOLL_CTL_MOD, descriptor, &event); } if (result == -1) { return -1; } } epoll_descriptor->registered_events = epoll_descriptor->waiting_events; return 1; } inline static int IO_Event_Selector_EPoll_Waiting_register(struct IO_Event_Selector_EPoll *selector, VALUE io, int descriptor, struct IO_Event_Selector_EPoll_Waiting *waiting) { struct IO_Event_Selector_EPoll_Descriptor *epoll_descriptor = IO_Event_Selector_EPoll_Descriptor_lookup(selector, descriptor); // We are waiting for these events: epoll_descriptor->waiting_events |= waiting->events; int result = IO_Event_Selector_EPoll_Descriptor_update(selector, io, descriptor, epoll_descriptor); if (result == -1) return -1; IO_Event_List_prepend(&epoll_descriptor->list, &waiting->list); return result; } inline static void IO_Event_Selector_EPoll_Waiting_cancel(struct IO_Event_Selector_EPoll_Waiting *waiting) { IO_Event_List_pop(&waiting->list); waiting->fiber = 0; } void IO_Event_Selector_EPoll_Descriptor_initialize(void *element) { struct IO_Event_Selector_EPoll_Descriptor *epoll_descriptor = element; IO_Event_List_initialize(&epoll_descriptor->list); epoll_descriptor->io = 0; epoll_descriptor->waiting_events = 0; epoll_descriptor->registered_events = 0; } void IO_Event_Selector_EPoll_Descriptor_free(void *element) { struct IO_Event_Selector_EPoll_Descriptor *epoll_descriptor = element; IO_Event_List_free(&epoll_descriptor->list); } VALUE IO_Event_Selector_EPoll_allocate(VALUE self) { struct IO_Event_Selector_EPoll *selector = NULL; VALUE instance = TypedData_Make_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); IO_Event_Selector_initialize(&selector->backend, self, Qnil); selector->descriptor = -1; selector->blocked = 0; selector->descriptors.element_initialize = IO_Event_Selector_EPoll_Descriptor_initialize; selector->descriptors.element_free = IO_Event_Selector_EPoll_Descriptor_free; int result = IO_Event_Array_initialize(&selector->descriptors, IO_EVENT_ARRAY_DEFAULT_COUNT, sizeof(struct IO_Event_Selector_EPoll_Descriptor)); if (result < 0) { rb_sys_fail("IO_Event_Selector_EPoll_allocate:IO_Event_Array_initialize"); } return instance; } void IO_Event_Interrupt_add(struct IO_Event_Interrupt *interrupt, struct IO_Event_Selector_EPoll *selector) { int descriptor = IO_Event_Interrupt_descriptor(interrupt); struct epoll_event event = { .events = EPOLLIN|EPOLLRDHUP, .data = {.fd = -1}, }; int result = epoll_ctl(selector->descriptor, EPOLL_CTL_ADD, descriptor, &event); if (result == -1) { rb_sys_fail("IO_Event_Interrupt_add:epoll_ctl"); } } VALUE IO_Event_Selector_EPoll_initialize(VALUE self, VALUE loop) { struct IO_Event_Selector_EPoll *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); IO_Event_Selector_initialize(&selector->backend, self, loop); int result = epoll_create1(EPOLL_CLOEXEC); if (result == -1) { rb_sys_fail("IO_Event_Selector_EPoll_initialize:epoll_create"); } else { selector->descriptor = result; rb_update_max_fd(selector->descriptor); } IO_Event_Interrupt_open(&selector->interrupt); IO_Event_Interrupt_add(&selector->interrupt, selector); return self; } VALUE IO_Event_Selector_EPoll_loop(VALUE self) { struct IO_Event_Selector_EPoll *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); return selector->backend.loop; } VALUE IO_Event_Selector_EPoll_idle_duration(VALUE self) { struct IO_Event_Selector_EPoll *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); double duration = selector->idle_duration.tv_sec + (selector->idle_duration.tv_nsec / 1000000000.0); return DBL2NUM(duration); } VALUE IO_Event_Selector_EPoll_close(VALUE self) { struct IO_Event_Selector_EPoll *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); close_internal(selector); return Qnil; } VALUE IO_Event_Selector_EPoll_transfer(VALUE self) { struct IO_Event_Selector_EPoll *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); return IO_Event_Selector_loop_yield(&selector->backend); } VALUE IO_Event_Selector_EPoll_resume(int argc, VALUE *argv, VALUE self) { struct IO_Event_Selector_EPoll *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); return IO_Event_Selector_resume(&selector->backend, argc, argv); } VALUE IO_Event_Selector_EPoll_yield(VALUE self) { struct IO_Event_Selector_EPoll *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); return IO_Event_Selector_yield(&selector->backend); } VALUE IO_Event_Selector_EPoll_push(VALUE self, VALUE fiber) { struct IO_Event_Selector_EPoll *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); IO_Event_Selector_ready_push(&selector->backend, fiber); return Qnil; } VALUE IO_Event_Selector_EPoll_raise(int argc, VALUE *argv, VALUE self) { struct IO_Event_Selector_EPoll *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); return IO_Event_Selector_raise(&selector->backend, argc, argv); } VALUE IO_Event_Selector_EPoll_ready_p(VALUE self) { struct IO_Event_Selector_EPoll *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); return selector->backend.ready ? Qtrue : Qfalse; } struct process_wait_arguments { struct IO_Event_Selector_EPoll *selector; struct IO_Event_Selector_EPoll_Waiting *waiting; int pid; int flags; int descriptor; }; static VALUE process_wait_transfer(VALUE _arguments) { struct process_wait_arguments *arguments = (struct process_wait_arguments *)_arguments; IO_Event_Selector_loop_yield(&arguments->selector->backend); if (arguments->waiting->ready) { return IO_Event_Selector_process_status_wait(arguments->pid, arguments->flags); } else { return Qfalse; } } static VALUE process_wait_ensure(VALUE _arguments) { struct process_wait_arguments *arguments = (struct process_wait_arguments *)_arguments; close(arguments->descriptor); IO_Event_Selector_EPoll_Waiting_cancel(arguments->waiting); return Qnil; } struct IO_Event_List_Type IO_Event_Selector_EPoll_process_wait_list_type = {}; VALUE IO_Event_Selector_EPoll_process_wait(VALUE self, VALUE fiber, VALUE _pid, VALUE _flags) { struct IO_Event_Selector_EPoll *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); pid_t pid = NUM2PIDT(_pid); int flags = NUM2INT(_flags); int descriptor = pidfd_open(pid, 0); if (descriptor == -1) { rb_sys_fail("IO_Event_Selector_EPoll_process_wait:pidfd_open"); } rb_update_max_fd(descriptor); // `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. VALUE status = IO_Event_Selector_process_status_wait(pid, flags); if (status != Qnil) { close(descriptor); return status; } struct IO_Event_Selector_EPoll_Waiting waiting = { .list = {.type = &IO_Event_Selector_EPoll_process_wait_list_type}, .fiber = fiber, .events = IO_EVENT_READABLE, }; RB_OBJ_WRITTEN(self, Qundef, fiber); int result = IO_Event_Selector_EPoll_Waiting_register(selector, _pid, descriptor, &waiting); if (result == -1) { close(descriptor); rb_sys_fail("IO_Event_Selector_EPoll_process_wait:IO_Event_Selector_EPoll_Waiting_register"); } struct process_wait_arguments process_wait_arguments = { .selector = selector, .pid = pid, .flags = flags, .descriptor = descriptor, .waiting = &waiting, }; return rb_ensure(process_wait_transfer, (VALUE)&process_wait_arguments, process_wait_ensure, (VALUE)&process_wait_arguments); } struct io_wait_arguments { struct IO_Event_Selector_EPoll *selector; struct IO_Event_Selector_EPoll_Waiting *waiting; }; static VALUE io_wait_ensure(VALUE _arguments) { struct io_wait_arguments *arguments = (struct io_wait_arguments *)_arguments; IO_Event_Selector_EPoll_Waiting_cancel(arguments->waiting); return Qnil; }; static VALUE io_wait_transfer(VALUE _arguments) { struct io_wait_arguments *arguments = (struct io_wait_arguments *)_arguments; IO_Event_Selector_loop_yield(&arguments->selector->backend); if (arguments->waiting->ready) { return RB_INT2NUM(arguments->waiting->ready); } else { return Qfalse; } }; struct IO_Event_List_Type IO_Event_Selector_EPoll_io_wait_list_type = {}; VALUE IO_Event_Selector_EPoll_io_wait(VALUE self, VALUE fiber, VALUE io, VALUE events) { struct IO_Event_Selector_EPoll *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); int descriptor = IO_Event_Selector_io_descriptor(io); struct IO_Event_Selector_EPoll_Waiting waiting = { .list = {.type = &IO_Event_Selector_EPoll_io_wait_list_type}, .fiber = fiber, .events = RB_NUM2INT(events), }; RB_OBJ_WRITTEN(self, Qundef, fiber); int result = IO_Event_Selector_EPoll_Waiting_register(selector, io, descriptor, &waiting); if (result == -1) { if (errno == EPERM) { IO_Event_Selector_ready_push(&selector->backend, fiber); IO_Event_Selector_yield(&selector->backend); return events; } rb_sys_fail("IO_Event_Selector_EPoll_io_wait:IO_Event_Selector_EPoll_Waiting_register"); } struct io_wait_arguments io_wait_arguments = { .selector = selector, .waiting = &waiting, }; return rb_ensure(io_wait_transfer, (VALUE)&io_wait_arguments, io_wait_ensure, (VALUE)&io_wait_arguments); } #ifdef HAVE_RUBY_IO_BUFFER_H struct io_read_arguments { VALUE self; VALUE fiber; VALUE io; int flags; int descriptor; VALUE buffer; size_t length; size_t offset; }; static VALUE io_read_loop(VALUE _arguments) { struct io_read_arguments *arguments = (struct io_read_arguments *)_arguments; void *base; size_t size; rb_io_buffer_get_bytes_for_writing(arguments->buffer, &base, &size); size_t length = arguments->length; size_t offset = arguments->offset; size_t total = 0; // Ensure offset is within the bounds of the buffer to avoid size_t underflow and out-of-bounds pointer arithmetic on (char *)base + offset. if (offset > size) { return rb_fiber_scheduler_io_result(-1, EINVAL); } size_t maximum_size = size - offset; while (maximum_size) { ssize_t result = read(arguments->descriptor, (char*)base+offset, maximum_size); if (result > 0) { total += result; offset += result; if ((size_t)result >= length) break; length -= result; } else if (result == 0) { break; } else if (length > 0 && IO_Event_try_again(errno)) { IO_Event_Selector_EPoll_io_wait(arguments->self, arguments->fiber, arguments->io, RB_INT2NUM(IO_EVENT_READABLE)); } else { return rb_fiber_scheduler_io_result(-1, errno); } maximum_size = size - offset; } return rb_fiber_scheduler_io_result(total, 0); } static VALUE io_read_ensure(VALUE _arguments) { struct io_read_arguments *arguments = (struct io_read_arguments *)_arguments; IO_Event_Selector_nonblock_restore(arguments->descriptor, arguments->flags); return Qnil; } VALUE IO_Event_Selector_EPoll_io_read(VALUE self, VALUE fiber, VALUE io, VALUE buffer, VALUE _length, VALUE _offset) { int descriptor = IO_Event_Selector_io_descriptor(io); size_t offset = NUM2SIZET(_offset); size_t length = NUM2SIZET(_length); struct io_read_arguments io_read_arguments = { .self = self, .fiber = fiber, .io = io, .flags = IO_Event_Selector_nonblock_set(descriptor), .descriptor = descriptor, .buffer = buffer, .length = length, .offset = offset, }; RB_OBJ_WRITTEN(self, Qundef, fiber); return rb_ensure(io_read_loop, (VALUE)&io_read_arguments, io_read_ensure, (VALUE)&io_read_arguments); } VALUE IO_Event_Selector_EPoll_io_read_compatible(int argc, VALUE *argv, VALUE self) { rb_check_arity(argc, 4, 5); VALUE _offset = SIZET2NUM(0); if (argc == 5) { _offset = argv[4]; } return IO_Event_Selector_EPoll_io_read(self, argv[0], argv[1], argv[2], argv[3], _offset); } struct io_write_arguments { VALUE self; VALUE fiber; VALUE io; int flags; int descriptor; VALUE buffer; size_t length; size_t offset; }; static VALUE io_write_loop(VALUE _arguments) { struct io_write_arguments *arguments = (struct io_write_arguments *)_arguments; const void *base; size_t size; rb_io_buffer_get_bytes_for_reading(arguments->buffer, &base, &size); size_t length = arguments->length; size_t offset = arguments->offset; size_t total = 0; if (length > size) { rb_raise(rb_eRuntimeError, "Length exceeds size of buffer!"); } // Ensure offset is within the bounds of the buffer to avoid size_t underflow and out-of-bounds pointer arithmetic on (char *)base + offset. if (offset > size) { return rb_fiber_scheduler_io_result(-1, EINVAL); } size_t maximum_size = size - offset; while (maximum_size) { ssize_t result = write(arguments->descriptor, (char*)base+offset, maximum_size); if (result > 0) { total += result; offset += result; if ((size_t)result >= length) break; length -= result; } else if (result == 0) { break; } else if (length > 0 && IO_Event_try_again(errno)) { IO_Event_Selector_EPoll_io_wait(arguments->self, arguments->fiber, arguments->io, RB_INT2NUM(IO_EVENT_WRITABLE)); } else { return rb_fiber_scheduler_io_result(-1, errno); } maximum_size = size - offset; } return rb_fiber_scheduler_io_result(total, 0); }; static VALUE io_write_ensure(VALUE _arguments) { struct io_write_arguments *arguments = (struct io_write_arguments *)_arguments; IO_Event_Selector_nonblock_restore(arguments->descriptor, arguments->flags); return Qnil; }; VALUE IO_Event_Selector_EPoll_io_write(VALUE self, VALUE fiber, VALUE io, VALUE buffer, VALUE _length, VALUE _offset) { int descriptor = IO_Event_Selector_io_descriptor(io); size_t length = NUM2SIZET(_length); size_t offset = NUM2SIZET(_offset); struct io_write_arguments io_write_arguments = { .self = self, .fiber = fiber, .io = io, .flags = IO_Event_Selector_nonblock_set(descriptor), .descriptor = descriptor, .buffer = buffer, .length = length, .offset = offset, }; RB_OBJ_WRITTEN(self, Qundef, fiber); return rb_ensure(io_write_loop, (VALUE)&io_write_arguments, io_write_ensure, (VALUE)&io_write_arguments); } VALUE IO_Event_Selector_EPoll_io_write_compatible(int argc, VALUE *argv, VALUE self) { rb_check_arity(argc, 4, 5); VALUE _offset = SIZET2NUM(0); if (argc == 5) { _offset = argv[4]; } return IO_Event_Selector_EPoll_io_write(self, argv[0], argv[1], argv[2], argv[3], _offset); } #endif static struct timespec * make_timeout(VALUE duration, struct timespec * storage) { if (duration == Qnil) { return NULL; } if (RB_INTEGER_TYPE_P(duration)) { storage->tv_sec = NUM2TIMET(duration); storage->tv_nsec = 0; return storage; } duration = rb_to_float(duration); double value = RFLOAT_VALUE(duration); time_t seconds = value; storage->tv_sec = seconds; storage->tv_nsec = (value - seconds) * 1000000000L; return storage; } static int timeout_nonblocking(struct timespec * timespec) { return timespec && timespec->tv_sec == 0 && timespec->tv_nsec == 0; } struct select_arguments { struct IO_Event_Selector_EPoll *selector; int count; struct epoll_event events[EPOLL_MAX_EVENTS]; struct timespec * timeout; struct timespec storage; struct IO_Event_List saved; }; static int make_timeout_ms(struct timespec * timeout) { if (timeout == NULL) { return -1; } if (timeout_nonblocking(timeout)) { return 0; } return (timeout->tv_sec * 1000) + (timeout->tv_nsec / 1000000); } static int enosys_error(int result) { if (result == -1) { return errno == ENOSYS; } return 0; } static void * select_internal(void *_arguments) { struct select_arguments * arguments = (struct select_arguments *)_arguments; #if defined(HAVE_EPOLL_PWAIT2) arguments->count = epoll_pwait2(arguments->selector->descriptor, arguments->events, EPOLL_MAX_EVENTS, arguments->timeout, NULL); // Comment out the above line and enable the below lines to test ENOSYS code path. // arguments->count = -1; // errno = ENOSYS; if (!enosys_error(arguments->count)) { return NULL; } else { // Fall through and execute epoll_wait fallback. } #endif arguments->count = epoll_wait(arguments->selector->descriptor, arguments->events, EPOLL_MAX_EVENTS, make_timeout_ms(arguments->timeout)); return NULL; } static void select_internal_without_gvl(struct select_arguments *arguments) { arguments->selector->blocked = 1; rb_thread_call_without_gvl(select_internal, (void *)arguments, RUBY_UBF_IO, 0); arguments->selector->blocked = 0; if (arguments->count == -1) { if (errno != EINTR) { rb_sys_fail("select_internal_without_gvl:epoll_wait"); } else { arguments->count = 0; } } } static void select_internal_with_gvl(struct select_arguments *arguments) { select_internal((void *)arguments); if (arguments->count == -1) { if (errno != EINTR) { rb_sys_fail("select_internal_with_gvl:epoll_wait"); } else { arguments->count = 0; } } } static int IO_Event_Selector_EPoll_handle(struct IO_Event_Selector_EPoll *selector, const struct epoll_event *event, struct IO_Event_List *saved) { int descriptor = event->data.fd; // This is the mask of all events that occured for the given descriptor: enum IO_Event ready_events = events_from_epoll_flags(event->events); struct IO_Event_Selector_EPoll_Descriptor *epoll_descriptor = IO_Event_Selector_EPoll_Descriptor_lookup(selector, descriptor); struct IO_Event_List *list = &epoll_descriptor->list; struct IO_Event_List *node = list->tail; // Reset the events back to 0 so that we can re-arm if necessary: epoll_descriptor->waiting_events = 0; if (DEBUG) fprintf(stderr, "IO_Event_Selector_EPoll_handle: descriptor=%d, ready_events=%d epoll_descriptor=%p\n", descriptor, ready_events, epoll_descriptor); // It's possible (but unlikely) that the address of list will changing during iteration. while (node != list) { if (DEBUG) fprintf(stderr, "IO_Event_Selector_EPoll_handle: node=%p list=%p type=%p\n", node, list, node->type); struct IO_Event_Selector_EPoll_Waiting *waiting = (struct IO_Event_Selector_EPoll_Waiting *)node; // Compute the intersection of the events we are waiting for and the events that occured: enum IO_Event matching_events = waiting->events & ready_events; 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); if (matching_events) { IO_Event_List_append(node, saved); // Resume the fiber: waiting->ready = matching_events; IO_Event_Selector_loop_resume(&selector->backend, waiting->fiber, 0, NULL); node = saved->tail; IO_Event_List_pop(saved); } else { // We are still waiting for the events: epoll_descriptor->waiting_events |= waiting->events; node = node->tail; } } return IO_Event_Selector_EPoll_Descriptor_update(selector, epoll_descriptor->io, descriptor, epoll_descriptor); } static VALUE select_handle_events(VALUE _arguments) { struct select_arguments *arguments = (struct select_arguments *)_arguments; struct IO_Event_Selector_EPoll *selector = arguments->selector; for (int i = 0; i < arguments->count; i += 1) { const struct epoll_event *event = &arguments->events[i]; if (DEBUG) fprintf(stderr, "-> fd=%d events=%d\n", event->data.fd, event->events); if (event->data.fd >= 0) { IO_Event_Selector_EPoll_handle(selector, event, &arguments->saved); } else { IO_Event_Interrupt_clear(&selector->interrupt); } } return INT2NUM(arguments->count); } static VALUE select_handle_events_ensure(VALUE _arguments) { struct select_arguments *arguments = (struct select_arguments *)_arguments; IO_Event_List_free(&arguments->saved); return Qnil; } // TODO This function is not re-entrant and we should document and assert as such. VALUE IO_Event_Selector_EPoll_select(VALUE self, VALUE duration) { struct IO_Event_Selector_EPoll *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); selector->idle_duration.tv_sec = 0; selector->idle_duration.tv_nsec = 0; int ready = IO_Event_Selector_ready_flush(&selector->backend); struct select_arguments arguments = { .selector = selector, .storage = { .tv_sec = 0, .tv_nsec = 0 }, .saved = {}, }; arguments.timeout = &arguments.storage; // Process any currently pending events: select_internal_with_gvl(&arguments); // If we: // 1. Didn't process any ready fibers, and // 2. Didn't process any events from non-blocking select (above), and // 3. There are no items in the ready list, // then we can perform a blocking select. if (!ready && !arguments.count && !selector->backend.ready) { arguments.timeout = make_timeout(duration, &arguments.storage); if (!timeout_nonblocking(arguments.timeout)) { struct timespec start_time; IO_Event_Time_current(&start_time); // Wait for events to occur: select_internal_without_gvl(&arguments); struct timespec end_time; IO_Event_Time_current(&end_time); IO_Event_Time_elapsed(&start_time, &end_time, &selector->idle_duration); } } if (arguments.count) { return rb_ensure(select_handle_events, (VALUE)&arguments, select_handle_events_ensure, (VALUE)&arguments); } else { return RB_INT2NUM(0); } } VALUE IO_Event_Selector_EPoll_wakeup(VALUE self) { struct IO_Event_Selector_EPoll *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_EPoll, &IO_Event_Selector_EPoll_Type, selector); // If we are blocking, we can schedule a nop event to wake up the selector: if (selector->blocked) { IO_Event_Interrupt_signal(&selector->interrupt); return Qtrue; } return Qfalse; } static int IO_Event_Selector_EPoll_supported_p(void) { int fd = epoll_create1(EPOLL_CLOEXEC); if (fd < 0) { rb_warn("epoll_create1() was available at compile time but failed at run time: %s\n", strerror(errno)); return 0; } close(fd); return 1; } void Init_IO_Event_Selector_EPoll(VALUE IO_Event_Selector) { if (!IO_Event_Selector_EPoll_supported_p()) { return; } VALUE IO_Event_Selector_EPoll = rb_define_class_under(IO_Event_Selector, "EPoll", rb_cObject); rb_define_alloc_func(IO_Event_Selector_EPoll, IO_Event_Selector_EPoll_allocate); rb_define_method(IO_Event_Selector_EPoll, "initialize", IO_Event_Selector_EPoll_initialize, 1); rb_define_method(IO_Event_Selector_EPoll, "loop", IO_Event_Selector_EPoll_loop, 0); rb_define_method(IO_Event_Selector_EPoll, "idle_duration", IO_Event_Selector_EPoll_idle_duration, 0); rb_define_method(IO_Event_Selector_EPoll, "transfer", IO_Event_Selector_EPoll_transfer, 0); rb_define_method(IO_Event_Selector_EPoll, "resume", IO_Event_Selector_EPoll_resume, -1); rb_define_method(IO_Event_Selector_EPoll, "yield", IO_Event_Selector_EPoll_yield, 0); rb_define_method(IO_Event_Selector_EPoll, "push", IO_Event_Selector_EPoll_push, 1); rb_define_method(IO_Event_Selector_EPoll, "raise", IO_Event_Selector_EPoll_raise, -1); rb_define_method(IO_Event_Selector_EPoll, "ready?", IO_Event_Selector_EPoll_ready_p, 0); rb_define_method(IO_Event_Selector_EPoll, "select", IO_Event_Selector_EPoll_select, 1); rb_define_method(IO_Event_Selector_EPoll, "wakeup", IO_Event_Selector_EPoll_wakeup, 0); rb_define_method(IO_Event_Selector_EPoll, "close", IO_Event_Selector_EPoll_close, 0); rb_define_method(IO_Event_Selector_EPoll, "io_wait", IO_Event_Selector_EPoll_io_wait, 3); #ifdef HAVE_RUBY_IO_BUFFER_H rb_define_method(IO_Event_Selector_EPoll, "io_read", IO_Event_Selector_EPoll_io_read_compatible, -1); rb_define_method(IO_Event_Selector_EPoll, "io_write", IO_Event_Selector_EPoll_io_write_compatible, -1); #endif // Once compatibility isn't a concern, we can do this: // rb_define_method(IO_Event_Selector_EPoll, "io_read", IO_Event_Selector_EPoll_io_read, 5); // rb_define_method(IO_Event_Selector_EPoll, "io_write", IO_Event_Selector_EPoll_io_write, 5); rb_define_method(IO_Event_Selector_EPoll, "process_wait", IO_Event_Selector_EPoll_process_wait, 3); } socketry-io-event-ccd0953/ext/io/event/selector/epoll.h000066400000000000000000000003171516444210200231510ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2021-2025, by Samuel Williams. #pragma once #include #define IO_EVENT_SELECTOR_EPOLL void Init_IO_Event_Selector_EPoll(VALUE IO_Event_Selector); socketry-io-event-ccd0953/ext/io/event/selector/kqueue.c000066400000000000000000001021541516444210200233320ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2021-2025, by Samuel Williams. #include "kqueue.h" #include "selector.h" #include "../list.h" #include "../array.h" #include #include #include #include #include #include #include "../interrupt.h" enum { DEBUG = 0, DEBUG_IO_READ = 0, DEBUG_IO_WRITE = 0, DEBUG_IO_WAIT = 0 }; #ifndef EVFILT_USER #define IO_EVENT_SELECTOR_KQUEUE_USE_INTERRUPT #endif enum {KQUEUE_MAX_EVENTS = 64}; // This represents an actual fiber waiting for a specific event. struct IO_Event_Selector_KQueue_Waiting { struct IO_Event_List list; // The events the fiber is waiting for. enum IO_Event events; // The events that are currently ready. enum IO_Event ready; // The fiber value itself. VALUE fiber; }; struct IO_Event_Selector_KQueue { struct IO_Event_Selector backend; int descriptor; // Flag indicating whether the selector is currently blocked in a system call. // Set to 1 when blocked in kevent() without GVL, 0 otherwise. // Used by wakeup() to determine if an interrupt signal is needed. int blocked; struct timespec idle_duration; #ifdef IO_EVENT_SELECTOR_KQUEUE_USE_INTERRUPT struct IO_Event_Interrupt interrupt; #endif struct IO_Event_Array descriptors; }; // This represents zero or more fibers waiting for a specific descriptor. struct IO_Event_Selector_KQueue_Descriptor { struct IO_Event_List list; // The union of all events we are waiting for: enum IO_Event waiting_events; // The union of events we are registered for: enum IO_Event registered_events; // The events that are currently ready: enum IO_Event ready_events; }; static void IO_Event_Selector_KQueue_Waiting_mark(struct IO_Event_List *_waiting) { struct IO_Event_Selector_KQueue_Waiting *waiting = (void*)_waiting; if (waiting->fiber) { rb_gc_mark_movable(waiting->fiber); } } static void IO_Event_Selector_KQueue_Descriptor_mark(void *_descriptor) { struct IO_Event_Selector_KQueue_Descriptor *descriptor = _descriptor; IO_Event_List_immutable_each(&descriptor->list, IO_Event_Selector_KQueue_Waiting_mark); } static void IO_Event_Selector_KQueue_Type_mark(void *_selector) { struct IO_Event_Selector_KQueue *selector = _selector; IO_Event_Selector_mark(&selector->backend); IO_Event_Array_each(&selector->descriptors, IO_Event_Selector_KQueue_Descriptor_mark); } static void IO_Event_Selector_KQueue_Waiting_compact(struct IO_Event_List *_waiting) { struct IO_Event_Selector_KQueue_Waiting *waiting = (void*)_waiting; if (waiting->fiber) { waiting->fiber = rb_gc_location(waiting->fiber); } } static void IO_Event_Selector_KQueue_Descriptor_compact(void *_descriptor) { struct IO_Event_Selector_KQueue_Descriptor *descriptor = _descriptor; IO_Event_List_immutable_each(&descriptor->list, IO_Event_Selector_KQueue_Waiting_compact); } static void IO_Event_Selector_KQueue_Type_compact(void *_selector) { struct IO_Event_Selector_KQueue *selector = _selector; IO_Event_Selector_compact(&selector->backend); IO_Event_Array_each(&selector->descriptors, IO_Event_Selector_KQueue_Descriptor_compact); } static void close_internal(struct IO_Event_Selector_KQueue *selector) { if (selector->descriptor >= 0) { close(selector->descriptor); selector->descriptor = -1; } } static void IO_Event_Selector_KQueue_Type_free(void *_selector) { struct IO_Event_Selector_KQueue *selector = _selector; close_internal(selector); IO_Event_Array_free(&selector->descriptors); xfree(selector); } static size_t IO_Event_Selector_KQueue_Type_size(const void *_selector) { const struct IO_Event_Selector_KQueue *selector = _selector; return sizeof(struct IO_Event_Selector_KQueue) + IO_Event_Array_memory_size(&selector->descriptors) ; } static const rb_data_type_t IO_Event_Selector_KQueue_Type = { .wrap_struct_name = "IO::Event::Backend::KQueue", .function = { .dmark = IO_Event_Selector_KQueue_Type_mark, .dcompact = IO_Event_Selector_KQueue_Type_compact, .dfree = IO_Event_Selector_KQueue_Type_free, .dsize = IO_Event_Selector_KQueue_Type_size, }, .data = NULL, .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED, }; inline static struct IO_Event_Selector_KQueue_Descriptor * IO_Event_Selector_KQueue_Descriptor_lookup(struct IO_Event_Selector_KQueue *selector, uintptr_t descriptor) { struct IO_Event_Selector_KQueue_Descriptor *kqueue_descriptor = IO_Event_Array_lookup(&selector->descriptors, descriptor); if (!kqueue_descriptor) { rb_sys_fail("IO_Event_Selector_KQueue_Descriptor_lookup:IO_Event_Array_lookup"); } return kqueue_descriptor; } inline static enum IO_Event events_from_kevent_filter(int filter) { switch (filter) { case EVFILT_READ: return IO_EVENT_READABLE; case EVFILT_WRITE: return IO_EVENT_WRITABLE; case EVFILT_PROC: return IO_EVENT_EXIT; default: return 0; } } inline static int IO_Event_Selector_KQueue_Descriptor_update(struct IO_Event_Selector_KQueue *selector, uintptr_t identifier, struct IO_Event_Selector_KQueue_Descriptor *kqueue_descriptor) { int count = 0; struct kevent kevents[3] = {0}; if (kqueue_descriptor->waiting_events & IO_EVENT_READABLE) { kevents[count].ident = identifier; kevents[count].filter = EVFILT_READ; kevents[count].flags = EV_ADD | EV_ONESHOT; kevents[count].udata = (void *)kqueue_descriptor; // #ifdef EV_OOBAND // if (events & IO_EVENT_PRIORITY) { // kevents[count].flags |= EV_OOBAND; // } // #endif count++; } if (kqueue_descriptor->waiting_events & IO_EVENT_WRITABLE) { kevents[count].ident = identifier; kevents[count].filter = EVFILT_WRITE; kevents[count].flags = EV_ADD | EV_ONESHOT; kevents[count].udata = (void *)kqueue_descriptor; count++; } if (kqueue_descriptor->waiting_events & IO_EVENT_EXIT) { kevents[count].ident = identifier; kevents[count].filter = EVFILT_PROC; kevents[count].flags = EV_ADD | EV_ONESHOT; kevents[count].fflags = NOTE_EXIT; kevents[count].udata = (void *)kqueue_descriptor; count++; } if (count == 0) { return 0; } int result = kevent(selector->descriptor, kevents, count, NULL, 0, NULL); if (result == -1) { return result; } kqueue_descriptor->registered_events = kqueue_descriptor->waiting_events; return result; } inline static int IO_Event_Selector_KQueue_Waiting_register(struct IO_Event_Selector_KQueue *selector, uintptr_t identifier, struct IO_Event_Selector_KQueue_Waiting *waiting) { struct IO_Event_Selector_KQueue_Descriptor *kqueue_descriptor = IO_Event_Selector_KQueue_Descriptor_lookup(selector, identifier); // We are waiting for these events: kqueue_descriptor->waiting_events |= waiting->events; int result = IO_Event_Selector_KQueue_Descriptor_update(selector, identifier, kqueue_descriptor); if (result == -1) return -1; IO_Event_List_prepend(&kqueue_descriptor->list, &waiting->list); return result; } inline static void IO_Event_Selector_KQueue_Waiting_cancel(struct IO_Event_Selector_KQueue_Waiting *waiting) { IO_Event_List_pop(&waiting->list); waiting->fiber = 0; } void IO_Event_Selector_KQueue_Descriptor_initialize(void *element) { struct IO_Event_Selector_KQueue_Descriptor *kqueue_descriptor = element; IO_Event_List_initialize(&kqueue_descriptor->list); kqueue_descriptor->waiting_events = 0; kqueue_descriptor->registered_events = 0; kqueue_descriptor->ready_events = 0; } void IO_Event_Selector_KQueue_Descriptor_free(void *element) { struct IO_Event_Selector_KQueue_Descriptor *kqueue_descriptor = element; IO_Event_List_free(&kqueue_descriptor->list); } VALUE IO_Event_Selector_KQueue_allocate(VALUE self) { struct IO_Event_Selector_KQueue *selector = NULL; VALUE instance = TypedData_Make_Struct(self, struct IO_Event_Selector_KQueue, &IO_Event_Selector_KQueue_Type, selector); IO_Event_Selector_initialize(&selector->backend, self, Qnil); selector->descriptor = -1; selector->blocked = 0; selector->descriptors.element_initialize = IO_Event_Selector_KQueue_Descriptor_initialize; selector->descriptors.element_free = IO_Event_Selector_KQueue_Descriptor_free; int result = IO_Event_Array_initialize(&selector->descriptors, IO_EVENT_ARRAY_DEFAULT_COUNT, sizeof(struct IO_Event_Selector_KQueue_Descriptor)); if (result < 0) { rb_sys_fail("IO_Event_Selector_KQueue_allocate:IO_Event_Array_initialize"); } return instance; } #ifdef IO_EVENT_SELECTOR_KQUEUE_USE_INTERRUPT void IO_Event_Interrupt_add(struct IO_Event_Interrupt *interrupt, struct IO_Event_Selector_KQueue *selector) { int descriptor = IO_Event_Interrupt_descriptor(interrupt); struct kevent kev = { .filter = EVFILT_READ, .ident = descriptor, .flags = EV_ADD | EV_CLEAR, }; int result = kevent(selector->descriptor, &kev, 1, NULL, 0, NULL); if (result == -1) { rb_sys_fail("IO_Event_Interrupt_add:kevent"); } } #endif VALUE IO_Event_Selector_KQueue_initialize(VALUE self, VALUE loop) { struct IO_Event_Selector_KQueue *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_KQueue, &IO_Event_Selector_KQueue_Type, selector); IO_Event_Selector_initialize(&selector->backend, self, loop); int result = kqueue(); if (result == -1) { rb_sys_fail("IO_Event_Selector_KQueue_initialize:kqueue"); } else { // Make sure the descriptor is closed on exec. ioctl(result, FIOCLEX); selector->descriptor = result; rb_update_max_fd(selector->descriptor); } #ifdef IO_EVENT_SELECTOR_KQUEUE_USE_INTERRUPT IO_Event_Interrupt_open(&selector->interrupt); IO_Event_Interrupt_add(&selector->interrupt, selector); #endif return self; } VALUE IO_Event_Selector_KQueue_loop(VALUE self) { struct IO_Event_Selector_KQueue *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_KQueue, &IO_Event_Selector_KQueue_Type, selector); return selector->backend.loop; } VALUE IO_Event_Selector_KQueue_idle_duration(VALUE self) { struct IO_Event_Selector_KQueue *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_KQueue, &IO_Event_Selector_KQueue_Type, selector); double duration = selector->idle_duration.tv_sec + (selector->idle_duration.tv_nsec / 1000000000.0); return DBL2NUM(duration); } VALUE IO_Event_Selector_KQueue_close(VALUE self) { struct IO_Event_Selector_KQueue *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_KQueue, &IO_Event_Selector_KQueue_Type, selector); close_internal(selector); #ifdef IO_EVENT_SELECTOR_KQUEUE_USE_INTERRUPT IO_Event_Interrupt_close(&selector->interrupt); #endif return Qnil; } VALUE IO_Event_Selector_KQueue_transfer(VALUE self) { struct IO_Event_Selector_KQueue *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_KQueue, &IO_Event_Selector_KQueue_Type, selector); return IO_Event_Selector_loop_yield(&selector->backend); } VALUE IO_Event_Selector_KQueue_resume(int argc, VALUE *argv, VALUE self) { struct IO_Event_Selector_KQueue *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_KQueue, &IO_Event_Selector_KQueue_Type, selector); return IO_Event_Selector_resume(&selector->backend, argc, argv); } VALUE IO_Event_Selector_KQueue_yield(VALUE self) { struct IO_Event_Selector_KQueue *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_KQueue, &IO_Event_Selector_KQueue_Type, selector); return IO_Event_Selector_yield(&selector->backend); } VALUE IO_Event_Selector_KQueue_push(VALUE self, VALUE fiber) { struct IO_Event_Selector_KQueue *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_KQueue, &IO_Event_Selector_KQueue_Type, selector); IO_Event_Selector_ready_push(&selector->backend, fiber); return Qnil; } VALUE IO_Event_Selector_KQueue_raise(int argc, VALUE *argv, VALUE self) { struct IO_Event_Selector_KQueue *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_KQueue, &IO_Event_Selector_KQueue_Type, selector); return IO_Event_Selector_raise(&selector->backend, argc, argv); } VALUE IO_Event_Selector_KQueue_ready_p(VALUE self) { struct IO_Event_Selector_KQueue *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_KQueue, &IO_Event_Selector_KQueue_Type, selector); return selector->backend.ready ? Qtrue : Qfalse; } struct process_wait_arguments { struct IO_Event_Selector_KQueue *selector; struct IO_Event_Selector_KQueue_Waiting *waiting; pid_t pid; int flags; }; static void process_prewait(pid_t pid) { #if defined(WNOWAIT) // FreeBSD seems to have an issue where kevent() can return an EVFILT_PROC/NOTE_EXIT event for a process even though a wait with WNOHANG on it immediately after will not return it (but it does after a small delay). Similarly, OpenBSD/NetBSD seem to sometimes fail the kevent() call with ESRCH (indicating the process has already terminated) even though a WNOHANG may not return it immediately after. // To deal with this, do a hanging WNOWAIT wait on the process to make sure it is "terminated enough" for future WNOHANG waits to return it. // Using waitid() for this because OpenBSD only supports WNOWAIT with waitid(). int result; do { siginfo_t info; result = waitid(P_PID, pid, &info, WEXITED | WNOWAIT); // This can sometimes get interrupted by SIGCHLD. } while (result == -1 && errno == EINTR); if (result == -1) { rb_sys_fail("process_prewait:waitid"); } #endif } static VALUE process_wait_transfer(VALUE _arguments) { struct process_wait_arguments *arguments = (struct process_wait_arguments *)_arguments; IO_Event_Selector_loop_yield(&arguments->selector->backend); if (arguments->waiting->ready) { process_prewait(arguments->pid); return IO_Event_Selector_process_status_wait(arguments->pid, arguments->flags); } else { return Qfalse; } } static VALUE process_wait_ensure(VALUE _arguments) { struct process_wait_arguments *arguments = (struct process_wait_arguments *)_arguments; IO_Event_Selector_KQueue_Waiting_cancel(arguments->waiting); return Qnil; } struct IO_Event_List_Type IO_Event_Selector_KQueue_process_wait_list_type = {}; VALUE IO_Event_Selector_KQueue_process_wait(VALUE self, VALUE fiber, VALUE _pid, VALUE _flags) { struct IO_Event_Selector_KQueue *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_KQueue, &IO_Event_Selector_KQueue_Type, selector); pid_t pid = NUM2PIDT(_pid); int flags = NUM2INT(_flags); struct IO_Event_Selector_KQueue_Waiting waiting = { .list = {.type = &IO_Event_Selector_KQueue_process_wait_list_type}, .fiber = fiber, .events = IO_EVENT_EXIT, }; RB_OBJ_WRITTEN(self, Qundef, fiber); struct process_wait_arguments process_wait_arguments = { .selector = selector, .waiting = &waiting, .pid = pid, .flags = flags, }; int result = IO_Event_Selector_KQueue_Waiting_register(selector, pid, &waiting); if (result == -1) { // OpenBSD/NetBSD return ESRCH when attempting to register an EVFILT_PROC event for a zombie process. if (errno == ESRCH) { process_prewait(pid); return IO_Event_Selector_process_status_wait(pid, flags); } rb_sys_fail("IO_Event_Selector_KQueue_process_wait:IO_Event_Selector_KQueue_Waiting_register"); } return rb_ensure(process_wait_transfer, (VALUE)&process_wait_arguments, process_wait_ensure, (VALUE)&process_wait_arguments); } struct io_wait_arguments { struct IO_Event_Selector_KQueue *selector; struct IO_Event_Selector_KQueue_Waiting *waiting; }; static VALUE io_wait_ensure(VALUE _arguments) { struct io_wait_arguments *arguments = (struct io_wait_arguments *)_arguments; IO_Event_Selector_KQueue_Waiting_cancel(arguments->waiting); return Qnil; } static VALUE io_wait_transfer(VALUE _arguments) { struct io_wait_arguments *arguments = (struct io_wait_arguments *)_arguments; IO_Event_Selector_loop_yield(&arguments->selector->backend); if (arguments->waiting->ready) { return RB_INT2NUM(arguments->waiting->ready); } else { return Qfalse; } } struct IO_Event_List_Type IO_Event_Selector_KQueue_io_wait_list_type = {}; VALUE IO_Event_Selector_KQueue_io_wait(VALUE self, VALUE fiber, VALUE io, VALUE events) { struct IO_Event_Selector_KQueue *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_KQueue, &IO_Event_Selector_KQueue_Type, selector); int descriptor = IO_Event_Selector_io_descriptor(io); struct IO_Event_Selector_KQueue_Waiting waiting = { .list = {.type = &IO_Event_Selector_KQueue_io_wait_list_type}, .fiber = fiber, .events = RB_NUM2INT(events), }; RB_OBJ_WRITTEN(self, Qundef, fiber); int result = IO_Event_Selector_KQueue_Waiting_register(selector, descriptor, &waiting); if (result == -1) { rb_sys_fail("IO_Event_Selector_KQueue_io_wait:IO_Event_Selector_KQueue_Waiting_register"); } struct io_wait_arguments io_wait_arguments = { .selector = selector, .waiting = &waiting, }; if (DEBUG_IO_WAIT) fprintf(stderr, "IO_Event_Selector_KQueue_io_wait descriptor=%d\n", descriptor); return rb_ensure(io_wait_transfer, (VALUE)&io_wait_arguments, io_wait_ensure, (VALUE)&io_wait_arguments); } #ifdef HAVE_RUBY_IO_BUFFER_H struct io_read_arguments { VALUE self; VALUE fiber; VALUE io; int flags; int descriptor; VALUE buffer; size_t length; size_t offset; }; static VALUE io_read_loop(VALUE _arguments) { struct io_read_arguments *arguments = (struct io_read_arguments *)_arguments; void *base; size_t size; rb_io_buffer_get_bytes_for_writing(arguments->buffer, &base, &size); size_t length = arguments->length; size_t offset = arguments->offset; size_t total = 0; if (DEBUG_IO_READ) fprintf(stderr, "io_read_loop(fd=%d, length=%zu)\n", arguments->descriptor, length); // Ensure offset is within the bounds of the buffer to avoid size_t underflow and out-of-bounds pointer arithmetic on (char *)base + offset. if (offset > size) { return rb_fiber_scheduler_io_result(-1, EINVAL); } size_t maximum_size = size - offset; while (maximum_size) { if (DEBUG_IO_READ) fprintf(stderr, "read(%d, +%ld, %ld)\n", arguments->descriptor, offset, maximum_size); ssize_t result = read(arguments->descriptor, (char*)base+offset, maximum_size); if (DEBUG_IO_READ) fprintf(stderr, "read(%d, +%ld, %ld) -> %zd\n", arguments->descriptor, offset, maximum_size, result); if (result > 0) { total += result; offset += result; if ((size_t)result >= length) break; length -= result; } else if (result == 0) { break; } else if (length > 0 && IO_Event_try_again(errno)) { if (DEBUG_IO_READ) fprintf(stderr, "IO_Event_Selector_KQueue_io_wait(fd=%d, length=%zu)\n", arguments->descriptor, length); IO_Event_Selector_KQueue_io_wait(arguments->self, arguments->fiber, arguments->io, RB_INT2NUM(IO_EVENT_READABLE)); } else { if (DEBUG_IO_READ) fprintf(stderr, "io_read_loop(fd=%d, length=%zu) -> errno=%d\n", arguments->descriptor, length, errno); return rb_fiber_scheduler_io_result(-1, errno); } maximum_size = size - offset; } if (DEBUG_IO_READ) fprintf(stderr, "io_read_loop(fd=%d, length=%zu) -> %zu\n", arguments->descriptor, length, offset); return rb_fiber_scheduler_io_result(total, 0); } static VALUE io_read_ensure(VALUE _arguments) { struct io_read_arguments *arguments = (struct io_read_arguments *)_arguments; IO_Event_Selector_nonblock_restore(arguments->descriptor, arguments->flags); return Qnil; } VALUE IO_Event_Selector_KQueue_io_read(VALUE self, VALUE fiber, VALUE io, VALUE buffer, VALUE _length, VALUE _offset) { struct IO_Event_Selector_KQueue *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_KQueue, &IO_Event_Selector_KQueue_Type, selector); int descriptor = IO_Event_Selector_io_descriptor(io); size_t length = NUM2SIZET(_length); size_t offset = NUM2SIZET(_offset); struct io_read_arguments io_read_arguments = { .self = self, .fiber = fiber, .io = io, .flags = IO_Event_Selector_nonblock_set(descriptor), .descriptor = descriptor, .buffer = buffer, .length = length, .offset = offset, }; RB_OBJ_WRITTEN(self, Qundef, fiber); return rb_ensure(io_read_loop, (VALUE)&io_read_arguments, io_read_ensure, (VALUE)&io_read_arguments); } static VALUE IO_Event_Selector_KQueue_io_read_compatible(int argc, VALUE *argv, VALUE self) { rb_check_arity(argc, 4, 5); VALUE _offset = SIZET2NUM(0); if (argc == 5) { _offset = argv[4]; } return IO_Event_Selector_KQueue_io_read(self, argv[0], argv[1], argv[2], argv[3], _offset); } struct io_write_arguments { VALUE self; VALUE fiber; VALUE io; int flags; int descriptor; VALUE buffer; size_t length; size_t offset; }; static VALUE io_write_loop(VALUE _arguments) { struct io_write_arguments *arguments = (struct io_write_arguments *)_arguments; const void *base; size_t size; rb_io_buffer_get_bytes_for_reading(arguments->buffer, &base, &size); size_t length = arguments->length; size_t offset = arguments->offset; size_t total = 0; if (length > size) { rb_raise(rb_eRuntimeError, "Length exceeds size of buffer!"); } if (DEBUG_IO_WRITE) fprintf(stderr, "io_write_loop(fd=%d, length=%zu)\n", arguments->descriptor, length); // Ensure offset is within the bounds of the buffer to avoid size_t underflow and out-of-bounds pointer arithmetic on (char *)base + offset. if (offset > size) { return rb_fiber_scheduler_io_result(-1, EINVAL); } size_t maximum_size = size - offset; while (maximum_size) { if (DEBUG_IO_WRITE) fprintf(stderr, "write(%d, +%ld, %ld, length=%zu)\n", arguments->descriptor, offset, maximum_size, length); ssize_t result = write(arguments->descriptor, (char*)base+offset, maximum_size); if (DEBUG_IO_WRITE) fprintf(stderr, "write(%d, +%ld, %ld) -> %zd\n", arguments->descriptor, offset, maximum_size, result); if (result > 0) { total += result; offset += result; if ((size_t)result >= length) break; length -= result; } else if (result == 0) { break; } else if (length > 0 && IO_Event_try_again(errno)) { if (DEBUG_IO_WRITE) fprintf(stderr, "IO_Event_Selector_KQueue_io_wait(fd=%d, length=%zu)\n", arguments->descriptor, length); IO_Event_Selector_KQueue_io_wait(arguments->self, arguments->fiber, arguments->io, RB_INT2NUM(IO_EVENT_WRITABLE)); } else { if (DEBUG_IO_WRITE) fprintf(stderr, "io_write_loop(fd=%d, length=%zu) -> errno=%d\n", arguments->descriptor, length, errno); return rb_fiber_scheduler_io_result(-1, errno); } maximum_size = size - offset; } if (DEBUG_IO_WRITE) fprintf(stderr, "io_write_loop(fd=%d, length=%zu) -> %zu\n", arguments->descriptor, length, offset); return rb_fiber_scheduler_io_result(total, 0); }; static VALUE io_write_ensure(VALUE _arguments) { struct io_write_arguments *arguments = (struct io_write_arguments *)_arguments; IO_Event_Selector_nonblock_restore(arguments->descriptor, arguments->flags); return Qnil; }; VALUE IO_Event_Selector_KQueue_io_write(VALUE self, VALUE fiber, VALUE io, VALUE buffer, VALUE _length, VALUE _offset) { struct IO_Event_Selector_KQueue *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_KQueue, &IO_Event_Selector_KQueue_Type, selector); int descriptor = IO_Event_Selector_io_descriptor(io); size_t length = NUM2SIZET(_length); size_t offset = NUM2SIZET(_offset); struct io_write_arguments io_write_arguments = { .self = self, .fiber = fiber, .io = io, .flags = IO_Event_Selector_nonblock_set(descriptor), .descriptor = descriptor, .buffer = buffer, .length = length, .offset = offset, }; RB_OBJ_WRITTEN(self, Qundef, fiber); return rb_ensure(io_write_loop, (VALUE)&io_write_arguments, io_write_ensure, (VALUE)&io_write_arguments); } static VALUE IO_Event_Selector_KQueue_io_write_compatible(int argc, VALUE *argv, VALUE self) { rb_check_arity(argc, 4, 5); VALUE _offset = SIZET2NUM(0); if (argc == 5) { _offset = argv[4]; } return IO_Event_Selector_KQueue_io_write(self, argv[0], argv[1], argv[2], argv[3], _offset); } #endif static struct timespec * make_timeout(VALUE duration, struct timespec * storage) { if (duration == Qnil) { return NULL; } if (RB_INTEGER_TYPE_P(duration)) { storage->tv_sec = NUM2TIMET(duration); storage->tv_nsec = 0; return storage; } duration = rb_to_float(duration); double value = RFLOAT_VALUE(duration); time_t seconds = value; storage->tv_sec = seconds; storage->tv_nsec = (value - seconds) * 1000000000L; return storage; } static int timeout_nonblocking(struct timespec * timespec) { return timespec && timespec->tv_sec == 0 && timespec->tv_nsec == 0; } struct select_arguments { struct IO_Event_Selector_KQueue *selector; int count; struct kevent events[KQUEUE_MAX_EVENTS]; struct timespec storage; struct timespec *timeout; struct IO_Event_List saved; }; static void * select_internal(void *_arguments) { struct select_arguments * arguments = (struct select_arguments *)_arguments; arguments->count = kevent(arguments->selector->descriptor, NULL, 0, arguments->events, arguments->count, arguments->timeout); return NULL; } static void select_internal_without_gvl(struct select_arguments *arguments) { arguments->selector->blocked = 1; rb_thread_call_without_gvl(select_internal, (void *)arguments, RUBY_UBF_IO, 0); arguments->selector->blocked = 0; if (arguments->count == -1) { if (errno != EINTR) { rb_sys_fail("select_internal_without_gvl:kevent"); } else { arguments->count = 0; } } } static void select_internal_with_gvl(struct select_arguments *arguments) { select_internal((void *)arguments); if (arguments->count == -1) { if (errno != EINTR) { rb_sys_fail("select_internal_with_gvl:kevent"); } else { arguments->count = 0; } } } static int IO_Event_Selector_KQueue_handle(struct IO_Event_Selector_KQueue *selector, uintptr_t identifier, struct IO_Event_Selector_KQueue_Descriptor *kqueue_descriptor, struct IO_Event_List *saved) { // This is the mask of all events that occured for the given descriptor: enum IO_Event ready_events = kqueue_descriptor->ready_events; if (ready_events) { kqueue_descriptor->ready_events = 0; // Since we use one-shot semantics, we need to re-arm the events that are ready if needed: kqueue_descriptor->registered_events &= ~ready_events; } else { return 0; } struct IO_Event_List *list = &kqueue_descriptor->list; struct IO_Event_List *node = list->tail; // Reset the events back to 0 so that we can re-arm if necessary: kqueue_descriptor->waiting_events = 0; // It's possible (but unlikely) that the address of list will changing during iteration. while (node != list) { struct IO_Event_Selector_KQueue_Waiting *waiting = (struct IO_Event_Selector_KQueue_Waiting *)node; enum IO_Event matching_events = waiting->events & ready_events; if (DEBUG) fprintf(stderr, "IO_Event_Selector_KQueue_handle: identifier=%lu, ready_events=%d, matching_events=%d\n", identifier, ready_events, matching_events); if (matching_events) { IO_Event_List_append(node, saved); waiting->ready = matching_events; IO_Event_Selector_loop_resume(&selector->backend, waiting->fiber, 0, NULL); node = saved->tail; IO_Event_List_pop(saved); } else { kqueue_descriptor->waiting_events |= waiting->events; node = node->tail; } } return IO_Event_Selector_KQueue_Descriptor_update(selector, identifier, kqueue_descriptor); } static VALUE select_handle_events(VALUE _arguments) { struct select_arguments *arguments = (struct select_arguments *)_arguments; struct IO_Event_Selector_KQueue *selector = arguments->selector; for (int i = 0; i < arguments->count; i += 1) { if (arguments->events[i].udata) { struct IO_Event_Selector_KQueue_Descriptor *kqueue_descriptor = arguments->events[i].udata; kqueue_descriptor->ready_events |= events_from_kevent_filter(arguments->events[i].filter); } } for (int i = 0; i < arguments->count; i += 1) { if (arguments->events[i].udata) { struct IO_Event_Selector_KQueue_Descriptor *kqueue_descriptor = arguments->events[i].udata; IO_Event_Selector_KQueue_handle(selector, arguments->events[i].ident, kqueue_descriptor, &arguments->saved); } else { #ifdef IO_EVENT_SELECTOR_KQUEUE_USE_INTERRUPT IO_Event_Interrupt_clear(&selector->interrupt); #endif } } return RB_INT2NUM(arguments->count); } static VALUE select_handle_events_ensure(VALUE _arguments) { struct select_arguments *arguments = (struct select_arguments *)_arguments; IO_Event_List_free(&arguments->saved); return Qnil; } VALUE IO_Event_Selector_KQueue_select(VALUE self, VALUE duration) { struct IO_Event_Selector_KQueue *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_KQueue, &IO_Event_Selector_KQueue_Type, selector); selector->idle_duration.tv_sec = 0; selector->idle_duration.tv_nsec = 0; int ready = IO_Event_Selector_ready_flush(&selector->backend); struct select_arguments arguments = { .selector = selector, .count = KQUEUE_MAX_EVENTS, .storage = { .tv_sec = 0, .tv_nsec = 0 }, .saved = {}, }; arguments.timeout = &arguments.storage; // We break this implementation into two parts. // (1) count = kevent(..., timeout = 0) // (2) without gvl: kevent(..., timeout = 0) if count == 0 and timeout != 0 // This allows us to avoid releasing and reacquiring the GVL. // Non-comprehensive testing shows this gives a 1.5x speedup. // First do the syscall with no timeout to get any immediately available events: if (DEBUG) fprintf(stderr, "\r\nselect_internal_with_gvl timeout=" IO_EVENT_TIME_PRINTF_TIMESPEC "\r\n", IO_EVENT_TIME_PRINTF_TIMESPEC_ARGUMENTS(arguments.storage)); select_internal_with_gvl(&arguments); if (DEBUG) fprintf(stderr, "\r\nselect_internal_with_gvl done\r\n"); // If we: // 1. Didn't process any ready fibers, and // 2. Didn't process any events from non-blocking select (above), and // 3. There are no items in the ready list, // then we can perform a blocking select. if (!ready && !arguments.count && !selector->backend.ready) { arguments.timeout = make_timeout(duration, &arguments.storage); if (!timeout_nonblocking(arguments.timeout)) { arguments.count = KQUEUE_MAX_EVENTS; struct timespec start_time; IO_Event_Time_current(&start_time); if (DEBUG) fprintf(stderr, "IO_Event_Selector_KQueue_select timeout=" IO_EVENT_TIME_PRINTF_TIMESPEC "\n", IO_EVENT_TIME_PRINTF_TIMESPEC_ARGUMENTS(arguments.storage)); select_internal_without_gvl(&arguments); struct timespec end_time; IO_Event_Time_current(&end_time); IO_Event_Time_elapsed(&start_time, &end_time, &selector->idle_duration); } } if (arguments.count) { return rb_ensure(select_handle_events, (VALUE)&arguments, select_handle_events_ensure, (VALUE)&arguments); } else { return RB_INT2NUM(0); } } VALUE IO_Event_Selector_KQueue_wakeup(VALUE self) { struct IO_Event_Selector_KQueue *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_KQueue, &IO_Event_Selector_KQueue_Type, selector); if (selector->blocked) { #ifdef IO_EVENT_SELECTOR_KQUEUE_USE_INTERRUPT IO_Event_Interrupt_signal(&selector->interrupt); #else struct kevent trigger = {0}; trigger.filter = EVFILT_USER; trigger.flags = EV_ADD | EV_CLEAR; int result = kevent(selector->descriptor, &trigger, 1, NULL, 0, NULL); if (result == -1) { rb_sys_fail("IO_Event_Selector_KQueue_wakeup:kevent"); } // FreeBSD apparently only works if the NOTE_TRIGGER is done as a separate call. trigger.flags = 0; trigger.fflags = NOTE_TRIGGER; result = kevent(selector->descriptor, &trigger, 1, NULL, 0, NULL); if (result == -1) { rb_sys_fail("IO_Event_Selector_KQueue_wakeup:kevent"); } #endif return Qtrue; } return Qfalse; } static int IO_Event_Selector_KQueue_supported_p(void) { int fd = kqueue(); if (fd < 0) { rb_warn("kqueue() was available at compile time but failed at run time: %s\n", strerror(errno)); return 0; } close(fd); return 1; } void Init_IO_Event_Selector_KQueue(VALUE IO_Event_Selector) { if (!IO_Event_Selector_KQueue_supported_p()) { return; } VALUE IO_Event_Selector_KQueue = rb_define_class_under(IO_Event_Selector, "KQueue", rb_cObject); rb_define_alloc_func(IO_Event_Selector_KQueue, IO_Event_Selector_KQueue_allocate); rb_define_method(IO_Event_Selector_KQueue, "initialize", IO_Event_Selector_KQueue_initialize, 1); rb_define_method(IO_Event_Selector_KQueue, "loop", IO_Event_Selector_KQueue_loop, 0); rb_define_method(IO_Event_Selector_KQueue, "idle_duration", IO_Event_Selector_KQueue_idle_duration, 0); rb_define_method(IO_Event_Selector_KQueue, "transfer", IO_Event_Selector_KQueue_transfer, 0); rb_define_method(IO_Event_Selector_KQueue, "resume", IO_Event_Selector_KQueue_resume, -1); rb_define_method(IO_Event_Selector_KQueue, "yield", IO_Event_Selector_KQueue_yield, 0); rb_define_method(IO_Event_Selector_KQueue, "push", IO_Event_Selector_KQueue_push, 1); rb_define_method(IO_Event_Selector_KQueue, "raise", IO_Event_Selector_KQueue_raise, -1); rb_define_method(IO_Event_Selector_KQueue, "ready?", IO_Event_Selector_KQueue_ready_p, 0); rb_define_method(IO_Event_Selector_KQueue, "select", IO_Event_Selector_KQueue_select, 1); rb_define_method(IO_Event_Selector_KQueue, "wakeup", IO_Event_Selector_KQueue_wakeup, 0); rb_define_method(IO_Event_Selector_KQueue, "close", IO_Event_Selector_KQueue_close, 0); rb_define_method(IO_Event_Selector_KQueue, "io_wait", IO_Event_Selector_KQueue_io_wait, 3); #ifdef HAVE_RUBY_IO_BUFFER_H rb_define_method(IO_Event_Selector_KQueue, "io_read", IO_Event_Selector_KQueue_io_read_compatible, -1); rb_define_method(IO_Event_Selector_KQueue, "io_write", IO_Event_Selector_KQueue_io_write_compatible, -1); #endif rb_define_method(IO_Event_Selector_KQueue, "process_wait", IO_Event_Selector_KQueue_process_wait, 3); } socketry-io-event-ccd0953/ext/io/event/selector/kqueue.h000066400000000000000000000003211516444210200233300ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2021-2025, by Samuel Williams. #pragma once #include #define IO_EVENT_SELECTOR_KQUEUE void Init_IO_Event_Selector_KQueue(VALUE IO_Event_Selector); socketry-io-event-ccd0953/ext/io/event/selector/pidfd.c000066400000000000000000000006611516444210200231210ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2021-2025, by Samuel Williams. #include #include #include #include #include #include #include #ifndef __NR_pidfd_open #define __NR_pidfd_open 434 /* System call # on most architectures */ #endif static int pidfd_open(pid_t pid, unsigned int flags) { return syscall(__NR_pidfd_open, pid, flags); } socketry-io-event-ccd0953/ext/io/event/selector/selector.c000066400000000000000000000220571516444210200236560ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2021-2025, by Samuel Williams. #include "selector.h" #include #include static const int DEBUG = 0; #ifndef HAVE_RB_IO_DESCRIPTOR static ID id_fileno; int IO_Event_Selector_io_descriptor(VALUE io) { return RB_NUM2INT(rb_funcall(io, id_fileno, 0)); } #endif #ifndef HAVE_RB_PROCESS_STATUS_WAIT static ID id_wait; static VALUE rb_Process_Status = Qnil; VALUE IO_Event_Selector_process_status_wait(rb_pid_t pid, int flags) { return rb_funcall(rb_Process_Status, id_wait, 2, PIDT2NUM(pid), INT2NUM(flags | WNOHANG)); } #endif int IO_Event_Selector_nonblock_set(int file_descriptor) { #ifdef _WIN32 return rb_w32_set_nonblock(file_descriptor); #else // Get the current mode: int flags = fcntl(file_descriptor, F_GETFL, 0); // Set the non-blocking flag if it isn't already: if (!(flags & O_NONBLOCK)) { fcntl(file_descriptor, F_SETFL, flags | O_NONBLOCK); } return flags; #endif } void IO_Event_Selector_nonblock_restore(int file_descriptor, int flags) { #ifdef _WIN32 // Yolo... #else // The flags didn't have O_NONBLOCK set, so it would have been set, so we need to restore it: if (!(flags & O_NONBLOCK)) { fcntl(file_descriptor, F_SETFL, flags); } #endif } struct IO_Event_Selector_nonblock_arguments { int file_descriptor; int flags; }; static VALUE IO_Event_Selector_nonblock_ensure(VALUE _arguments) { struct IO_Event_Selector_nonblock_arguments *arguments = (struct IO_Event_Selector_nonblock_arguments *)_arguments; IO_Event_Selector_nonblock_restore(arguments->file_descriptor, arguments->flags); return Qnil; } static VALUE IO_Event_Selector_nonblock(VALUE class, VALUE io) { struct IO_Event_Selector_nonblock_arguments arguments = { .file_descriptor = IO_Event_Selector_io_descriptor(io), .flags = IO_Event_Selector_nonblock_set(arguments.file_descriptor) }; return rb_ensure(rb_yield, io, IO_Event_Selector_nonblock_ensure, (VALUE)&arguments); } void Init_IO_Event_Selector(VALUE IO_Event_Selector) { #ifndef HAVE_RB_IO_DESCRIPTOR id_fileno = rb_intern("fileno"); #endif #ifndef HAVE_RB_PROCESS_STATUS_WAIT id_wait = rb_intern("wait"); rb_Process_Status = rb_const_get_at(rb_mProcess, rb_intern("Status")); rb_gc_register_mark_object(rb_Process_Status); #endif rb_define_singleton_method(IO_Event_Selector, "nonblock", IO_Event_Selector_nonblock, 1); } void IO_Event_Selector_initialize(struct IO_Event_Selector *backend, VALUE self, VALUE loop) { RB_OBJ_WRITE(self, &backend->self, self); RB_OBJ_WRITE(self, &backend->loop, loop); backend->waiting = NULL; backend->ready = NULL; } VALUE IO_Event_Selector_loop_resume(struct IO_Event_Selector *backend, VALUE fiber, int argc, VALUE *argv) { return IO_Event_Fiber_transfer(fiber, argc, argv); } VALUE IO_Event_Selector_loop_yield(struct IO_Event_Selector *backend) { // Under normal operation, a user fiber yields back to the event loop fiber. // However, in some cases (e.g. blocking IO called from within the scheduler // fiber itself), the current fiber may already be the loop fiber. In that case, // transferring to ourselves would be a no-op in Ruby, but it signals a misuse: // the event loop fiber should never need to yield to itself, as nothing else // would be running to resume it. We return immediately rather than self-transferring. if (backend->loop == IO_Event_Fiber_current()) { // Uncomment to investigate the callsite that triggers this condition: // rb_warning("IO_Event_Selector_loop_yield: current fiber is the loop fiber"); // rb_funcall(rb_mKernel, rb_intern("puts"), 1, rb_funcall(rb_cThread, rb_intern("current"), 0)); return Qnil; } return IO_Event_Fiber_transfer(backend->loop, 0, NULL); } struct wait_and_transfer_arguments { int argc; VALUE *argv; struct IO_Event_Selector *backend; struct IO_Event_Selector_Queue *waiting; }; static void queue_pop(struct IO_Event_Selector *backend, struct IO_Event_Selector_Queue *waiting) { if (waiting->head) { waiting->head->tail = waiting->tail; } else { // We must have been at the head of the queue: backend->waiting = waiting->tail; } if (waiting->tail) { waiting->tail->head = waiting->head; } else { // We must have been at the tail of the queue: backend->ready = waiting->head; } waiting->head = NULL; waiting->tail = NULL; } static void queue_push(struct IO_Event_Selector *backend, struct IO_Event_Selector_Queue *waiting) { assert(waiting->head == NULL); assert(waiting->tail == NULL); if (backend->waiting) { // If there was an item in the queue already, we shift it along: backend->waiting->head = waiting; waiting->tail = backend->waiting; } else { // If the queue was empty, we update the tail too: backend->ready = waiting; } // We always push to the front/head: backend->waiting = waiting; } static VALUE wait_and_transfer(VALUE _arguments) { struct wait_and_transfer_arguments *arguments = (struct wait_and_transfer_arguments *)_arguments; VALUE fiber = arguments->argv[0]; int argc = arguments->argc - 1; VALUE *argv = arguments->argv + 1; return IO_Event_Selector_loop_resume(arguments->backend, fiber, argc, argv); } static VALUE wait_and_transfer_ensure(VALUE _arguments) { struct wait_and_transfer_arguments *arguments = (struct wait_and_transfer_arguments *)_arguments; queue_pop(arguments->backend, arguments->waiting); return Qnil; } VALUE IO_Event_Selector_resume(struct IO_Event_Selector *backend, int argc, VALUE *argv) { rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS); struct IO_Event_Selector_Queue waiting = { .head = NULL, .tail = NULL, .flags = IO_EVENT_SELECTOR_QUEUE_FIBER, .fiber = IO_Event_Fiber_current() }; RB_OBJ_WRITTEN(backend->self, Qundef, waiting.fiber); queue_push(backend, &waiting); struct wait_and_transfer_arguments arguments = { .argc = argc, .argv = argv, .backend = backend, .waiting = &waiting, }; return rb_ensure(wait_and_transfer, (VALUE)&arguments, wait_and_transfer_ensure, (VALUE)&arguments); } static VALUE wait_and_raise(VALUE _arguments) { struct wait_and_transfer_arguments *arguments = (struct wait_and_transfer_arguments *)_arguments; VALUE fiber = arguments->argv[0]; int argc = arguments->argc - 1; VALUE *argv = arguments->argv + 1; return IO_Event_Fiber_raise(fiber, argc, argv); } VALUE IO_Event_Selector_raise(struct IO_Event_Selector *backend, int argc, VALUE *argv) { rb_check_arity(argc, 2, UNLIMITED_ARGUMENTS); struct IO_Event_Selector_Queue waiting = { .head = NULL, .tail = NULL, .flags = IO_EVENT_SELECTOR_QUEUE_FIBER, .fiber = IO_Event_Fiber_current() }; RB_OBJ_WRITTEN(backend->self, Qundef, waiting.fiber); queue_push(backend, &waiting); struct wait_and_transfer_arguments arguments = { .argc = argc, .argv = argv, .backend = backend, .waiting = &waiting, }; return rb_ensure(wait_and_raise, (VALUE)&arguments, wait_and_transfer_ensure, (VALUE)&arguments); } void IO_Event_Selector_ready_push(struct IO_Event_Selector *backend, VALUE fiber) { struct IO_Event_Selector_Queue *waiting = malloc(sizeof(struct IO_Event_Selector_Queue)); assert(waiting); waiting->head = NULL; waiting->tail = NULL; waiting->flags = IO_EVENT_SELECTOR_QUEUE_INTERNAL; RB_OBJ_WRITE(backend->self, &waiting->fiber, fiber); queue_push(backend, waiting); } static inline void IO_Event_Selector_ready_pop(struct IO_Event_Selector *backend, struct IO_Event_Selector_Queue *ready) { if (DEBUG) fprintf(stderr, "IO_Event_Selector_ready_pop -> %p\n", (void*)ready->fiber); VALUE fiber = ready->fiber; if (ready->flags & IO_EVENT_SELECTOR_QUEUE_INTERNAL) { // 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. queue_pop(backend, ready); free(ready); } else if (ready->flags & IO_EVENT_SELECTOR_QUEUE_FIBER) { // 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. } else { rb_raise(rb_eRuntimeError, "Unknown queue type!"); } IO_Event_Selector_loop_resume(backend, fiber, 0, NULL); } int IO_Event_Selector_ready_flush(struct IO_Event_Selector *backend) { int count = 0; // 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. // Get the current tail and head of the queue: struct IO_Event_Selector_Queue *waiting = backend->waiting; if (DEBUG) fprintf(stderr, "IO_Event_Selector_ready_flush waiting = %p\n", waiting); // Process from head to tail in order: // During this, more items may be appended to tail. while (backend->ready) { if (DEBUG) fprintf(stderr, "backend->ready = %p\n", backend->ready); struct IO_Event_Selector_Queue *ready = backend->ready; count += 1; IO_Event_Selector_ready_pop(backend, ready); if (ready == waiting) break; } return count; } socketry-io-event-ccd0953/ext/io/event/selector/selector.h000066400000000000000000000122711516444210200236600ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2021-2025, by Samuel Williams. #pragma once #include #include #include #include "../time.h" #include "../fiber.h" #ifdef HAVE_RUBY_IO_BUFFER_H #include #include #endif #ifndef RUBY_FIBER_SCHEDULER_VERSION #define RUBY_FIBER_SCHEDULER_VERSION 1 #endif #ifdef HAVE_SYS_WAIT_H #include #endif enum IO_Event { IO_EVENT_READABLE = 1, IO_EVENT_PRIORITY = 2, IO_EVENT_WRITABLE = 4, IO_EVENT_ERROR = 8, IO_EVENT_HANGUP = 16, // Used by kqueue to differentiate between process exit and file descriptor events: IO_EVENT_EXIT = 32, }; void Init_IO_Event_Selector(VALUE IO_Event_Selector); static inline int IO_Event_try_again(int error) { return error == EAGAIN || error == EWOULDBLOCK; } #ifdef HAVE_RB_IO_DESCRIPTOR #define IO_Event_Selector_io_descriptor(io) rb_io_descriptor(io) #else int IO_Event_Selector_io_descriptor(VALUE io); #endif // Reap a process without hanging. #ifdef HAVE_RB_PROCESS_STATUS_WAIT #define IO_Event_Selector_process_status_wait(pid, flags) rb_process_status_wait(pid, flags | WNOHANG) #else VALUE IO_Event_Selector_process_status_wait(rb_pid_t pid, int flags); #endif int IO_Event_Selector_nonblock_set(int file_descriptor); void IO_Event_Selector_nonblock_restore(int file_descriptor, int flags); enum IO_Event_Selector_Queue_Flags { IO_EVENT_SELECTOR_QUEUE_FIBER = 1, IO_EVENT_SELECTOR_QUEUE_INTERNAL = 2, }; struct IO_Event_Selector_Queue { struct IO_Event_Selector_Queue *head; struct IO_Event_Selector_Queue *tail; enum IO_Event_Selector_Queue_Flags flags; VALUE fiber; }; // The internal state of the event selector. // The event selector is responsible for managing the scheduling of fibers, as well as selecting for events. struct IO_Event_Selector { VALUE self; VALUE loop; // The ready queue is a list of fibers that are ready to be resumed from the event loop fiber. // Append to waiting (front/head of queue). struct IO_Event_Selector_Queue *waiting; // Process from ready (back/tail of queue). struct IO_Event_Selector_Queue *ready; }; void IO_Event_Selector_initialize(struct IO_Event_Selector *backend, VALUE self, VALUE loop); static inline void IO_Event_Selector_mark(struct IO_Event_Selector *backend) { rb_gc_mark_movable(backend->self); rb_gc_mark_movable(backend->loop); // Walk backwards through the ready queue: struct IO_Event_Selector_Queue *ready = backend->ready; while (ready) { rb_gc_mark_movable(ready->fiber); ready = ready->head; } } static inline void IO_Event_Selector_compact(struct IO_Event_Selector *backend) { backend->self = rb_gc_location(backend->self); backend->loop = rb_gc_location(backend->loop); struct IO_Event_Selector_Queue *ready = backend->ready; while (ready) { ready->fiber = rb_gc_location(ready->fiber); ready = ready->head; } } // Transfer control from the event loop to a user fiber. // This is used to transfer control to a user fiber when it may proceed. // Strictly speaking, it's not a scheduling operation (does not schedule the current fiber). VALUE IO_Event_Selector_loop_resume(struct IO_Event_Selector *backend, VALUE fiber, int argc, VALUE *argv); // Transfer from a user fiber back to the event loop. // This is used to transfer control back to the event loop in order to wait for events. // Strictly speaking, it's not a scheduling operation (does not schedule the current fiber). VALUE IO_Event_Selector_loop_yield(struct IO_Event_Selector *backend); // Resume a specific fiber. This is a scheduling operation. // The first argument is the fiber, the rest are the arguments to the resume. // // The implementation has two possible strategies: // 1. Add the current fiber to the ready queue and transfer control to the target fiber. // 2. Schedule the target fiber to be resumed by the event loop later on. // // We currently only implement the first strategy. VALUE IO_Event_Selector_resume(struct IO_Event_Selector *backend, int argc, VALUE *argv); // Raise an exception on a specific fiber. // The first argument is the fiber, the rest are the arguments to the exception. // // The implementation has two possible strategies: // 1. Add the current fiber to the ready queue and transfer control to the target fiber. // 2. Schedule the target fiber to be resumed by the event loop with an exception later on. // // We currently only implement the first strategy. VALUE IO_Event_Selector_raise(struct IO_Event_Selector *backend, int argc, VALUE *argv); // Yield control to the event loop. This is a scheduling operation. // // The implementation adds the current fiber to the ready queue and transfers control to the event loop. static inline VALUE IO_Event_Selector_yield(struct IO_Event_Selector *backend) { return IO_Event_Selector_resume(backend, 1, &backend->loop); } // Append a specific fiber to the ready queue. // The fiber can be an actual fiber or an object that responds to `alive?` and `transfer`. // The implementation will transfer control to the fiber later on. void IO_Event_Selector_ready_push(struct IO_Event_Selector *backend, VALUE fiber); // Flush the ready queue by transferring control one at a time. int IO_Event_Selector_ready_flush(struct IO_Event_Selector *backend); socketry-io-event-ccd0953/ext/io/event/selector/uring.c000066400000000000000000001150331516444210200231570ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2021-2025, by Samuel Williams. #include "uring.h" #include "selector.h" #include "../list.h" #include "../array.h" #include #include #include #include #include "pidfd.c" #include enum { DEBUG = 0, DEBUG_COMPLETION = 0, DEBUG_CQE = 0, }; enum {URING_ENTRIES = 64}; #pragma mark - Data Type struct IO_Event_Selector_URing { struct IO_Event_Selector backend; struct io_uring ring; size_t pending; // Flag indicating whether the selector is currently blocked in a system call. // Set to 1 when blocked in io_uring_wait_cqe_timeout() without GVL, 0 otherwise. // Used by wakeup() to determine if an interrupt signal is needed. int blocked; struct timespec idle_duration; struct IO_Event_Array completions; struct IO_Event_List free_list; }; struct IO_Event_Selector_URing_Completion; struct IO_Event_Selector_URing_Waiting { struct IO_Event_Selector_URing_Completion *completion; VALUE fiber; // The result of the operation. int32_t result; // Any associated flags. uint32_t flags; }; struct IO_Event_Selector_URing_Completion { struct IO_Event_List list; struct IO_Event_Selector_URing_Waiting *waiting; }; static void IO_Event_Selector_URing_Completion_mark(void *_completion) { struct IO_Event_Selector_URing_Completion *completion = _completion; if (completion->waiting) { rb_gc_mark_movable(completion->waiting->fiber); } } void IO_Event_Selector_URing_Type_mark(void *_selector) { struct IO_Event_Selector_URing *selector = _selector; IO_Event_Selector_mark(&selector->backend); IO_Event_Array_each(&selector->completions, IO_Event_Selector_URing_Completion_mark); } static void IO_Event_Selector_URing_Completion_compact(void *_completion) { struct IO_Event_Selector_URing_Completion *completion = _completion; if (completion->waiting) { completion->waiting->fiber = rb_gc_location(completion->waiting->fiber); } } void IO_Event_Selector_URing_Type_compact(void *_selector) { struct IO_Event_Selector_URing *selector = _selector; IO_Event_Selector_compact(&selector->backend); IO_Event_Array_each(&selector->completions, IO_Event_Selector_URing_Completion_compact); } static void close_internal(struct IO_Event_Selector_URing *selector) { if (selector->ring.ring_fd >= 0) { io_uring_queue_exit(&selector->ring); selector->ring.ring_fd = -1; } } static void IO_Event_Selector_URing_Type_free(void *_selector) { struct IO_Event_Selector_URing *selector = _selector; close_internal(selector); IO_Event_Array_free(&selector->completions); xfree(selector); } static size_t IO_Event_Selector_URing_Type_size(const void *_selector) { const struct IO_Event_Selector_URing *selector = _selector; return sizeof(struct IO_Event_Selector_URing) + IO_Event_Array_memory_size(&selector->completions) + IO_Event_List_memory_size(&selector->free_list) ; } static const rb_data_type_t IO_Event_Selector_URing_Type = { .wrap_struct_name = "IO::Event::Backend::URing", .function = { .dmark = IO_Event_Selector_URing_Type_mark, .dcompact = IO_Event_Selector_URing_Type_compact, .dfree = IO_Event_Selector_URing_Type_free, .dsize = IO_Event_Selector_URing_Type_size, }, .data = NULL, .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED, }; inline static struct IO_Event_Selector_URing_Completion * IO_Event_Selector_URing_Completion_acquire(struct IO_Event_Selector_URing *selector, struct IO_Event_Selector_URing_Waiting *waiting) { struct IO_Event_Selector_URing_Completion *completion = NULL; if (!IO_Event_List_empty(&selector->free_list)) { completion = (struct IO_Event_Selector_URing_Completion*)selector->free_list.tail; IO_Event_List_pop(&completion->list); } else { completion = IO_Event_Array_push(&selector->completions); IO_Event_List_clear(&completion->list); } if (DEBUG_COMPLETION) fprintf(stderr, "IO_Event_Selector_URing_Completion_acquire(%p, limit=%ld)\n", (void*)completion, selector->completions.limit); waiting->completion = completion; completion->waiting = waiting; return completion; } inline static void IO_Event_Selector_URing_Completion_cancel(struct IO_Event_Selector_URing_Completion *completion) { if (DEBUG_COMPLETION) fprintf(stderr, "IO_Event_Selector_URing_Completion_cancel(%p)\n", (void*)completion); if (completion->waiting) { completion->waiting->completion = NULL; completion->waiting = NULL; } } inline static void IO_Event_Selector_URing_Completion_release(struct IO_Event_Selector_URing *selector, struct IO_Event_Selector_URing_Completion *completion) { if (DEBUG_COMPLETION) fprintf(stderr, "IO_Event_Selector_URing_Completion_release(%p)\n", (void*)completion); IO_Event_Selector_URing_Completion_cancel(completion); IO_Event_List_prepend(&selector->free_list, &completion->list); } inline static void IO_Event_Selector_URing_Waiting_cancel(struct IO_Event_Selector_URing_Waiting *waiting) { if (DEBUG_COMPLETION) fprintf(stderr, "IO_Event_Selector_URing_Waiting_cancel(%p, %p)\n", (void*)waiting, (void*)waiting->completion); if (waiting->completion) { waiting->completion->waiting = NULL; waiting->completion = NULL; } waiting->fiber = 0; } struct IO_Event_List_Type IO_Event_Selector_URing_Completion_Type = {}; void IO_Event_Selector_URing_Completion_initialize(void *element) { struct IO_Event_Selector_URing_Completion *completion = element; IO_Event_List_initialize(&completion->list); completion->list.type = &IO_Event_Selector_URing_Completion_Type; } void IO_Event_Selector_URing_Completion_free(void *element) { struct IO_Event_Selector_URing_Completion *completion = element; IO_Event_Selector_URing_Completion_cancel(completion); } VALUE IO_Event_Selector_URing_allocate(VALUE self) { struct IO_Event_Selector_URing *selector = NULL; VALUE instance = TypedData_Make_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); IO_Event_Selector_initialize(&selector->backend, self, Qnil); selector->ring.ring_fd = -1; selector->pending = 0; selector->blocked = 0; IO_Event_List_initialize(&selector->free_list); selector->completions.element_initialize = IO_Event_Selector_URing_Completion_initialize; selector->completions.element_free = IO_Event_Selector_URing_Completion_free; int result = IO_Event_Array_initialize(&selector->completions, IO_EVENT_ARRAY_DEFAULT_COUNT, sizeof(struct IO_Event_Selector_URing_Completion)); if (result < 0) { rb_sys_fail("IO_Event_Selector_URing_allocate:IO_Event_Array_initialize"); } return instance; } #pragma mark - Methods VALUE IO_Event_Selector_URing_initialize(VALUE self, VALUE loop) { struct IO_Event_Selector_URing *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); IO_Event_Selector_initialize(&selector->backend, self, loop); int result = io_uring_queue_init(URING_ENTRIES, &selector->ring, 0); if (result < 0) { rb_syserr_fail(-result, "IO_Event_Selector_URing_initialize:io_uring_queue_init"); } rb_update_max_fd(selector->ring.ring_fd); return self; } VALUE IO_Event_Selector_URing_loop(VALUE self) { struct IO_Event_Selector_URing *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); return selector->backend.loop; } VALUE IO_Event_Selector_URing_idle_duration(VALUE self) { struct IO_Event_Selector_URing *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); double duration = selector->idle_duration.tv_sec + (selector->idle_duration.tv_nsec / 1000000000.0); return DBL2NUM(duration); } VALUE IO_Event_Selector_URing_close(VALUE self) { struct IO_Event_Selector_URing *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); close_internal(selector); return Qnil; } VALUE IO_Event_Selector_URing_transfer(VALUE self) { struct IO_Event_Selector_URing *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); return IO_Event_Selector_loop_yield(&selector->backend); } VALUE IO_Event_Selector_URing_resume(int argc, VALUE *argv, VALUE self) { struct IO_Event_Selector_URing *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); return IO_Event_Selector_resume(&selector->backend, argc, argv); } VALUE IO_Event_Selector_URing_yield(VALUE self) { struct IO_Event_Selector_URing *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); return IO_Event_Selector_yield(&selector->backend); } VALUE IO_Event_Selector_URing_push(VALUE self, VALUE fiber) { struct IO_Event_Selector_URing *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); IO_Event_Selector_ready_push(&selector->backend, fiber); return Qnil; } VALUE IO_Event_Selector_URing_raise(int argc, VALUE *argv, VALUE self) { struct IO_Event_Selector_URing *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); return IO_Event_Selector_raise(&selector->backend, argc, argv); } VALUE IO_Event_Selector_URing_ready_p(VALUE self) { struct IO_Event_Selector_URing *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); return selector->backend.ready ? Qtrue : Qfalse; } #pragma mark - Submission Queue static void IO_Event_Selector_URing_dump_completion_queue(struct IO_Event_Selector_URing *selector) { struct io_uring *ring = &selector->ring; unsigned head; struct io_uring_cqe *cqe; if (DEBUG) { int first = 1; io_uring_for_each_cqe(ring, head, cqe) { if (!first) { fprintf(stderr, ", "); } else { fprintf(stderr, "CQ: ["); first = 0; } fprintf(stderr, "%d:%p", (int)cqe->res, (void*)cqe->user_data); } if (!first) { fprintf(stderr, "]\n"); } } } // Flush the submission queue if pending operations are present. static int io_uring_submit_flush(struct IO_Event_Selector_URing *selector) { if (selector->pending) { if (DEBUG) fprintf(stderr, "io_uring_submit_flush(pending=%ld)\n", selector->pending); // Try to submit: int result = io_uring_submit(&selector->ring); if (result >= 0) { // If it was submitted, reset pending count: selector->pending = 0; } else if (result != -EBUSY && result != -EAGAIN) { rb_syserr_fail(-result, "io_uring_submit_flush:io_uring_submit"); } return result; } if (DEBUG) { IO_Event_Selector_URing_dump_completion_queue(selector); } return 0; } // Immediately flush the submission queue, yielding to the event loop if it was not successful. static int io_uring_submit_now(struct IO_Event_Selector_URing *selector) { if (DEBUG) fprintf(stderr, "io_uring_submit_now(pending=%ld)\n", selector->pending); while (true) { int result = io_uring_submit(&selector->ring); if (result >= 0) { selector->pending = 0; if (DEBUG) IO_Event_Selector_URing_dump_completion_queue(selector); return result; } if (result == -EBUSY || result == -EAGAIN) { IO_Event_Selector_yield(&selector->backend); } else { rb_syserr_fail(-result, "io_uring_submit_now:io_uring_submit"); } } } // Submit a pending operation. This does not submit the operation immediately, but instead defers it to the next call to `io_uring_submit_flush` or `io_uring_submit_now`. This is useful for operations that are not urgent, but should be used with care as it can lead to a deadlock if the submission queue is not flushed. static void io_uring_submit_pending(struct IO_Event_Selector_URing *selector) { selector->pending += 1; if (DEBUG) fprintf(stderr, "io_uring_submit_pending(ring=%p, pending=%ld)\n", &selector->ring, selector->pending); } struct io_uring_sqe * io_get_sqe(struct IO_Event_Selector_URing *selector) { struct io_uring_sqe *sqe = io_uring_get_sqe(&selector->ring); while (sqe == NULL) { // The submit queue is full, we need to drain it: io_uring_submit_now(selector); sqe = io_uring_get_sqe(&selector->ring); } return sqe; } #pragma mark - Process.wait struct process_wait_arguments { struct IO_Event_Selector_URing *selector; struct IO_Event_Selector_URing_Waiting *waiting; pid_t pid; int flags; int descriptor; }; static VALUE process_wait_transfer(VALUE _arguments) { struct process_wait_arguments *arguments = (struct process_wait_arguments *)_arguments; IO_Event_Selector_loop_yield(&arguments->selector->backend); if (arguments->waiting->result) { return IO_Event_Selector_process_status_wait(arguments->pid, arguments->flags); } else { return Qfalse; } } static VALUE process_wait_ensure(VALUE _arguments) { struct process_wait_arguments *arguments = (struct process_wait_arguments *)_arguments; close(arguments->descriptor); IO_Event_Selector_URing_Waiting_cancel(arguments->waiting); return Qnil; } VALUE IO_Event_Selector_URing_process_wait(VALUE self, VALUE fiber, VALUE _pid, VALUE _flags) { struct IO_Event_Selector_URing *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); pid_t pid = NUM2PIDT(_pid); int flags = NUM2INT(_flags); int descriptor = pidfd_open(pid, 0); if (descriptor < 0) { rb_syserr_fail(errno, "IO_Event_Selector_URing_process_wait:pidfd_open"); } rb_update_max_fd(descriptor); struct IO_Event_Selector_URing_Waiting waiting = { .fiber = fiber, }; RB_OBJ_WRITTEN(self, Qundef, fiber); struct IO_Event_Selector_URing_Completion *completion = IO_Event_Selector_URing_Completion_acquire(selector, &waiting); struct process_wait_arguments process_wait_arguments = { .selector = selector, .waiting = &waiting, .pid = pid, .flags = flags, .descriptor = descriptor, }; if (DEBUG) fprintf(stderr, "IO_Event_Selector_URing_process_wait:io_uring_prep_poll_add(%p)\n", (void*)fiber); struct io_uring_sqe *sqe = io_get_sqe(selector); io_uring_prep_poll_add(sqe, descriptor, POLLIN|POLLHUP|POLLERR); io_uring_sqe_set_data(sqe, completion); io_uring_submit_pending(selector); return rb_ensure(process_wait_transfer, (VALUE)&process_wait_arguments, process_wait_ensure, (VALUE)&process_wait_arguments); } #pragma mark - IO#wait static inline short poll_flags_from_events(int events) { short flags = 0; if (events & IO_EVENT_READABLE) flags |= POLLIN; if (events & IO_EVENT_PRIORITY) flags |= POLLPRI; if (events & IO_EVENT_WRITABLE) flags |= POLLOUT; flags |= POLLHUP; flags |= POLLERR; return flags; } static inline int events_from_poll_flags(short flags) { int events = 0; // See `epoll.c` for details regarding POLLHUP: if (flags & (POLLIN|POLLHUP|POLLERR)) events |= IO_EVENT_READABLE; if (flags & POLLPRI) events |= IO_EVENT_PRIORITY; if (flags & POLLOUT) events |= IO_EVENT_WRITABLE; return events; } struct io_wait_arguments { struct IO_Event_Selector_URing *selector; struct IO_Event_Selector_URing_Waiting *waiting; short flags; }; static VALUE io_wait_ensure(VALUE _arguments) { struct io_wait_arguments *arguments = (struct io_wait_arguments *)_arguments; // If the operation is still in progress, cancel it: if (arguments->waiting->completion) { if (DEBUG) fprintf(stderr, "io_wait_ensure:io_uring_prep_cancel(waiting=%p, completion=%p)\n", (void*)arguments->waiting, (void*)arguments->waiting->completion); struct io_uring_sqe *sqe = io_get_sqe(arguments->selector); io_uring_prep_cancel(sqe, (void*)arguments->waiting->completion, 0); io_uring_sqe_set_data(sqe, NULL); io_uring_submit_now(arguments->selector); } IO_Event_Selector_URing_Waiting_cancel(arguments->waiting); return Qnil; }; static VALUE io_wait_transfer(VALUE _arguments) { struct io_wait_arguments *arguments = (struct io_wait_arguments *)_arguments; struct IO_Event_Selector_URing *selector = arguments->selector; IO_Event_Selector_loop_yield(&selector->backend); if (DEBUG) fprintf(stderr, "io_wait_transfer:waiting=%p, result=%d\n", (void*)arguments->waiting, arguments->waiting->result); int32_t result = arguments->waiting->result; if (result < 0) { rb_syserr_fail(-result, "io_wait_transfer:io_uring_poll_add"); } else if (result > 0) { // We explicitly filter the resulting events based on the requested events. // In some cases, poll will report events we didn't ask for. return RB_INT2NUM(events_from_poll_flags(arguments->waiting->result & arguments->flags)); } else { return Qfalse; } }; VALUE IO_Event_Selector_URing_io_wait(VALUE self, VALUE fiber, VALUE io, VALUE events) { struct IO_Event_Selector_URing *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); int descriptor = IO_Event_Selector_io_descriptor(io); short flags = poll_flags_from_events(NUM2INT(events)); if (DEBUG) fprintf(stderr, "IO_Event_Selector_URing_io_wait:io_uring_prep_poll_add(descriptor=%d, flags=%d, fiber=%p)\n", descriptor, flags, (void*)fiber); struct IO_Event_Selector_URing_Waiting waiting = { .fiber = fiber, }; RB_OBJ_WRITTEN(self, Qundef, fiber); struct IO_Event_Selector_URing_Completion *completion = IO_Event_Selector_URing_Completion_acquire(selector, &waiting); struct io_uring_sqe *sqe = io_get_sqe(selector); io_uring_prep_poll_add(sqe, descriptor, flags); io_uring_sqe_set_data(sqe, completion); // If we are going to wait, we assume that we are waiting for a while: io_uring_submit_pending(selector); struct io_wait_arguments io_wait_arguments = { .selector = selector, .waiting = &waiting, .flags = flags }; return rb_ensure(io_wait_transfer, (VALUE)&io_wait_arguments, io_wait_ensure, (VALUE)&io_wait_arguments); } #ifdef HAVE_RUBY_IO_BUFFER_H #pragma mark - IO#read #if LINUX_VERSION_CODE >= KERNEL_VERSION(5,16,0) static inline off_t io_seekable(int descriptor) { return -1; } #else #warning Upgrade your kernel to 5.16+! io_uring bugs prevent efficient io_read/io_write hooks. static inline off_t io_seekable(int descriptor) { if (lseek(descriptor, 0, SEEK_CUR) == -1) { return 0; } else { return -1; } } #endif #pragma mark - IO#read struct io_read_arguments { struct IO_Event_Selector_URing *selector; struct IO_Event_Selector_URing_Waiting *waiting; int descriptor; off_t offset; char *buffer; size_t length; }; static VALUE io_read_submit(VALUE _arguments) { struct io_read_arguments *arguments = (struct io_read_arguments *)_arguments; struct IO_Event_Selector_URing *selector = arguments->selector; if (DEBUG) fprintf(stderr, "io_read_submit:io_uring_prep_read(waiting=%p, completion=%p, descriptor=%d, buffer=%p, length=%ld)\n", (void*)arguments->waiting, (void*)arguments->waiting->completion, arguments->descriptor, arguments->buffer, arguments->length); struct io_uring_sqe *sqe = io_get_sqe(selector); io_uring_prep_read(sqe, arguments->descriptor, arguments->buffer, arguments->length, arguments->offset); io_uring_sqe_set_data(sqe, arguments->waiting->completion); io_uring_submit_now(selector); IO_Event_Selector_loop_yield(&selector->backend); return RB_INT2NUM(arguments->waiting->result); } static VALUE io_read_ensure(VALUE _arguments) { struct io_read_arguments *arguments = (struct io_read_arguments *)_arguments; struct IO_Event_Selector_URing *selector = arguments->selector; // If the operation is still in progress, cancel it: if (arguments->waiting->completion) { if (DEBUG) fprintf(stderr, "io_read_ensure:io_uring_prep_cancel(waiting=%p, completion=%p)\n", (void*)arguments->waiting, (void*)arguments->waiting->completion); struct io_uring_sqe *sqe = io_get_sqe(selector); io_uring_prep_cancel(sqe, (void*)arguments->waiting->completion, 0); io_uring_sqe_set_data(sqe, NULL); io_uring_submit_now(selector); } IO_Event_Selector_URing_Waiting_cancel(arguments->waiting); return Qnil; } static int io_read(struct IO_Event_Selector_URing *selector, VALUE fiber, int descriptor, char *buffer, size_t length, off_t offset) { struct IO_Event_Selector_URing_Waiting waiting = { .fiber = fiber, }; RB_OBJ_WRITTEN(selector->backend.self, Qundef, fiber); IO_Event_Selector_URing_Completion_acquire(selector, &waiting); struct io_read_arguments io_read_arguments = { .selector = selector, .waiting = &waiting, .descriptor = descriptor, .offset = offset, .buffer = buffer, .length = length }; return RB_NUM2INT( rb_ensure(io_read_submit, (VALUE)&io_read_arguments, io_read_ensure, (VALUE)&io_read_arguments) ); } VALUE IO_Event_Selector_URing_io_read(VALUE self, VALUE fiber, VALUE io, VALUE buffer, VALUE _length, VALUE _offset) { struct IO_Event_Selector_URing *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); int descriptor = IO_Event_Selector_io_descriptor(io); void *base; size_t size; rb_io_buffer_get_bytes_for_writing(buffer, &base, &size); size_t length = NUM2SIZET(_length); size_t offset = NUM2SIZET(_offset); size_t total = 0; off_t from = io_seekable(descriptor); // Ensure offset is within the bounds of the buffer to avoid size_t underflow and out-of-bounds pointer arithmetic on (char *)base + offset. if (offset > size) { return rb_fiber_scheduler_io_result(-1, EINVAL); } size_t maximum_size = size - offset; // Are we performing a non-blocking read? if (!length) { // If the (maximum) length is zero, that indicates we just want to read whatever is available without blocking. // If we schedule this read into the URing, it will block until data is available, rather than returning immediately. int state = IO_Event_Selector_nonblock_set(descriptor); int result = read(descriptor, (char*)base+offset, maximum_size); int error = errno; IO_Event_Selector_nonblock_restore(descriptor, state); return rb_fiber_scheduler_io_result(result, error); } while (maximum_size) { int result = io_read(selector, fiber, descriptor, (char*)base+offset, maximum_size, from); if (result > 0) { total += result; offset += result; if ((size_t)result >= length) break; length -= result; } else if (result == 0) { break; } else if (length > 0 && IO_Event_try_again(-result)) { IO_Event_Selector_URing_io_wait(self, fiber, io, RB_INT2NUM(IO_EVENT_READABLE)); } else { return rb_fiber_scheduler_io_result(-1, -result); } maximum_size = size - offset; } return rb_fiber_scheduler_io_result(total, 0); } static VALUE IO_Event_Selector_URing_io_read_compatible(int argc, VALUE *argv, VALUE self) { rb_check_arity(argc, 4, 5); VALUE _offset = SIZET2NUM(0); if (argc == 5) { _offset = argv[4]; } return IO_Event_Selector_URing_io_read(self, argv[0], argv[1], argv[2], argv[3], _offset); } VALUE IO_Event_Selector_URing_io_pread(VALUE self, VALUE fiber, VALUE io, VALUE buffer, VALUE _from, VALUE _length, VALUE _offset) { struct IO_Event_Selector_URing *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); int descriptor = IO_Event_Selector_io_descriptor(io); void *base; size_t size; rb_io_buffer_get_bytes_for_writing(buffer, &base, &size); size_t length = NUM2SIZET(_length); size_t offset = NUM2SIZET(_offset); size_t total = 0; off_t from = NUM2OFFT(_from); // Ensure offset is within the bounds of the buffer to avoid size_t underflow and out-of-bounds pointer arithmetic on (char *)base + offset. if (offset > size) { return rb_fiber_scheduler_io_result(-1, EINVAL); } size_t maximum_size = size - offset; while (maximum_size) { int result = io_read(selector, fiber, descriptor, (char*)base+offset, maximum_size, from); if (result > 0) { total += result; offset += result; from += result; if ((size_t)result >= length) break; length -= result; } else if (result == 0) { break; } else if (length > 0 && IO_Event_try_again(-result)) { IO_Event_Selector_URing_io_wait(self, fiber, io, RB_INT2NUM(IO_EVENT_READABLE)); } else { return rb_fiber_scheduler_io_result(-1, -result); } maximum_size = size - offset; } return rb_fiber_scheduler_io_result(total, 0); } #pragma mark - IO#write struct io_write_arguments { struct IO_Event_Selector_URing *selector; struct IO_Event_Selector_URing_Waiting *waiting; int descriptor; off_t offset; char *buffer; size_t length; }; static VALUE io_write_submit(VALUE _argument) { struct io_write_arguments *arguments = (struct io_write_arguments*)_argument; struct IO_Event_Selector_URing *selector = arguments->selector; if (DEBUG) fprintf(stderr, "io_write_submit:io_uring_prep_write(waiting=%p, completion=%p, descriptor=%d, buffer=%p, length=%ld)\n", (void*)arguments->waiting, (void*)arguments->waiting->completion, arguments->descriptor, arguments->buffer, arguments->length); struct io_uring_sqe *sqe = io_get_sqe(selector); io_uring_prep_write(sqe, arguments->descriptor, arguments->buffer, arguments->length, arguments->offset); io_uring_sqe_set_data(sqe, arguments->waiting->completion); io_uring_submit_pending(selector); IO_Event_Selector_loop_yield(&selector->backend); return RB_INT2NUM(arguments->waiting->result); } static VALUE io_write_ensure(VALUE _argument) { struct io_write_arguments *arguments = (struct io_write_arguments*)_argument; struct IO_Event_Selector_URing *selector = arguments->selector; // If the operation is still in progress, cancel it: if (arguments->waiting->completion) { if (DEBUG) fprintf(stderr, "io_write_ensure:io_uring_prep_cancel(waiting=%p, completion=%p)\n", (void*)arguments->waiting, (void*)arguments->waiting->completion); struct io_uring_sqe *sqe = io_get_sqe(selector); io_uring_prep_cancel(sqe, (void*)arguments->waiting->completion, 0); io_uring_sqe_set_data(sqe, NULL); io_uring_submit_now(selector); } IO_Event_Selector_URing_Waiting_cancel(arguments->waiting); return Qnil; } static int io_write(struct IO_Event_Selector_URing *selector, VALUE fiber, int descriptor, char *buffer, size_t length, off_t offset) { struct IO_Event_Selector_URing_Waiting waiting = { .fiber = fiber, }; RB_OBJ_WRITTEN(selector->backend.self, Qundef, fiber); IO_Event_Selector_URing_Completion_acquire(selector, &waiting); struct io_write_arguments arguments = { .selector = selector, .waiting = &waiting, .descriptor = descriptor, .offset = offset, .buffer = buffer, .length = length, }; return RB_NUM2INT( rb_ensure(io_write_submit, (VALUE)&arguments, io_write_ensure, (VALUE)&arguments) ); } VALUE IO_Event_Selector_URing_io_write(VALUE self, VALUE fiber, VALUE io, VALUE buffer, VALUE _length, VALUE _offset) { struct IO_Event_Selector_URing *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); int descriptor = IO_Event_Selector_io_descriptor(io); const void *base; size_t size; rb_io_buffer_get_bytes_for_reading(buffer, &base, &size); size_t length = NUM2SIZET(_length); size_t offset = NUM2SIZET(_offset); size_t total = 0; off_t from = io_seekable(descriptor); if (length > size) { rb_raise(rb_eRuntimeError, "Length exceeds size of buffer!"); } // Ensure offset is within the bounds of the buffer to avoid size_t underflow and out-of-bounds pointer arithmetic on (char *)base + offset. if (offset > size) { return rb_fiber_scheduler_io_result(-1, EINVAL); } size_t maximum_size = size - offset; while (maximum_size) { int result = io_write(selector, fiber, descriptor, (char*)base+offset, maximum_size, from); if (result > 0) { total += result; offset += result; if ((size_t)result >= length) break; length -= result; } else if (result == 0) { break; } else if (length > 0 && IO_Event_try_again(-result)) { IO_Event_Selector_URing_io_wait(self, fiber, io, RB_INT2NUM(IO_EVENT_WRITABLE)); } else { return rb_fiber_scheduler_io_result(-1, -result); } maximum_size = size - offset; } return rb_fiber_scheduler_io_result(total, 0); } static VALUE IO_Event_Selector_URing_io_write_compatible(int argc, VALUE *argv, VALUE self) { rb_check_arity(argc, 4, 5); VALUE _offset = SIZET2NUM(0); if (argc == 5) { _offset = argv[4]; } return IO_Event_Selector_URing_io_write(self, argv[0], argv[1], argv[2], argv[3], _offset); } VALUE IO_Event_Selector_URing_io_pwrite(VALUE self, VALUE fiber, VALUE io, VALUE buffer, VALUE _from, VALUE _length, VALUE _offset) { struct IO_Event_Selector_URing *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); int descriptor = IO_Event_Selector_io_descriptor(io); const void *base; size_t size; rb_io_buffer_get_bytes_for_reading(buffer, &base, &size); size_t length = NUM2SIZET(_length); size_t offset = NUM2SIZET(_offset); size_t total = 0; off_t from = NUM2OFFT(_from); if (length > size) { rb_raise(rb_eRuntimeError, "Length exceeds size of buffer!"); } // Ensure offset is within the bounds of the buffer to avoid size_t underflow and out-of-bounds pointer arithmetic on (char *)base + offset. if (offset > size) { return rb_fiber_scheduler_io_result(-1, EINVAL); } size_t maximum_size = size - offset; while (maximum_size) { int result = io_write(selector, fiber, descriptor, (char*)base+offset, maximum_size, from); if (result > 0) { total += result; offset += result; from += result; if ((size_t)result >= length) break; length -= result; } else if (result == 0) { break; } else if (length > 0 && IO_Event_try_again(-result)) { IO_Event_Selector_URing_io_wait(self, fiber, io, RB_INT2NUM(IO_EVENT_WRITABLE)); } else { return rb_fiber_scheduler_io_result(-1, -result); } maximum_size = size - offset; } return rb_fiber_scheduler_io_result(total, 0); } #endif #pragma mark - IO#close static const int ASYNC_CLOSE = 1; VALUE IO_Event_Selector_URing_io_close(VALUE self, VALUE io) { struct IO_Event_Selector_URing *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); int descriptor = IO_Event_Selector_io_descriptor(io); if (ASYNC_CLOSE) { struct io_uring_sqe *sqe = io_get_sqe(selector); io_uring_prep_close(sqe, descriptor); io_uring_sqe_set_data(sqe, NULL); io_uring_submit_now(selector); // It would be nice to explore not flushing immediately, but instead deferring to the next select cycle. // The problem with this approach is that if the user expects the file descriptor to be closed immediately, (e.g. before fork), it may not be closed in time. // io_uring_submit_pending(selector); } else { close(descriptor); } // We don't wait for the result of close since it has no use in pratice: return Qtrue; } #pragma mark - Event Loop static struct __kernel_timespec * make_timeout(VALUE duration, struct __kernel_timespec *storage) { if (duration == Qnil) { return NULL; } if (RB_INTEGER_TYPE_P(duration)) { storage->tv_sec = NUM2TIMET(duration); storage->tv_nsec = 0; return storage; } duration = rb_to_float(duration); double value = RFLOAT_VALUE(duration); time_t seconds = value; storage->tv_sec = seconds; storage->tv_nsec = (value - seconds) * 1000000000L; return storage; } static int timeout_nonblocking(struct __kernel_timespec *timespec) { return timespec && timespec->tv_sec == 0 && timespec->tv_nsec == 0; } struct select_arguments { struct IO_Event_Selector_URing *selector; int result; struct __kernel_timespec storage; struct __kernel_timespec *timeout; }; static void * select_internal(void *_arguments) { struct select_arguments * arguments = (struct select_arguments *)_arguments; struct io_uring_cqe *cqe = NULL; arguments->result = io_uring_wait_cqe_timeout(&arguments->selector->ring, &cqe, arguments->timeout); return NULL; } static int select_internal_without_gvl(struct select_arguments *arguments) { io_uring_submit_flush(arguments->selector); arguments->selector->blocked = 1; rb_thread_call_without_gvl(select_internal, (void *)arguments, RUBY_UBF_IO, 0); arguments->selector->blocked = 0; if (arguments->result == -ETIME) { arguments->result = 0; } else if (arguments->result == -EINTR) { arguments->result = 0; } else if (arguments->result < 0) { rb_syserr_fail(-arguments->result, "select_internal_without_gvl:io_uring_wait_cqe_timeout"); } else { // At least 1 event is waiting: arguments->result = 1; } return arguments->result; } static inline unsigned select_process_completions(struct IO_Event_Selector_URing *selector) { struct io_uring *ring = &selector->ring; unsigned completed = 0; unsigned head; struct io_uring_cqe *cqe; if (DEBUG) { fprintf(stderr, "select_process_completions: selector=%p\n", (void*)selector); IO_Event_Selector_URing_dump_completion_queue(selector); } io_uring_for_each_cqe(ring, head, cqe) { if (DEBUG_CQE) fprintf(stderr, "select_process_completions: cqe res=%d user_data=%p\n", cqe->res, (void*)cqe->user_data); ++completed; // If the operation was cancelled, or the operation has no user data: if (cqe->user_data == 0 || cqe->user_data == LIBURING_UDATA_TIMEOUT) { io_uring_cq_advance(ring, 1); continue; } struct IO_Event_Selector_URing_Completion *completion = (void*)cqe->user_data; struct IO_Event_Selector_URing_Waiting *waiting = completion->waiting; if (DEBUG) fprintf(stderr, "select_process_completions: completion=%p waiting=%p\n", (void*)completion, (void*)waiting); if (waiting) { waiting->result = cqe->res; waiting->flags = cqe->flags; } io_uring_cq_advance(ring, 1); VALUE fiber = 0; if (waiting && waiting->fiber) { assert(waiting->result != -ECANCELED); fiber = waiting->fiber; } // This marks the waiting operation as "complete": IO_Event_Selector_URing_Completion_release(selector, completion); if (fiber) { IO_Event_Selector_loop_resume(&selector->backend, fiber, 0, NULL); } } if (DEBUG && completed > 0) fprintf(stderr, "select_process_completions: completed=%d\n", completed); return completed; } VALUE IO_Event_Selector_URing_select(VALUE self, VALUE duration) { struct IO_Event_Selector_URing *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); selector->idle_duration.tv_sec = 0; selector->idle_duration.tv_nsec = 0; // Flush any pending events: io_uring_submit_flush(selector); int ready = IO_Event_Selector_ready_flush(&selector->backend); int result = select_process_completions(selector); // If we: // 1. Didn't process any ready fibers, and // 2. Didn't process any events from non-blocking select (above), and // 3. There are no items in the ready list, // then we can perform a blocking select. if (!ready && !result && !selector->backend.ready) { // We might need to wait for events: struct select_arguments arguments = { .selector = selector, .timeout = NULL, }; arguments.timeout = make_timeout(duration, &arguments.storage); if (!selector->backend.ready && !timeout_nonblocking(arguments.timeout)) { struct timespec start_time; IO_Event_Time_current(&start_time); // This is a blocking operation, we wait for events: result = select_internal_without_gvl(&arguments); struct timespec end_time; IO_Event_Time_current(&end_time); IO_Event_Time_elapsed(&start_time, &end_time, &selector->idle_duration); // After waiting/flushing the SQ, check if there are any completions: if (result > 0) { result = select_process_completions(selector); } } } return RB_INT2NUM(result); } VALUE IO_Event_Selector_URing_wakeup(VALUE self) { struct IO_Event_Selector_URing *selector = NULL; TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector); // If we are blocking, we can schedule a nop event to wake up the selector: if (selector->blocked) { struct io_uring_sqe *sqe = NULL; while (true) { sqe = io_uring_get_sqe(&selector->ring); if (sqe) break; rb_thread_schedule(); // It's possible we became unblocked already, so we can assume the selector has already cycled at least once: if (!selector->blocked) return Qfalse; } io_uring_prep_nop(sqe); // If you don't set this line, the SQE will eventually be recycled and have valid user selector which can cause odd behaviour: io_uring_sqe_set_data(sqe, NULL); io_uring_submit(&selector->ring); return Qtrue; } return Qfalse; } #pragma mark - Native Methods static int IO_Event_Selector_URing_supported_p(void) { struct io_uring ring; int result = io_uring_queue_init(32, &ring, 0); if (result < 0) { rb_warn("io_uring_queue_init() was available at compile time but failed at run time: %s\n", strerror(-result)); return 0; } io_uring_queue_exit(&ring); return 1; } void Init_IO_Event_Selector_URing(VALUE IO_Event_Selector) { if (!IO_Event_Selector_URing_supported_p()) { return; } VALUE IO_Event_Selector_URing = rb_define_class_under(IO_Event_Selector, "URing", rb_cObject); rb_define_alloc_func(IO_Event_Selector_URing, IO_Event_Selector_URing_allocate); rb_define_method(IO_Event_Selector_URing, "initialize", IO_Event_Selector_URing_initialize, 1); rb_define_method(IO_Event_Selector_URing, "loop", IO_Event_Selector_URing_loop, 0); rb_define_method(IO_Event_Selector_URing, "idle_duration", IO_Event_Selector_URing_idle_duration, 0); rb_define_method(IO_Event_Selector_URing, "transfer", IO_Event_Selector_URing_transfer, 0); rb_define_method(IO_Event_Selector_URing, "resume", IO_Event_Selector_URing_resume, -1); rb_define_method(IO_Event_Selector_URing, "yield", IO_Event_Selector_URing_yield, 0); rb_define_method(IO_Event_Selector_URing, "push", IO_Event_Selector_URing_push, 1); rb_define_method(IO_Event_Selector_URing, "raise", IO_Event_Selector_URing_raise, -1); rb_define_method(IO_Event_Selector_URing, "ready?", IO_Event_Selector_URing_ready_p, 0); rb_define_method(IO_Event_Selector_URing, "select", IO_Event_Selector_URing_select, 1); rb_define_method(IO_Event_Selector_URing, "wakeup", IO_Event_Selector_URing_wakeup, 0); rb_define_method(IO_Event_Selector_URing, "close", IO_Event_Selector_URing_close, 0); rb_define_method(IO_Event_Selector_URing, "io_wait", IO_Event_Selector_URing_io_wait, 3); #ifdef HAVE_RUBY_IO_BUFFER_H rb_define_method(IO_Event_Selector_URing, "io_read", IO_Event_Selector_URing_io_read_compatible, -1); rb_define_method(IO_Event_Selector_URing, "io_write", IO_Event_Selector_URing_io_write_compatible, -1); rb_define_method(IO_Event_Selector_URing, "io_pread", IO_Event_Selector_URing_io_pread, 6); rb_define_method(IO_Event_Selector_URing, "io_pwrite", IO_Event_Selector_URing_io_pwrite, 6); #endif rb_define_method(IO_Event_Selector_URing, "io_close", IO_Event_Selector_URing_io_close, 1); rb_define_method(IO_Event_Selector_URing, "process_wait", IO_Event_Selector_URing_process_wait, 3); } socketry-io-event-ccd0953/ext/io/event/selector/uring.h000066400000000000000000000003171516444210200231620ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2021-2025, by Samuel Williams. #pragma once #include #define IO_EVENT_SELECTOR_URING void Init_IO_Event_Selector_URing(VALUE IO_Event_Selector); socketry-io-event-ccd0953/ext/io/event/time.c000066400000000000000000000021401516444210200211430ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2025, by Samuel Williams. #include "time.h" void IO_Event_Time_elapsed(const struct timespec* start, const struct timespec* stop, struct timespec *duration) { if ((stop->tv_nsec - start->tv_nsec) < 0) { duration->tv_sec = stop->tv_sec - start->tv_sec - 1; duration->tv_nsec = stop->tv_nsec - start->tv_nsec + 1000000000; } else { duration->tv_sec = stop->tv_sec - start->tv_sec; duration->tv_nsec = stop->tv_nsec - start->tv_nsec; } } float IO_Event_Time_duration(const struct timespec *duration) { return duration->tv_sec + duration->tv_nsec / 1000000000.0; } void IO_Event_Time_current(struct timespec *time) { clock_gettime(CLOCK_MONOTONIC, time); } float IO_Event_Time_proportion(const struct timespec *duration, const struct timespec *total_duration) { return IO_Event_Time_duration(duration) / IO_Event_Time_duration(total_duration); } float IO_Event_Time_delta(const struct timespec *start, const struct timespec *stop) { struct timespec duration; IO_Event_Time_elapsed(start, stop, &duration); return IO_Event_Time_duration(&duration); } socketry-io-event-ccd0953/ext/io/event/time.h000066400000000000000000000012571516444210200211600ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2025, by Samuel Williams. #pragma once #include #include void IO_Event_Time_elapsed(const struct timespec* start, const struct timespec* stop, struct timespec *duration); float IO_Event_Time_duration(const struct timespec *duration); void IO_Event_Time_current(struct timespec *time); float IO_Event_Time_delta(const struct timespec *start, const struct timespec *stop); float IO_Event_Time_proportion(const struct timespec *duration, const struct timespec *total_duration); #define IO_EVENT_TIME_PRINTF_TIMESPEC "%.3g" #define IO_EVENT_TIME_PRINTF_TIMESPEC_ARGUMENTS(ts) ((double)(ts).tv_sec + (ts).tv_nsec / 1e9) socketry-io-event-ccd0953/ext/io/event/worker_pool.c000066400000000000000000000343171516444210200225620ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2025, by Samuel Williams. #include "worker_pool.h" #include "worker_pool_test.h" #include "fiber.h" #include #include #include #include #include #include #include enum { DEBUG = 0, }; static VALUE IO_Event_WorkerPool; static ID id_maximum_worker_count; // Thread pool structure struct IO_Event_WorkerPool_Worker { VALUE thread; // Flag to indicate this specific worker should exit: bool interrupted; // Currently executing operation: rb_fiber_scheduler_blocking_operation_t *current_blocking_operation; struct IO_Event_WorkerPool *pool; struct IO_Event_WorkerPool_Worker *next; }; // Work item structure struct IO_Event_WorkerPool_Work { rb_fiber_scheduler_blocking_operation_t *blocking_operation; bool completed; VALUE scheduler; VALUE blocker; VALUE fiber; struct IO_Event_WorkerPool_Work *next; }; // Worker pool structure struct IO_Event_WorkerPool { pthread_mutex_t mutex; pthread_cond_t work_available; struct IO_Event_WorkerPool_Work *work_queue; struct IO_Event_WorkerPool_Work *work_queue_tail; struct IO_Event_WorkerPool_Worker *workers; size_t current_worker_count; size_t maximum_worker_count; size_t call_count; size_t completed_count; size_t cancelled_count; bool shutdown; }; // Free functions for Ruby GC static void worker_pool_free(void *ptr) { struct IO_Event_WorkerPool *pool = (struct IO_Event_WorkerPool *)ptr; if (pool) { // Signal shutdown to all workers if (!pool->shutdown) { pthread_mutex_lock(&pool->mutex); pool->shutdown = true; pthread_cond_broadcast(&pool->work_available); pthread_mutex_unlock(&pool->mutex); } // Note: We don't free worker structures or wait for threads during GC // as this can cause deadlocks. The Ruby GC will handle the thread objects. // Workers will see the shutdown flag and exit cleanly. } } static void worker_pool_mark(void *ptr) { struct IO_Event_WorkerPool *pool = (struct IO_Event_WorkerPool *)ptr; struct IO_Event_WorkerPool_Worker *worker = pool->workers; while (worker) { struct IO_Event_WorkerPool_Worker *next = worker->next; // We need to mark the thread even though its marked through the VM's ractors because we call `join` // on them after their completion. They could be freed by then. rb_gc_mark(worker->thread); // thread objects are currently pinned anyway worker = next; } } // Size functions for Ruby GC static size_t worker_pool_size(const void *ptr) { return sizeof(struct IO_Event_WorkerPool); } // Ruby TypedData structures static const rb_data_type_t IO_Event_WorkerPool_type = { "IO::Event::WorkerPool", {worker_pool_mark, worker_pool_free, worker_pool_size,}, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY }; // Helper function to enqueue work (must be called with mutex held) static void enqueue_work(struct IO_Event_WorkerPool *pool, struct IO_Event_WorkerPool_Work *work) { if (pool->work_queue_tail) { pool->work_queue_tail->next = work; } else { pool->work_queue = work; } pool->work_queue_tail = work; } // Helper function to dequeue work (must be called with mutex held) static struct IO_Event_WorkerPool_Work *dequeue_work(struct IO_Event_WorkerPool *pool) { struct IO_Event_WorkerPool_Work *work = pool->work_queue; if (work) { pool->work_queue = work->next; if (!pool->work_queue) { pool->work_queue_tail = NULL; } work->next = NULL; // Clear the next pointer for safety } return work; } // Unblock function to interrupt a specific worker. static void worker_unblock_func(void *_worker) { struct IO_Event_WorkerPool_Worker *worker = (struct IO_Event_WorkerPool_Worker *)_worker; struct IO_Event_WorkerPool *pool = worker->pool; // Mark this specific worker as interrupted pthread_mutex_lock(&pool->mutex); worker->interrupted = true; pthread_cond_broadcast(&pool->work_available); pthread_mutex_unlock(&pool->mutex); // If there's a currently executing blocking operation, cancel it if (worker->current_blocking_operation) { rb_fiber_scheduler_blocking_operation_cancel(worker->current_blocking_operation); } } // Function to wait for work and execute it without GVL. static void *worker_wait_and_execute(void *_worker) { struct IO_Event_WorkerPool_Worker *worker = (struct IO_Event_WorkerPool_Worker *)_worker; struct IO_Event_WorkerPool *pool = worker->pool; while (true) { struct IO_Event_WorkerPool_Work *work = NULL; pthread_mutex_lock(&pool->mutex); // Wait for work, shutdown, or interruption while (!pool->work_queue && !pool->shutdown && !worker->interrupted) { pthread_cond_wait(&pool->work_available, &pool->mutex); } if (pool->shutdown || worker->interrupted) { pthread_mutex_unlock(&pool->mutex); break; } work = dequeue_work(pool); pthread_mutex_unlock(&pool->mutex); // Execute work WITHOUT GVL (this is the whole point!) if (work) { worker->current_blocking_operation = work->blocking_operation; rb_fiber_scheduler_blocking_operation_execute(work->blocking_operation); worker->current_blocking_operation = NULL; } return work; } return NULL; // Shutdown signal } static VALUE worker_thread_func(void *_worker) { struct IO_Event_WorkerPool_Worker *worker = (struct IO_Event_WorkerPool_Worker *)_worker; while (true) { // Wait for work and execute it without holding GVL struct IO_Event_WorkerPool_Work *work = (struct IO_Event_WorkerPool_Work *)rb_thread_call_without_gvl(worker_wait_and_execute, worker, worker_unblock_func, worker); if (!work) { // Shutdown signal received break; } // Protected by GVL: work->completed = true; worker->pool->completed_count++; // Work was executed without GVL, now unblock the waiting fiber (we have GVL here) rb_fiber_scheduler_unblock(work->scheduler, work->blocker, work->fiber); } return Qnil; } // Create a new worker thread static int create_worker_thread(VALUE self, struct IO_Event_WorkerPool *pool) { if (pool->current_worker_count >= pool->maximum_worker_count) { return -1; } struct IO_Event_WorkerPool_Worker *worker = malloc(sizeof(struct IO_Event_WorkerPool_Worker)); if (!worker) { return -1; } worker->pool = pool; worker->interrupted = false; worker->current_blocking_operation = NULL; worker->next = pool->workers; RB_OBJ_WRITE(self, &worker->thread, rb_thread_create(worker_thread_func, worker)); if (NIL_P(worker->thread)) { free(worker); return -1; } pool->workers = worker; pool->current_worker_count++; return 0; } // Ruby constructor for WorkerPool static VALUE worker_pool_initialize(int argc, VALUE *argv, VALUE self) { size_t maximum_worker_count = 1; // Default // Extract keyword arguments VALUE kwargs = Qnil; VALUE rb_maximum_worker_count = Qnil; rb_scan_args(argc, argv, "0:", &kwargs); if (!NIL_P(kwargs)) { VALUE kwvals[1]; ID kwkeys[1] = {id_maximum_worker_count}; rb_get_kwargs(kwargs, kwkeys, 0, 1, kwvals); rb_maximum_worker_count = kwvals[0]; } if (!NIL_P(rb_maximum_worker_count)) { maximum_worker_count = NUM2SIZET(rb_maximum_worker_count); if (maximum_worker_count == 0) { rb_raise(rb_eArgError, "maximum_worker_count must be greater than 0!"); } } // Get the pool that was allocated by worker_pool_allocate struct IO_Event_WorkerPool *pool; TypedData_Get_Struct(self, struct IO_Event_WorkerPool, &IO_Event_WorkerPool_type, pool); if (!pool) { rb_raise(rb_eRuntimeError, "WorkerPool allocation failed!"); } pthread_mutex_init(&pool->mutex, NULL); pthread_cond_init(&pool->work_available, NULL); pool->work_queue = NULL; pool->work_queue_tail = NULL; pool->workers = NULL; pool->current_worker_count = 0; pool->maximum_worker_count = maximum_worker_count; pool->call_count = 0; pool->completed_count = 0; pool->cancelled_count = 0; pool->shutdown = false; // Create initial workers for (size_t i = 0; i < maximum_worker_count; i++) { if (create_worker_thread(self, pool) != 0) { // Just set the maximum_worker_count for debugging, don't fail completely // worker_pool_free(pool); // rb_raise(rb_eRuntimeError, "Failed to create workers"); break; } } return self; } static VALUE worker_pool_work_begin(VALUE _work) { struct IO_Event_WorkerPool_Work *work = (void*)_work; if (DEBUG) fprintf(stderr, "worker_pool_work_begin:rb_fiber_scheduler_block work=%p\n", work); rb_fiber_scheduler_block(work->scheduler, work->blocker, Qnil); return Qnil; } // Ruby method to submit work and wait for completion static VALUE worker_pool_call(VALUE self, VALUE _blocking_operation) { struct IO_Event_WorkerPool *pool; TypedData_Get_Struct(self, struct IO_Event_WorkerPool, &IO_Event_WorkerPool_type, pool); if (pool->shutdown) { rb_raise(rb_eRuntimeError, "Worker pool is shut down!"); } // Increment call count (protected by GVL) pool->call_count++; // Get current fiber and scheduler VALUE fiber = rb_fiber_current(); VALUE scheduler = rb_fiber_scheduler_current(); if (NIL_P(scheduler)) { rb_raise(rb_eRuntimeError, "WorkerPool requires a fiber scheduler!"); } // Extract blocking operation handle rb_fiber_scheduler_blocking_operation_t *blocking_operation = rb_fiber_scheduler_blocking_operation_extract(_blocking_operation); if (!blocking_operation) { rb_raise(rb_eArgError, "Invalid blocking operation!"); } // Create work item struct IO_Event_WorkerPool_Work work = { .blocking_operation = blocking_operation, .completed = false, .scheduler = scheduler, .blocker = self, .fiber = fiber, .next = NULL }; // Enqueue work: pthread_mutex_lock(&pool->mutex); enqueue_work(pool, &work); pthread_cond_signal(&pool->work_available); pthread_mutex_unlock(&pool->mutex); // Block the current fiber until work is completed: int state = 0; while (true) { int current_state = 0; rb_protect(worker_pool_work_begin, (VALUE)&work, ¤t_state); if (DEBUG) fprintf(stderr, "-- worker_pool_call:work completed=%d, current_state=%d, state=%d\n", work.completed, current_state, state); // Store the first exception state: if (!state) { state = current_state; } // If the work is still in the queue, we must wait for a worker to complete it (even if cancelled): if (work.completed) { // The work was completed, we can exit the loop: break; } else { if (DEBUG) fprintf(stderr, "worker_pool_call:rb_fiber_scheduler_blocking_operation_cancel\n"); // Ensure the blocking operation is cancelled: rb_fiber_scheduler_blocking_operation_cancel(blocking_operation); // The work was not completed, we need to wait for it to be completed, so we go around the loop again. } } if (DEBUG) fprintf(stderr, "<- worker_pool_call:work completed=%d, state=%d\n", work.completed, state); if (state) { rb_jump_tag(state); } else { return Qtrue; } } static VALUE worker_pool_allocate(VALUE klass) { struct IO_Event_WorkerPool *pool; VALUE self = TypedData_Make_Struct(klass, struct IO_Event_WorkerPool, &IO_Event_WorkerPool_type, pool); // Initialize to NULL/zero so we can detect uninitialized pools memset(pool, 0, sizeof(struct IO_Event_WorkerPool)); return self; } // Ruby method to close the worker pool static VALUE worker_pool_close(VALUE self) { struct IO_Event_WorkerPool *pool; TypedData_Get_Struct(self, struct IO_Event_WorkerPool, &IO_Event_WorkerPool_type, pool); if (!pool) { rb_raise(rb_eRuntimeError, "WorkerPool not initialized!"); } if (pool->shutdown) { return Qnil; // Already closed } // Signal shutdown to all workers pthread_mutex_lock(&pool->mutex); pool->shutdown = true; pthread_cond_broadcast(&pool->work_available); pthread_mutex_unlock(&pool->mutex); // Wait for all worker threads to finish struct IO_Event_WorkerPool_Worker *worker = pool->workers; while (worker) { if (!NIL_P(worker->thread)) { rb_funcall(worker->thread, rb_intern("join"), 0); } worker = worker->next; } // Clean up worker structures worker = pool->workers; while (worker) { struct IO_Event_WorkerPool_Worker *next = worker->next; free(worker); worker = next; } pool->workers = NULL; pool->current_worker_count = 0; // Clean up mutex and condition variable pthread_mutex_destroy(&pool->mutex); pthread_cond_destroy(&pool->work_available); return Qnil; } // Test helper: get pool statistics for debugging/testing static VALUE worker_pool_statistics(VALUE self) { struct IO_Event_WorkerPool *pool; TypedData_Get_Struct(self, struct IO_Event_WorkerPool, &IO_Event_WorkerPool_type, pool); if (!pool) { rb_raise(rb_eRuntimeError, "WorkerPool not initialized!"); } VALUE stats = rb_hash_new(); rb_hash_aset(stats, ID2SYM(rb_intern("current_worker_count")), SIZET2NUM(pool->current_worker_count)); rb_hash_aset(stats, ID2SYM(rb_intern("maximum_worker_count")), SIZET2NUM(pool->maximum_worker_count)); rb_hash_aset(stats, ID2SYM(rb_intern("call_count")), SIZET2NUM(pool->call_count)); rb_hash_aset(stats, ID2SYM(rb_intern("completed_count")), SIZET2NUM(pool->completed_count)); rb_hash_aset(stats, ID2SYM(rb_intern("cancelled_count")), SIZET2NUM(pool->cancelled_count)); rb_hash_aset(stats, ID2SYM(rb_intern("shutdown")), pool->shutdown ? Qtrue : Qfalse); // Count work items in queue (only if properly initialized) if (pool->maximum_worker_count > 0) { pthread_mutex_lock(&pool->mutex); size_t current_queue_size = 0; struct IO_Event_WorkerPool_Work *work = pool->work_queue; while (work) { current_queue_size++; work = work->next; } pthread_mutex_unlock(&pool->mutex); rb_hash_aset(stats, ID2SYM(rb_intern("current_queue_size")), SIZET2NUM(current_queue_size)); } else { rb_hash_aset(stats, ID2SYM(rb_intern("current_queue_size")), SIZET2NUM(0)); } return stats; } void Init_IO_Event_WorkerPool(VALUE IO_Event) { // Initialize symbols id_maximum_worker_count = rb_intern("maximum_worker_count"); IO_Event_WorkerPool = rb_define_class_under(IO_Event, "WorkerPool", rb_cObject); rb_define_alloc_func(IO_Event_WorkerPool, worker_pool_allocate); rb_define_method(IO_Event_WorkerPool, "initialize", worker_pool_initialize, -1); rb_define_method(IO_Event_WorkerPool, "call", worker_pool_call, 1); rb_define_method(IO_Event_WorkerPool, "close", worker_pool_close, 0); rb_define_method(IO_Event_WorkerPool, "statistics", worker_pool_statistics, 0); // Initialize test functions Init_IO_Event_WorkerPool_Test(IO_Event_WorkerPool); } socketry-io-event-ccd0953/ext/io/event/worker_pool.h000066400000000000000000000002351516444210200225570ustar00rootroot00000000000000// Released under the MIT License. // Copyright, 2025, by Samuel Williams. #pragma once #include void Init_IO_Event_WorkerPool(VALUE IO_Event); socketry-io-event-ccd0953/ext/io/event/worker_pool_test.c000066400000000000000000000133121516444210200236110ustar00rootroot00000000000000// worker_pool_test.c - Test functions for WorkerPool cancellation // Released under the MIT License. // Copyright, 2025, by Samuel Williams. #include "worker_pool_test.h" #include #include #include #include #include #include static ID id_duration; struct BusyOperationData { int read_fd; int write_fd; volatile int cancelled; double duration; // How long to wait (for testing) clock_t start_time; clock_t end_time; int operation_result; VALUE exception; }; // The actual blocking operation that can be cancelled static void* busy_blocking_operation(void *data) { struct BusyOperationData *busy_data = (struct BusyOperationData*)data; // Use select() to wait for the pipe to become readable fd_set read_fds; struct timeval timeout; FD_ZERO(&read_fds); FD_SET(busy_data->read_fd, &read_fds); // Set timeout based on duration timeout.tv_sec = (long)busy_data->duration; timeout.tv_usec = ((busy_data->duration - timeout.tv_sec) * 1000000); // This will block until: // 1. The pipe becomes readable (cancellation) // 2. The timeout expires // 3. An error occurs int result = select(busy_data->read_fd + 1, &read_fds, NULL, NULL, &timeout); if (result > 0 && FD_ISSET(busy_data->read_fd, &read_fds)) { // Pipe became readable - we were cancelled char buffer; read(busy_data->read_fd, &buffer, 1); // Consume the byte busy_data->cancelled = 1; return (void*)-1; // Indicate cancellation } else if (result == 0) { // Timeout - operation completed normally return (void*)0; // Indicate success } else { // Error occurred return (void*)-2; // Indicate error } } // Unblock function that writes to the pipe to cancel the operation static void busy_unblock_function(void *data) { struct BusyOperationData *busy_data = (struct BusyOperationData*)data; busy_data->cancelled = 1; // Write a byte to the pipe to wake up the select() char wake_byte = 1; write(busy_data->write_fd, &wake_byte, 1); } // Function for the main operation execution (for rb_rescue) static VALUE busy_operation_execute(VALUE data_value) { struct BusyOperationData *busy_data = (struct BusyOperationData*)data_value; // Record start time busy_data->start_time = clock(); // Execute the blocking operation void *block_result = rb_nogvl( busy_blocking_operation, busy_data, busy_unblock_function, busy_data, RB_NOGVL_UBF_ASYNC_SAFE | RB_NOGVL_OFFLOAD_SAFE ); // Record end time busy_data->end_time = clock(); // Store the operation result busy_data->operation_result = (int)(intptr_t)block_result; return Qnil; } // Function for exception handling (for rb_rescue) static VALUE busy_operation_rescue(VALUE data_value, VALUE exception) { struct BusyOperationData *busy_data = (struct BusyOperationData*)data_value; // Record end time even in case of exception busy_data->end_time = clock(); // Mark that an exception was caught busy_data->exception = exception; return exception; } // Ruby method: IO::Event::WorkerPool.busy(duration: 1.0) // This creates a cancellable blocking operation for testing static VALUE worker_pool_test_busy(int argc, VALUE *argv, VALUE self) { double duration = 1.0; // Default 1 second // Extract keyword arguments VALUE kwargs = Qnil; VALUE rb_duration = Qnil; rb_scan_args(argc, argv, "0:", &kwargs); if (!NIL_P(kwargs)) { VALUE kwvals[1]; ID kwkeys[1] = {id_duration}; rb_get_kwargs(kwargs, kwkeys, 0, 1, kwvals); rb_duration = kwvals[0]; } if (!NIL_P(rb_duration)) { duration = NUM2DBL(rb_duration); } // Create pipe for cancellation int pipe_fds[2]; if (pipe(pipe_fds) != 0) { rb_sys_fail("pipe creation failed"); } // Stack allocate operation data struct BusyOperationData busy_data = { .read_fd = pipe_fds[0], .write_fd = pipe_fds[1], .cancelled = 0, .duration = duration, .start_time = 0, .end_time = 0, .operation_result = 0, .exception = Qnil }; // Execute the blocking operation with exception handling using function pointers rb_rescue( busy_operation_execute, (VALUE)&busy_data, busy_operation_rescue, (VALUE)&busy_data ); // Calculate elapsed time from the state stored in busy_data double elapsed = ((double)(busy_data.end_time - busy_data.start_time)) / CLOCKS_PER_SEC; // Create result hash using the state from busy_data VALUE result = rb_hash_new(); rb_hash_aset(result, ID2SYM(rb_intern("duration")), DBL2NUM(duration)); rb_hash_aset(result, ID2SYM(rb_intern("elapsed")), DBL2NUM(elapsed)); // Determine result based on operation outcome if (busy_data.exception != Qnil) { rb_hash_aset(result, ID2SYM(rb_intern("result")), ID2SYM(rb_intern("exception"))); rb_hash_aset(result, ID2SYM(rb_intern("cancelled")), Qtrue); rb_hash_aset(result, ID2SYM(rb_intern("exception")), busy_data.exception); } else if (busy_data.operation_result == -1) { rb_hash_aset(result, ID2SYM(rb_intern("result")), ID2SYM(rb_intern("cancelled"))); rb_hash_aset(result, ID2SYM(rb_intern("cancelled")), Qtrue); } else if (busy_data.operation_result == 0) { rb_hash_aset(result, ID2SYM(rb_intern("result")), ID2SYM(rb_intern("completed"))); rb_hash_aset(result, ID2SYM(rb_intern("cancelled")), Qfalse); } else { rb_hash_aset(result, ID2SYM(rb_intern("result")), ID2SYM(rb_intern("error"))); rb_hash_aset(result, ID2SYM(rb_intern("cancelled")), Qfalse); } // Clean up pipe file descriptors close(pipe_fds[0]); close(pipe_fds[1]); return result; } // Initialize the test functions void Init_IO_Event_WorkerPool_Test(VALUE IO_Event_WorkerPool) { // Initialize symbols id_duration = rb_intern("duration"); // Add test methods to IO::Event::WorkerPool class rb_define_singleton_method(IO_Event_WorkerPool, "busy", worker_pool_test_busy, -1); } socketry-io-event-ccd0953/ext/io/event/worker_pool_test.h000066400000000000000000000003511516444210200236150ustar00rootroot00000000000000// worker_pool_test.h - Header for WorkerPool test functions // Released under the MIT License. // Copyright, 2025, by Samuel Williams. #pragma once #include void Init_IO_Event_WorkerPool_Test(VALUE IO_Event_WorkerPool); socketry-io-event-ccd0953/fixtures/000077500000000000000000000000001516444210200173655ustar00rootroot00000000000000socketry-io-event-ccd0953/fixtures/io/000077500000000000000000000000001516444210200177745ustar00rootroot00000000000000socketry-io-event-ccd0953/fixtures/io/event/000077500000000000000000000000001516444210200211155ustar00rootroot00000000000000socketry-io-event-ccd0953/fixtures/io/event/test_scheduler.rb000066400000000000000000000074161516444210200244670ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2025, by Samuel Williams. require "io/event/timers" module IO::Event # A test fiber scheduler that uses WorkerPool for blocking operations. # # This scheduler implements the fiber scheduler interface and delegates # blocking operations to a WorkerPool instance for testing. # # @example Testing usage # ```ruby # # Create with default selector and worker pool # scheduler = IO::Event::TestScheduler.new # # # Or provide custom selector and/or worker pool # selector = IO::Event::Selector.new(Fiber.current) # worker_pool = IO::Event::WorkerPool.new(maximum_worker_count: 4) # scheduler = IO::Event::TestScheduler.new(selector: selector, worker_pool: worker_pool) # # Fiber.set_scheduler(scheduler) # # # Standard Ruby operations that use rb_nogvl will be handled by the worker pool # # Examples: sleep, file I/O, network operations, etc. # Fiber.schedule do # sleep(0.001) # This triggers rb_nogvl and uses the worker pool # end.resume # ``` class TestScheduler def initialize(selector: nil, worker_pool: nil, maximum_worker_count: nil) @selector = selector || ::IO::Event::Selector.new(Fiber.current) if ::IO::Event.const_defined?(:WorkerPool) @worker_pool = worker_pool || ::IO::Event::WorkerPool.new(maximum_worker_count: maximum_worker_count) end @timers = ::IO::Event::Timers.new # Track the number of fibers that are blocked. @blocked = 0 end # @attribute [WorkerPool] The worker pool used for executing blocking operations. attr_reader :worker_pool # @attribute [IO::Event::Selector] The I/O event selector used for managing fiber scheduling. attr_reader :selector if ::IO::Event.const_defined?(:WorkerPool) # Optional fiber scheduler hook - delegates to WorkerPool: def blocking_operation_wait(operation) @blocked += 1 # Submit the operation to the worker pool and wait for completion @worker_pool&.call(operation) ensure @blocked -= 1 end end # Required fiber scheduler hooks def close @selector&.close @worker_pool&.close @worker_pool = nil end def block(blocker, timeout = nil) fiber = Fiber.current if timeout timer = @timers.after(timeout) do if fiber.alive? fiber.transfer(false) end end end begin @blocked += 1 @selector.transfer ensure @blocked -= 1 end ensure timer&.cancel! end def unblock(blocker, fiber) if selector = @selector selector.push(fiber) selector.wakeup end end class FiberInterrupt def initialize(fiber, exception) @fiber = fiber @exception = exception end def alive? @fiber.alive? end def transfer @fiber.raise(@exception) end end def fiber_interrupt(fiber, exception) unblock(nil, FiberInterrupt.new(fiber, exception)) end def io_wait(io, events, timeout = nil) fiber = Fiber.current if timeout timer = @timers.after(timeout) do fiber.transfer end end begin @blocked += 1 return @selector.io_wait(fiber, io, events) ensure @blocked -= 1 end ensure timer&.cancel! end def kernel_sleep(duration = nil) if duration self.block(nil, duration) else @selector.transfer end end def fiber(&block) Fiber.new(&block).tap(&:transfer) end # Run the scheduler event loop def run while @blocked > 0 or @timers.size > 0 interval = @timers.wait_interval @selector.select(interval) @timers.fire end end def scheduler_close(error = $!) self.run ensure self.close end private def transfer @selector.transfer end def push(fiber) @selector.push(fiber) end def wakeup @selector.wakeup end end end socketry-io-event-ccd0953/fixtures/unix_socket.rb000066400000000000000000000003731516444210200222500ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2023, by Samuel Williams. unless Object.const_defined?(:UNIXSocket) class UNIXSocket def self.pair(&block) Socket.pair(:INET, :STREAM, 0, &block) end end end socketry-io-event-ccd0953/gems.rb000066400000000000000000000007331516444210200167770ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2026, by Samuel Williams. source "https://rubygems.org" gemspec group :maintenance, optional: true do gem "bake-gem" gem "bake-modernize" gem "bake-releases" gem "agent-context" gem "utopia-project" end group :test do gem "sus" gem "covered" gem "decode" gem "rubocop" gem "rubocop-md" gem "rubocop-socketry" gem "bake-test" gem "bake-test-external" gem "async" end socketry-io-event-ccd0953/guides/000077500000000000000000000000001516444210200167745ustar00rootroot00000000000000socketry-io-event-ccd0953/guides/getting-started/000077500000000000000000000000001516444210200221015ustar00rootroot00000000000000socketry-io-event-ccd0953/guides/getting-started/readme.md000066400000000000000000000027251516444210200236660ustar00rootroot00000000000000# Getting Started This guide explains how to use `io-event` for non-blocking IO. ## Installation Add the gem to your project: ~~~ bash $ bundle add io-event ~~~ ## Core Concepts `io-event` has several core concepts: - A {ruby IO::Event::Selector} implementation which provides the primitive operations for implementation an event loop. - A {ruby IO::Event::Debug::Selector} which adds extra validations and checks at the expense of performance. You should generally use this during tests. ## Basic Event Loop This example shows how to perform a blocking operation ```ruby require "fiber" require "io/event" selector = IO::Event::Selector.new(Fiber.current) input, output = IO.pipe writer = Fiber.new do output.write("Hello World") output.close end reader = Fiber.new do selector.io_wait(Fiber.current, input, IO::READABLE) pp read: input.read end # The reader will be blocked until the IO has data available: reader.transfer # Write some data to the pipe and close the writing end: writer.transfer selector.select(1) # Results in: # {:read=>"Hello World"} ``` ## Debugging 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: ```shell $ IO_EVENT_SELECTOR_DEBUG=y IO_EVENT_SELECTOR_DEBUG_LOG=/dev/stderr bundle exec ./my_script.rb ``` The format of the log is subject to change, but it may be useful for debugging. socketry-io-event-ccd0953/guides/links.yaml000066400000000000000000000000341516444210200207750ustar00rootroot00000000000000getting-started: order: 1 socketry-io-event-ccd0953/io-event.gemspec000066400000000000000000000020001516444210200205770ustar00rootroot00000000000000# frozen_string_literal: true require_relative "lib/io/event/version" Gem::Specification.new do |spec| spec.name = "io-event" spec.version = IO::Event::VERSION spec.summary = "An event loop." spec.authors = ["Samuel Williams", "Math Ieu", "Wander Hillen", "Jean Boussier", "Benoit Daloze", "Bruno Sutic", "Shizuo Fujita", "Alex Matchneer", "Anthony Ross", "Delton Ding", "Italo Brandão", "John Hawthorn", "Luke Gruber", "Pavel Rosický", "Stan Hu", "Stanislav (Stas) Katkov", "William T. Nelson"] spec.license = "MIT" spec.cert_chain = ["release.cert"] spec.signing_key = File.expand_path("~/.gem/release.pem") spec.homepage = "https://github.com/socketry/io-event" spec.metadata = { "documentation_uri" => "https://socketry.github.io/io-event/", "source_code_uri" => "https://github.com/socketry/io-event.git", } spec.files = Dir["{context,ext,lib}/**/*", "*.md", base: __dir__] spec.require_paths = ["lib"] spec.extensions = ["ext/extconf.rb"] spec.required_ruby_version = ">= 3.3" end socketry-io-event-ccd0953/lib/000077500000000000000000000000001516444210200162625ustar00rootroot00000000000000socketry-io-event-ccd0953/lib/io/000077500000000000000000000000001516444210200166715ustar00rootroot00000000000000socketry-io-event-ccd0953/lib/io/event.rb000066400000000000000000000004221516444210200203350ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2026, by Samuel Williams. require_relative "event/version" require_relative "event/support" require_relative "event/selector" require_relative "event/timers" require_relative "event/native" socketry-io-event-ccd0953/lib/io/event/000077500000000000000000000000001516444210200200125ustar00rootroot00000000000000socketry-io-event-ccd0953/lib/io/event/debug/000077500000000000000000000000001516444210200211005ustar00rootroot00000000000000socketry-io-event-ccd0953/lib/io/event/debug/selector.rb000066400000000000000000000122031516444210200232430ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2026, by Samuel Williams. module IO::Event # @namespace module Debug # Enforces the selector interface and delegates operations to a wrapped selector instance. # # 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. class Selector # Wrap the given selector with debugging. # # @parameter selector [Selector] The selector to wrap. # @parameter env [Hash] The environment to read configuration from. def self.wrap(selector, env = ENV) log = nil if log_path = env["IO_EVENT_DEBUG_SELECTOR_LOG"] log = File.open(log_path, "w") end return self.new(selector, log: log) end # Initialize the debug selector with the given selector and optional log. # # @parameter selector [Selector] The selector to wrap. # @parameter log [IO] The log to write debug messages to. def initialize(selector, log: nil) @selector = selector @readable = {} @writable = {} @priority = {} unless Fiber.current == selector.loop Kernel::raise "Selector must be initialized on event loop fiber!" end @log = log end # The idle duration of the underlying selector. # # @returns [Numeric] The idle duration. def idle_duration @selector.idle_duration end # The current time. # # @returns [Numeric] The current time. def now Process.clock_gettime(Process::CLOCK_MONOTONIC) end # Log the given message. # # @asynchronous Will block the calling fiber and the entire event loop. def log(message) return unless @log Fiber.blocking do @log.puts("T+%10.1f; %s" % [now, message]) end end # Wakeup the the selector. def wakeup @selector.wakeup end # Close the selector. def close log("Closing selector") if @selector.nil? Kernel::raise "Selector already closed!" end @selector.close @selector = nil end # Transfer from the calling fiber to the selector. def transfer log("Transfering to event loop") @selector.transfer end # Resume the given fiber with the given arguments. def resume(*arguments) log("Resuming fiber with #{arguments.inspect}") @selector.resume(*arguments) end # Yield to the selector. def yield log("Yielding to event loop") @selector.yield end # Push the given fiber to the selector ready list, such that it will be resumed on the next call to {select}. # # @parameter fiber [Fiber] The fiber that is ready. def push(fiber) log("Pushing fiber #{fiber.inspect} to ready list") @selector.push(fiber) end # Raise the given exception on the given fiber. # # @parameter fiber [Fiber] The fiber to raise the exception on. # @parameter arguments [Array] The arguments to use when raising the exception. def raise(fiber, *arguments, **options) log("Raising exception on fiber #{fiber.inspect} with #{arguments.inspect}") @selector.raise(fiber, *arguments, **options) end # Check if the selector is ready. # # @returns [Boolean] Whether the selector is ready. def ready? @selector.ready? end # Run the given blocking operation and wait for its completion. def blocking_operation_wait(operation) log("Waiting for blocking operation #{operation.inspect}") @selector.blocking_operation_wait(operation) end # Wait for the given process, forwarded to the underlying selector. def process_wait(*arguments) log("Waiting for process with #{arguments.inspect}") @selector.process_wait(*arguments) end # Wait for the given IO, forwarded to the underlying selector. def io_wait(fiber, io, events) log("Waiting for IO #{io.inspect} for events #{events.inspect}") @selector.io_wait(fiber, io, events) end # Read from the given IO, forwarded to the underlying selector. def io_read(fiber, io, buffer, length, offset = 0) log("Reading from IO #{io.inspect} with buffer #{buffer}; length #{length} offset #{offset}") @selector.io_read(fiber, io, buffer, length, offset) end # Write to the given IO, forwarded to the underlying selector. def io_write(fiber, io, buffer, length, offset = 0) log("Writing to IO #{io.inspect} with buffer #{buffer}; length #{length} offset #{offset}") @selector.io_write(fiber, io, buffer, length, offset) end # Forward the given method to the underlying selector. def respond_to?(name, include_private = false) @selector.respond_to?(name, include_private) end # Select for the given duration, forwarded to the underlying selector. def select(duration = nil) log("Selecting for #{duration.inspect}") unless Fiber.current == @selector.loop Kernel::raise "Selector must be run on event loop fiber!" end @selector.select(duration) end end end end socketry-io-event-ccd0953/lib/io/event/interrupt.rb000066400000000000000000000013711516444210200223750ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2024, by Samuel Williams. module IO::Event # A thread safe synchronisation primative. class Interrupt def self.attach(selector) self.new(selector) end def initialize(selector) @selector = selector @input, @output = ::IO.pipe @fiber = Fiber.new do while true if @selector.io_wait(@fiber, @input, IO::READABLE) @input.read_nonblock(1) end end end @fiber.transfer end # Send a sigle byte interrupt. def signal @output.write(".") @output.flush rescue IOError # Ignore. end def close @input.close @output.close # @fiber.raise(::Interrupt) end end private_constant :Interrupt end socketry-io-event-ccd0953/lib/io/event/native.rb000066400000000000000000000003761516444210200216330ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2025, by Samuel Williams. begin require "IO_Event" rescue LoadError => error warn "Could not load native event selector: #{error}" require_relative "selector/nonblock" end socketry-io-event-ccd0953/lib/io/event/priority_heap.rb000066400000000000000000000162551516444210200232260ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2021, by Wander Hillen. # Copyright, 2021-2026, by Samuel Williams. class IO module Event # A priority queue implementation using a standard binary minheap. It uses straight comparison # of its contents to determine priority. # See for explanations of the main methods. class PriorityHeap # Initializes the heap. def initialize # The heap is represented with an array containing a binary tree. See # https://en.wikipedia.org/wiki/Binary_heap#Heap_implementation for how this array # is built up. @contents = [] end # @returns [Object | Nil] the smallest element in the heap without removing it, or nil if the heap is empty. def peek @contents[0] end # @returns [Integer] the number of elements in the heap. def size @contents.size end # @returns [Boolean] true if the heap is empty, false otherwise. def empty? @contents.empty? end # Removes and returns the smallest element in the heap, or nil if the heap is empty. # # @returns [Object | Nil] The smallest element in the heap, or nil if the heap is empty. def pop # If the heap is empty: if @contents.empty? return nil end # If we have only one item, no swapping is required: if @contents.size == 1 return @contents.pop end # Take the root of the tree: value = @contents[0] # Remove the last item in the tree: last = @contents.pop # Overwrite the root of the tree with the item: @contents[0] = last # Bubble it down into place: bubble_down(0) # validate! return value end # Add a new element to the heap, then rearrange elements until the heap invariant is true again. # # @parameter element [Object] The element to add to the heap. def push(element) # Insert the item at the end of the heap: @contents.push(element) # Bubble it up into position: bubble_up(@contents.size - 1) # validate! return self end # Add multiple elements to the heap efficiently in O(n) time. # This is more efficient than calling push multiple times (O(n log n)). # # @parameter elements [Array] The elements to add to the heap. # @returns [self] Returns self for method chaining. def concat(elements) return self if elements.empty? # Add all elements to the array without maintaining heap property - O(n) @contents.concat(elements) # Rebuild the heap property for the entire array - O(n) heapify! return self end # Empties out the heap, discarding all elements def clear! @contents = [] end # Remove a specific element from the heap. # # O(n) where n is the number of elements in the heap. # # @parameter element [Object] The element to remove. # @returns [Object | Nil] The removed element, or nil if not found. def delete(element) # Find the index of the element - O(n) linear search index = @contents.index(element) return nil unless index # If it's the last element, just remove it if index == @contents.size - 1 return @contents.pop end # Store the value we're removing removed_value = @contents[index] # Replace with the last element last = @contents.pop @contents[index] = last # Restore heap property - might need to bubble up or down if index > 0 && @contents[index] < @contents[(index - 1) / 2] # New element is smaller than parent, bubble up bubble_up(index) else # New element might be larger than children, bubble down bubble_down(index) end # validate! return removed_value end # Remove elements matching the given block condition by rebuilding the heap. # # This is more efficient than multiple delete operations when removing many elements. # # O(n) where n is the number of elements in the heap. # # @yields [Object] Each element in the heap for testing # @returns [Integer] The number of elements removed def delete_if return enum_for(:delete_if) unless block_given? original_size = @contents.size # Filter out elements that match the condition - O(n) @contents.reject!{|element| yield(element)} # If we removed elements, rebuild the heap - O(n) if @contents.size < original_size heapify! end # Return number of elements removed original_size - @contents.size end # Validate the heap invariant. Every element except the root must not be smaller than its parent element. Note that it MAY be equal. def valid? # Notice we skip index 0 on purpose, because it has no parent: (1..(@contents.size - 1)).all?{|index| @contents[index] >= @contents[(index - 1) / 2]} end private # Rebuild the heap property from an arbitrary array in O(n) time. # Uses bottom-up heapify algorithm starting from the last non-leaf node. def heapify! return if @contents.size <= 1 # Start from the last non-leaf node and work backwards to root: last_non_leaf_index = (@contents.size / 2) - 1 last_non_leaf_index.downto(0) do |index| bubble_down(index) end # validate! end # Left here for reference, but unused. # def swap(i, j) # @contents[i], @contents[j] = @contents[j], @contents[i] # end def bubble_up(index) parent_index = (index - 1) / 2 # watch out, integer division! while index > 0 && @contents[index] < @contents[parent_index] # 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. # swap(index, parent_index) @contents[index], @contents[parent_index] = @contents[parent_index], @contents[index] index = parent_index parent_index = (index - 1) / 2 # watch out, integer division! end end def bubble_down(index) swap_value = 0 swap_index = nil while true left_index = (2 * index) + 1 left_value = @contents[left_index] if left_value.nil? # This node has no children so it can't bubble down any further. We're done here! return end # Determine which of the child nodes has the smallest value: right_index = left_index + 1 right_value = @contents[right_index] if right_value.nil? or right_value > left_value swap_value = left_value swap_index = left_index else swap_value = right_value swap_index = right_index end if @contents[index] < swap_value # No need to swap, the minheap invariant is already satisfied: return else # 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: # swap(index, swap_index) @contents[index], @contents[swap_index] = @contents[swap_index], @contents[index] index = swap_index end end end end end end socketry-io-event-ccd0953/lib/io/event/selector.rb000066400000000000000000000023231516444210200221570ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2026, by Samuel Williams. require_relative "selector/select" require_relative "debug/selector" module IO::Event # @namespace module Selector # The default selector implementation, which is chosen based on the environment and available implementations. # # @parameter env [Hash] The environment to read configuration from. # @returns [Class] The default selector implementation. def self.default(env = ENV) if name = env["IO_EVENT_SELECTOR"]&.to_sym return const_get(name) end if self.const_defined?(:URing) URing elsif self.const_defined?(:EPoll) EPoll elsif self.const_defined?(:KQueue) KQueue else Select end end # Create a new selector instance, according to the best available implementation. # # @parameter loop [Fiber] The event loop fiber. # @parameter env [Hash] The environment to read configuration from. # @returns [Selector] The new selector instance. def self.new(loop, env = ENV) selector = default(env).new(loop) if debug = env["IO_EVENT_DEBUG_SELECTOR"] selector = Debug::Selector.wrap(selector, env) end return selector end end end socketry-io-event-ccd0953/lib/io/event/selector/000077500000000000000000000000001516444210200216325ustar00rootroot00000000000000socketry-io-event-ccd0953/lib/io/event/selector/nonblock.rb000066400000000000000000000006541516444210200237710ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2024, by Samuel Williams. require "io/nonblock" module IO::Event module Selector # Execute the given block in non-blocking mode. # # @parameter io [IO] The IO object to operate on. # @yields {...} The block to execute. def self.nonblock(io, &block) io.nonblock(&block) rescue Errno::EBADF # Windows. yield end end end socketry-io-event-ccd0953/lib/io/event/selector/select.rb000066400000000000000000000237271516444210200234510ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2026, by Samuel Williams. # Copyright, 2023, by Math Ieu. require_relative "../interrupt" module IO::Event module Selector # A pure-Ruby implementation of the event selector. class Select # Initialize the selector with the given event loop fiber. def initialize(loop) @loop = loop @waiting = Hash.new.compare_by_identity # Flag indicating whether the selector is currently blocked in a system call. # Set to true when blocked in ::IO.select, false otherwise. # Used by wakeup() to determine if an interrupt signal is needed. @blocked = false @ready = Queue.new @interrupt = Interrupt.attach(self) @idle_duration = 0.0 end # @attribute [Fiber] The event loop fiber. attr :loop # @attribute [Float] This is the amount of time the event loop was idle during the last select call. attr :idle_duration # Wake up the event loop if it is currently sleeping. def wakeup if @blocked @interrupt.signal return true end return false end # Close the selector and release any resources. def close @interrupt.close @loop = nil @waiting = nil end Optional = Struct.new(:fiber) do def transfer(*arguments) fiber&.transfer(*arguments) end def alive? fiber&.alive? end def nullify self.fiber = nil end end # Transfer from the current fiber to the event loop. def transfer @loop.transfer end # Transfer from the current fiber to the specified fiber. Put the current fiber into the ready list. def resume(fiber, *arguments) optional = Optional.new(Fiber.current) @ready.push(optional) fiber.transfer(*arguments) ensure optional.nullify end # Yield from the current fiber back to the event loop. Put the current fiber into the ready list. def yield optional = Optional.new(Fiber.current) @ready.push(optional) @loop.transfer ensure optional.nullify end # Append the given fiber into the ready list. def push(fiber) @ready.push(fiber) end # Transfer to the given fiber and raise an exception. Put the current fiber into the ready list. def raise(fiber, *arguments, **options) optional = Optional.new(Fiber.current) @ready.push(optional) fiber.raise(*arguments, **options) ensure optional.nullify end # @returns [Boolean] Whether the ready list is not empty, i.e. there are fibers ready to be resumed. def ready? !@ready.empty? end Waiter = Struct.new(:fiber, :events, :tail) do def alive? self.fiber&.alive? end # 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. def dispatch(events, &reactivate) # We capture the tail here, because calling reactivate might modify it: tail = self.tail if fiber = self.fiber if fiber.alive? revents = events & self.events if revents.zero? reactivate.call(self) else self.fiber = nil fiber.transfer(revents) end else self.fiber = nil end end tail&.dispatch(events, &reactivate) end def invalidate self.fiber = nil end def each(&block) if fiber = self.fiber yield fiber, self.events end self.tail&.each(&block) end end # Wait for the given IO to become readable or writable. # # @parameter fiber [Fiber] The fiber that is waiting. # @parameter io [IO] The IO object to wait on. # @parameter events [Integer] The events to wait for. def io_wait(fiber, io, events) waiter = @waiting[io] = Waiter.new(fiber, events, @waiting[io]) @loop.transfer ensure waiter&.invalidate end # Wait for multiple IO objects to become readable or writable. # # @parameter readable [Array(IO)] The list of IO objects to wait for readability. # @parameter writable [Array(IO)] The list of IO objects to wait for writability. # @parameter priority [Array(IO)] The list of IO objects to wait for priority events. def io_select(readable, writable, priority, timeout) Thread.new do IO.select(readable, writable, priority, timeout) end.value end EAGAIN = -Errno::EAGAIN::Errno EWOULDBLOCK = -Errno::EWOULDBLOCK::Errno # Whether the given error code indicates that the operation should be retried. protected def again?(errno) errno == EAGAIN or errno == EWOULDBLOCK end # Read from the given IO to the buffer. # # @parameter length [Integer] The minimum number of bytes to read. # @parameter offset [Integer] The offset into the buffer to read to. def io_read(fiber, io, buffer, length, offset = 0) # Ensure offset is within the bounds of the buffer to avoid ArgumentError if offset > buffer.size return -Errno::EINVAL::Errno end total = 0 Selector.nonblock(io) do while true result = Fiber.blocking{buffer.read(io, 0, offset)} if result < 0 if length > 0 and again?(result) self.io_wait(fiber, io, IO::READABLE) else return result end elsif result == 0 break else total += result break if total >= length offset += result end end end return total end # Write to the given IO from the buffer. # # @parameter length [Integer] The minimum number of bytes to write. # @parameter offset [Integer] The offset into the buffer to write from. def io_write(fiber, io, buffer, length, offset = 0) # Ensure offset is within the bounds of the buffer to avoid ArgumentError if offset > buffer.size return -Errno::EINVAL::Errno end total = 0 Selector.nonblock(io) do while true result = Fiber.blocking{buffer.write(io, 0, offset)} if result < 0 if length > 0 and again?(result) self.io_wait(fiber, io, IO::WRITABLE) else return result end elsif result == 0 break result else total += result break if total >= length offset += result end end end return total end # Wait for a process to change state. # # @parameter fiber [Fiber] The fiber to resume after waiting. # @parameter pid [Integer] The process ID to wait for. # @parameter flags [Integer] Flags to pass to Process::Status.wait. # @returns [Process::Status] The status of the waited process. def process_wait(fiber, pid, flags) Thread.new do Process::Status.wait(pid, flags) end.value end private def pop_ready unless @ready.empty? count = @ready.size count.times do fiber = @ready.pop fiber.transfer if fiber.alive? end return true end end # Wait for IO events or a timeout. # # @parameter duration [Numeric | Nil] The maximum time to wait, or nil for no timeout. # @returns [Integer] The number of ready IO objects. def select(duration = nil) if pop_ready # If we have popped items from the ready list, they may influence the duration calculation, so we don't delay the event loop: duration = 0 end readable = Array.new writable = Array.new priority = Array.new @waiting.delete_if do |io, waiter| if io.closed? # When an IO is closed, we silently drop it. Ruby 4's `rb_thread_io_close_interrupt` will take care of interrupting any fibers waiting on the closed IO, so we don't need to do anything here. true else waiter.each do |fiber, events| if (events & IO::READABLE) > 0 readable << io end if (events & IO::WRITABLE) > 0 writable << io end if (events & IO::PRIORITY) > 0 priority << io end end false end end duration = 0 unless @ready.empty? error = nil if duration&.>(0) start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) else @idle_duration = 0.0 end # 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. Thread.handle_interrupt(::Exception => :on_blocking) do @blocked = true readable, writable, priority = ::IO.select(readable, writable, priority, duration) rescue ::Exception => error # Requeue below... ensure @blocked = false if start_time end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) @idle_duration = end_time - start_time end end if error if error.is_a?(IOError) || error.is_a?(Errno::EBADF) # This can happen if an IO is closed while we're blocked in ::IO.select. Ruby 4's `rb_thread_io_close_interrupt` will take care of interrupting any fibers waiting on the closed IO, so we don't need to do anything here, except try again: return 0 end # For all other errors (e.g. thread interrupts), re-queue on the scheduler thread: Thread.current.raise(error) return 0 end ready = Hash.new(0).compare_by_identity readable&.each do |io| # Skip any IO that was closed/reused after IO.select returned - its fd number # may now belong to a different file, so resuming the waiter would be wrong: ready[io] |= IO::READABLE unless io.closed? end writable&.each do |io| ready[io] |= IO::WRITABLE unless io.closed? end priority&.each do |io| ready[io] |= IO::PRIORITY unless io.closed? end ready.each do |io, events| @waiting.delete(io).dispatch(events) do |waiter| # Re-schedule the waiting IO: waiter.tail = @waiting[io] @waiting[io] = waiter end end return ready.size end end end end socketry-io-event-ccd0953/lib/io/event/support.rb000066400000000000000000000004411516444210200220520ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2026, by Samuel Williams. class IO module Event # @namespace module Support # Check if the `IO::Buffer` class is available. def self.buffer? IO.const_defined?(:Buffer) end end end end socketry-io-event-ccd0953/lib/io/event/timers.rb000066400000000000000000000071551516444210200216520ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2024-2025, by Samuel Williams. require_relative "priority_heap" class IO module Event # An efficient sorted set of timers. class Timers # A handle to a scheduled timer. class Handle # Initialize the handle with the given time and block. # # @parameter time [Float] The time at which the block should be called. # @parameter block [Proc] The block to call. def initialize(time, block) @time = time @block = block end # @attribute [Float] The time at which the block should be called. attr :time # @attribute [Proc | Nil] The block to call when the timer fires. attr :block # Compare the handle with another handle. # # @parameter other [Handle] The other handle to compare with. # @returns [Boolean] Whether the handle is less than the other handle. def < other @time < other.time end # Compare the handle with another handle. # # @parameter other [Handle] The other handle to compare with. # @returns [Boolean] Whether the handle is greater than the other handle. def > other @time > other.time end # Invoke the block. def call(...) @block.call(...) end # Cancel the timer. def cancel! @block = nil end # @returns [Boolean] Whether the timer has been cancelled. def cancelled? @block.nil? end end # Initialize the timers. def initialize @heap = PriorityHeap.new @scheduled = [] end # @returns [Integer] The number of timers in the heap. def size flush! return @heap.size end # Schedule a block to be called at a specific time in the future. # # @parameter time [Float] The time at which the block should be called, relative to {#now}. # @parameter block [Proc] The block to call. def schedule(time, block) handle = Handle.new(time, block) @scheduled << handle return handle end # Schedule a block to be called after a specific time offset, relative to the current time as returned by {#now}. # # @parameter offset [#to_f] The time offset from the current time at which the block should be called. # @yields {|now| ...} When the timer fires. def after(offset, &block) schedule(self.now + offset.to_f, block) end # Compute the time interval until the next timer fires. # # @parameter now [Float] The current time. # @returns [Float | Nil] The time interval until the next timer fires, if any. def wait_interval(now = self.now) flush! while handle = @heap.peek if handle.cancelled? @heap.pop else return handle.time - now end end end # @returns [Float] The current time. def now ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) end # Fire all timers that are ready to fire. # # @parameter now [Float] The current time. def fire(now = self.now) # Flush scheduled timers into the heap: flush! # Get the earliest timer: while handle = @heap.peek if handle.cancelled? @heap.pop elsif handle.time <= now # Remove the earliest timer from the heap: @heap.pop # Call the block: handle.call(now) else break end end end # Flush all scheduled timers into the heap. # # This is a small optimization which assumes that most timers (timeouts) will be cancelled. protected def flush! while handle = @scheduled.pop @heap.push(handle) unless handle.cancelled? end end end end end socketry-io-event-ccd0953/lib/io/event/version.rb000066400000000000000000000002761516444210200220310ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2026, by Samuel Williams. # @namespace class IO # @namespace module Event VERSION = "1.15.1" end end socketry-io-event-ccd0953/license.md000066400000000000000000000032241516444210200174610ustar00rootroot00000000000000# MIT License Copyright, 2021, by Wander Hillen. Copyright, 2021-2026, by Samuel Williams. Copyright, 2021, by Delton Ding. Copyright, 2021-2024, by Benoit Daloze. Copyright, 2022, by Alex Matchneer. Copyright, 2022, by Bruno Sutic. Copyright, 2023, by Math Ieu. Copyright, 2024, by Pavel Rosický. Copyright, 2024, by Anthony Ross. Copyright, 2024-2025, by Shizuo Fujita. Copyright, 2024, by Jean Boussier. Copyright, 2025, by Stanislav (Stas) Katkov. Copyright, 2025, by Luke Gruber. Copyright, 2026, by William T. Nelson. Copyright, 2026, by Stan Hu. Copyright, 2026, by John Hawthorn. Copyright, 2026, by Italo Brandão. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. socketry-io-event-ccd0953/logo.svg000066400000000000000000000201101516444210200171670ustar00rootroot00000000000000 E V E N T socketry-io-event-ccd0953/readme.md000066400000000000000000000075721516444210200173060ustar00rootroot00000000000000# ![IO::Event](logo.svg) Provides low level cross-platform primitives for constructing event loops, with support for `select`, `kqueue`, `epoll` and `io_uring`. [![Development Status](https://github.com/socketry/io-event/workflows/Test/badge.svg)](https://github.com/socketry/io-event/actions?workflow=Test) ## Motivation 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. ## Usage Please see the [project documentation](https://socketry.github.io/io-event/) for more details. - [Getting Started](https://socketry.github.io/io-event/guides/getting-started/index) - This guide explains how to use `io-event` for non-blocking IO. ## Releases Please see the [project releases](https://socketry.github.io/io-event/releases/index) for all releases. ### v1.15.0 - Add bounds checks, in the unlikely event of a user providing an invalid offset that exceeds the buffer size. This prevents potential memory corruption and ensures safe operation when using buffered IO methods. ### v1.14.4 - Allow `epoll_pwait2` to be disabled via `--disable-epoll_pwait2`. ### v1.14.3 - Fix several implementation bugs that could cause deadlocks on blocking writes. ### v1.14.0 - [Enhanced `IO::Event::PriorityHeap` with deletion and bulk insertion methods](https://socketry.github.io/io-event/releases/index#enhanced-io::event::priorityheap-with-deletion-and-bulk-insertion-methods) ### v1.11.2 - Fix Windows build. ### v1.11.1 - Fix `read_nonblock` when using the `URing` selector, which was not handling zero-length reads correctly. This allows reading available data without blocking. ### v1.11.0 - [Introduce `IO::Event::WorkerPool` for off-loading blocking operations.](https://socketry.github.io/io-event/releases/index#introduce-io::event::workerpool-for-off-loading-blocking-operations.) ### v1.10.2 - Improved consistency of handling closed IO when invoking `#select`. ### v1.10.0 - `IO::Event::Profiler` is moved to dedicated gem: [fiber-profiler](https://github.com/socketry/fiber-profiler). - 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. ### v1.9.0 - Improved `IO::Event::Profiler` for detecting stalls. ## Contributing We welcome contributions to this project. 1. Fork it. 2. Create your feature branch (`git checkout -b my-new-feature`). 3. Commit your changes (`git commit -am 'Add some feature'`). 4. Push to the branch (`git push origin my-new-feature`). 5. Create new Pull Request. ### Running Tests To run the test suite: ``` shell bundle exec bake build bundle exec sus ``` ### Making Releases To make a new release: ``` shell bundle exec bake gem:release:patch # or minor or major ``` ### Developer Certificate of Origin 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. ### Community Guidelines 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. socketry-io-event-ccd0953/release.cert000066400000000000000000000033141516444210200200140ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11 ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/ c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8 voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg= -----END CERTIFICATE----- socketry-io-event-ccd0953/releases.md000066400000000000000000000070321516444210200176430ustar00rootroot00000000000000# Releases ## v1.15.0 - Add bounds checks, in the unlikely event of a user providing an invalid offset that exceeds the buffer size. This prevents potential memory corruption and ensures safe operation when using buffered IO methods. ## v1.14.4 - Allow `epoll_pwait2` to be disabled via `--disable-epoll_pwait2`. ## v1.14.3 - Fix several implementation bugs that could cause deadlocks on blocking writes. ## v1.14.0 ### Enhanced `IO::Event::PriorityHeap` with deletion and bulk insertion methods The {ruby IO::Event::PriorityHeap} now supports efficient element removal and bulk insertion: - **`delete(element)`**: Remove a specific element from the heap in O(n) time - **`delete_if(&block)`**: Remove elements matching a condition with O(n) amortized bulk deletion - **`concat(elements)`**: Add multiple elements efficiently in O(n) time ``` ruby heap = IO::Event::PriorityHeap.new # Efficient bulk insertion - O(n) instead of O(n log n) heap.concat([5, 2, 8, 1, 9, 3]) # Remove specific element removed = heap.delete(5) # Returns 5, heap maintains order # Bulk removal with condition count = heap.delete_if{|x| x.even?} # Removes 2, 8 efficiently ``` The `delete_if` and `concat` methods are particularly efficient for bulk operations, using bottom-up heapification to maintain the heap property in O(n) time. This provides significant performance improvements: - **Bulk insertion**: O(n log n) → O(n) for adding multiple elements - **Bulk deletion**: O(k×n) → O(n) for removing k elements Both methods maintain the heap invariant and include comprehensive test coverage with edge case validation. ## v1.11.2 - Fix Windows build. ## v1.11.1 - Fix `read_nonblock` when using the `URing` selector, which was not handling zero-length reads correctly. This allows reading available data without blocking. ## v1.11.0 ### Introduce `IO::Event::WorkerPool` for off-loading blocking operations. The {ruby IO::Event::WorkerPool} provides a mechanism for executing blocking operations on separate OS threads while properly integrating with Ruby's fiber scheduler and GVL (Global VM Lock) management. This enables true parallelism for CPU-intensive or blocking operations that would otherwise block the event loop. ``` ruby # Fiber scheduler integration via blocking_operation_wait hook class MyScheduler def initialize @worker_pool = IO::Event::WorkerPool.new end def blocking_operation_wait(operation) @worker_pool.call(operation) end end # Usage with automatic offloading Fiber.set_scheduler(MyScheduler.new) # Automatically offload `rb_nogvl(..., RB_NOGVL_OFFLOAD_SAFE)` to a background thread: result = some_blocking_operation() ``` The implementation uses one or more background threads and a list of pending blocking operations. Those operations either execute through to completion or may be cancelled, which executes the "unblock function" provided to `rb_nogvl`. ## v1.10.2 - Improved consistency of handling closed IO when invoking `#select`. ## v1.10.0 - `IO::Event::Profiler` is moved to dedicated gem: [fiber-profiler](https://github.com/socketry/fiber-profiler). - 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. ## v1.9.0 - Improved `IO::Event::Profiler` for detecting stalls. ## v1.8.0 - Detecting fibers that are stalling the event loop. ## v1.7.5 - Fix `process_wait` race condition on EPoll that could cause a hang. socketry-io-event-ccd0953/test/000077500000000000000000000000001516444210200164735ustar00rootroot00000000000000socketry-io-event-ccd0953/test/io/000077500000000000000000000000001516444210200171025ustar00rootroot00000000000000socketry-io-event-ccd0953/test/io/event.rb000066400000000000000000000003631516444210200205520ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2024, by Samuel Williams. require "io/event" describe IO::Event::VERSION do it "has a version number" do expect(subject).to be =~ /\d+\.\d+\.\d+/ end end socketry-io-event-ccd0953/test/io/event/000077500000000000000000000000001516444210200202235ustar00rootroot00000000000000socketry-io-event-ccd0953/test/io/event/priority_heap.rb000066400000000000000000000410601516444210200234270ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2024-2026, by Samuel Williams. require "io/event/priority_heap" describe IO::Event::PriorityHeap do let(:priority_heap) {subject.new} with "empty heap" do it "should return nil when the first element is requested" do expect(priority_heap.peek).to be_nil end it "should return nil when the first element is extracted" do expect(priority_heap.pop).to be_nil end it "should report its size as zero" do expect(priority_heap.size).to be(:zero?) end it "should report as empty" do expect(priority_heap).to be(:empty?) end end it "returns the same element after inserting a single element" do priority_heap.push(1) expect(priority_heap.size).to be == 1 expect(priority_heap.pop).to be == 1 expect(priority_heap.size).to be(:zero?) end with "#empty?" do it "should return false when heap contains elements" do priority_heap.push(1) expect(priority_heap).not.to be(:empty?) end it "should return true after popping all elements" do priority_heap.push(1) priority_heap.push(2) expect(priority_heap).not.to be(:empty?) priority_heap.pop expect(priority_heap).not.to be(:empty?) priority_heap.pop expect(priority_heap).to be(:empty?) end end it "should return inserted elements in ascending order no matter the insertion order" do (1..10).to_a.shuffle.each do |e| priority_heap.push(e) end expect(priority_heap.size).to be == 10 expect(priority_heap.peek).to be == 1 result = [] 10.times do result << priority_heap.pop end expect(result.size).to be == 10 expect(priority_heap.size).to be(:zero?) expect(result.sort).to be == result end with "maintaining the heap invariant" do it "for empty heaps" do expect(priority_heap).to be(:valid?) end it "for heap of size 1" do priority_heap.push(123) expect(priority_heap).to be(:valid?) end it "for all permutations of size 5" do [1,2,3,4,5].permutation do |permutation| priority_heap.clear! permutation.each{|element| priority_heap.push(element)} expect(priority_heap).to be(:valid?) end end # A few examples with more elements (but not ALL permutations) it "for larger amounts of values" do 5.times do priority_heap.clear! (1..1000).to_a.shuffle.each{|element| priority_heap.push(element)} expect(priority_heap).to be(:valid?) end end # What if we insert several of the same item along with others? it "with several elements of the same value" do test_values = (1..10).to_a + [4] * 5 test_values.each{|element| priority_heap.push(element)} expect(priority_heap).to be(:valid?) end end with "#delete" do it "should return nil when deleting from empty heap" do expect(priority_heap.delete(42)).to be_nil end it "should return nil when deleting non-existent element" do priority_heap.push(1) priority_heap.push(2) priority_heap.push(3) expect(priority_heap.delete(42)).to be_nil expect(priority_heap.size).to be == 3 end it "should delete the only element from single-element heap" do priority_heap.push(42) expect(priority_heap.delete(42)).to be == 42 expect(priority_heap.size).to be(:zero?) expect(priority_heap).to be(:empty?) end it "should delete first element (root) and maintain heap property" do elements = [5, 2, 8, 1, 9, 3] elements.each{|e| priority_heap.push(e)} # Root should be the minimum (1) expect(priority_heap.peek).to be == 1 # Delete the root expect(priority_heap.delete(1)).to be == 1 expect(priority_heap.size).to be == 5 expect(priority_heap).to be(:valid?) # New root should be the next minimum (2) expect(priority_heap.peek).to be == 2 end it "should delete last element without affecting heap structure" do elements = [5, 2, 8, 1, 9, 3] elements.each{|e| priority_heap.push(e)} original_root = priority_heap.peek # Delete one element (not necessarily the last in array, but some element) expect(priority_heap.delete(5)).to be == 5 expect(priority_heap.size).to be == 5 expect(priority_heap).to be(:valid?) expect(priority_heap.peek).to be == original_root # Root shouldn't change end it "should delete middle elements and maintain heap property" do elements = [10, 5, 15, 3, 7, 12, 18, 1, 4, 6, 8] elements.each{|e| priority_heap.push(e)} # Delete some middle elements expect(priority_heap.delete(7)).to be == 7 expect(priority_heap).to be(:valid?) expect(priority_heap.size).to be == 10 expect(priority_heap.delete(12)).to be == 12 expect(priority_heap).to be(:valid?) expect(priority_heap.size).to be == 9 end it "should handle deleting duplicate elements" do elements = [5, 3, 5, 1, 5, 2] elements.each{|e| priority_heap.push(e)} # Should delete only the first occurrence of 5 expect(priority_heap.delete(5)).to be == 5 expect(priority_heap.size).to be == 5 expect(priority_heap).to be(:valid?) # Should still have other 5s in the heap remaining_elements = [] while !priority_heap.empty? remaining_elements << priority_heap.pop end expect(remaining_elements.count(5)).to be == 2 end it "should maintain heap property after multiple deletions" do elements = (1..20).to_a.shuffle elements.each{|e| priority_heap.push(e)} # Delete several elements to_delete = [5, 10, 15, 1, 20, 8] to_delete.each do |element| expect(priority_heap.delete(element)).to be == element expect(priority_heap).to be(:valid?) end expect(priority_heap.size).to be == 14 # Remaining elements should still come out in sorted order result = [] while !priority_heap.empty? result << priority_heap.pop end expect(result.sort).to be == result expected_remaining = (1..20).to_a - to_delete expect(result.sort).to be == expected_remaining.sort end it "should work correctly when deleting all elements one by one" do elements = [4, 2, 6, 1, 3, 5, 7] elements.each{|e| priority_heap.push(e)} elements.shuffle.each do |element| expect(priority_heap.delete(element)).to be == element expect(priority_heap).to be(:valid?) end expect(priority_heap).to be(:empty?) end it "should handle complex deletion patterns" do # Insert elements in random order elements = (1..100).to_a.shuffle elements.each{|e| priority_heap.push(e)} # Delete every 3rd element (by value, not position) deleted = [] (1..100).each do |i| if i % 3 == 0 expect(priority_heap.delete(i)).to be == i deleted << i expect(priority_heap).to be(:valid?) end end # Verify remaining elements remaining = [] while !priority_heap.empty? remaining << priority_heap.pop end expected_remaining = (1..100).to_a - deleted expect(remaining).to be == expected_remaining end end with "#delete_if" do it "should return 0 when no elements match condition" do elements = [1, 2, 3, 4, 5] elements.each{|e| priority_heap.push(e)} removed_count = priority_heap.delete_if{|e| e > 10} expect(removed_count).to be == 0 expect(priority_heap.size).to be == 5 expect(priority_heap).to be(:valid?) end it "should return 0 when heap is empty" do removed_count = priority_heap.delete_if{|e| true} expect(removed_count).to be == 0 expect(priority_heap).to be(:empty?) end it "should remove all elements when condition always true" do elements = [5, 2, 8, 1, 9, 3] elements.each{|e| priority_heap.push(e)} removed_count = priority_heap.delete_if{|e| true} expect(removed_count).to be == 6 expect(priority_heap).to be(:empty?) end it "should remove even numbers and maintain heap property" do elements = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] elements.each{|e| priority_heap.push(e)} removed_count = priority_heap.delete_if{|e| e.even?} expect(removed_count).to be == 5 # 2, 4, 6, 8, 10 expect(priority_heap.size).to be == 5 expect(priority_heap).to be(:valid?) # Remaining elements should be odd numbers in sorted order remaining = [] while !priority_heap.empty? remaining << priority_heap.pop end expect(remaining).to be == [1, 3, 5, 7, 9] end it "should handle removing elements from specific ranges" do elements = (1..20).to_a elements.each{|e| priority_heap.push(e)} # Remove elements between 5 and 15 (inclusive) removed_count = priority_heap.delete_if{|e| e >= 5 && e <= 15} expect(removed_count).to be == 11 # 5,6,7,8,9,10,11,12,13,14,15 expect(priority_heap.size).to be == 9 expect(priority_heap).to be(:valid?) # Should have 1,2,3,4,16,17,18,19,20 remaining = [] while !priority_heap.empty? remaining << priority_heap.pop end expected = [1, 2, 3, 4, 16, 17, 18, 19, 20] expect(remaining).to be == expected end it "should work with duplicate elements" do elements = [5, 3, 5, 1, 5, 2, 5, 4] elements.each{|e| priority_heap.push(e)} # Remove all 5s removed_count = priority_heap.delete_if{|e| e == 5} expect(removed_count).to be == 4 expect(priority_heap.size).to be == 4 expect(priority_heap).to be(:valid?) remaining = [] while !priority_heap.empty? remaining << priority_heap.pop end expect(remaining).to be == [1, 2, 3, 4] end it "should return enumerator when no block given" do elements = [1, 2, 3, 4, 5] elements.each{|e| priority_heap.push(e)} enum = priority_heap.delete_if expect(enum).to be_a(Enumerator) # Use the enumerator to delete even numbers removed_count = enum.select{|e| e.even?} # Note: select doesn't actually delete, we need to call the enumerator differently # Better test: create enumerator and then call with condition enum = priority_heap.delete_if removed_count = enum.each{|e| e.even?} expect(removed_count).to be == 2 expect(priority_heap.size).to be == 3 end it "should be more efficient than multiple delete operations" do # Large dataset to demonstrate efficiency elements = (1..1000).to_a.shuffle elements.each{|e| priority_heap.push(e)} # Remove all multiples of 7 using delete_if start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) removed_count = priority_heap.delete_if{|e| e % 7 == 0} delete_if_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time expected_removed = (1..1000).count{|x| x % 7 == 0} # Should be 142 expect(removed_count).to be == expected_removed expect(priority_heap.size).to be == 1000 - expected_removed expect(priority_heap).to be(:valid?) # Verify remaining elements are correct remaining = [] while !priority_heap.empty? remaining << priority_heap.pop end expected_remaining = (1..1000).reject{|x| x % 7 == 0} expect(remaining).to be == expected_remaining end it "should handle complex conditions" do elements = (1..50).to_a elements.each{|e| priority_heap.push(e)} # Remove numbers that are prime (simple prime test for small numbers) is_prime = lambda do |n| return false if n < 2 return true if n == 2 return false if n.even? (3..Math.sqrt(n)).step(2).none?{|i| n % i == 0} end removed_count = priority_heap.delete_if(&is_prime) # Count expected primes in range 1-50 expected_prime_count = (1..50).count(&is_prime) expect(removed_count).to be == expected_prime_count expect(priority_heap.size).to be == 50 - expected_prime_count expect(priority_heap).to be(:valid?) end it "should maintain heap invariant after bulk deletions" do # Multiple rounds of delete_if to stress test heap maintenance elements = (1..100).to_a.shuffle elements.each{|e| priority_heap.push(e)} # First: remove multiples of 3 removed1 = priority_heap.delete_if{|e| e % 3 == 0} expect(priority_heap).to be(:valid?) # Second: remove multiples of 7 from remaining removed2 = priority_heap.delete_if{|e| e % 7 == 0} expect(priority_heap).to be(:valid?) # Third: remove numbers greater than 80 removed3 = priority_heap.delete_if{|e| e > 80} expect(priority_heap).to be(:valid?) # Verify final result comes out in sorted order remaining = [] while !priority_heap.empty? remaining << priority_heap.pop end expect(remaining.sort).to be == remaining end end with "#concat" do it "should return self when concatenating empty array" do elements = [1, 2, 3] elements.each{|e| priority_heap.push(e)} result = priority_heap.concat([]) expect(result).to be == priority_heap expect(priority_heap.size).to be == 3 expect(priority_heap).to be(:valid?) end it "should efficiently add multiple elements to empty heap" do elements = [5, 2, 8, 1, 9, 3] result = priority_heap.concat(elements) expect(result).to be == priority_heap # Returns self expect(priority_heap.size).to be == 6 expect(priority_heap).to be(:valid?) # Should extract in sorted order sorted_result = [] while !priority_heap.empty? sorted_result << priority_heap.pop end expect(sorted_result).to be == elements.sort end it "should add elements to existing heap and maintain order" do # Start with some elements initial = [10, 15, 20] initial.each{|e| priority_heap.push(e)} # Add more elements in bulk additional = [5, 12, 25, 8] priority_heap.concat(additional) expect(priority_heap.size).to be == 7 expect(priority_heap).to be(:valid?) # Should extract all in sorted order all_elements = initial + additional sorted_result = [] while !priority_heap.empty? sorted_result << priority_heap.pop end expect(sorted_result).to be == all_elements.sort end it "should handle duplicate elements correctly" do priority_heap.concat([5, 3, 5, 1, 5]) expect(priority_heap.size).to be == 5 expect(priority_heap).to be(:valid?) # Should have three 5s result = [] while !priority_heap.empty? result << priority_heap.pop end expect(result.count(5)).to be == 3 expect(result).to be == [1, 3, 5, 5, 5] end it "should be more efficient than multiple push operations" do # Large dataset to demonstrate efficiency elements = (1..1000).to_a.shuffle # Test concat performance heap1 = subject.new start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) heap1.concat(elements) concat_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time # Test individual push performance heap2 = subject.new start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) elements.each{|e| heap2.push(e)} push_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time # Both should produce same result expect(heap1.size).to be == heap2.size expect(heap1).to be(:valid?) expect(heap2).to be(:valid?) # Verify both heaps contain same elements result1 = [] while !heap1.empty? result1 << heap1.pop end result2 = [] while !heap2.empty? result2 << heap2.pop end expect(result1).to be == result2 expect(result1).to be == (1..1000).to_a end it "should handle single element concat" do priority_heap.concat([42]) expect(priority_heap.size).to be == 1 expect(priority_heap.peek).to be == 42 expect(priority_heap.pop).to be == 42 expect(priority_heap).to be(:empty?) end it "should handle incomparable mixed data types" do # Mix strings and numbers (not comparable) elements = [3, "apple", 1, "zebra", 5] # This should raise an exception when trying to compare incomparable types expect do priority_heap.concat(elements) # Force comparison by trying to extract elements priority_heap.pop end.to raise_exception end it "should maintain heap property after large bulk inserts" do # Multiple rounds of concat to stress test (1..10).each do |round| elements = ((round-1)*100 + 1..round*100).to_a.shuffle priority_heap.concat(elements) expect(priority_heap).to be(:valid?) end expect(priority_heap.size).to be == 1000 # Should extract in perfect sorted order result = [] while !priority_heap.empty? result << priority_heap.pop end expect(result).to be == (1..1000).to_a end it "should support method chaining" do result = priority_heap .concat([10, 5]) .concat([15, 1]) .concat([8]) expect(result).to be == priority_heap expect(priority_heap.size).to be == 5 expect(priority_heap).to be(:valid?) # Verify all elements are there sorted = [] while !priority_heap.empty? sorted << priority_heap.pop end expect(sorted).to be == [1, 5, 8, 10, 15] end end end socketry-io-event-ccd0953/test/io/event/selector.rb000066400000000000000000000362561516444210200224040ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2025, by Samuel Williams. # Copyright, 2023, by Math Ieu. require "io/event" require "io/event/selector" require "io/event/debug/selector" require "socket" require "fiber" require "unix_socket" class FakeFiber def initialize(alive = true) @alive = alive @count = 0 end attr :count def alive? @alive end def transfer @count += 1 end end Selector = Sus::Shared("a selector") do with "#select" do let(:quantum) {0.2} it "can select with 0s timeout" do expect do selector.select(0) end.to have_duration(be < quantum) end it "can select with a short timeout" do expect do selector.select(0.01) end.to have_duration(be <= (0.01 + quantum)) end it "raises an error when given an invalid duration" do expect do selector.select("invalid") end.to raise_exception end end with "#idle_duration" do it "can report idle duration" do 10.times do selector.select(0.001) expect(selector.idle_duration).to be > 0.0 selector.select(0) expect(selector.idle_duration).to be == 0.0 end end end with "#wakeup" do it "can wakeup selector from different thread" do thread = Thread.new do sleep 0.001 selector.wakeup end expect do selector.select(1) end.to have_duration(be < 1) ensure thread.join end it "can wakeup selector from different thread twice in a row" do 2.times do thread = Thread.new do sleep 0.001 selector.wakeup end expect do selector.select(1) end.to have_duration(be < 1) ensure thread.join end end it "ignores wakeup if not selecting" do expect(selector.wakeup).to be == false end it "doesn't block when readying another fiber" do fiber = FakeFiber.new 10.times do |i| thread = Thread.new do sleep(i / 10000.0) selector.push(fiber) selector.wakeup end expect do selector.select(1.0) end.to have_duration(be < 1.0) ensure thread.join end end end with "#io_wait" do let(:events) {Array.new} let(:sockets) {UNIXSocket.pair} let(:local) {sockets.first} let(:remote) {sockets.last} it "can wait for an io to become readable" do fiber = Fiber.new do events << :wait_readable expect( selector.io_wait(Fiber.current, local, IO::READABLE) ).to be == IO::READABLE events << :readable end events << :transfer fiber.transfer remote.puts "Hello World" events << :select selector.select(1) expect(events).to be == [ :transfer, :wait_readable, :select, :readable ] end it "can wait for an io to become writable" do fiber = Fiber.new do events << :wait_writable expect( selector.io_wait(Fiber.current, local, IO::WRITABLE) ).to be == IO::WRITABLE events << :writable end events << :transfer fiber.transfer events << :select selector.select(1) expect(events).to be == [ :transfer, :wait_writable, :select, :writable ] end it "can read and write from two different fibers" do readable = writable = false read_fiber = Fiber.new do events << :wait_readable expect( selector.io_wait(Fiber.current, local, IO::READABLE) ).to be == IO::READABLE readable = true end write_fiber = Fiber.new do events << :wait_writable expect( selector.io_wait(Fiber.current, local, IO::WRITABLE) ).to be == IO::WRITABLE writable = true end events << :transfer read_fiber.transfer write_fiber.transfer remote.puts "Hello World" events << :select selector.select(1) expect(events).to be == [ :transfer, :wait_readable, :wait_writable, :select ] expect(readable).to be == true expect(writable).to be == true end it "can read and write from two different fibers (alternate)" do read_fiber = Fiber.new do events << :wait_readable expect( selector.io_wait(Fiber.current, local, IO::READABLE) ).to be == IO::READABLE events << :readable end write_fiber = Fiber.new do events << :wait_writable expect( selector.io_wait(Fiber.current, local, IO::WRITABLE) ).to be == IO::WRITABLE events << :writable end events << :transfer read_fiber.transfer write_fiber.transfer events << :select1 selector.select(1) remote.puts "Hello World" events << :select2 selector.select(1) expect(events).to be == [ :transfer, :wait_readable, :wait_writable, :select1, :writable, :select2, :readable, ] end it "can wait consecutively on two different io objects that share the same file descriptor" do fiber = Fiber.new do events << :write1 remote.puts "Hello World" events << :wait_readable1 expect( selector.io_wait(Fiber.current, local, IO::READABLE) ).to be == IO::READABLE events << :readable1 events << :new_io fileno = local.fileno local.close new_local, new_remote = UNIXSocket.pair # Make sure we attempt to wait on the same file descriptor: if new_remote.fileno == fileno new_local, new_remote = new_remote, new_local end if new_local.fileno != fileno warn "Could not create new IO object with same FD, test ineffective!" end events << :write2 new_remote.puts "Hello World" events << :wait_readable2 expect( selector.io_wait(Fiber.current, new_local, IO::READABLE) ).to be == IO::READABLE events << :readable2 end events << :transfer fiber.transfer events << :select1 selector.select(1) events << :select2 selector.select(1) expect(events).to be == [ :transfer, :write1, :wait_readable1, :select1, :readable1, :new_io, :write2, :wait_readable2, :select2, :readable2, ] end it "can handle exception during wait" do fiber = Fiber.new do events << :wait_readable expect do while true selector.io_wait(Fiber.current, local, IO::READABLE) events << :readable end end.to raise_exception(RuntimeError, message: be =~ /Boom/) events << :error end events << :transfer fiber.transfer events << :select selector.select(0) fiber.raise(RuntimeError.new("Boom")) events << :puts remote.puts "Hello World" selector.select(0) expect(events).to be == [ :transfer, :wait_readable, :select, :error, :puts ] end it "can have two fibers reading from the same io" do fiber1 = Fiber.new do events << :wait_readable1 selector.io_wait(Fiber.current, local, IO::READABLE) events << :readable rescue events << :error1 end fiber2 = Fiber.new do events << :wait_readable2 selector.io_wait(Fiber.current, local, IO::READABLE) events << :readable rescue events << :error2 end events << :transfer fiber1.transfer fiber2.transfer remote.puts "Hello World" events << :select selector.select(1) expect(events).to be == [ :transfer, :wait_readable1, :wait_readable2, :select, :readable, :readable ] end it "can handle exception raised during wait from another fiber that was waiting on the same io" do [false, true].each do |swapped| # Try both orderings. writable1 = writable2 = false error1 = false raised1 = false boom = Class.new(RuntimeError) fiber1 = fiber2 = nil fiber1 = Fiber.new do begin selector.io_wait(Fiber.current, local, IO::WRITABLE) rescue boom error1 = true # Transfer back to the signaling fiber to simulate doing something similar to raising an exception in an asynchronous task or thread. fiber2.transfer end writable1 = true end fiber2 = Fiber.new do selector.io_wait(Fiber.current, local, IO::WRITABLE) # Don't do anything if the other fiber was resumed before we were by the selector. unless writable1 raised1 = true fiber1.raise(boom) # Will return here. end writable2 = true end fiber1.transfer unless swapped fiber2.transfer fiber1.transfer if swapped selector.select(0) # 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. expect(error1).to be == raised1 expect(writable1).to be == !raised1 expect(writable2).to be == true end end it "can handle io being closed by another fiber while waiting" do error = nil wait_fiber = Fiber.new do wait_fiber_started = true events << :wait_readable begin result = selector.io_wait(Fiber.current, local, IO::READABLE) # This should never trigger: events << :readable rescue => error # This isn't a reliable state transition. # events << :error end end close_fiber = Fiber.new do events << :close_io local.close end events << :transfer wait_fiber.transfer close_fiber.transfer expect do events << :select selector.select(0) end.not.to raise_exception expect(events).to be == [ :transfer, :wait_readable, :close_io, :select ] # io_uring will raise an EBADF error if the IO is closed while waiting. # But other selectors are not capable of detecting this. # expect(error).to be_nil end end with "#io_read" do let(:message) {"Hello World"} let(:events) {Array.new} let(:sockets) {UNIXSocket.pair} let(:local) {sockets.first} let(:remote) {sockets.last} let(:buffer) {IO::Buffer.new(1024, IO::Buffer::MAPPED)} it "can read a single message" do return unless selector.respond_to?(:io_read) fiber = Fiber.new do events << :io_read offset = selector.io_read(Fiber.current, local, buffer, message.bytesize) expect(buffer.get_string(0, offset)).to be == message end fiber.transfer events << :write remote.write(message) selector.select(1) expect(events).to be == [ :io_read, :write ] end it "can handle partial reads" do return unless selector.respond_to?(:io_read) fiber = Fiber.new do events << :io_read offset = selector.io_read(Fiber.current, local, buffer, message.bytesize) expect(buffer.get_string(0, offset)).to be == message end fiber.transfer events << :write remote.write(message[0...5]) selector.select(1) remote.write(message[5...message.bytesize]) selector.select(1) expect(events).to be == [ :io_read, :write ] end it "can stop reading when reads are ready" do # This could trigger a busy-loop in the KQueue selector. return unless selector.respond_to?(:io_read) fiber = Fiber.new do offset = selector.io_read(Fiber.current, local, buffer, message.bytesize) expect(buffer.get_string(0, offset)).to be == message sleep(0.001) end fiber.transfer remote.write(message) expect(selector.select(0)).to be == 1 remote.write(message) result = nil 3.times do result = selector.select(0) break if result == 0 end expect(result).to be == 0 end end with "#io_write" do let(:message) {"Hello World"} let(:events) {Array.new} let(:sockets) {UNIXSocket.pair} let(:local) {sockets.first} let(:remote) {sockets.last} it "can write a single message" do skip_if_ruby_platform(/mswin|mingw|cygwin/) return unless selector.respond_to?(:io_write) fiber = Fiber.new do events << :io_write buffer = IO::Buffer.for(message.dup) result = selector.io_write(Fiber.current, local, buffer, buffer.size) expect(result).to be == message.bytesize local.close end fiber.transfer selector.select(0) events << :read expect(remote.read).to be == message expect(events).to be == [ :io_write, :read ] end end with "#process_wait" do it "can wait for a process which has terminated already" do result = nil events = [] fiber = Fiber.new do pid = Process.spawn("true") result = selector.process_wait(Fiber.current, pid, 0) expect(result).to be(:success?) events << :process_finished end fiber.transfer while fiber.alive? selector.select(1) end expect(events).to be == [:process_finished] expect(result.success?).to be == true end it "can wait for a process to terminate" do result = nil events = [] fiber = Fiber.new do pid = Process.spawn("sleep 0.001") result = selector.process_wait(Fiber.current, pid, 0) expect(result).to be(:success?) events << :process_finished end fiber.transfer while fiber.alive? selector.select(0) end expect(events).to be == [:process_finished] expect(result).to be(:success?) end it "can wait for two processes sequentially" do result1 = result2 = nil events = [] fiber = Fiber.new do pid1 = Process.spawn("sleep 0") pid2 = Process.spawn("sleep 0") result1 = selector.process_wait(Fiber.current, pid1, 0) events << :process_finished1 result2 = selector.process_wait(Fiber.current, pid2, 0) events << :process_finished2 end fiber.transfer while fiber.alive? selector.select(0) end expect(events).to be == [:process_finished1, :process_finished2] expect(result1).to be(:success?) expect(result2).to be(:success?) end end with "#resume" do it "can resume a fiber" do other_fiber_count = 0 5.times do fiber = Fiber.new do other_fiber_count += 1 end selector.resume(fiber) end expect(other_fiber_count).to be == 5 end end end describe IO::Event::Selector do with ".default" do it "can get the default selector" do expect(subject.default).to be_a(Module) end it "returns the default if an invalid name is provided" do env = {"IO_EVENT_SELECTOR" => "invalid"} expect{subject.default(env)}.to raise_exception(NameError) end end end IO::Event::Selector.constants.each do |name| klass = IO::Event::Selector.const_get(name) describe(klass, unique: name) do with ".default" do it "can get the specified selector" do env = {"IO_EVENT_SELECTOR" => name} expect(IO::Event::Selector.default(env)).to be == klass end end with ".new" do let(:count) {8} let(:loop) {Fiber.current} it "can create multiple selectors" do selectors = count.times.map do |i| subject.new(loop) end expect(selectors.size).to be == count selectors.each(&:close) end end with "an instance" do before do @loop = Fiber.current @selector = subject.new(@loop) end after do @selector&.close end attr :loop attr :selector it_behaves_like Selector end end end describe IO::Event::Debug::Selector do before do @loop = Fiber.current @selector = subject.new(IO::Event::Selector.new(loop)) end after do @selector&.close end attr :loop attr :selector it_behaves_like Selector end socketry-io-event-ccd0953/test/io/event/selector/000077500000000000000000000000001516444210200220435ustar00rootroot00000000000000socketry-io-event-ccd0953/test/io/event/selector/buffered_io.rb000066400000000000000000000101441516444210200246410ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2026, by Samuel Williams. # Copyright, 2023, by Math Ieu. require "io/event" require "io/event/selector" require "socket" require "unix_socket" BufferedIO = Sus::Shared("buffered io") do with "a pipe" do let(:pipe) {IO.pipe} let(:input) {pipe.first} let(:output) {pipe.last} it "can read using a buffer" do skip_if_ruby_platform(/mswin|mingw|cygwin/) writer = Fiber.new do buffer = IO::Buffer.new(128) expect(selector.io_write(Fiber.current, output, buffer, 128, 0)).to be == 128 end reader = Fiber.new do buffer = IO::Buffer.new(64) expect(selector.io_read(Fiber.current, input, buffer, 1, 0)).to be == 64 end reader.transfer writer.transfer expect(selector.select(1)).to be >= 1 end it "can write zero length buffers" do skip_if_ruby_platform(/mswin|mingw|cygwin/) buffer = IO::Buffer.new(1).slice(0, 0) expect(selector.io_write(Fiber.current, output, buffer, 0, 0)).to be == 0 end it "can read and write at the specified offset" do skip_if_ruby_platform(/mswin|mingw|cygwin/) writer = Fiber.new do buffer = IO::Buffer.new(128) # We can't write 128 bytes because there are only +64 bytes from offset 64. expect(selector.io_write(Fiber.current, output, buffer, 128, 64)).to be == 64 end reader = Fiber.new do buffer = IO::Buffer.new(128) # Only 64 bytes are available to read. expect(selector.io_read(Fiber.current, input, buffer, 1, 64)).to be == 64 end reader.transfer writer.transfer expect(selector.select(1)).to be >= 1 end it "can't write to the read end of a pipe" do skip_if_ruby_platform(/mswin|mingw|cygwin/) output.close writer = Fiber.new do buffer = IO::Buffer.new(64) result = selector.io_write(Fiber.current, input, buffer, 64, 0) expect(result).to be < 0 end writer.transfer selector.select(0) end it "can perform non-blocking read" do skip_if_ruby_platform(/mswin|mingw|cygwin/) buffer = IO::Buffer.new(64) result = nil output.puts "Hello World\n" output.close reader = Fiber.new do result = selector.io_read(Fiber.current, input, buffer, 0, 0) end reader.transfer selector.select(0) expect(buffer.get_string(0, 12)).to be == "Hello World\n" end # Whether the given error code indicates that the operation should be retried. def be_again? (be == -Errno::EAGAIN::Errno).or(be == -Errno::EWOULDBLOCK::Errno) end it "can perform non-blocking read with empty input" do skip_if_ruby_platform(/mswin|mingw|cygwin/) buffer = IO::Buffer.new(64) result = nil reader = Fiber.new do result = selector.io_read(Fiber.current, input, buffer, 0, 0) end reader.transfer selector.select(0) expect(result).to be_again? end it "returns EINVAL when read offset exceeds buffer size" do skip_if_ruby_platform(/mswin|mingw|cygwin/) buffer = IO::Buffer.new(64) reader = Fiber.new do # Offset 128 exceeds buffer size of 64 result = selector.io_read(Fiber.current, input, buffer, 1, 128) expect(result).to be == -Errno::EINVAL::Errno end reader.transfer selector.select(0) end it "returns EINVAL when write offset exceeds buffer size" do skip_if_ruby_platform(/mswin|mingw|cygwin/) buffer = IO::Buffer.new(64) writer = Fiber.new do # Offset 128 exceeds buffer size of 64 result = selector.io_write(Fiber.current, output, buffer, 1, 128) expect(result).to be == -Errno::EINVAL::Errno end writer.transfer selector.select(0) end end end IO::Event::Selector.constants.each do |name| klass = IO::Event::Selector.const_get(name) # Don't run the test if the selector doesn't support `io_read`/`io_write`: next unless klass.instance_methods.include?(:io_read) describe(klass, unique: name) do before do @loop = Fiber.current @selector = subject.new(@loop) end after do @selector&.close end attr :loop attr :selector it_behaves_like BufferedIO end end socketry-io-event-ccd0953/test/io/event/selector/cancellable.rb000066400000000000000000000031771516444210200246250ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2023-2024, by Samuel Williams. require "io/event" require "io/event/selector" require "socket" require "unix_socket" Cancellable = Sus::Shared("cancellable") do with "a pipe" do let(:pipe) {IO.pipe} let(:input) {pipe.first} let(:output) {pipe.last} after do input.close output.close end it "can cancel reads" do reader = Fiber.new do buffer = IO::Buffer.new(64) 10.times do expect{selector.io_read(Fiber.current, input, buffer, 1)}.to raise_exception(Interrupt) end end # Enter the `io_read` operation: reader.transfer while reader.alive? reader.raise(Interrupt) selector.select(0) end end it "can cancel waits" do reader = Fiber.new do buffer = IO::Buffer.new(64) 10.times do expect{selector.io_wait(Fiber.current, input, IO::READABLE)}.to raise_exception(Interrupt) selector.io_read(Fiber.current, input, buffer, 1) end end # Enter the `io_read` operation: reader.transfer while reader.alive? reader.raise(Interrupt) output.write(".") selector.select(0.1) end end end end IO::Event::Selector.constants.each do |name| klass = IO::Event::Selector.const_get(name) # Don't run the test if the selector doesn't support `io_read`/`io_write`: next unless klass.instance_methods.include?(:io_read) describe(klass, unique: name) do before do @loop = Fiber.current @selector = subject.new(@loop) end after do @selector&.close end attr :loop attr :selector it_behaves_like Cancellable end end socketry-io-event-ccd0953/test/io/event/selector/closed_io.rb000066400000000000000000000044521516444210200243350ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2026, by Samuel Williams. require "io/event" require "io/event/selector" require "socket" require_relative "../../../../fixtures/io/event/test_scheduler" ClosedIO = Sus::Shared("closed io while selecting") do with "a pipe" do let(:pipe) {IO.pipe} let(:input) {pipe.first} let(:output) {pipe.last} after do input.close unless input.closed? output.close unless output.closed? end it "does not raise when IO is closed from the same fiber before selecting" do skip_unless_minimum_ruby_version("4") thread = Thread.new do Thread.current.report_on_exception = false scheduler = IO::Event::TestScheduler.new(selector: subject.new(Fiber.current)) Fiber.set_scheduler(scheduler) wait_fiber = Fiber.new do input.wait_readable rescue IOError # acceptable: the IO was closed while waiting end # Close must happen in a separate fiber so that rb_thread_io_close_wait # can yield (via kernel_sleep) back to the loop fiber instead of deadlocking: close_fiber = Fiber.new do input.close end wait_fiber.transfer close_fiber.transfer scheduler.run ensure Fiber.set_scheduler(nil) scheduler&.close end thread.join end it "does not raise when IO is closed from another thread while selecting" do skip_unless_minimum_ruby_version("4") thread = Thread.new do Thread.current.report_on_exception = false scheduler = IO::Event::TestScheduler.new(selector: subject.new(Fiber.current)) Fiber.set_scheduler(scheduler) wait_fiber = Fiber.new do input.wait_readable rescue IOError # acceptable: the IO was closed while waiting end wait_fiber.transfer # Close the IO from another thread while the selector is blocking: closer = Thread.new do sleep(0.01) input.close end scheduler.run ensure closer&.join Fiber.set_scheduler(nil) scheduler&.close end error = nil begin thread.join rescue => error end expect(error).to be_nil end end end IO::Event::Selector.constants.each do |name| klass = IO::Event::Selector.const_get(name) describe(klass, unique: name) do it_behaves_like ClosedIO end end socketry-io-event-ccd0953/test/io/event/selector/fifo_io.rb000066400000000000000000000024661516444210200240120ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2024, by Samuel Williams. require "io/event" require "io/event/selector" require "fileutils" require "tmpdir" FifoIO = Sus::Shared("fifo io") do with "a fifo" do def around(&block) @root = Dir.mktmpdir super ensure FileUtils.rm_rf(@root) if @root end let(:path) {File.join(@root, "fifo")} it "can read and write" do skip_if_ruby_platform(/mswin|mingw|cygwin/) File.mkfifo(path) output = File.open(path, "w+") input = File.open(path, "r") buffer = IO::Buffer.new(128) reader = Fiber.new do @selector.io_wait(Fiber.current, input, IO::READABLE) result = buffer.read(input, 0) buffer.resize(result) end writer = Fiber.new do output.puts("Hello World\n") output.close end reader.transfer writer.transfer 2.times do @selector.select(0) end expect(buffer.get_string).to be == "Hello World\n" end end end IO::Event::Selector.constants.each do |name| klass = IO::Event::Selector.const_get(name) describe(klass, unique: name) do def before @loop = Fiber.current @selector = subject.new(@loop) end def after(error = nil) @selector&.close end attr :loop attr :selector it_behaves_like FifoIO end end socketry-io-event-ccd0953/test/io/event/selector/file_io.rb000066400000000000000000000073731516444210200240100ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2026, by Samuel Williams. require "io/event" require "io/event/selector" require "tempfile" FileIO = Sus::Shared("file io") do with "a file" do let(:file) {Tempfile.new} it "can read using a buffer" do skip_if_ruby_platform(/mswin|mingw|cygwin/) write_result = nil read_result = nil writer = Fiber.new do buffer = IO::Buffer.new(128) file.seek(0) write_result = selector.io_write(Fiber.current, file, buffer, 128) end reader = Fiber.new do buffer = IO::Buffer.new(64) file.seek(0) # The read will return 0 if the data is not written yet: read_result = selector.io_read(Fiber.current, file, buffer, 0) end writer.transfer while write_result.nil? selector.select(0) end reader.transfer while read_result.nil? selector.select(0) end expect(write_result).to be == 128 expect(read_result).to be == 64 end it "can pread using a buffer" do skip "io_pread is not implemented" unless selector.respond_to?(:io_pread) write_result = nil read_result = nil writer = Fiber.new do buffer = IO::Buffer.new(128) write_result = selector.io_pwrite(Fiber.current, file, buffer, 0, 128, 0) end reader = Fiber.new do buffer = IO::Buffer.new(64) read_result = selector.io_pread(Fiber.current, file, buffer, 0, 64, 0) end writer.transfer while write_result.nil? selector.select(0) end reader.transfer while read_result.nil? selector.select(0) end expect(write_result).to be == 128 expect(read_result).to be == 64 end it "can wait for the file to become writable" do wait_result = nil writer = Fiber.new do wait_result = selector.io_wait(Fiber.current, file, IO::WRITABLE) end writer.transfer selector.select(0) expect(wait_result).to be == IO::WRITABLE end it "returns EINVAL when read offset exceeds buffer size" do skip_if_ruby_platform(/mswin|mingw|cygwin/) buffer = IO::Buffer.new(64) file.seek(0) # Offset 128 exceeds buffer size of 64 result = selector.io_read(Fiber.current, file, buffer, 1, 128) expect(result).to be == -Errno::EINVAL::Errno end it "returns EINVAL when write offset exceeds buffer size" do skip_if_ruby_platform(/mswin|mingw|cygwin/) buffer = IO::Buffer.new(64) file.seek(0) # Offset 128 exceeds buffer size of 64 result = selector.io_write(Fiber.current, file, buffer, 1, 128) expect(result).to be == -Errno::EINVAL::Errno end it "returns EINVAL when pread offset exceeds buffer size" do skip "io_pread is not implemented" unless selector.respond_to?(:io_pread) buffer = IO::Buffer.new(64) # Offset 128 exceeds buffer size of 64 result = selector.io_pread(Fiber.current, file, buffer, 0, 1, 128) expect(result).to be == -Errno::EINVAL::Errno end it "returns EINVAL when pwrite offset exceeds buffer size" do skip "io_pwrite is not implemented" unless selector.respond_to?(:io_pwrite) buffer = IO::Buffer.new(64) # Offset 128 exceeds buffer size of 64 result = selector.io_pwrite(Fiber.current, file, buffer, 0, 1, 128) expect(result).to be == -Errno::EINVAL::Errno end end end IO::Event::Selector.constants.each do |name| klass = IO::Event::Selector.const_get(name) # Don't run the test if the selector doesn't support `io_read`/`io_write`: next unless klass.instance_methods.include?(:io_read) describe(klass, unique: name) do before do @loop = Fiber.current @selector = subject.new(@loop) end after do @selector&.close end attr :loop attr :selector it_behaves_like FileIO end end socketry-io-event-ccd0953/test/io/event/selector/interruptable.rb000066400000000000000000000027771516444210200252650ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2023-2024, by Samuel Williams. require "io/event" require "io/event/selector" require "socket" Interruptable = Sus::Shared("interruptable") do it "can interrupt sleeping selector" do result = nil thread = Thread.new do Thread.current.report_on_exception = false selector = subject.new(Fiber.current) Thread.handle_interrupt(::SignalException => :never) do result = selector.select(nil) end end # Wait for thread to enter the selector: sleep(0.001) until thread.status == "sleep" thread.raise(::Interrupt) expect{thread.join}.to raise_exception(::Interrupt) expect(result).to be == 0 end with "pipe" do let(:pipe) {IO.pipe} let(:input) {pipe.first} let(:output) {pipe.last} it "can interrupt waiting selector" do thread = Thread.new do Thread.current.report_on_exception = false selector = subject.new(Fiber.current) Fiber.new do selector.io_wait(Fiber.current, input, IO::READABLE) end Thread.handle_interrupt(::SignalException => :never) do selector.select(nil) end end # Wait for thread to enter the selector: sleep(0.001) until thread.status == "sleep" thread.raise(::Interrupt) expect{thread.join}.to raise_exception(::Interrupt) end end end IO::Event::Selector.constants.each do |name| klass = IO::Event::Selector.const_get(name) describe(klass, unique: name) do it_behaves_like Interruptable end end socketry-io-event-ccd0953/test/io/event/selector/nonblock.rb000066400000000000000000000013011516444210200241700ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2024, by Samuel Williams. require "io/event" require "io/nonblock" require "io/event/selector" describe IO::Event::Selector do with ".nonblock" do it "makes non-blocking IO" do executed = false UNIXSocket.pair do |input, output| input.nonblock = false output.nonblock = false IO::Event::Selector.nonblock(input) do executed = true # This does not work on Windows... unless RUBY_PLATFORM =~ /mswin|mingw|cygwin/ expect(input).to be(:nonblock?) expect(output).not.to be(:nonblock?) end end end expect(executed).to be == true end end end socketry-io-event-ccd0953/test/io/event/selector/process_io.rb000066400000000000000000000023461516444210200245420ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2022-2024, by Samuel Williams. # Copyright, 2023, by Math Ieu. require "io/event" require "io/event/selector" require "io/event/debug/selector" require "socket" require "fiber" ProcessIO = Sus::Shared("process io") do it "can wait for a process which has terminated already" do result = nil fiber = Fiber.new do input, output = IO.pipe # For some reason, sleep 0.1 here is very unreliable...? pid = Process.spawn("true", out: output) output.close # Internally, this should generate POLLHUP, which is what we want to test: expect(selector.io_wait(Fiber.current, input, IO::READABLE)).to be == IO::READABLE input.close _, result = Process.wait2(pid) end fiber.transfer # Wait until the result is collected: until result selector.select(1) end expect(result.success?).to be == true end end IO::Event::Selector.constants.each do |name| klass = IO::Event::Selector.const_get(name) describe(klass, unique: name) do before do @loop = Fiber.current @selector = subject.new(@loop) end after do @selector&.close end attr :loop attr :selector it_behaves_like ProcessIO end end socketry-io-event-ccd0953/test/io/event/selector/queue.rb000066400000000000000000000075771516444210200235340ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2021-2024, by Samuel Williams. require "io/event" require "io/event/selector" require "socket" Queue = Sus::Shared("queue") do with "#transfer" do it "can transfer back to event loop" do sequence = [] fiber = Fiber.new do while true sequence << :transfer selector.transfer end end selector.push(fiber) sequence << :select selector.select(0) sequence << :select selector.select(0) expect(sequence).to be == [:select, :transfer, :select] end end with "#push" do it "can push fiber into queue" do sequence = [] fiber = Fiber.new do sequence << :executed end selector.push(fiber) selector.select(0) expect(sequence).to be == [:executed] end it "can push non-fiber object into queue" do object = Object.new def object.alive? true end def object.transfer end selector.push(object) selector.select(0) end it "defers push during push to next iteration" do sequence = [] fiber = Fiber.new do sequence << :yield selector.yield sequence << :resume end selector.push(fiber) sequence << :select selector.select(0) sequence << :select selector.select(0) expect(sequence).to be == [:select, :yield, :select, :resume] end it "can push a fiber into the queue while processing queue" do sequence = [] second = Fiber.new do sequence << :second end first = Fiber.new do sequence << :first selector.push(second) end selector.push(first) selector.select(0) expect(sequence).to be == [:first] selector.select(0) expect(sequence).to be == [:first, :second] end end with "#raise" do it "can raise exception on fiber" do sequence = [] fiber = Fiber.new do begin selector.yield rescue sequence << :rescue end end selector.push(fiber) selector.select(0) sequence << :raise selector.raise(fiber, "Boom") expect(sequence).to be == [:raise, :rescue] end end with "#resume" do it "can resume a fiber for execution from the main fiber" do sequence = [] fiber = Fiber.new do |argument| sequence << argument end selector.resume(fiber, :resumed) sequence << :select selector.select(0) expect(sequence).to be == [:resumed, :select] end it "can resume a fiber for execution from a nested fiber" do sequence = [] child = Fiber.new do |argument| sequence << argument end parent = Fiber.new do |argument| sequence << argument selector.resume(child, :child) sequence << :parent end selector.resume(parent, :resumed) sequence << :select selector.select(0) expect(sequence).to be == [:resumed, :child, :select, :parent] end end with "#yield" do it "can yield to the scheduler and later resume execution" do sequence = [] fiber = Fiber.new do |argument| sequence << :yield selector.yield sequence << :resumed end selector.resume(fiber) sequence << :select selector.select(0) expect(sequence).to be == [:yield, :select, :resumed] end it "can yield from resumed fiber" do sequence = [] child = Fiber.new do |argument| sequence << :yield selector.yield sequence << :resumed end parent = Fiber.new do child.resume end selector.resume(parent) sequence << :select selector.select(0) expect(sequence).to be == [:yield, :select, :resumed] end end end IO::Event::Selector.constants.each do |name| klass = IO::Event::Selector.const_get(name) describe(klass, unique: name) do before do @loop = Fiber.current @selector = subject.new(@loop) end after do @selector&.close end attr :loop attr :selector it_behaves_like Queue end end socketry-io-event-ccd0953/test/io/event/selector/write_deadlock.rb000066400000000000000000000047671516444210200253660ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2026, by Samuel Williams. require "io/event" require "io/event/selector" require "socket" WriteDeadlock = Sus::Shared("write deadlock") do with "a pipe that fills up" do it "should not deadlock when waiting for writable" do # Skip on Windows which doesn't have the same socket behavior skip_if_ruby_platform(/mswin|mingw|cygwin/) # Use UNIXSocket pair for more predictable behavior local, remote = UNIXSocket.pair(:STREAM) # Set small buffer to encourage EAGAIN local.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDBUF, 4096) remote.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVBUF, 4096) eagain_hit = false write_completed = false # Fill buffer until we actually hit EAGAIN begin chunk = "X" * 1024 # 1KB chunks 100.times{local.write_nonblock(chunk)} # Write up to 100KB rescue IO::WaitWritable eagain_hit = true end # Skip test if we can't create EAGAIN condition skip "Could not trigger EAGAIN condition" unless eagain_hit # Writer fiber that should hit EAGAIN and wait for WRITABLE writer = Fiber.new do buffer = IO::Buffer.for("test" * 64) # 256 bytes @selector.io_write(Fiber.current, local, buffer, buffer.size) write_completed = true end # Start writer - should yield back when hitting EAGAIN writer.transfer # Writer should be stuck waiting (either for right or wrong event) expect(writer.alive?).to be == true expect(write_completed).to be == false # Drain some data to make socket writable remote.read_nonblock(4096) # Give selector multiple chances to process writable event. # With fix: writer should wake up and complete. # With bug: writer stays stuck because it's waiting for READABLE. timeout_count = 0 while writer.alive? && timeout_count < 10 @selector.select(1.0) # Short intervals for responsiveness, many iterations for tolerance timeout_count += 1 end expect(write_completed).to be == true expect(writer).not.to be(:alive?) ensure local.close rescue nil remote.close rescue nil end end end # Test all available selectors IO::Event::Selector.constants.each do |name| klass = IO::Event::Selector.const_get(name) describe(klass, unique: name) do def before @loop = Fiber.current @selector = subject.new(@loop) end def after(error = nil) @selector&.close end attr :loop attr :selector it_behaves_like WriteDeadlock end end socketry-io-event-ccd0953/test/io/event/timers.rb000066400000000000000000000043011516444210200220510ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2024-2026, by Samuel Williams. require "io/event/timers" class FloatWrapper def initialize(value) @value = value end def to_f @value end end describe IO::Event::Timers do let(:timers) {subject.new} it "should register an event" do fired = false callback = proc do |_time| fired = true end timers.after(0.1, &callback) expect(timers.size).to be == 1 timers.fire(timers.now + 0.15) expect(timers.size).to be == 0 expect(fired).to be == true end it "should register timers in order" do fired = [] offsets = [0.95, 0.1, 0.3, 0.5, 0.4, 0.2, 0.01, 0.9] offsets.each do |offset| timers.after(offset) do fired << offset end end timers.fire(timers.now + 0.5) expect(fired).to be == offsets.sort.first(6) timers.fire(timers.now + 1.0) expect(fired).to be == offsets.sort end it "should fire timers with the time they were fired at" do fired_at = :not_fired timers.after(0.5) do |time| # The time we actually were fired at: fired_at = time end now = timers.now + 1.0 timers.fire(now) expect(fired_at).to be == now end it "should flush cancelled timers" do 10.times do handle = timers.after(0.1){} handle.cancel! end expect(timers.size).to be == 0 end with "#schedule" do it "raises an error if given an invalid time" do expect do timers.after(Object.new){} end.to raise_exception(NoMethodError, message: be =~ /to_f/) end it "converts the offset to a float" do fired = false timers.after(FloatWrapper.new(0.1)) do fired = true end timers.fire(timers.now + 0.15) expect(fired).to be == true end end with "#wait_interval" do it "should return nil if no timers are scheduled" do expect(timers.wait_interval).to be_nil end it "should return nil if all timers are cancelled" do handle = timers.after(0.1){} handle.cancel! expect(timers.wait_interval).to be_nil end it "should return the time until the next timer" do timers.after(0.1){} timers.after(0.2){} expect(timers.wait_interval).to be_within(0.01).of(0.1) end end end socketry-io-event-ccd0953/test/io/event/worker_pool.rb000066400000000000000000000122751516444210200231210ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2025, by Samuel Williams. require "io/event" require "io/event/test_scheduler" return unless defined?(IO::Event::WorkerPool) describe IO::Event::WorkerPool do with "an instance" do let(:worker_pool) {subject.new} after do worker_pool&.close end it "can create a worker pool" do expect(worker_pool).to be_a(IO::Event::WorkerPool) end it "provides statistics" do # Force initialization by calling a method on the pool pool = worker_pool # This should trigger initialization statistics = pool.statistics expect(statistics).to be_a(Hash) expect(statistics).to have_keys( current_worker_count: be_a(Integer), maximum_worker_count: be == 1, current_queue_size: be == 0, shutdown: be == false ) end it "can close the worker pool" do pool = worker_pool # Check that it's not shut down initially expect(pool.statistics[:shutdown]).to be == false # Close the pool result = pool.close expect(result).to be_nil # Check that it's now shut down expect(pool.statistics[:shutdown]).to be == true expect(pool.statistics[:current_worker_count]).to be == 0 end it "can close the worker pool multiple times safely" do pool = worker_pool # Close the pool twice pool.close pool.close # Should still be shut down expect(pool.statistics[:shutdown]).to be == true end end with IO::Event::TestScheduler do let(:scheduler) {IO::Event::TestScheduler.new} it "can create a test scheduler" do expect(scheduler).to be_a(IO::Event::TestScheduler) expect(scheduler.worker_pool).to be_a(IO::Event::WorkerPool) end it "interrupts IO::Buffer.copy operations larger than 1MiB" do skip "IO::Buffer not available" unless defined?(IO::Buffer) # Create buffers larger than 1MiB to trigger GVL release buffer_size = 2 * 1024 * 1024 # 2MiB source = IO::Buffer.new(buffer_size) destination = IO::Buffer.new(buffer_size) # Fill source buffer with some data source.clear("A".ord) worker_pool = nil Thread.new do Fiber.set_scheduler(scheduler) worker_pool = scheduler.worker_pool # Perform the large copy operation in a scheduled fiber Fiber.schedule do destination.copy(source, 0, buffer_size, 0) end end.join # Confirm that the copy worked: expect(destination.get_string(0, 10)).to be == "AAAAAAAAAA" expect(worker_pool.statistics[:call_count]).to be > 0 expect(worker_pool.statistics[:completed_count]).to be > 0 inform worker_pool.statistics end end with "cancellable busy operation" do let(:scheduler) {IO::Event::TestScheduler.new} it "can perform a busy operation that completes normally" do start_time = Time.now result = IO::Event::WorkerPool.busy(duration: 0.1) end_time = Time.now elapsed = end_time - start_time expect(result).to be_a(Hash) expect(result[:cancelled]).to be == false expect(result[:result]).to be == :completed end it "can perform a busy operation with different durations" do result = IO::Event::WorkerPool.busy(duration: 0.05) expect(result).to be_a(Hash) expect(result[:cancelled]).to be == false expect(result[:result]).to be == :completed expect(result[:duration]).to be == 0.05 end it "can cancel a busy operation using unblock function" do # This tests the cancellation mechanism through rb_thread_call_without_gvl completed = false thread = Thread.new do start_time = Time.now result = IO::Event::WorkerPool.busy(duration: 1.0) # Long operation end_time = Time.now elapsed = end_time - start_time completed = true {result: result, elapsed: elapsed} end # Let it start, then kill the thread (which should trigger the unblock function) sleep(0.1) thread.kill thread.join(0.5) # Wait up to 0.5s for thread to finish # The operation should have been interrupted before completion expect(completed).to be == false end it "can be cancelled when executed in a worker pool" do result = nil elapsed = nil error = nil Thread.new do Fiber.set_scheduler(scheduler) busy_fiber = Fiber.schedule do start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) result = IO::Event::WorkerPool.busy(duration: 2.0) rescue Interrupt => error # Ignore. ensure end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) elapsed = end_time - start_time end Fiber.schedule do sleep(0.5) Fiber.scheduler.fiber_interrupt(busy_fiber, StandardError) end end.join expect(result[:cancelled]).to be == true expect(elapsed).to be < 1.0 expect(error).to be_nil end it "can be cancelled before even starting" do result = nil Thread.new do Fiber.set_scheduler(scheduler) busy_fiber = Fiber.schedule do result = IO::Event::WorkerPool.busy(duration: 2.0) end Fiber.schedule do Fiber.scheduler.fiber_interrupt(busy_fiber, StandardError) end end.join expect(result).to have_keys( cancelled: be == true, result: be == :exception, exception: be_a(StandardError) ) end end end socketry-io-event-ccd0953/test/tcp_socket.rb000066400000000000000000000022771516444210200211660ustar00rootroot00000000000000# frozen_string_literal: true # Released under the MIT License. # Copyright, 2025, by Samuel Williams. require "io/event" require "io/event/test_scheduler" require "socket" require "io/nonblock" describe TCPSocket do let(:scheduler) {IO::Event::TestScheduler.new} it "can read and write data" do chunk_size = 1024*6 buffer_size = 1024*64 server_socket = TCPServer.new("localhost", 0) port = server_socket.addr[1] client = TCPSocket.new("localhost", port) client.nonblock = true server = server_socket.accept server.nonblock = true Fiber.set_scheduler(scheduler) writers = Thread::Queue.new 2.times do |i| Fiber.schedule do buffer = i.to_s * chunk_size 128.times do server.write(buffer) server.flush end writers << :done end end Fiber.schedule do 2.times do writers.pop end server.close end Fiber.schedule do while result = client.read_nonblock(buffer_size, exception: false) case result when :wait_readable client.wait_readable when :wait_writable client.wait_writable else # Done. end end end scheduler.run ensure Fiber.set_scheduler(nil) end end