diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1c9a10d..5dce29e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,11 +8,11 @@ jobs: strategy: fail-fast: false matrix: - ruby: [3.1, 3.2, 3.3] + ruby: [3.1, 3.2, 3.3, 3.4] gemfile: - Gemfile - - gemfiles/rails_6_1.gemfile - gemfiles/rails_7_0.gemfile + - gemfiles/rails_7_1.gemfile env: BUNDLE_GEMFILE: ${{ matrix.gemfile }} steps: @@ -20,6 +20,5 @@ jobs: - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - bundler: 2.2.29 bundler-cache: true - run: bundle exec rspec diff --git a/.ruby-version b/.ruby-version index 9cec716..37d02a6 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.1.6 +3.3.8 diff --git a/Appraisals b/Appraisals index 3ff90ab..3444d9b 100644 --- a/Appraisals +++ b/Appraisals @@ -2,4 +2,10 @@ require "appraisal/matrix" -appraisal_matrix(rails: [">= 6.1", "< 7.1"]) +appraisal_matrix(rails: [">= 7.0", "< 7.2"]) do |rails:| + if rails < "7.1" + gem "mutex_m" + gem "base64" + gem "bigdecimal" + end +end diff --git a/CHANGELOG.md b/CHANGELOG.md index ece9058..b7266cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ Inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Note: this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.4.0] - Unreleased +### Added +- Support for Rails 7.1 + +### Removed +- Removed support for Rails 6.1 and below. + +### Changed +#### FiberedMysql2Adapter +- Updated to no longer include EM::Synchrony::ActiveRecord::Adapter_4_2 as it is no longer necessary. +- Removed TransactionManager overrides as they are no longer necessary. + +#### FiberedMysql2::FiberedDatabaseConnectionPool +- Updated to only override methods needed in Rails 7.0. +- Removed double-checking in #connection for cached connection. +- Updated #checkout patch to use #reap instead of our custom #reaped_connections method. + ## [0.3.1] - 2024-10-30 ### Fixed - Fixed bug in FiberedMysqlAdapter.new_client that was causing `uninitialized constant` errors. diff --git a/Gemfile b/Gemfile index 6382727..993cf35 100644 --- a/Gemfile +++ b/Gemfile @@ -7,10 +7,9 @@ gemspec gem 'appraisal' gem 'appraisal-matrix' -gem 'coveralls', require: false gem 'mysql2', '~> 0.5' gem 'nokogiri' gem 'pry' gem 'pry-byebug' gem 'rake' -gem 'rspec' +gem 'rspec', '~> 3.12.0' # Rspec 3.13 is causing segfaults for some reason in CI... diff --git a/Gemfile.lock b/Gemfile.lock index a068550..a54f0a8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,77 +1,89 @@ PATH remote: . specs: - fibered_mysql2 (0.3.1) + fibered_mysql2 (0.4.0.pre.tstarck.3) em-synchrony (~> 1.0) - rails (>= 6.1, < 7.1) + rails (>= 7.0, < 7.2) GEM remote: https://rubygems.org/ specs: - actioncable (7.0.8.6) - actionpack (= 7.0.8.6) - activesupport (= 7.0.8.6) + actioncable (7.1.5.1) + actionpack (= 7.1.5.1) + activesupport (= 7.1.5.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8.6) - actionpack (= 7.0.8.6) - activejob (= 7.0.8.6) - activerecord (= 7.0.8.6) - activestorage (= 7.0.8.6) - activesupport (= 7.0.8.6) + zeitwerk (~> 2.6) + actionmailbox (7.1.5.1) + actionpack (= 7.1.5.1) + activejob (= 7.1.5.1) + activerecord (= 7.1.5.1) + activestorage (= 7.1.5.1) + activesupport (= 7.1.5.1) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8.6) - actionpack (= 7.0.8.6) - actionview (= 7.0.8.6) - activejob (= 7.0.8.6) - activesupport (= 7.0.8.6) + actionmailer (7.1.5.1) + actionpack (= 7.1.5.1) + actionview (= 7.1.5.1) + activejob (= 7.1.5.1) + activesupport (= 7.1.5.1) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp - rails-dom-testing (~> 2.0) - actionpack (7.0.8.6) - actionview (= 7.0.8.6) - activesupport (= 7.0.8.6) - rack (~> 2.0, >= 2.2.4) + rails-dom-testing (~> 2.2) + actionpack (7.1.5.1) + actionview (= 7.1.5.1) + activesupport (= 7.1.5.1) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8.6) - actionpack (= 7.0.8.6) - activerecord (= 7.0.8.6) - activestorage (= 7.0.8.6) - activesupport (= 7.0.8.6) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actiontext (7.1.5.1) + actionpack (= 7.1.5.1) + activerecord (= 7.1.5.1) + activestorage (= 7.1.5.1) + activesupport (= 7.1.5.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8.6) - activesupport (= 7.0.8.6) + actionview (7.1.5.1) + activesupport (= 7.1.5.1) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.8.6) - activesupport (= 7.0.8.6) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.1.5.1) + activesupport (= 7.1.5.1) globalid (>= 0.3.6) - activemodel (7.0.8.6) - activesupport (= 7.0.8.6) - activerecord (7.0.8.6) - activemodel (= 7.0.8.6) - activesupport (= 7.0.8.6) - activestorage (7.0.8.6) - actionpack (= 7.0.8.6) - activejob (= 7.0.8.6) - activerecord (= 7.0.8.6) - activesupport (= 7.0.8.6) + activemodel (7.1.5.1) + activesupport (= 7.1.5.1) + activerecord (7.1.5.1) + activemodel (= 7.1.5.1) + activesupport (= 7.1.5.1) + timeout (>= 0.4.0) + activestorage (7.1.5.1) + actionpack (= 7.1.5.1) + activejob (= 7.1.5.1) + activerecord (= 7.1.5.1) + activesupport (= 7.1.5.1) marcel (~> 1.0) - mini_mime (>= 1.1.0) - activesupport (7.0.8.6) + activesupport (7.1.5.1) + base64 + benchmark (>= 0.3) + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) + mutex_m + securerandom (>= 0.3) tzinfo (~> 2.0) appraisal (2.5.0) bundler @@ -79,31 +91,36 @@ GEM thor (>= 0.14.0) appraisal-matrix (0.3.0) appraisal (~> 2.2) - bigdecimal (3.1.8) + base64 (0.3.0) + benchmark (0.4.1) + bigdecimal (3.2.2) builder (3.3.0) - byebug (11.1.3) + byebug (12.0.0) + cgi (0.5.0) coderay (1.1.3) - concurrent-ruby (1.3.4) - coveralls (0.8.23) - json (>= 1.8, < 3) - simplecov (~> 0.16.1) - term-ansicolor (~> 1.3) - thor (>= 0.19.4, < 2.0) - tins (~> 1.6) + concurrent-ruby (1.3.5) + connection_pool (2.5.3) crass (1.0.6) - date (3.3.4) - diff-lcs (1.5.1) - docile (1.4.1) + date (3.4.1) + diff-lcs (1.6.2) + drb (2.2.3) em-synchrony (1.0.6) eventmachine (>= 1.0.0.beta.1) - erubi (1.13.0) + erb (4.0.4) + cgi (>= 0.3.3) + erubi (1.13.1) eventmachine (1.2.7) globalid (1.2.1) activesupport (>= 6.1) - i18n (1.14.6) + i18n (1.14.7) concurrent-ruby (~> 1.0) - json (2.7.2) - loofah (2.23.1) + io-console (0.8.1) + irb (1.15.2) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + logger (1.7.0) + loofah (2.24.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -114,90 +131,100 @@ GEM marcel (1.0.4) method_source (1.1.0) mini_mime (1.1.5) - mini_portile2 (2.8.7) - minitest (5.25.1) + mini_portile2 (2.8.9) + minitest (5.25.5) + mutex_m (0.3.0) mysql2 (0.5.6) - net-imap (0.5.0) + net-imap (0.5.9) date net-protocol net-pop (0.1.2) net-protocol net-protocol (0.2.2) timeout - net-smtp (0.5.0) + net-smtp (0.5.1) net-protocol nio4r (2.7.4) - nokogiri (1.16.7) + nokogiri (1.18.9) mini_portile2 (~> 2.8.2) racc (~> 1.4) - pry (0.14.2) + pp (0.6.2) + prettyprint + prettyprint (0.2.0) + pry (0.15.2) coderay (~> 1.1) method_source (~> 1.0) - pry-byebug (3.10.1) - byebug (~> 11.0) - pry (>= 0.13, < 0.15) + pry-byebug (3.11.0) + byebug (~> 12.0) + pry (>= 0.13, < 0.16) + psych (5.2.6) + date + stringio racc (1.8.1) - rack (2.2.10) - rack-test (2.1.0) + rack (3.2.0) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) rack (>= 1.3) - rails (7.0.8.6) - actioncable (= 7.0.8.6) - actionmailbox (= 7.0.8.6) - actionmailer (= 7.0.8.6) - actionpack (= 7.0.8.6) - actiontext (= 7.0.8.6) - actionview (= 7.0.8.6) - activejob (= 7.0.8.6) - activemodel (= 7.0.8.6) - activerecord (= 7.0.8.6) - activestorage (= 7.0.8.6) - activesupport (= 7.0.8.6) + rackup (2.2.1) + rack (>= 3) + rails (7.1.5.1) + actioncable (= 7.1.5.1) + actionmailbox (= 7.1.5.1) + actionmailer (= 7.1.5.1) + actionpack (= 7.1.5.1) + actiontext (= 7.1.5.1) + actionview (= 7.1.5.1) + activejob (= 7.1.5.1) + activemodel (= 7.1.5.1) + activerecord (= 7.1.5.1) + activestorage (= 7.1.5.1) + activesupport (= 7.1.5.1) bundler (>= 1.15.0) - railties (= 7.0.8.6) - rails-dom-testing (2.2.0) + railties (= 7.1.5.1) + rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) + rails-html-sanitizer (1.6.2) loofah (~> 2.21) - nokogiri (~> 1.14) - railties (7.0.8.6) - actionpack (= 7.0.8.6) - activesupport (= 7.0.8.6) - method_source + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (7.1.5.1) + actionpack (= 7.1.5.1) + activesupport (= 7.1.5.1) + irb + rackup (>= 1.0.0) rake (>= 12.2) - thor (~> 1.0) - zeitwerk (~> 2.5) - rake (13.2.1) - rspec (3.13.0) - rspec-core (~> 3.13.0) - rspec-expectations (~> 3.13.0) - rspec-mocks (~> 3.13.0) - rspec-core (3.13.0) - rspec-support (~> 3.13.0) - rspec-expectations (3.13.1) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rake (13.3.0) + rdoc (6.14.2) + erb + psych (>= 4.0.0) + reline (0.6.2) + io-console (~> 0.5) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-core (3.12.3) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.4) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.13.0) - rspec-mocks (3.13.1) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.7) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.13.0) - rspec-support (3.13.1) - simplecov (0.16.1) - docile (~> 1.1) - json (>= 1.8, < 3) - simplecov-html (~> 0.10.0) - simplecov-html (0.10.2) - sync (0.5.0) - term-ansicolor (1.11.2) - tins (~> 1.0) - thor (1.3.2) - timeout (0.4.1) - tins (1.33.0) - bigdecimal - sync + rspec-support (~> 3.12.0) + rspec-support (3.12.2) + securerandom (0.4.1) + stringio (3.1.7) + thor (1.4.0) + timeout (0.4.3) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - websocket-driver (0.7.6) + websocket-driver (0.8.0) + base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) zeitwerk (2.6.18) @@ -208,14 +235,13 @@ PLATFORMS DEPENDENCIES appraisal appraisal-matrix - coveralls fibered_mysql2! mysql2 (~> 0.5) nokogiri pry pry-byebug rake - rspec + rspec (~> 3.12.0) BUNDLED WITH - 2.2.29 + 2.6.9 diff --git a/fibered_mysql2.gemspec b/fibered_mysql2.gemspec index 8f97bbf..619719f 100644 --- a/fibered_mysql2.gemspec +++ b/fibered_mysql2.gemspec @@ -30,5 +30,5 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.add_dependency 'em-synchrony', '~> 1.0' - spec.add_dependency 'rails', '>= 6.1', '< 7.1' + spec.add_dependency 'rails', '>= 7.0', '< 7.2' end diff --git a/gemfiles/rails_7_0.gemfile b/gemfiles/rails_7_0.gemfile index 5fd8832..cb772ad 100644 --- a/gemfiles/rails_7_0.gemfile +++ b/gemfiles/rails_7_0.gemfile @@ -4,13 +4,15 @@ source "https://rubygems.org" gem "appraisal" gem "appraisal-matrix" -gem "coveralls", require: false gem "mysql2", "~> 0.5" gem "nokogiri" gem "pry" gem "pry-byebug" gem "rake" -gem "rspec" +gem "rspec", "~> 3.12.0" gem "rails", "~> 7.0.0" +gem "mutex_m" +gem "base64" +gem "bigdecimal" gemspec path: "../" diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_7_1.gemfile similarity index 75% rename from gemfiles/rails_6_1.gemfile rename to gemfiles/rails_7_1.gemfile index 95aa0d4..8aac72f 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_7_1.gemfile @@ -4,13 +4,12 @@ source "https://rubygems.org" gem "appraisal" gem "appraisal-matrix" -gem "coveralls", require: false gem "mysql2", "~> 0.5" gem "nokogiri" gem "pry" gem "pry-byebug" gem "rake" -gem "rspec" -gem "rails", "~> 6.1.0" +gem "rspec", "~> 3.12.0" +gem "rails", "~> 7.1.0" gemspec path: "../" diff --git a/lib/active_record/connection_adapters/fibered_mysql2_adapter.rb b/lib/active_record/connection_adapters/fibered_mysql2_adapter.rb index 06383b2..7c6455f 100644 --- a/lib/active_record/connection_adapters/fibered_mysql2_adapter.rb +++ b/lib/active_record/connection_adapters/fibered_mysql2_adapter.rb @@ -3,11 +3,12 @@ require 'em-synchrony' require 'active_model' require 'active_record/errors' + require 'active_record/connection_adapters/mysql2_adapter' -require 'active_record/connection_adapters/em_mysql2_adapter' +require 'em-synchrony/mysql2' module FiberedMysql2 - module FiberedMysql2Adapter_5_2 + module FiberedMysql2Adapter_7_0 def lease if in_use? msg = "Cannot lease connection, ".dup @@ -53,6 +54,98 @@ def steal! end end + def reset_transaction #:nodoc: + @transaction_manager = ::FiberedMysql2::FiberedMysql2Adapter_7_0::TransactionManager.new(self) + end + + class TransactionManager < ::ActiveRecord::ConnectionAdapters::TransactionManager + def initialize(...) + super + @stack = Hash.new { |h, k| h[k] = [] } + end + + def current_transaction #:nodoc: + _current_stack.last || ::ActiveRecord::ConnectionAdapters::TransactionManager::NULL_TRANSACTION + end + + def open_transactions + _current_stack.size + end + + def begin_transaction(isolation: nil, joinable: true, _lazy: true) + @connection.lock.synchronize do + run_commit_callbacks = !current_transaction.joinable? + transaction = + if _current_stack.empty? + ::ActiveRecord::ConnectionAdapters::RealTransaction.new(@connection, isolation:, joinable:, run_commit_callbacks: run_commit_callbacks) + else + ::ActiveRecord::ConnectionAdapters::SavepointTransaction.new(@connection, "active_record_#{Fiber.current.object_id}_#{open_transactions}", _current_stack.last, isolation:, joinable:, run_commit_callbacks: run_commit_callbacks) + end + + if @connection.supports_lazy_transactions? && lazy_transactions_enabled? && _lazy + @has_unmaterialized_transactions = true + else + transaction.materialize! + end + _current_stack.push(transaction) + transaction + end + end + + # Overriding the ActiveRecord::TransactionManager#materialize_transactions method to use + # fiber safe the _current_stack instead of the @stack instance variable. when marterializing + # transactions. + def materialize_transactions + return if @materializing_transactions + return unless @has_unmaterialized_transactions + + @connection.lock.synchronize do + begin + @materializing_transactions = true + _current_stack.each { |t| t.materialize! unless t.materialized? } + ensure + @materializing_transactions = false + end + @has_unmaterialized_transactions = false + end + end + + # Overriding the ActiveRecord::TransactionManager#commit_transaction method to use + # fiber safe the _current_stack instead of the @stack instance variable. when marterializing + # transactions. + def commit_transaction + @connection.lock.synchronize do + transaction = _current_stack.last + + begin + transaction.before_commit_records + ensure + _current_stack.pop + end + + transaction.commit + transaction.commit_records + end + end + + # Overriding the ActiveRecord::TransactionManager#rollback_transaction method to use + # fiber safe the _current_stack instead of the @stack instance variable. when marterializing + # transactions. + def rollback_transaction(transaction = nil) + @connection.lock.synchronize do + transaction ||= _current_stack.pop + transaction.rollback + transaction.rollback_records + end + end + + private + + def _current_stack + @stack[Fiber.current.object_id] + end + end + private def owner_fiber @@ -62,8 +155,10 @@ def owner_fiber end end - class FiberedMysql2Adapter < ::ActiveRecord::ConnectionAdapters::EMMysql2Adapter - include FiberedMysql2Adapter_5_2 + class FiberedMysql2Adapter < ::ActiveRecord::ConnectionAdapters::Mysql2Adapter + if ::ActiveRecord.gem_version < "7.1" + include FiberedMysql2Adapter_7_0 + end class << self # Copied from Mysql2Adapter, except with the EM Mysql2 client @@ -77,9 +172,5 @@ def new_client(config) end end end - - def initialize(*args) - super - end end end diff --git a/lib/fibered_mysql2/fibered_database_connection_pool.rb b/lib/fibered_mysql2/fibered_database_connection_pool.rb index 4c49a14..18f867a 100644 --- a/lib/fibered_mysql2/fibered_database_connection_pool.rb +++ b/lib/fibered_mysql2/fibered_database_connection_pool.rb @@ -184,33 +184,32 @@ def mon_exit_for_cond end module FiberedDatabaseConnectionPool - include FiberedMonitorMixin - - module Adapter_5_2 - def cached_connections - @thread_cached_conns - end - - def current_connection_id - connection_cache_key(current_thread) + module Adapter_7_0 + def release_connection(owner_thread = Fiber.current) + if (conn = @thread_cached_conns.delete(connection_cache_key(owner_thread))) + checkin(conn) + end end - def checkout(checkout_timeout = @checkout_timeout) - begin - reap_connections - rescue => ex - ActiveRecord::Base.logger.error("Exception occurred while executing reap_connections: #{ex}") + def with_connection + unless (conn = cached_connections[current_connection_id]) # Invoca Patch to use Fiber + conn = connection + fresh_connection = true end - super + yield conn + ensure + release_connection if fresh_connection end - def release_connection(owner_thread = Fiber.current) - if (conn = @thread_cached_conns.delete(connection_cache_key(owner_thread))) - checkin(conn) - end + def current_thread + Fiber.current end end - include Adapter_5_2 + + if ::ActiveRecord.gem_version < "7.1" + include Adapter_7_0 + end + include FiberedMonitorMixin # This is switches the connection pool's mutex and condition variables to event machine / Fiber compatible ones. def initialize(pool_config) if pool_config.db_config.reaping_frequency @@ -222,6 +221,26 @@ def initialize(pool_config) @reaper = nil # no need to keep a reference to this since it does nothing in this sub-class end + def current_connection_id + connection_cache_key(current_thread) + end + + def cached_connections + @thread_cached_conns + end + + # Invoca patch that reaps orphaned connections on checkout. This lets us immediately use a connection left open by dead fibers + # instead of waiting for all connections to be used in the pool before they are reaped. + def checkout(checkout_timeout = @checkout_timeout) + begin + reap + rescue => ex + ActiveRecord::Base.logger.error("Exception occurred while executing reap_connections: #{ex}") + end + super + end + + # Invoca patch to ensure that we are using the current fiber's connection. def connection # this is correctly done double-checked locking # (ThreadSafe::Cache's lookups have volatile semantics) @@ -237,27 +256,6 @@ def connection end end end - - def reap_connections - cached_connections.values.each do |connection| - unless connection.owner.alive? - checkin(connection) - end - end - end - - private - - #-- - # This hook-in method allows for easier monkey-patching fixes needed by - # JRuby users that use Fibers. - def connection_cache_key(fiber) - fiber - end - - def current_thread - Fiber.current - end end end diff --git a/lib/fibered_mysql2/fibered_mysql2_connection_factory.rb b/lib/fibered_mysql2/fibered_mysql2_connection_factory.rb index fb24cb9..87caddb 100644 --- a/lib/fibered_mysql2/fibered_mysql2_connection_factory.rb +++ b/lib/fibered_mysql2/fibered_mysql2_connection_factory.rb @@ -2,101 +2,6 @@ require_relative '../active_record/connection_adapters/fibered_mysql2_adapter' -module EM::Synchrony - module ActiveRecord - _ = Adapter_4_2 - module Adapter_4_2 - def configure_connection - super # undo EM::Synchrony's override here - end - - def transaction(*args) - super # and here - end - - _ = TransactionManager - class TransactionManager < _ - # Overriding the em-synchrony override to bring it up to rails 6 requirements. - # Changes from the original Rails 6 source are: - # 1. the usage of _current_stack created by em-synchrony instead of the Rails provided @stack instance variable - # 2. the usage of Fiber.current.object_id as a part of the savepoint transaction name - # - # Original EM Synchrony Source: - # https://github.com/igrigorik/em-synchrony/blob/master/lib/em-synchrony/activerecord_4_2.rb#L35-L44 - # - # Original Rails Source: - # https://github.com/rails/rails/blob/6-0-stable/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb#L205-L224 - def begin_transaction(isolation: nil, joinable: true, _lazy: true) - @connection.lock.synchronize do - run_commit_callbacks = !current_transaction.joinable? - transaction = - if _current_stack.empty? - ::ActiveRecord::ConnectionAdapters::RealTransaction.new(@connection, isolation:, joinable:, run_commit_callbacks: run_commit_callbacks) - else - ::ActiveRecord::ConnectionAdapters::SavepointTransaction.new(@connection, "active_record_#{Fiber.current.object_id}_#{open_transactions}", _current_stack.last, isolation:, joinable:, run_commit_callbacks: run_commit_callbacks) - end - - if @connection.supports_lazy_transactions? && lazy_transactions_enabled? && _lazy - @has_unmaterialized_transactions = true - else - transaction.materialize! - end - _current_stack.push(transaction) - transaction - end - end - - # Overriding the ActiveRecord::TransactionManager#materialize_transactions method to use - # fiber safe the _current_stack instead of the @stack instance variable. when marterializing - # transactions. - def materialize_transactions - return if @materializing_transactions - return unless @has_unmaterialized_transactions - - @connection.lock.synchronize do - begin - @materializing_transactions = true - _current_stack.each { |t| t.materialize! unless t.materialized? } - ensure - @materializing_transactions = false - end - @has_unmaterialized_transactions = false - end - end - - # Overriding the ActiveRecord::TransactionManager#commit_transaction method to use - # fiber safe the _current_stack instead of the @stack instance variable. when marterializing - # transactions. - def commit_transaction - @connection.lock.synchronize do - transaction = _current_stack.last - - begin - transaction.before_commit_records - ensure - _current_stack.pop - end - - transaction.commit - transaction.commit_records - end - end - - # Overriding the ActiveRecord::TransactionManager#rollback_transaction method to use - # fiber safe the _current_stack instead of the @stack instance variable. when marterializing - # transactions. - def rollback_transaction(transaction = nil) - @connection.lock.synchronize do - transaction ||= _current_stack.pop - transaction.rollback - transaction.rollback_records - end - end - end - end - end -end - module FiberedMysql2 module FiberedMysql2ConnectionFactory def fibered_mysql2_connection(raw_config) diff --git a/lib/fibered_mysql2/version.rb b/lib/fibered_mysql2/version.rb index b984364..6398d9d 100644 --- a/lib/fibered_mysql2/version.rb +++ b/lib/fibered_mysql2/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module FiberedMysql2 - VERSION = "0.3.1" + VERSION = "0.4.0.pre.tstarck.3" end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 690d6d4..ec3e7bb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,10 +1,7 @@ # frozen_string_literal: true -require 'coveralls' - -Coveralls.wear! - require 'bundler/setup' +require 'logger' require 'rails' require 'active_record' require 'fibered_mysql2' @@ -24,4 +21,9 @@ config.mock_with :rspec do |mocks| mocks.verify_partial_doubles = true end + + config.before(:all) do + ActiveSupport::IsolatedExecutionState.isolation_level = :fiber + ActiveRecord::Base.logger = Logger.new("/dev/null") + end end diff --git a/spec/unit/fibered_database_connection_pool_spec.rb b/spec/unit/fibered_database_connection_pool_spec.rb index 0c6766f..2266df7 100644 --- a/spec/unit/fibered_database_connection_pool_spec.rb +++ b/spec/unit/fibered_database_connection_pool_spec.rb @@ -374,10 +374,12 @@ def cancel_timer(timer_block) allow(client).to receive(:query_options) { {} } allow(client).to receive(:escape) { |query| query } allow(client).to receive(:ping) { true } + allow(client).to receive(:query) allow(client).to receive(:close) + allow(client).to receive(:closed?) { false } allow(client).to receive(:info).and_return({ version: "5.7.27" }) allow(client).to receive(:server_info).and_return({ version: "5.7.27" }) - allow(Mysql2::EM::Client).to receive(:new) { |config| client } + allow(FiberedMysql2::FiberedMysql2Adapter).to receive(:new_client) { client } establish_connection end @@ -392,38 +394,24 @@ def cancel_timer(timer_block) context "with more than 1 connection in the pool" do it "should serve separate connections per fiber" do - expected_query = if Rails::VERSION::MAJOR > 4 - "SET @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483" - else - "SET @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483, @@SESSION.sql_mode = 'STRICT_ALL_TABLES'" - end - expect(client).to receive(:query) do |*args| - expect(args).to eq([expected_query]) - end.exactly(2).times - c0 = ActiveRecord::Base.connection c1 = nil - fiber = Fiber.new { c1 = ActiveRecord::Base.connection } + fiber = Fiber.new do + c1 = ActiveRecord::Base.connection + end fiber.resume - expect(c0).to be - expect(c1).to be + expect(c0).to be_truthy + expect(c1).to be_truthy expect(c1).to_not eq(c0) expect(c0.owner).to eq(Fiber.current) expect(c1.owner).to eq(fiber) - expect(c0.in_use?).to be - expect(c1.in_use?).to be end it "should reclaim connections when the fiber has exited" do - expect(client).to receive(:query) { }.exactly(2).times - - reap_connection_count = Rails::VERSION::MAJOR > 4 ? 5 : 3 - expect(ActiveRecord::Base.connection_pool).to receive(:reap_connections).with(no_args).exactly(reap_connection_count).times.and_call_original - ActiveRecord::Base.connection c1 = nil - fiber1 = Fiber.new { c1 = ActiveRecord::Base.connection } + fiber1 = Fiber.new { c1 = ActiveRecord::Base.connection.tap(&:verify!) } # Force configuring the raw mysql client. c2 = nil fiber2 = Fiber.new { c2 = ActiveRecord::Base.connection } @@ -446,12 +434,7 @@ def cancel_timer(timer_block) end it "should hand off connection on checkin to any fiber waiting on checkout" do - expect(client).to receive(:query) { }.exactly(1).times - EM.run do - reap_connection_count = Rails::VERSION::MAJOR > 4 ? 4 : 3 - expect(ActiveRecord::Base.connection_pool).to receive(:reap_connections).with(no_args).exactly(reap_connection_count).times.and_call_original - c0 = ActiveRecord::Base.connection connection_pool = c0.pool c1 = nil @@ -460,12 +443,12 @@ def cancel_timer(timer_block) em_helper.run_next_ticks c1 = ActiveRecord::Base.connection.tap { em_helper.run_next_ticks } end - fiber1.resume + fiber1.resume # Fiber should yield back immediately because the connection pool has no connections available. - expect(c1).to eq(nil) # should block because there is only one connection + expect(c1).to eq(nil) connection_pool.checkin(c0) - em_helper.run_next_ticks + em_helper.run_next_ticks # Resume fiber1 now that c0 is checked in. expect(c1).to eq(c0) diff --git a/spec/unit/fibered_mysql2_adapter_spec.rb b/spec/unit/fibered_mysql2_adapter_spec.rb index cc2ac98..a93acaa 100644 --- a/spec/unit/fibered_mysql2_adapter_spec.rb +++ b/spec/unit/fibered_mysql2_adapter_spec.rb @@ -37,7 +37,7 @@ context "when the connection is unsuccessful" do before do - allow(Mysql2::EM::Client).to receive(:new).and_raise(Mysql2::Error.new("error", nil, error_number)) + allow_any_instance_of(Mysql2::EM::Client).to receive(:connect).and_raise(Mysql2::Error.new("error", nil, error_number)) end context "when the error is a bad database error" do @@ -58,7 +58,7 @@ end end - context '#lease' do + context '#lease', if: ActiveRecord.gem_version < "7.1" do subject { adapter.lease } it { should eq(Fiber.current) } @@ -77,7 +77,7 @@ end end - context '#expire' do + context '#expire', if: ActiveRecord.gem_version < "7.1" do subject { adapter.expire } context 'if the connection is not in use' do @@ -131,7 +131,7 @@ end end - context 'other mixins' do + context 'other mixins', if: ActiveRecord.gem_version < "7.1" do it 'raises if @owner has been overwritten with a non-Fiber' do adapter.instance_variable_set(:@owner, Thread.new { }) diff --git a/spec/unit/fibered_mysql2_connection_factory_spec.rb b/spec/unit/fibered_mysql2_connection_factory_spec.rb index 38d2dd1..9843abc 100644 --- a/spec/unit/fibered_mysql2_connection_factory_spec.rb +++ b/spec/unit/fibered_mysql2_connection_factory_spec.rb @@ -12,10 +12,12 @@ subject { ActiveRecord::Base.connection } before do - expect(Mysql2::EM::Client).to receive(:new).and_return(client) + allow(FiberedMysql2::FiberedMysql2Adapter).to receive(:new_client).and_return(client) allow(client).to receive(:query_options) { {} } allow(client).to receive(:server_info).and_return({ version: "5.7.27" }) allow(client).to receive(:ping) { true } + allow(client).to receive(:close) + allow(client).to receive(:closed?) { false } allow(client).to receive(:query).and_return(stub_mysql_client_result) ActiveRecord::Base.establish_connection( :adapter => 'fibered_mysql2', @@ -40,6 +42,8 @@ allow(client).to receive(:query_options) { {} } allow(client).to receive(:escape) { |query| query } allow(client).to receive(:ping) { true } + allow(client).to receive(:close) + allow(client).to receive(:closed?) { false } allow(client).to receive(:server_info).and_return({ version: "5.7.27" }) allow(client).to receive(:query).and_return(stub_mysql_client_result) end