rails / solid_queue

Database-backed Active Job backend
MIT License
1.95k stars 130 forks source link

Enqueue jobs inside a connected_to block #369

Open bubiche opened 1 month ago

bubiche commented 1 month ago

My rails application connects to multiple databases with the configs below:

database.yml ``` production: primary_shard: ... secondary_shard: ... queue: ... ```
config/initializers/database.rb ```ruby # This is needed due to us using another gem whose model writes to both primary_shard and secondary_shard and its model inherits from ActiveRecord::Base ActiveRecord::Base.instance_eval do connects_to shards: { primary_shard: { writing: :primary_shard, }, secondary_shard: { writing: :secondary_shard, }, } end ```

SolidQueue has been working well, the only time where we cannot enqueue is if we have a block of code like below:

ActiveRecord::Base.connected_to(role: :writing, shard: :secondary_shard) do
  # ...
  SomeJob.perform_later(some_argument)
  # ...
end

I think it is because SolidQueue is trying to insert the job into secondary_shard instead of the queue database. Is there any way to circumvent this besides rewriting the code to call perform_later outside of the connected_to block? SomeJob.perform_later might be called inside a function with the connected_to block so it's quite a big refactor for us.

rosa commented 1 month ago

Ohhh, totally! Let me look into this one to see how to fix, I think it's something Solid Queue should handle. Thanks for the report!

djpate commented 1 month ago

Same issue here. is there a workaround? my entire controller is in a connected_to block? I tried wrapping the perform_later in another connection switch but no luck.

ActiveRecord::Base.connected_to(role: :writing, shard: :queue) do
  Workers::Foobar.perform_later(account_id: account_id, id: id)
end
thibaudgg commented 2 weeks ago

Same problem for me, I'm working on switching my multi-tenant app to SQLite (one database per tenant) using sharding, while keep the SolidQueue database separated.

Please let me know how I could help, or if sharing more code would be helpful. I'm happy to help fix the issue as well.

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  connects_to shards: Tenant.shards.map { |shard|
    [ shard, { writing: shard } ]
  }.to_h
end

Production config:

  config.active_job.queue_adapter = :solid_queue
  config.solid_queue.connects_to = { database: { writing: :queue } }

Tenant switching logic (simplified):

module Tenant
  extend self
  def switch(tenant)
    ActiveRecord::Base.connected_to(shard: tenant.to_sym) do
        yield
    end
  end
  # ...
end

But trying to enqueue jobs gives me that error trace:

Stacktrace ``` ActiveRecord::ConnectionNotDefined: No database connection defined for SolidQueue::Record with 'TENANT_A' shard. (ActiveRecord::ConnectionNotDefined) from active_record/connection_adapters/abstract/connection_handler.rb:230:in `retrieve_connection_pool' from active_record/connection_handling.rb:343:in `connection_pool' from active_record/connection_handling.rb:310:in `with_connection' from active_record/transactions.rb:410:in `with_transaction_returning_status' from active_record/transactions.rb:366:in `save!' from active_record/suppressor.rb:56:in `save!' from active_record/persistence.rb:55:in `create!' from bundle/ruby/3.3.0/bundler/gems/solid_queue-51c75bec01c8/app/models/solid_queue/job.rb:41:in `create_from_active_job' from bundle/ruby/3.3.0/bundler/gems/solid_queue-51c75bec01c8/app/models/solid_queue/job.rb:31:in `enqueue' from active_job/queue_adapters/solid_queue_adapter.rb:16:in `enqueue' from active_job/enqueuing.rb:132:in `raw_enqueue' from active_job/enqueue_after_transaction_commit.rb:40:in `raw_enqueue' from active_job/enqueuing.rb:117:in `block in enqueue' from active_support/callbacks.rb:120:in `block in run_callbacks' from active_job/instrumentation.rb:40:in `block in instrument' from active_support/notifications.rb:210:in `block in instrument' from active_support/notifications/instrumenter.rb:58:in `instrument' from sentry/rails/tracing.rb:56:in `instrument' from active_support/notifications.rb:210:in `instrument' from active_job/instrumentation.rb:39:in `instrument' from active_record/railties/job_runtime.rb:18:in `instrument' from active_job/instrumentation.rb:21:in `block (2 levels) in ' from active_support/callbacks.rb:129:in `instance_exec' from active_support/callbacks.rb:129:in `block in run_callbacks' from active_support/tagged_logging.rb:143:in `block in tagged' from active_support/tagged_logging.rb:38:in `tagged' from active_support/tagged_logging.rb:143:in `tagged' from active_support/broadcast_logger.rb:241:in `method_missing' from active_job/logging.rb:39:in `tag_logger' from active_job/logging.rb:28:in `block (2 levels) in ' from active_support/callbacks.rb:129:in `instance_exec' from active_support/callbacks.rb:129:in `block in run_callbacks' from active_support/callbacks.rb:140:in `run_callbacks' from active_job/enqueuing.rb:116:in `enqueue' from active_job/configured_job.rb:15:in `perform_later' from action_mailer/parameterized.rb:148:in `enqueue_delivery' from action_mailer/message_delivery.rb:103:in `deliver_later' from app/controllers/sessions_controller.rb:26:in `create' from action_controller/metal/basic_implicit_render.rb:8:in `send_action' from abstract_controller/base.rb:226:in `process_action' from action_controller/metal/rendering.rb:193:in `process_action' from abstract_controller/callbacks.rb:261:in `block in process_action' from active_support/callbacks.rb:120:in `block in run_callbacks' from turbo-rails.rb:24:in `with_request_id' from bundle/ruby/3.3.0/gems/turbo-rails-2.0.11/app/controllers/concerns/turbo/request_id_tracking.rb:10:in `turbo_tracking_request_id' from active_support/callbacks.rb:129:in `block in run_callbacks' from action_text/rendering.rb:25:in `with_renderer' from action_text/engine.rb:71:in `block (4 levels) in ' from active_support/callbacks.rb:129:in `instance_exec' from active_support/callbacks.rb:129:in `block in run_callbacks' from sentry/rails/controller_transaction.rb:21:in `block in sentry_around_action' from sentry/hub.rb:115:in `block in with_child_span' from sentry/span.rb:232:in `with_child_span' from sentry/hub.rb:113:in `with_child_span' from sentry-ruby.rb:499:in `with_child_span' from sentry/rails/controller_transaction.rb:18:in `sentry_around_action' from active_support/callbacks.rb:129:in `block in run_callbacks' from active_support/callbacks.rb:140:in `run_callbacks' from abstract_controller/callbacks.rb:260:in `process_action' from action_controller/metal/rescue.rb:27:in `process_action' from action_controller/metal/instrumentation.rb:76:in `block in process_action' from active_support/notifications.rb:210:in `block in instrument' from active_support/notifications/instrumenter.rb:58:in `instrument' from sentry/rails/tracing.rb:56:in `instrument' from active_support/notifications.rb:210:in `instrument' from action_controller/metal/instrumentation.rb:75:in `process_action' from action_controller/metal/params_wrapper.rb:259:in `process_action' from active_record/railties/controller_runtime.rb:39:in `process_action' from abstract_controller/base.rb:163:in `process' from action_view/rendering.rb:40:in `process' from action_controller/metal.rb:252:in `dispatch' from action_controller/metal.rb:335:in `dispatch' from action_dispatch/routing/route_set.rb:67:in `dispatch' from action_dispatch/routing/route_set.rb:50:in `serve' from action_dispatch/journey/router.rb:53:in `block in serve' from action_dispatch/journey/router.rb:133:in `block in find_routes' from action_dispatch/journey/router.rb:126:in `each' from action_dispatch/journey/router.rb:126:in `find_routes' from action_dispatch/journey/router.rb:34:in `serve' from action_dispatch/routing/route_set.rb:908:in `call' from lib/tenant/middleware.rb:12:in `block in call' from lib/tenant.rb:56:in `block (2 levels) in switch' from active_record/connection_handling.rb:214:in `prohibit_shard_swapping' from lib/tenant.rb:55:in `block in switch' from active_record/connection_handling.rb:398:in `with_role_and_shard' from active_record/connection_handling.rb:149:in `connected_to' from lib/tenant.rb:54:in `switch' from lib/tenant/middleware.rb:12:in `call' from rack/tempfile_reaper.rb:20:in `call' from rack/etag.rb:29:in `call' from rack/conditional_get.rb:43:in `call' from rack/head.rb:15:in `call' from action_dispatch/http/permissions_policy.rb:38:in `call' from action_dispatch/http/content_security_policy.rb:35:in `call' from rack/session/abstract/id.rb:272:in `context' from rack/session/abstract/id.rb:266:in `call' from action_dispatch/middleware/cookies.rb:706:in `call' from action_dispatch/middleware/callbacks.rb:31:in `block in call' from active_support/callbacks.rb:100:in `run_callbacks' from action_dispatch/middleware/callbacks.rb:30:in `call' from sentry/rails/rescued_exception_interceptor.rb:14:in `call' from action_dispatch/middleware/debug_exceptions.rb:31:in `call' from sentry/rack/capture_exceptions.rb:30:in `block (2 levels) in call' from sentry/hub.rb:265:in `with_session_tracking' from sentry-ruby.rb:412:in `with_session_tracking' from sentry/rack/capture_exceptions.rb:21:in `block in call' from sentry/hub.rb:59:in `with_scope' from sentry-ruby.rb:392:in `with_scope' from sentry/rack/capture_exceptions.rb:20:in `call' from action_dispatch/middleware/show_exceptions.rb:32:in `call' from lograge/rails_ext/rack/logger.rb:18:in `call_app' from rails/rack/logger.rb:29:in `call' from rails/rack/silence_request.rb:28:in `call' from action_dispatch/middleware/remote_ip.rb:96:in `call' from request_store/middleware.rb:19:in `call' from action_dispatch/middleware/request_id.rb:34:in `call' from rack/method_override.rb:28:in `call' from rack/runtime.rb:24:in `call' from action_dispatch/middleware/executor.rb:16:in `call' from action_dispatch/middleware/static.rb:27:in `call' from rack/sendfile.rb:114:in `call' from action_dispatch/middleware/assume_ssl.rb:24:in `call' from rack/cors.rb:102:in `call' from rails/engine.rb:535:in `call' from rack/status.rb:13:in `call' from puma/configuration.rb:272:in `call' from puma/request.rb:100:in `block in handle_request' from puma/thread_pool.rb:378:in `with_force_shutdown' from puma/request.rb:99:in `handle_request' from puma/server.rb:464:in `process_client' from puma/server.rb:245:in `block in run' from puma/thread_pool.rb:155:in `block in spawn_thread' ```
thibaudgg commented 2 weeks ago

Added a comment on this other ticket (https://github.com/rails/solid_queue/issues/353), that might help here.

rosa commented 2 weeks ago

Sorry for the delay on this one, I haven't managed to look into it yet, but it's definitely on my radar. I'll try next week 🤞

thibaudgg commented 2 weeks ago

Thanks @rosa!

I just gave a tried to this patch on production, and it seems to work like a charm.

# config/initializers/solid_queue_patch.rb
Rails.application.config.to_prepare do
  module CurrentShardPatch
    def current_shard; :queue end
  end

  SolidQueue::Record.send(:extend, CurrentShardPatch)
end

My app is open-source, so you can give it a look if that's helpful to you. Here some relevant parts:

rosa commented 2 weeks ago

Oh, awesome! I'm sure this is going to be super helpful! I'll look into all those next week, too 🙏 Thank you so much!