Giter VIP home page Giter VIP logo

isolator's Introduction

Cult Of Martians Gem Version Build

Isolator

Detect non-atomic interactions within DB transactions.

Examples:

# HTTP calls within transaction
User.transaction do
  user = User.new(user_params)
  user.save!
  # HTTP API call
  PaymentsService.charge!(user)
end

#=> raises Isolator::HTTPError

# background job
User.transaction do
  user.update!(confirmed_at: Time.now)
  UserMailer.successful_confirmation(user).deliver_later
end

#=> raises Isolator::BackgroundJobError

Of course, Isolator can detect implicit transactions too. Consider this pretty common bad practice–enqueueing background job from after_create callback:

class Comment < ApplicationRecord
  # the good way is to use after_create_commit
  # (or not use callbacks at all)
  after_create :notify_author

  private

  def notify_author
    CommentMailer.comment_created(self).deliver_later
  end
end

Comment.create(text: "Mars is watching you!")
#=> raises Isolator::BackgroundJobError

Isolator is supposed to be used in tests and on staging.

Installation

Add this line to your application's Gemfile:

# We suppose that Isolator is used in development and test
# environments.
group :development, :test do
  gem "isolator"
end

# Or you can add it to Gemfile with `require: false`
# and require it manually in your code.
#
# This approach is useful when you want to use it in staging env too.
gem "isolator", require: false

Usage

Isolator is a plug-n-play tool, so, it begins to work right after required.

However, there are some potential caveats:

  1. Isolator tries to detect the environment automatically and includes only necessary adapters. Thus the order of loading gems matters: make sure that isolator is required in the end (NOTE: in Rails, all adapters loaded after application initialization).

  2. Isolator does not distinguish framework-level adapters. For example, :active_job spy doesn't take into account which AJ adapter you use; if you are using a safe one (e.g. Que) just disable the :active_job adapter to avoid false negatives. You can do this by adding an initializer:

    require "active_job/base"
    Isolator.adapters.active_job.disable!
  3. Isolator tries to detect the test environment and slightly change its behavior: first, it respect transactional tests; secondly, error raising is turned on by default (see below).

  4. Experimental multiple databases has been added in v0.7.0. Please, let us know if you encounter any issues.

Configuration

Isolator.configure do |config|
  # Specify a custom logger to log offenses
  config.logger = nil

  # Raise exception on offense
  config.raise_exceptions = false # true in test env

  # Send notifications to uniform_notifier
  config.send_notifications = false

  # Customize backtrace filtering (provide a callable)
  # By default, just takes the top-5 lines
  config.backtrace_filter = ->(backtrace) { backtrace.take(5) }

  # Define a custom ignorer class (must implement .prepare)
  # uses a row number based list from the .isolator_todo.yml file
  config.ignorer = Isolator::Ignorer

  # Turn on/off raising exceptions for simultaneous transactions to different databases
  config.disallow_cross_database_transactions = false
end

Isolator relies on uniform_notifier to send custom notifications.

NOTE: uniform_notifier should be installed separately (i.e., added to Gemfile).

Callbacks

Isolator different callbacks so you can inject your own logic or build custom extensions.

# This callback is called when Isolator enters the "danger zone"—a within-transaction context
Isolator.before_isolate do
  puts "Entering a database transaction. Be careful!"
end

# This callback is called when Isolator leaves the "danger zone"
Isolator.after_isolate do
  puts "Leaving a database transaction. Everything is fine. Feel free to call slow HTTP APIs"
end

# This callback is called every time a new transaction is open (root or nested)
Isolator.on_transaction_open do |event|
  puts "New transaction from #{event[:connection_id]}. " \
       "Current depth: #{event[:depth]}"
end

# This callback is called every time a transaction is completed
Isolator.on_transaction_close do |event|
  puts "Transaction completed from #{event[:connection_id]}. " \
       "Current depth: #{event[:depth]}"
end

Transactional tests support

Supported ORMs

  • ActiveRecord >= 6.0 (see older versions of Isolator for previous versions)
  • ROM::SQL (only if Active Support instrumentation extension is loaded)

Adapters

Isolator has a bunch of built-in adapters:

  • :http – built on top of Sniffer
  • :active_job
  • :sidekiq
  • :resque
  • :resque_scheduler
  • :sucker_punch
  • :mailer
  • :webmock – track mocked HTTP requests (unseen by Sniffer) in tests
  • :action_cable

You can dynamically enable/disable adapters, e.g.:

# Disable HTTP adapter == do not spy on HTTP requests
Isolator.adapters.http.disable!

# Enable back

Isolator.adapters.http.enable!

For active_job, be sure to first require "active_job/base".

Fix Offenses

For the actions that should be executed only after successful transaction commit (which is mostly always so), you can try to use the after_commit callback from after_commit_everywhere gem (or use native AR callback in models if it's applicable).

Ignore Offenses

Since Isolator adapter is just a wrapper over original code, it may lead to false positives when there is another library patching the same behaviour. In that case you might want to ignore some offenses.

Consider an example: we use Sidekiq along with sidekiq-postpone–gem that patches Sidekiq::Client#raw_push and allows you to postpone jobs enqueueing (e.g. to enqueue everything when a transaction is commited–we don't want to raise exceptions in such situation).

To ignore offenses when sidekiq-postpone is active, you can add an ignore proc:

Isolator.adapters.sidekiq.ignore_if { Thread.current[:sidekiq_postpone] }

You can add as many ignores as you want, the offense is registered iff all of them return false.

Using with sidekiq/testing

If you require sidekiq/testing in your tests after isolator is required then it will blow away isolator's hooks, so you need to require isolator after requiring sidekiq/testing.

If you're using Rails and want to use isolator in development and staging, then here is a way to do this.

# Gemfile
gem "isolator", require: false # so it delays loading till after sidekiq/testing

# config/initializers/isolator.rb
require "sidekiq/testing" if Rails.env.test?

unless Rails.env.production? # so we get it in staging too
  require "isolator"
  Isolator.configure do |config|
    config.send_notifications = true # ...
  end
end

Using with legacy Rails codebases

If you already have a huge Rails project it can be tricky to turn Isolator on because you'll immediately get a lot of failed specs. If you want to fix detected issues one by one, you can list all of them in the special files .isolator_todo.yml and .isolator_ignore.yml in the following way:

sidekiq:
  - app/models/user.rb:20
  - app/models/sales/**/*.rb

You can ignore the same files in multiple adapters using YML aliases in the following way:

http_common: &http_common
  - app/models/user.rb:20

http: *http_common
webmock: *http_common

All the exceptions raised in the listed lines will be ignored.

The .isolator_todo.yml file is intended to point to the code that should be fixed later, and .isolator_ignore.yml points to the code that for some reasons is not expected to be fixed. (See #40)

Using with legacy Ruby codebases

If you are not using Rails, you'll have to load ignores from file manually, using Isolator::Ignorer.prepare(path:), for instance Isolator::Ignorer.prepare(path: "./config/.isolator_todo.yml")

Custom Adapters

An adapter is just a combination of a method wrapper and lifecycle hooks.

Suppose that you have a class Danger with a method #explode, which is not safe to be run within a DB transaction. Then you can isolate it (i.e., register with Isolator):

# The first argument is a unique adapter id,
# you can use it later to enable/disable the adapter
#
# The second argument is the method owner and
# the third one is a method name.
Isolator.isolate :danger, Danger, :explode, options

# NOTE: if you want to isolate a class method, use singleton_class instead
Isolator.isolate :danger, Danger.singleton_class, :explode, options

Possible options are:

  • exception_class – an exception class to raise in case of offense
  • exception_message – custom exception message (could be specified without a class)
  • details_message – a block to generate additional exception message information:
Isolator.isolate :active_job,
  target: ActiveJob::Base,
  method_name: :enqueue,
  exception_class: Isolator::BackgroundJobError,
  details_message: ->(obj) {
    "#{obj.class.name}(#{obj.arguments})"
  }

Isolator.isolate :promoter,
  target: UserPromoter,
  method_name: :call,
  details_message: ->(obj_, args, kwargs) {
    # UserPromoter.call(user, role, by: nil)
    user, role = args
    by = kwargs[:by]
    "#{user.name} promoted to #{role} by #{by&.name || "system"})"
  }

Trying to register the same adapter name twice will raise an error. You can guard for it, or remove old adapters before in order to replace them.

unless Isolator.has_adapter?(:promoter)
  Isolator.isolate(:promoter, *rest)
end
# Handle code reloading
class Messager
end

Isolator.remove_adapter(:messager)
Isolator.isolate(:messager, target: Messager, **rest)

You can also add some callbacks to be run before and after the transaction:

Isolator.before_isolate do
 # right after we enter the transaction
end

Isolator.after_isolate do
 # right after the transaction has been committed/rolled back
end

Troubleshooting

Verbose output

In most cases, turning on verbose output for Isolator helps to identify the issue. To do that, you can either specify ISOLATOR_DEBUG=true environment variable or set Isolator.debug_enabled manually.

Tests failing after upgrading to Rails 6.0.3 while using Combustion

The reason is that Rails started using a separate connection pool for advisory locks since 6.0.3. Since Combustion usually applies migrations for every test run, this pool becomse visible to test fixtures, which resulted in 2 transactional commits tracked by Isolator, which only expects one. That leads to false negatives.

To fix this disable migrations advisory locks by adding advisory_locks: false to your database configuration in (spec|test)/internal/config/database.yml.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/palkan/isolator.

License

The gem is available as open source under the terms of the MIT License.

isolator's People

Contributors

aderyabin avatar alexshagov avatar arthurwd avatar bobbymcwho avatar dmitrytsepelev avatar dsalahutdinov avatar envek avatar iiwo avatar jooeycheng avatar mange avatar mquan avatar n-epifanov avatar ohbarye avatar palkan avatar petergoldstein avatar sergioro9 avatar shivanshgaur avatar sunny avatar tagirahmad avatar technicalpickles avatar thesmartnik avatar timdiggins avatar tomgi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

isolator's Issues

Corrupt transaction counts when connection closes unexpectedly

What did you do?

I doubt this is a bug in practice, since connections don't tend to go away all of a sudden in test (maybe in staging if the gem is used there?). But I figured I'd report it anyway, at least as further discouragement from using this in production environments.

I wrote a test to demonstrate the problem. If the database connection closes before the transaction completes, we can fail to either commit or rollback, leaving the transaction count permanently incremented.

    context "when the connection fails" do
      specify do
        expect(Isolator).to_not be_within_transaction

        begin
          ar_class.transaction do
            ar_class.all.to_a
            expect(Isolator).to be_within_transaction

            # Imitate a problem with the underlying connection, preventing us from committing or rolling back
            allow(ar_class.connection).to receive(:commit_db_transaction).and_raise("Failed to commit")
            allow(ar_class.connection).to receive(:rollback_db_transaction).and_raise("Failed to rollback")
          end
        rescue
        end

        # The counts are off now, so this fails
        expect(Isolator).to_not be_within_transaction
      end
    end

We've got some transaction tracking stuff at GitHub that patches the Rails TransactionManager, and we've got an ensure there that allows us to decrement counters even if we fail to commit or rollback. There's perhaps an argument to be made for adding some transaction-specific instrumentation to Rails itself that we could use instead.

What did you expect to happen?

Ideally this test would pass, but I understand why it doesn't. There's nothing coming through sql.active_record that would indicate a failed rollback.

What actually happened?

It fails

Additional context

Environment

Ruby Version:

Framework Version (Rails, whatever):

Isolator Version:

How to handle rescue of StandardError?

In the process of implementing this in our codebase, and I realized that it is very hard to track down some of these, due to broad rescue of StandardError in various places in our codebase.

Is there a suggested way to handle this?

Would it make sense to refactor Isolator's Error classes to inherit from Exception rather than StandardError?

Since this is a tool meant to identify poor-practice in pre-prod environments, I think the latter question may make sense to do, but may be missing some larger implications at the cursory level of thought I've given it.

Corrupt transaction counts with Rails 7.1 restarting savepoint transactions

This is similar to #64, but I'm filing it separately since this one really is a bug.

What did you do?

I tried to run isolator at GitHub and ran into some Trying to finalize an untracked transaction warnings. Here's a failing test (against Rails main) to demonstrate the issue:

    context "when rolling back a restarting savepoint transaction" do
      specify do
        expect(Isolator).to_not be_within_transaction

        begin
          ActiveRecord::Base.logger = Logger.new(STDOUT)
          # RealTransaction (begin..rollback)
          ar_class.transaction do
            ar_class.first
            # Savepoint Transaction (savepoint..rollback)
            ar_class.transaction(requires_new: true) do
              # ResetParentTransaction (rollback to outer savepoint)
              ar_class.transaction(requires_new: true) do
                ar_class.first
                expect(Isolator).to be_within_transaction
                raise "Rollback"
              end
            end
          ensure
            expect(Isolator).to be_within_transaction # Oops, transaction count is already 0 so this test fails
          end
        rescue
        end

        expect(Isolator).to_not be_within_transaction
      end
    end

This happens because of rails/rails#44526, which can cause the same savepoint to get rolled back more than once. In older versions of Rails the transaction queries were:

BEGIN
SAVEPOINT active_record_1
SAVEPOINT active_record_2
ROLLBACK TO SAVEPOINT active_record_2
ROLLBACK TO SAVEPOINT active_record_1
ROLLBACK

But in Rails 7.1 it becomes:

BEGIN
SAVEPOINT active_record_1
ROLLBACK TO SAVEPOINT active_record_1
ROLLBACK TO SAVEPOINT active_record_1
ROLLBACK

That's 2 starts and 3 finishes, so we end up decrementing the count too far.

Environment

Ruby Version:

3.2

Framework Version (Rails, whatever):

Rails 7.1 (unreleased)

Isolator Version:

0.10.0

Failure when saving attachment with action_text/active storage and AWS

What did you do?

Add an attachment to an action_text field, saved.

What did you expect to happen?

No error.

What actually happened?

Isolator::HTTPError - You are trying to make an outgoing network request inside db transaction.

Transaction is Rails's wrapping of an save call.
Callback is https://github.com/rails/rails/blob/d9fda555942bfb6bcb1f75477b1425e2ab3162f5/actiontext/app/models/action_text/rich_text.rb#L17-L19
HTTP request is coming from aws-sdk-core.

Return value is: ["/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/sniffer-0.4.0/lib/sniffer/adapters/net_http_adapter.rb:28:in `request_with_sniffer'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/seahorse/client/net_http/connection_pool.rb:341:in `request'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/seahorse/client/net_http/handler.rb:76:in `block in transmit'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/seahorse/client/net_http/handler.rb:128:in `block in session'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/seahorse/client/net_http/connection_pool.rb:103:in `session_for'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/seahorse/client/net_http/handler.rb:123:in `session'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/seahorse/client/net_http/handler.rb:75:in `transmit'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/seahorse/client/net_http/handler.rb:49:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/seahorse/client/plugins/content_length.rb:17:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/seahorse/client/plugins/request_callback.rb:85:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-s3-1.83.1/lib/aws-sdk-s3/plugins/streaming_retry.rb:60:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-s3-1.83.1/lib/aws-sdk-s3/plugins/s3_signer.rb:124:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-s3-1.83.1/lib/aws-sdk-s3/plugins/s3_signer.rb:61:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-s3-1.83.1/lib/aws-sdk-s3/plugins/s3_host_id.rb:17:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/aws-sdk-core/xml/error_handler.rb:10:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/aws-sdk-core/plugins/transfer_encoding.rb:26:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/aws-sdk-core/plugins/helpful_socket_errors.rb:12:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-s3-1.83.1/lib/aws-sdk-s3/plugins/s3_signer.rb:102:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-s3-1.83.1/lib/aws-sdk-s3/plugins/redirects.rb:20:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/aws-sdk-core/plugins/retry_errors.rb:348:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-s3-1.83.1/lib/aws-sdk-s3/plugins/dualstack.rb:38:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-s3-1.83.1/lib/aws-sdk-s3/plugins/accelerate.rb:58:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/aws-sdk-core/plugins/http_checksum.rb:18:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/aws-sdk-core/plugins/endpoint_pattern.rb:31:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-s3-1.83.1/lib/aws-sdk-s3/plugins/iad_regional_endpoint.rb:34:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-s3-1.83.1/lib/aws-sdk-s3/plugins/expect_100_continue.rb:21:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-s3-1.83.1/lib/aws-sdk-s3/plugins/bucket_name_restrictions.rb:26:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-s3-1.83.1/lib/aws-sdk-s3/plugins/bucket_dns.rb:35:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-s3-1.83.1/lib/aws-sdk-s3/plugins/arn.rb:49:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/aws-sdk-core/rest/handler.rb:10:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/aws-sdk-core/plugins/user_agent.rb:13:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/aws-sdk-core/plugins/endpoint_discovery.rb:80:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/seahorse/client/plugins/endpoint.rb:47:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/aws-sdk-core/plugins/param_validator.rb:26:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/seahorse/client/plugins/raise_response_errors.rb:16:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-s3-1.83.1/lib/aws-sdk-s3/plugins/sse_cpk.rb:24:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-s3-1.83.1/lib/aws-sdk-s3/plugins/dualstack.rb:30:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-s3-1.83.1/lib/aws-sdk-s3/plugins/accelerate.rb:47:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/aws-sdk-core/plugins/jsonvalue_converter.rb:22:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/aws-sdk-core/plugins/idempotency_token.rb:19:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/aws-sdk-core/plugins/param_converter.rb:26:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/seahorse/client/plugins/request_callback.rb:71:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/aws-sdk-core/plugins/response_paging.rb:12:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/seahorse/client/plugins/response_target.rb:24:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-core-3.109.1/lib/seahorse/client/request.rb:72:in `send_request'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-s3-1.83.1/lib/aws-sdk-s3/client.rb:5339:in `get_object'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/aws-sdk-s3-1.83.1/lib/aws-sdk-s3/object.rb:890:in `get'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activestorage-6.1.3.2/lib/active_storage/service/s3_service.rb:54:in `block in download_chunk'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/notifications.rb:205:in `instrument'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activestorage-6.1.3.2/lib/active_storage/service.rb:155:in `instrument'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activestorage-6.1.3.2/lib/active_storage/service/s3_service.rb:53:in `download_chunk'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activestorage-6.1.3.2/app/models/active_storage/blob/identifiable.rb:27:in `download_identifiable_chunk'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activestorage-6.1.3.2/app/models/active_storage/blob/identifiable.rb:22:in `identify_content_type'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activestorage-6.1.3.2/app/models/active_storage/blob/identifiable.rb:11:in `identify_without_saving'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activestorage-6.1.3.2/lib/active_storage/attached/changes/create_one.rb:12:in `initialize'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activestorage-6.1.3.2/lib/active_storage/attached/changes/create_many.rb:35:in `new'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activestorage-6.1.3.2/lib/active_storage/attached/changes/create_many.rb:35:in `build_subchange_from'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activestorage-6.1.3.2/lib/active_storage/attached/changes/create_many.rb:31:in `block in subchanges'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activestorage-6.1.3.2/lib/active_storage/attached/changes/create_many.rb:31:in `collect'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activestorage-6.1.3.2/lib/active_storage/attached/changes/create_many.rb:31:in `subchanges'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activestorage-6.1.3.2/lib/active_storage/attached/changes/create_many.rb:17:in `blobs'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activestorage-6.1.3.2/lib/active_storage/attached/changes/create_many.rb:9:in `initialize'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activestorage-6.1.3.2/lib/active_storage/attached/model.rb:144:in `new'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activestorage-6.1.3.2/lib/active_storage/attached/model.rb:144:in `embeds='", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actiontext-6.1.3.2/app/models/action_text/rich_text.rb:18:in `block in <class:RichText>'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:427:in `instance_exec'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:427:in `block in make_lambda'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:198:in `block (2 levels) in halting'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:604:in `block (2 levels) in default_terminator'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:603:in `catch'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:603:in `block in default_terminator'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:199:in `block in halting'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:512:in `block in invoke_before'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:512:in `each'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:512:in `invoke_before'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:115:in `block in run_callbacks'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/autosave_association.rb:385:in `around_save_collection_association'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:126:in `block in run_callbacks'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:137:in `run_callbacks'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:824:in `_run_save_callbacks'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/callbacks.rb:457:in `create_or_update'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/timestamp.rb:126:in `create_or_update'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/persistence.rb:474:in `save'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/validations.rb:47:in `save'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/transactions.rb:298:in `block in save'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/transactions.rb:354:in `block in with_transaction_returning_status'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/connection_adapters/abstract/database_statements.rb:318:in `transaction'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/transactions.rb:350:in `with_transaction_returning_status'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/transactions.rb:298:in `save'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/suppressor.rb:44:in `save'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/autosave_association.rb:470:in `save_has_one_association'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/autosave_association.rb:213:in `block in add_autosave_association_callbacks'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:427:in `block in make_lambda'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:235:in `block in halting_and_conditional'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:516:in `block in invoke_after'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:516:in `each'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:516:in `invoke_after'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:107:in `run_callbacks'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:824:in `_run_create_callbacks'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/callbacks.rb:461:in `_create_record'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/timestamp.rb:108:in `_create_record'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/persistence.rb:900:in `create_or_update'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/callbacks.rb:457:in `block in create_or_update'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:117:in `block in run_callbacks'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/autosave_association.rb:385:in `around_save_collection_association'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:126:in `block in run_callbacks'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:137:in `run_callbacks'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:824:in `_run_save_callbacks'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/callbacks.rb:457:in `create_or_update'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/timestamp.rb:126:in `create_or_update'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/persistence.rb:474:in `save'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/validations.rb:47:in `save'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/transactions.rb:298:in `block in save'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/transactions.rb:354:in `block in with_transaction_returning_status'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/connection_adapters/abstract/database_statements.rb:320:in `block in transaction'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/connection_adapters/abstract/transaction.rb:310:in `block in within_new_transaction'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/concurrency/load_interlock_aware_monitor.rb:26:in `block (2 levels) in synchronize'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/concurrency/load_interlock_aware_monitor.rb:25:in `handle_interrupt'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/concurrency/load_interlock_aware_monitor.rb:25:in `block in synchronize'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `handle_interrupt'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `synchronize'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/connection_adapters/abstract/transaction.rb:308:in `within_new_transaction'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/connection_adapters/abstract/database_statements.rb:320:in `transaction'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/transactions.rb:350:in `with_transaction_returning_status'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/transactions.rb:298:in `save'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/suppressor.rb:44:in `save'", "/www/Simplero/app/controllers/site/comments_controller.rb:21:in `create'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_controller/metal/basic_implicit_render.rb:6:in `send_action'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/abstract_controller/base.rb:228:in `process_action'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_controller/metal/rendering.rb:30:in `process_action'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/abstract_controller/callbacks.rb:42:in `block in process_action'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:117:in `block in run_callbacks'", "/www/Simplero/app/controllers/application_controller.rb:60:in `catch_unknown_format'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:126:in `block in run_callbacks'", "/www/Simplero/app/controllers/concerns/request_info_concerns.rb:51:in `block in set_request_info'", "/www/Simplero/lib/request_info.rb:73:in `with'", "/www/Simplero/app/controllers/concerns/request_info_concerns.rb:35:in `set_request_info'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:126:in `block in run_callbacks'", "/www/Simplero/app/controllers/concerns/logging_concerns.rb:23:in `set_time_zone'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:126:in `block in run_callbacks'", "/www/Simplero/app/controllers/application_controller.rb:209:in `find_current_account'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:126:in `block in run_callbacks'", "/www/Simplero/app/controllers/concerns/logging_concerns.rb:44:in `postgres_logger'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:126:in `block in run_callbacks'", "/www/Simplero/app/controllers/application_controller.rb:100:in `profile'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:126:in `block in run_callbacks'", "/www/Simplero/app/concerns/local_draft_clearer.rb:15:in `block in clear_local_storage_on_successful_save'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/wisper-1.3.0/lib/wisper/temporary_listeners.rb:15:in `with'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/wisper-1.3.0/lib/wisper/temporary_listeners.rb:6:in `with'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/wisper-1.3.0/lib/wisper.rb:17:in `with_listeners'", "/www/Simplero/app/concerns/local_draft_clearer.rb:14:in `clear_local_storage_on_successful_save'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:126:in `block in run_callbacks'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actiontext-6.1.3.2/lib/action_text/rendering.rb:20:in `with_renderer'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actiontext-6.1.3.2/lib/action_text/engine.rb:55:in `block (4 levels) in <class:Engine>'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:126:in `instance_exec'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:126:in `block in run_callbacks'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:137:in `run_callbacks'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/abstract_controller/callbacks.rb:41:in `process_action'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_controller/metal/rescue.rb:22:in `process_action'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_controller/metal/instrumentation.rb:34:in `block in process_action'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/notifications.rb:203:in `block in instrument'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/notifications/instrumenter.rb:24:in `instrument'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/notifications.rb:203:in `instrument'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_controller/metal/instrumentation.rb:33:in `process_action'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_controller/metal/params_wrapper.rb:249:in `process_action'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/railties/controller_runtime.rb:27:in `process_action'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/abstract_controller/base.rb:165:in `process'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionview-6.1.3.2/lib/action_view/rendering.rb:39:in `process'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_controller/metal.rb:190:in `dispatch'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_controller/metal.rb:254:in `dispatch'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/routing/route_set.rb:50:in `dispatch'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/routing/route_set.rb:33:in `serve'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/routing/mapper.rb:19:in `block in <class:Constraints>'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/routing/mapper.rb:49:in `serve'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/journey/router.rb:50:in `block in serve'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/journey/router.rb:32:in `each'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/journey/router.rb:32:in `serve'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/routing/route_set.rb:842:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/http_accept_language-2.0.0/lib/http_accept_language/middleware.rb:13:in `call'", "/www/Simplero/lib/middleware/development_middleware.rb:15:in `call'", "/www/Simplero/lib/middleware/routes_reloader.rb:14:in `call'", "/www/Simplero/lib/middleware/http_method_not_allowed.rb:9:in `call'", "/www/Simplero/lib/middleware/redirect_to_simplero_middleware.rb:16:in `call'", "/www/Simplero/lib/middleware/no_www_middleware.rb:17:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/rack-2.2.3/lib/rack/tempfile_reaper.rb:15:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/rack-2.2.3/lib/rack/etag.rb:27:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/rack-2.2.3/lib/rack/conditional_get.rb:40:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/rack-2.2.3/lib/rack/head.rb:12:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/http/permissions_policy.rb:22:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/http/content_security_policy.rb:18:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/rack-2.2.3/lib/rack/session/abstract/id.rb:266:in `context'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/rack-2.2.3/lib/rack/session/abstract/id.rb:260:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/middleware/cookies.rb:689:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activerecord-6.1.3.2/lib/active_record/migration.rb:601:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/middleware/callbacks.rb:27:in `block in call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/callbacks.rb:98:in `run_callbacks'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/middleware/callbacks.rb:26:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/middleware/executor.rb:14:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/middleware/actionable_exceptions.rb:18:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/better_errors-2.4.0/lib/better_errors/middleware.rb:84:in `protected_app_call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/better_errors-2.4.0/lib/better_errors/middleware.rb:79:in `better_errors_call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/better_errors-2.4.0/lib/better_errors/middleware.rb:57:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/middleware/debug_exceptions.rb:29:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/railties-6.1.3.2/lib/rails/rack/logger.rb:37:in `call_app'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/railties-6.1.3.2/lib/rails/rack/logger.rb:26:in `block in call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/tagged_logging.rb:99:in `block in tagged'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/tagged_logging.rb:37:in `tagged'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/tagged_logging.rb:99:in `tagged'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/railties-6.1.3.2/lib/rails/rack/logger.rb:26:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/sprockets-rails-3.2.2/lib/sprockets/rails/quiet_assets.rb:13:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/middleware/remote_ip.rb:81:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/request_store-1.5.0/lib/request_store/middleware.rb:19:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/middleware/request_id.rb:26:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/rack-2.2.3/lib/rack/method_override.rb:24:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/rails_same_site_cookie-0.1.8/lib/rails_same_site_cookie/middleware.rb:13:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/rack-2.2.3/lib/rack/runtime.rb:22:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/activesupport-6.1.3.2/lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/middleware/executor.rb:14:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/middleware/static.rb:24:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/rack-2.2.3/lib/rack/sendfile.rb:110:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/actionpack-6.1.3.2/lib/action_dispatch/middleware/host_authorization.rb:92:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/rack-cors-1.1.0/lib/rack/cors.rb:100:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/rack-attack-4.2.0/lib/rack/attack.rb:104:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/rack-utf8_sanitizer-1.3.2/lib/rack/utf8_sanitizer.rb:19:in `call'", "/www/Simplero/lib/middleware/sendgrid_charset_middleware.rb:38:in `call'", "/www/Simplero/lib/middleware/raw_post_middleware.rb:12:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/webpacker-5.2.1/lib/webpacker/dev_server_proxy.rb:25:in `perform_request'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/rack-proxy-0.6.5/lib/rack/proxy.rb:57:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/railties-6.1.3.2/lib/rails/engine.rb:539:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/puma-5.3.2/lib/puma/configuration.rb:249:in `call'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/puma-5.3.2/lib/puma/request.rb:77:in `block in handle_request'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/puma-5.3.2/lib/puma/thread_pool.rb:338:in `with_force_shutdown'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/puma-5.3.2/lib/puma/request.rb:76:in `handle_request'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/puma-5.3.2/lib/puma/server.rb:438:in `process_client'", "/home/jason/.rbenv/versions/2.6.6/lib/ruby/gems/2.6.0/gems/puma-5.3.2/lib/puma/thread_pool.rb:145:in `block in spawn_thread'"]

Additional context

Is there a way to allow these kinds of requests other than coming up with some ignore logic per #24 (comment) ?

Environment

Ruby 2.6.6
Rails 6.1.3.2
aws-sdk-s3 1.83.1
isolator 0.7.0

Isolator raises exceptions when using DatabaseCleaner with multiple DBs

What did you do?

We recently setup DatabaseCleaner to work with multiple DBs and Isolator is raising errors on tests in which we persist models
with after_commit callbacks that enqueue background jobs. I also checked with HTTP requests made in after after_commit callbacks and it also raises exceptions.

Happy to submit a PR for the fix. Thank you!

What did you expect to happen?

It should not raise errors for jobs enqueued or HTTP request performed on after_commit callbacks.

Additional context

To reproduce it:

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'

  gem 'activejob', '~> 7.0.5.1'
  gem 'activerecord', '~> 7.0.5.1'
  gem 'sqlite3'
  gem 'rspec'

  gem 'database_cleaner'
  gem 'isolator', '~> 0.9.0', require: false
end

require 'active_job'
require 'active_record'
require 'rspec/autorun'

require 'uri'
require 'net/http'

PRIMARY_DB   = 'primary.sqlite3'
SECONDARY_DB = 'secondary.sqlite3'

ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: PRIMARY_DB)
ActiveRecord::Schema.define do
  create_table :animals do |t|
    t.string :name, null: false
  end
end

ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: SECONDARY_DB)
ActiveRecord::Schema.define do
  create_table :pets do |t|
    t.string :nickname, null: false
  end
end

class DummyJob < ActiveJob::Base; end

class Animal < ActiveRecord::Base
  self.establish_connection(adapter: 'sqlite3', database: PRIMARY_DB)

  after_commit { DummyJob.perform_later }
end

class Pet < ActiveRecord::Base
  self.establish_connection(adapter: 'sqlite3', database: SECONDARY_DB)

  after_commit { Net::HTTP.get_response(URI('https://www.google.com/')) }
end

RSpec.configure do |config|
  DatabaseCleaner[:active_record, db: Pet].strategy    = :transaction
  DatabaseCleaner[:active_record, db: Animal].strategy = :transaction

  config.before(:each) { DatabaseCleaner.start }

  config.after(:each) { DatabaseCleaner.clean }

  require 'isolator'

  Isolator.configure { |isolator_config| isolator_config.raise_exceptions = true }
end

RSpec.describe 'Errors' do
  it { Pet.create(nickname: 'Spike') }
  it { Animal.create(name: 'Dog') }
end

It seems to always increment the transactions threshold for the same connection. In lib/isolator/database_cleaner_support.rb we could assign a lambda that leverages the connection_class used by DatabaseCleaner before increasing/decreasing the transactions threshold on the start and clean methods:

Isolator.default_connection_id = -> { connection_class.connection.object_id }

Thoughts?

Environment

Ruby Version: 3.2.1

Framework Version (Rails, whatever): ActiveRecord 7.0.5.1 and ActiveJob 7.0.5.1

Isolator Version: 0.9.0

Inline isolator exceptions

Is your feature request related to a problem? Please describe.

When we change code, .isolator_ignore.yml needs to be updated with new line numbers.

There are valid reasons to have ignored things, for example if a failed call to an external service should roll back the transaction

Describe the solution you'd like

It would be great if we could do what rubocop does, e.g .my_exception # rubocop:disable Some/Rule.

I think directly above the line makes the most sense, e.g. # isolator:disable MyException

Describe alternatives you've considered

I looked into whether the current API for the dependency injected ignore class could support this. I thought maybe I could check the frame where the exception happened and check for the comment above the line number of the frame where the exception was raised? I could potentially do this with no code in the gem, but it would be great if this was a first-class feature.

Additional context

n/a

NoMethodError when building an exception for Faraday request

NoMethodError: undefined method` []' for #<Sniffer::DataItem::Request:0x00007fe477c8c040>

/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/isolator-0.6.0/lib/isolator/adapters/http/sniffer.rb:13:in `block in <top (required)>'
/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/isolator-0.6.0/lib/isolator/adapters/base.rb:50:in `build_exception'
/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/isolator-0.6.0/lib/isolator/adapters/base.rb:27:in `notify'
/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/isolator-0.6.0/lib/isolator/adapter_builder.rb:25:in `block (2 levels) in add_patch_method'
/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/sniffer-0.3.2/lib/sniffer/adapters/net_http_adapter.rb:28:in `request_with_sniffer'
/.rbenv/versions/2.5.1/lib/ruby/2.5.0/net/http.rb:1457:in `block in request'
/.rbenv/versions/2.5.1/lib/ruby/2.5.0/net/http.rb:910:in `start'
/.rbenv/versions/2.5.1/lib/ruby/2.5.0/net/http.rb:1455:in `request'
/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/sniffer-0.3.2/lib/sniffer/adapters/net_http_adapter.rb:32:in `block in request_with_sniffer'
/.rbenv/versions/2.5.1/lib/ruby/2.5.0/benchmark.rb:308:in `realtime'
/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/sniffer-0.3.2/lib/sniffer/adapters/net_http_adapter.rb:31:in `request_with_sniffer'
/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/faraday-0.15.2/lib/faraday/adapter/net_http.rb:87:in `perform_request'
/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/faraday-0.15.2/lib/faraday/adapter/net_http.rb:43:in `block in call'
/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/faraday-0.15.2/lib/faraday/adapter/net_http.rb:92:in `with_net_http_connection'
/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/faraday-0.15.2/lib/faraday/adapter/net_http.rb:38:in `call'
/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/faraday-0.15.2/lib/faraday/rack_builder.rb:143:in `build_response'
/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/faraday-0.15.2/lib/faraday/connection.rb:387:in `run_request'
/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/faraday-0.15.2/lib/faraday/connection.rb:175:in `put'
/app/services/fake_ssn_uploader_service.rb:15:in `call'```

Failing code:

class FakeSsnUploaderService
  attr_reader :url, :value

  def initialize(url, value)
    @url = url
    @value = value
  end

  def call
    connection.put(url, value) do |f|
      f.headers["Content-Type"] = "text/plain"
    end
  end

  private

  def connection
    Faraday.new do |faraday|
      # faraday.use Faraday::Response::RaiseError
      faraday.adapter Faraday.default_adapter
    end
  end
end

ActiveRecord::Base.transaction do
  FakeSsnUploaderService.new(url, data).call
end

Errors when combined with `ActiveStorage::FixtureSet.blob`

What did you do?

We have a Rails 7 app which makes use of fixtures. Specifically, we're using FixtureSet to saturate ActiveStorage files.

When I add isolator to the app, my fixtures start throwing errors.

What did you expect to happen?

Fixtures to work as normally.

What actually happened?

Fixtures fail with the following backtrace:

 /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activesupport-7.0.7.2/lib/active_support/testing/file_fixtures.rb:27:in `join'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activesupport-7.0.7.2/lib/active_support/testing/file_fixtures.rb:27:in `file_fixture'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activestorage-7.0.7.2/lib/active_storage/fixture_set.rb:65:in `prepare'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activestorage-7.0.7.2/lib/active_storage/fixture_set.rb:61:in `blob'
    /Users/evan/Projects/get-weatherized/quote-app/test/fixtures/active_storage/blobs.yml:2:in `get_binding'
    /Users/evan/.rvm/rubies/ruby-3.2.2/lib/ruby/3.2.0/erb.rb:429:in `eval'
    /Users/evan/.rvm/rubies/ruby-3.2.2/lib/ruby/3.2.0/erb.rb:429:in `result'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activesupport-7.0.7.2/lib/active_support/configuration_file.rb:48:in `render'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activesupport-7.0.7.2/lib/active_support/configuration_file.rb:22:in `parse'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activesupport-7.0.7.2/lib/active_support/configuration_file.rb:18:in `parse'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activerecord-7.0.7.2/lib/active_record/fixture_set/file.rb:53:in `raw_rows'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activerecord-7.0.7.2/lib/active_record/fixture_set/file.rb:42:in `config_row'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activerecord-7.0.7.2/lib/active_record/fixture_set/file.rb:28:in `model_class'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activerecord-7.0.7.2/lib/active_record/fixtures.rb:725:in `block (2 levels) in read_fixture_files'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activerecord-7.0.7.2/lib/active_record/fixture_set/file.rb:16:in `open'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activerecord-7.0.7.2/lib/active_record/fixtures.rb:724:in `block in read_fixture_files'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activerecord-7.0.7.2/lib/active_record/fixtures.rb:723:in `each'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activerecord-7.0.7.2/lib/active_record/fixtures.rb:723:in `each_with_object'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activerecord-7.0.7.2/lib/active_record/fixtures.rb:723:in `read_fixture_files'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activerecord-7.0.7.2/lib/active_record/fixtures.rb:657:in `initialize'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activerecord-7.0.7.2/lib/active_record/fixtures.rb:598:in `new'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activerecord-7.0.7.2/lib/active_record/fixtures.rb:598:in `block in read_and_insert'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activerecord-7.0.7.2/lib/active_record/fixtures.rb:596:in `map'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activerecord-7.0.7.2/lib/active_record/fixtures.rb:596:in `read_and_insert'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activerecord-7.0.7.2/lib/active_record/fixtures.rb:567:in `create_fixtures'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activerecord-7.0.7.2/lib/active_record/test_fixtures.rb:275:in `load_fixtures'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activerecord-7.0.7.2/lib/active_record/test_fixtures.rb:125:in `setup_fixtures'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/isolator-0.10.0/lib/isolator/railtie.rb:38:in `setup_fixtures'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activerecord-7.0.7.2/lib/active_record/test_fixtures.rb:10:in `before_setup'
    /Users/evan/.rvm/gems/ruby-3.2.2@quote_app_dev/gems/activesupport-7.0.7.2/lib/active_support/testing/setup_and_teardown.rb:40:in `before_setup'

Additional context

Environment

Ruby Version:
3.2.2

Framework Version (Rails, whatever):
Rails 7.0.7

Isolator Version:
0.10.0

Using isolator with multiple database connections

In our app, we're using multiple db connections, and some of them establish connection lazily. In one instance, we notice that simply connecting to a new db increments Thread.current[:isolator_transactions]. I'm guessing a new connection matches the START_PATTERN

START_PATTERN = %r{(\ABEGIN|\ASAVEPOINT)}xi
. Do you have a recommended way for dealing with multiple connections?

Ignore http requests in models that save files to AWS using carrier_waive

In project we have models with such lines:

mount_uploader :file, DocUploader
mount_base64_uploader :signature, SignatureUploader

Isolator raises exception on all such models when find Model.create(file: file) or Model.update(file: file)

I tried to add these models and uploaders to .isolator_todo.yml, but still getting exceptions.

May be I can ignore such situation using Isolator.adapters.http.ignore_if, but I don't understand the trigger on which this condition may rely.

Can anyone help with this question?

KeyError: key not found: isolator_threshold

Hello :)

We've added Isolator to our app and are getting the following error on a regular basis:

KeyError: key not found: isolator_threshold
  from isolator/ext/thread_fetch.rb:7:in `raise'
  from isolator/ext/thread_fetch.rb:7:in `fetch'
  from isolator.rb:67:in `transactions_threshold'
  from isolator.rb:91:in `within_transaction?'
  from isolator/adapters/base.rb:31:in `notify?'
  from isolator/adapters/base.rb:26:in `notify'
  from isolator/adapter_builder.rb:24:in `block (2 levels) in add_patch_method'
  from sidekiq/client.rb:74:in `push'
  from sidekiq/client.rb:131:in `push'
  from sidekiq/scheduled.rb:26:in `block (2 levels) in enqueue_jobs'
  from sidekiq/scheduled.rb:15:in `each'
  from sidekiq/scheduled.rb:15:in `block in enqueue_jobs'
  from sidekiq.rb:96:in `block in redis'
  from connection_pool.rb:64:in `block (2 levels) in with'
  from connection_pool.rb:63:in `handle_interrupt'
  from connection_pool.rb:63:in `block in with'
  from connection_pool.rb:60:in `handle_interrupt'
  from connection_pool.rb:60:in `with'
  from sidekiq.rb:93:in `redis'
  from sidekiq/scheduled.rb:14:in `enqueue_jobs'
  from sidekiq/scheduled.rb:77:in `enqueue'
  from sidekiq/scheduled.rb:68:in `block in start'
  from sidekiq/util.rb:16:in `watchdog'
  from sidekiq/util.rb:25:in `block in safe_thread'

We use:

  • Ruby 2.5.0
  • Sidekiq 5.1.1
  • Isolator 0.2.1

Please advise.

Add .isolator_ignore support

The .isolator_todo.yml configuration file which is used to temporary ignore some parts of source code is a great feature, but its name might be misleading in case of code which intentionally contains some IO calls inside a transaction. IMO it'd be great if there was support for .isolator_ignore.yml or .isolatorignore.yml file with the same syntax as .isolator_todo.yml but different purpose. Current approach works perfectly with small development teams but if there are a lot of people working on making codebase sync with Isolator it might be a problem.

That's currently possible to implement a custom Ignorer class which respects both todo and ignore files but I think it'd be good to make it a built-in feature.

So, I'd like to suggest to add the configuration file which points to code that will not be fixed any time soon and keep .isolator_todo.yml as a TODO-list. Thanks for a great gem BTW.

Should we ignore by default Turbo::Streams::ActionBroadcastJob?

What did you do?

I am seeing error linked to turbo-rails

    Isolator::BackgroundJobError:
       You are trying to enqueue background job inside db transaction. In case of transaction failure, this may lead to data inconsistency and unexpected bugs
       Details: Turbo::Streams::ActionBroadcastJob (Z2lkOi8vZHJhZGlzL0F3eEpvYi8yNjc, {:action=>:replace, :target=>"awx_job_awx_job_267"

What did you expect to happen?

I am wondering if we should mark them as safe. Like in the README.

Isolator does not distinguish framework-level adapters. For example, :active_job spy doesn't take into account which AJ adapter you use; if you are using a safe one (e.g. Que) just disable the :active_job adapter to avoid false negatives (i.e. Isolator.adapters.active_job.disable!).

tag v0.10.0

Releasing new version without proper git tag is bad habit.

Rails 7.1 compatibility

What did you do?

Tried to upgrade an app from Rails 7.0.8 to Rails 7.1 with the following initializer:

# frozen_string_literal: true

if defined?(Isolator)
  # Check can be disabled since we have transactional_push enabled in Sidekiq.
  Isolator.adapters.active_job.disable!
end

What actually happened?

The following exception is raised on boot:

/Users/sunny/.rbenv/versions/3.1.3/lib/ruby/gems/3.1.0/gems/isolator-1.0.1/lib/isolator/simple_hashie.rb:12:in `block in method_missing': undefined method `active_job' for {"http"=>#<Module:0x000000010cdeb1d0>, "sidekiq"=>#<Module:0x000000010cde1270>, "mailer"=>#<Module:0x000000010cdd9c28>}:Isolator::SimpleHashie (NoMethodError)
	from /Users/sunny/.rbenv/versions/3.1.3/lib/ruby/gems/3.1.0/gems/isolator-1.0.1/lib/isolator/simple_hashie.rb:12:in `fetch'
	from /Users/sunny/.rbenv/versions/3.1.3/lib/ruby/gems/3.1.0/gems/isolator-1.0.1/lib/isolator/simple_hashie.rb:12:in `method_missing'
	from /Users/sunny/code/cults/config/initializers/isolator.rb:5:in `<main>'

Additional content

Commenting out the disabling boots the app with no issue.

Environment

Ruby Version: 3.1.3

Framework Version: Rails 7.1.2 (previously 7.0.8)

Isolator Version: 1.0.1

Crashes when used with `disable_ddl_transaction!` & `commit_db_transaction`

What did you do?

# Gemfile

group :development, :test do
  gem "isolator"
end
# db/migrate/20210824201715_test_migration.rb

class TestMigration < ActiveRecord::Migration[7.0]
  disable_ddl_transaction!

  def change
    commit_db_transaction
  end
end

When you run rake db:migrate, it crashes:

StandardError: An error has occurred, all later migrations canceled:

undefined method `[]' for nil:NilClass
/gems/isolator-0.7.0/lib/isolator.rb:138:in `decr_transactions!'
/gems/isolator-0.7.0/lib/isolator/orm_adapters/active_support_subscriber.rb:14:in `block in subscribe!'
/activesupport/lib/active_support/notifications/fanout.rb:202:in `finish'
/activesupport/lib/active_support/notifications/fanout.rb:66:in `block in finish'
/activesupport/lib/active_support/notifications/fanout.rb:66:in `each'
/activesupport/lib/active_support/notifications/fanout.rb:66:in `finish'
/activesupport/lib/active_support/notifications/instrumenter.rb:73:in `finish_with_state'
/activesupport/lib/active_support/notifications/instrumenter.rb:50:in `instrument'
/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb:715:in `log'
/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb:43:in `execute'
/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb:117:in `commit_db_transaction'
/activerecord/lib/active_record/migration.rb:931:in `block in method_missing'
/activerecord/lib/active_record/migration.rb:899:in `block in say_with_time'
/activerecord/lib/active_record/migration.rb:899:in `say_with_time'
/activerecord/lib/active_record/migration.rb:920:in `method_missing'
/db/migrate/20210824201715_test_migration.rb:5:in `change'

What did you expect to happen?

The migration to complete safely.

What actually happened?

It crashed.

I tried running ISOLATOR_DEBUG=true rake db:migrate but didn't see any additional logs.

Environment

Ruby Version: ruby 2.7.3p183 (2021-04-05 revision 6847ee089d) [x86_64-darwin19]

Framework Version (Rails, whatever): Rails 7.0.0.alpha (but have also seen this on 6.1)

Isolator Version: isolator (0.7.0)

Error raised when query has invalid encoding

We recently tried to add isolator to our app and it made some tests.

We have tests that confirm that sending invalid UTF-8 chars works (they are sanitized), but now that query is triggering a crash in isolator code.

This is the app code that triggers the failure:

User.find_or_initialize_by(email: "Invalid \xAD char")

We get the error ArgumentError: invalid byte sequence in UTF-8, this is the stack trace after that call is made:

"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/isolator-0.8.0/lib/isolator/orm_adapters/active_support_subscriber.rb:13:in `match?'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/isolator-0.8.0/lib/isolator/orm_adapters/active_support_subscriber.rb:13:in `block in subscribe!'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activesupport-6.0.4.4/lib/active_support/notifications/fanout.rb:189:in `finish'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activesupport-6.0.4.4/lib/active_support/notifications/fanout.rb:62:in `block in finish'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activesupport-6.0.4.4/lib/active_support/notifications/fanout.rb:62:in `each'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activesupport-6.0.4.4/lib/active_support/notifications/fanout.rb:62:in `finish'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activesupport-6.0.4.4/lib/active_support/notifications/instrumenter.rb:45:in `finish_with_state'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activesupport-6.0.4.4/lib/active_support/notifications/instrumenter.rb:30:in `instrument'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activerecord-6.0.4.4/lib/active_record/connection_adapters/abstract_adapter.rb:718:in `log'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activerecord-6.0.4.4/lib/active_record/connection_adapters/abstract_mysql_adapter.rb:199:in `execute'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activerecord-6.0.4.4/lib/active_record/connection_adapters/mysql/database_statements.rb:41:in `execute'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/marginalia-1.11.1/lib/marginalia.rb:71:in `execute_with_marginalia'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activerecord-6.0.4.4/lib/active_record/connection_adapters/abstract_mysql_adapter.rb:210:in `execute_and_free'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activerecord-6.0.4.4/lib/active_record/connection_adapters/mysql/database_statements.rb:46:in `exec_query'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activerecord-6.0.4.4/lib/active_record/connection_adapters/abstract/database_statements.rb:489:in `select'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activerecord-6.0.4.4/lib/active_record/connection_adapters/abstract/database_statements.rb:70:in `select_all'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activerecord-6.0.4.4/lib/active_record/connection_adapters/abstract/query_cache.rb:107:in `select_all'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activerecord-6.0.4.4/lib/active_record/connection_adapters/mysql/database_statements.rb:12:in `select_all'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activerecord-6.0.4.4/lib/active_record/querying.rb:46:in `find_by_sql'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activerecord-6.0.4.4/lib/active_record/relation.rb:824:in `block in exec_queries'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activerecord-6.0.4.4/lib/active_record/relation.rb:842:in `skip_query_cache_if_necessary'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activerecord-6.0.4.4/lib/active_record/relation.rb:811:in `exec_queries'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activerecord-6.0.4.4/lib/active_record/relation.rb:626:in `load'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activerecord-6.0.4.4/lib/active_record/relation.rb:250:in `records'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activerecord-6.0.4.4/lib/active_record/relation/finder_methods.rb:499:in `find_take'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activerecord-6.0.4.4/lib/active_record/relation/finder_methods.rb:98:in `take'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activerecord-6.0.4.4/lib/active_record/relation/finder_methods.rb:81:in `find_by'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activerecord-6.0.4.4/lib/active_record/relation.rb:227:in `find_or_initialize_by'",
"/dev/ruby/2.7.4/lib/ruby/gems/2.7.0/gems/activerecord-6.0.4.4/lib/active_record/querying.rb:21:in `find_or_initialize_by'"

The error comes from calling match? with a value that might not be in the right encoding.

module Isolator
  # ActiveSupport notifications listener
  # Used for ActiveRecord and ROM::SQL (when instrumentation is available)
  module ActiveSupportSubscriber
    START_PATTERN = %r{(\ABEGIN|\ASAVEPOINT)}xi
    FINISH_PATTERN = %r{(\ACOMMIT|\AROLLBACK|\ARELEASE|\AEND TRANSACTION)}xi

    def self.subscribe!(event)
      ::ActiveSupport::Notifications.subscribe(event) do |_name, _start, _finish, _id, query|
        connection_id = query[:connection_id] || query[:connection]&.object_id || 0
        Isolator.incr_transactions!(connection_id) if START_PATTERN.match?(query[:sql])
        Isolator.decr_transactions!(connection_id) if FINISH_PATTERN.match?(query[:sql])
      end
    end
  end
end

As ActiveRecord doesn't raise an error, I would expect Isolator to also handle strings that have the wrong encoding (maybe force UTF-8), what do you think?

I'm available to submit a PR for the fix. Thank you for the nice gem 💚

Active Storage - Analyze Job - Always fails because of after_create_commit callback

When used together with Rails 6 and ActiveStorage, this fails always, as ActiveStorage automatically always tries to schedule the AnalyzeJob after the record is created:

Isolator::BackgroundJobError:
        You are trying to enqueue background job inside db transaction. In case of transaction failure, this may lead to data inconsistency and unexpected bugs
        Details: ActiveStorage::AnalyzeJob (#<ActiveStorage::Blob:0x00005588a27565e0>)
      # /builds/smartq/qflow/vendor/bundle/ruby/2.6.0/gems/activejob-6.0.0/lib/active_job/enqueuing.rb:22:in `perform_later'
      # /builds/smartq/qflow/vendor/bundle/ruby/2.6.0/gems/activestorage-6.0.0/app/models/active_storage/blob/analyzable.rb:37:in `analyze_later'
      # /builds/smartq/qflow/vendor/bundle/ruby/2.6.0/gems/activestorage-6.0.0/app/models/active_storage/attachment.rb:37:in `analyze_blob_later'
      # /builds/smartq/qflow/vendor/bundle/ruby/2.6.0/gems/activesupport-6.0.0/lib/active_support/callbacks.rb:429:in `block in make_lambda'
      # /builds/smartq/qflow/vendor/bundle/ruby/2.6.0/gems/activesupport-6.0.0/lib/active_support/callbacks.rb:264:in `block in conditional'

Which is called from Blob: after_create_commit :analyze_blob_later, :identify_blob

Sidekiq isolator doesn't work if "sidekiq/testing" is required only AFTER "isolator"

What did you do?

Added isolator and required it at the end of my initializers

What did you expect to happen?

It would catch sidekiq calls duringt transactions in my existing specs

What actually happened?

Very few were caught (some were - perhaps to do with test ordering...)

My rspec support files were requiring "sidekiq/testing" after rails had been initialized.

When I required sidekiq/testing at the beginning of my initializer it then worked.

# config/initializers/z_isolator.rb
require "sidekiq/testing" if Rails.env.test?

unless Rails.env.production? # so we get it in staging too
  require "isolator"
  Isolator.configure do |config|
    config.send_notifications = true
  end
end

Additional context

Not 100% sure what you should do about this - could just put this in the README. It's a bit ugly to have a if Rails.env.testing? in an initializer but not the end of the world.

There may be additional context to how I'm using sidekiq testing mode (switching between fake and inline in different specs for example) but I think it's mainly when sidekiq/testing is required (and thus when it does a prepend on Sidekiq::Client)

Environment

Ruby Version:
2.7.6
Framework Version (Rails, whatever):
rails 6.0.2
Isolator Version:
0.8.0

Ruby 2.7 deprecation warning

If you run this test using ruby 2.7.7, it emits warning Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call. This was introduced in version 0.9.0 (presumably #51). The crash is in the call to enqueue here; that method is redefined here.

# frozen_string_literal: true

module CaptureRuby27DeprecationWarnings
  RUBY_2_7_DEPRECATIONS = [
    "Using the last argument as keyword parameters is deprecated",
    "Passing the keyword argument as the last hash parameter is deprecated",
    "Splitting the last argument into positional and keyword parameters is deprecated"
  ].freeze

  def self.extended(_module)
    Warning[:deprecated] = true
  end

  def warn(message)
    if ruby_2_7_deprecation_warning?(message)
      raise(SyntaxError, message)
    else
      super
    end
  end

  private

  def ruby_2_7_deprecation_warning?(message)
    RUBY_2_7_DEPRECATIONS.any? { |warning| message.include?(warning) }
  end
end

Warning.extend(CaptureRuby27DeprecationWarnings)


require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  # Activate the gem you are reporting the issue against.
  gem "activejob", "~> 7.0.0"
  gem "activesupport", "~> 7.0.0"

  # gem "isolator", "0.11.0", require: false # fail
  gem "isolator", "0.9.0", require: false # fail
  # gem "isolator", "0.8.0", require: false # pass
end

require "minitest/autorun"
require "active_job"
require "active_support"
require "isolator"

class BuggyJob < ActiveJob::Base
  def perform(message)
    puts message
  end
end

class BuggyJobTest < ActiveJob::TestCase
  def test_stuff
    assert_equal "2.7.7", RUBY_VERSION

    BuggyJob.set(priority: 1).perform_later("hi")
  end
end

Incompatible character encodings when logging jobs with non-utf8 arguments

What did you do?

Tried running isolator against the GitHub test suite and saw a number of incompatible character encodings: ASCII-8BIT and UTF-8 errors.

Here's a test to reproduce the problem:

  context "when logging non-utf8 details" do
    specify do
      logger = instance_double("Logger")
      Isolator.configure do |config|
        config.logger = logger
      end

      job_args = ["abc123", "\xff".dup.force_encoding(Encoding::ASCII_8BIT)]
      details = "MyJob (#{job_args.join(", ")})"
      exception = Isolator::BackgroundJobError.new(details: details)

      described_class.new(exception).call
    end
  end

Because of the ASCII_8BIT job argument, the whole exception message ends up as ASCII_8BIT. We're able to combine that just fine with other ascii-only strings, but we're not able to combine that with the UTF_8 used to print the backtrace.

What did you expect to happen?

I'm not certain. Encodings can be difficult...

A fallback to leave out the if there's any sort of encoding problem, perhaps? Or replace it with - or something else ascii that's likely to be more broadly compatible?

Environment

Ruby Version:

3.2

Framework Version (Rails, whatever):

Rails main (7.1)

Isolator Version:

0.10.0

Idea: Add an RSpec matcher

In our tests, we're actively mocking calls to our objects that wrap some external APIs instead of mocking raw http requests (e.g., with webmock). Because of this, isolator can't detect transaction violations out of the box in our app because we don't make any HTTP calls in our specs.

When we noticed that we thought “wouldn't it be awesome to have some method to restrict calling some service objects when we're in a transaction?”

So, @nepalez suggested following pseudocode:

allow(ExternalApi::GetUser).to receive(:call).outside_of_transaction.and_return({ "Name" => "Vasya" })

I started to investigate possibility to change rspec-mocks behavior and following proof-of-concept for existing matcher customization was born:

# spec/support/matchers/outside_of_transaction.rb

module RSpec
  module Mocks
    module Matchers
      class Receive
        def outside_of_transaction
          self
        end
      end

      module NotInTransaction
        def matches?(subject, *args, &block)
          if Isolator.within_transaction?
            # We doesn't raise Isolator::UnsafeOperationError because it may be
            # rescued somewhere in subject, while MockExpectationError may not.
            @error_generator.send(:__raise, <<~MSG.squish)
              #{@error_generator.intro} received #{subject.inspect}
              while in database transaction, but it is unsafe to call it so.
            MSG
          end

          super
        end
      end
    end

    class MessageExpectation
      def outside_of_transaction
        raise_already_invoked_error_if_necessary(__method__)
        extend Matchers::NotInTransaction
        self
      end
    end
  end
end

See rspec/rspec-mocks#1230 on how it is possible to change existing matcher's behavior in rspec-mocks (unfortunately I couldn't find any docs for this).

What do you think? Is it correct approach? Or maybe you have some better ideas?

Mailer adapter does not behave as expected

There seem to be two problems, but I'm unsure if they're related. Please let me know if you'd like to split the problems into two separate issues. Here's the setup:

class User
  after_create :send_first_email
  after_commit :send_second_email, on: :create

  private

  def send_first_email
    UserMailer.delay.first_email(id)
  end

  def send_second_email
    UserMailer.second_email(id).deliver_later
  end
end

When I run a simple create spec, I get Isolator::BackgroundJobError but it's because of send_second_email.

  1. I believe the first problem is the mailer adapter doesn't recognize UserMailer.delay.first_email(id) as a job enqueue. This is confirmed when I changed it to UserMailer.first_email(id).deliver_later and the error is correctly thrown at send_first_email

  2. I'm not sure if I understand the second problem, my expectation is error shouldn't be thrown because send_second_email it's done after the transaction in after_commit. Is this correct?

I'm using ruby 2.6.5 and rails 5.1.7

Sidekiq isolator doesn't work if "sidekiq/testing" is required only AFTER "isolator"

Is your feature request related to a problem? Please describe.

In a project I'm migrating from DelayedJob under ActiveRecord to raw Sidekiq (ie not through ActiveJob). I've discovered that I've missed a few areas where I forgot to consider done after_commit.

I installed this gem hoping it would pick up on them, It took me a while to realize that although it has a Sidekiq module, it only supports the calls for ActiveJob api (deliver_later).

It also (questionably!) seems to support a call that is I think DelayedJob specific (e.g. xxx.delay https://github.com/palkan/isolator/blob/master/spec/isolator/adapters/background_jobs/sidekiq_spec.rb#L24)

Describe the solution you'd like

I would like the gem to raise an error with SidekiqJobClass.perform_async when in a transaction

Additional context

While doing this it might be good to modernize terminology in the rspec test - Worker -> Job (sidekiq/sidekiq#4971)

Untracked transaction for multiple databases in System Tests

Our environment is as such:

  • Rails 7.1
  • RSpec
  • Puma
  • Multiple databases

We're noticing Trying to finalize an untracked transaction warnings sometimes in our system specs and were able to figure out the problem. Let's say you have 2 databases (A and B). When you start a system spec it could start with only 1 connection to A. Puma then spins up in a separate thread and starts serving requests. Keep in mind that Rails system tests makes it so that the connection pool is shared between the test suite and the Puma server. In serving the request, a connection to database B is made. Since ThreadStateProxy is thread-aware, it stores a state per thread. The connection increment is happening in the Puma thread. When the system test finishes and transactions are rolled back, the transaction in database B counter goes from 0 to -1 because according to the test suite thread, the transaction/connection was never made.

I'm still thinking about how to solve this, but I wanted to file this issue in the meantime.

[Help needed] `after_create_commit` raises `Isolator::BackgroundJobError`?

I've been trying to test isolator on my application, however, I keep receiving

     Isolator::BackgroundJobError:
       You are trying to enqueue background job inside db transaction. In case of transaction failure, this may lead to data inconsistency and unexpected bugs
       Details: MyWorker ([387])

This is raised from my model (ApplicationRecord) in a line like

after_commit :call_my_worker, on: :create

I'm opening this issue just to ask if there isn't any basic configuration I'm missing and should be aware of.

Isolator behaves differently between rails 6.1.3.2 and 6.1.4

We recently upgraded from rails 6.1.3.2 to 6.1.4 and found that when there's a ActiveRecord::Deadlocked exception, isolator does not clean up the stale connection properly. This may be due to rails changing how it handles deadlock. It seems rails 6.1.4 disconnects the db connection and establishes a new one.

In rails 6.1.3.2: when ActiveRecord::Deadlocked is raised, ActiveRecord::Base.connected? does not change and the same connection id is seen in Isolator.state[:transactions].

In rails 6.1.4 when ActiveRecord::Deadlocked is raised, ActiveRecord::Base.connected? changes from true to false. Inspecting Isolator.state[:transactions] subsequently will show both the dead and new connection ids. If ActiveRecord::Deadlocked is raised within a transaction, then the dead connection's transactions count remains higher and will fail within_transaction? check.

Do you think isolator should listen to disconnect event and remove the dead connection from Isolator.state[:transactions]?

Mailer is not present in Isolator.adapters after initialize

What did you do?

Installed gem
add .isolator_ignore.yml where add
mailer: - app/concepts/api/v1/lib/operation/bookings/create_transaction.rb

What did you expect to happen?

my tests are passed

What actually happened?

got
Isolator::MailerError ... ...

Additional context

Failures:

1) Api::V1::Workspaces::Bookings::Operation::Update Success updates booking
   Failure/Error: notify(*args, **kwargs, sync: true)
   
   Isolator::MailerError:
     You are trying to send email inside db transaction.
     Details: From: ["[email protected]"]
     To: ["[email protected]"]
     Subject: New Payment from Rosalee Feest is due Feb 22, 2023

Environment

Ruby Version: 3.1.2

Framework Version (Rails, whatever): rails 7.0.4

Isolator Version: 0.8.0

Subtransactions tracking/preventions

Is your feature request related to a problem? Please describe.

https://about.gitlab.com/blog/2021/09/29/why-we-spent-the-last-month-eliminating-postgresql-subtransactions/

Describe the solution you'd like

It would be great if Isolator could track subtransactions and report them.
The feature MUST be optional (for now):

Isolator.configure do |config|
  # Must be set to a non-zero value to be activated (so, default is 0)
  config.max_substransactions_depth = 32
end

Additional context

Gitlab implementation: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67918/diffs

Rails discussion: rails/rails#44518

Being able to allow/ignore inside a specific transaction

Sometimes you actually do want to allow actions to happen inside a transaction, for example a plain lock.
The failure case could be that you rather repeat the action than perhaps missing to run the action, so you wrap it inside a lock.

reminder.with_lock do
  Something.deliver_reminder(reminder)
  reminder.update!(sent: true)
end

This will cause a problem with Isolator, with no apparent way to protect from it.

It would be great with any one of these:

reminder.with_lock do
  Isolator.allow do
    Something.deliver_reminder(reminder)
  end
  reminder.update!(sent: true)
end

or if that's not feasible:

Isolator.adapter.something.ignore_if { |args, object, method|
  object.instance_of?(Something) && method == :deliver_reminder
}
# …
reminder.with_lock do
  Something.instance.deliver_reminder(reminder)
  reminder.update!(sent: true)
end

I've read the source code trying to find a good way to do this. I could try to hook into the current "TODO" system, but that is not optimal because:

  1. It implies that it's a problem that should be solved some day ("TODO")
  2. It's based on filenames and/or line numbers, and not context.

The current ignore_if only passes along the arguments, which means it's not possible to see which ActiveJob is being looked at. (SomeJob.perform_later would have args == [])

Writing my own Ignorer class seems excessive, and it lacks examples on how to do it. I find the interface I'm supposed to implement to be a bit hard to see.

I could call disable! and enable! on the adapter, but that is not thread-safe.

My proposed solutions have one glaring problem, which is: What if you only load Isolator in development/test? Then Isolator.allow will be undefined.
I would solve this by actually wrapping it in my own helpers, so that would not be a problem for me, but it would have to be documented.

private

def allow_inside_transaction
  if defined?(Isolator)
    Isolator.allow { yield }
  else
    yield
  end
end

Stack level too deep

The HTTP adapter requires sniffer which patches net-http with alias_method. This causes problems when other gems (rack-mini-profiler, APMs like datadog) use prepend to patch the same methods, leading to stack level too deep.

One workaround would be if isolator did not require sniffer unless the http isolator was activated.

Any reason to not run in production?

Isolator is supposed to be used in tests and on staging.

Clearly I don't want to raise exceptions in production, but if I used it in production, configured to send to my error reporting platform, would there be any issues?

  • Does isolator change behaviour somehow?
  • Or affect performance?
  • Or is there a risk that my error reporting itself will get flagged by isolator, leading to infinite recursion?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.