Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Read-Only Errors for Cache Deletion in MultiDB Setup with database_selector #238

Open
Dandush03 opened this issue Nov 17, 2024 · 3 comments

Comments

@Dandush03
Copy link

Description

When using Rails' MultiDB configuration with database_selector, attempts to delete entries from the SolidCache::Entry model may result in a read-only error. This issue arises because the database_selector configuration causes certain operations to route to the replica database, which is read-only.

The Error

The following error occurs when the application tries to execute a DELETE query in a replica marked as read-only by rails middleware

ActionView::Template::Error (Write query attempted while in readonly mode: DELETE FROM "solid_cache_entries" WHERE "solid_cache_entries"."key_hash" = 8850981528477303422)

This error disrupts operations that rely on cache clearing, such as session management or dynamic cache updates.

Reproduction Steps

  1. Add the following configuration to your Rails app:
Rails.application.configure do
  config.active_record.database_selector = { delay: 2.seconds }
  config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
  config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
end
  1. Configure a primary and replica database in database.yml.

  2. Attempt to delete a cache entry using SolidCache::Entry in a context where the replica database connection is active.
    Observe the error described above.

Hint: I suggest using devise user_sign_in? method in an index action since it would make it easier to reproduce

Expected Behavior

Cache-related models like SolidCache::Entry should always use the writable primary database for operations that require modifications, regardless of the database_selector configuration.

My current solution:

# config/initializer/override_read_only_for_cache.rb
module OverrideReadOnlyForCache
  def readonly?
    false
  end
end

Rails.application.config.after_initialize do
  SolidCache::Entry.prepend(OverrideReadOnlyForCache)
end

This ensures that the model is always writable, bypassing the default read-only behavior in a MultiDB setup.

@ausangshukla
Copy link

Same MultiDB setup. This fix is not working for me, I get the error

ActiveRecord::ReadOnlyError - Write query attempted while in readonly mode: INSERT INTO solid_cache_entries
solid_cache (1.0.6)

Note there is no error with solid_cache (1.0.2), however the cache is not being populated, ie
mysql> select * from solid_cache_entries;
Empty set (0.00 sec)

Please can you help

@Dandush03
Copy link
Author

Dandush03 commented Dec 15, 2024

Sorry for the delay, this should do the trick

module OverrideReadOnlyForCache
  # Instance method overrides
  def save(...)
    run_in_writing_role { super(...) }
  end

  def save!(...)
    run_in_writing_role { super(...) }
  end

  def create(...)
    run_in_writing_role { super(...) }
  end

  def create!(...)
    run_in_writing_role { super(...) }
  end

  def destroy(...)
    run_in_writing_role { super(...) }
  end

  def destroy!(...)
    run_in_writing_role { super(...) }
  end

  def destroy_all(...)
    run_in_writing_role { super(...) }
  end

  def delete(...)
    run_in_writing_role { super(...) }
  end

  def delete_all(...)
    run_in_writing_role { super(...) }
  end

  def update(...)
    run_in_writing_role { super(...) }
  end

  def update!(...)
    run_in_writing_role { super(...) }
  end

  def update_all(...)
    run_in_writing_role { super(...) }
  end

  def upsert_all(...)
    run_in_writing_role { super(...) }
  end

  def update_attribute(...)
    run_in_writing_role { super(...) }
  end

  def update_columns(...)
    run_in_writing_role { super(...) }
  end

  def touch(...)
    run_in_writing_role { super(...) }
  end

  def increment(...)
    run_in_writing_role { super(...) }
  end

  def increment!(...)
    run_in_writing_role { super(...) }
  end

  def decrement(...)
    run_in_writing_role { super(...) }
  end

  def decrement!(...)
    run_in_writing_role { super(...) }
  end

  def update_counters(...)
    run_in_writing_role { super(...) }
  end

  def self.prepended(base)
    base.singleton_class.prepend(ClassMethods)
  end

  module ClassMethods
    def save(...)
      run_in_writing_role { super(...) }
    end

    def save!(...)
      run_in_writing_role { super(...) }
    end

    def create(...)
      run_in_writing_role { super(...) }
    end

    def create!(...)
      run_in_writing_role { super(...) }
    end

    def destroy(...)
      run_in_writing_role { super(...) }
    end

    def destroy!(...)
      run_in_writing_role { super(...) }
    end

    def destroy_all(...)
      run_in_writing_role { super(...) }
    end

    def delete(...)
      run_in_writing_role { super(...) }
    end

    def delete_all(...)
      run_in_writing_role { super(...) }
    end

    def update(...)
      run_in_writing_role { super(...) }
    end

    def update!(...)
      run_in_writing_role { super(...) }
    end

    def update_all(...)
      run_in_writing_role { super(...) }
    end

    def upsert_all(...)
      run_in_writing_role { super(...) }
    end

    def update_attribute(...)
      run_in_writing_role { super(...) }
    end

    def update_columns(...)
      run_in_writing_role { super(...) }
    end

    def touch(...)
      run_in_writing_role { super(...) }
    end

    def increment(...)
      run_in_writing_role { super(...) }
    end

    def increment!(...)
      run_in_writing_role { super(...) }
    end

    def decrement(...)
      run_in_writing_role { super(...) }
    end

    def decrement!(...)
      run_in_writing_role { super(...) }
    end

    def update_counters(...)
      run_in_writing_role { super(...) }
    end

    private

    def run_in_writing_role(&)
      ActiveRecord::Base.connected_to(role: :writing, &)
    end
  end

  private

  def run_in_writing_role(&)
    ActiveRecord::Base.connected_to(role: :writing, &)
  end
end

Rails.application.config.to_prepare do
  SolidCache::Entry.prepend(OverrideReadOnlyForCache) unless Rails.env.test?
end

@djmb
Copy link
Collaborator

djmb commented Dec 23, 2024

@Dandush03, @ausangshukla - what do your config/[solid_]cache.yml and config/database.yml look like?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants