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

Remove excess memoization of ConnAdapter instances #318

Closed
wants to merge 6 commits into from

Conversation

kolen
Copy link

@kolen kolen commented Aug 11, 2019

  • Removed memoization of ConnAdapter returned by QC.default_conn_adapter in Queue

    QC.default_conn_adapter already memoizes ConnAdapter instance if needed, so memoizing it again is unnecessary.

    This double memoization had problem: despite default_conn_adapter memoizing its connection adapter instance as thread-local variable, there's global instance of Queue in @default_queue, and it memoizes @conn_adapter taken from QC.default_conn_adapter. So connection becomes global.

    # The default queue used by `QC.enqueue`.
    def default_queue
    @default_queue ||= Queue.new(QC.queue)
    end

    For example, this singleton connection causes problems with ActiveRecord pool reaper (ActiveRecord connection sharing: connection is returned to the pool #317 (comment)).

    If connection adapter (and its connection) was really intended to be global, and not per-thread, then I don't understand why QC.default_conn_adapter has thread-local memoization (8e208e2).

    As Queue has setter conn_adapter=, for backwards compatibility, if connection is set via setter, it's then returned by conn_adapter. However, I don't understand necessity of this setter API, as overwriting connection adapter for existing queue instance is a recipe for mess. Maybe should be deprecated in future.

    Potential problems with this change: it breaks assumption that there's single global connection for all threads. But 8e208e2 suggests that it's not design assumption but a bug, and there should be one connection per thread. If an app starts lots of threads, uses QC in them and then kills them, it would be really bad as connections will remain open until GC, however isn't it by design? ConnAdapter has disconnect method.

  • Removed memoization of ActiveRecord::Base.connection in default_conn_adapter

    Connection borrowed from ActiveRecord pool by calling connection shouldn't be stored anywhere as it will be likely returned back to pool with ActiveRecord::Base.clear_active_connections!, or with reap if thread that borrowed it is dead. Rails calls this method after handling each request. Again, ActiveRecord has its own memoization for connection assigned to current thread.

    This probably fixes ActiveRecord connection sharing: connection is returned to the pool #317 (common PG::ConnectionBad: connection is closed error).

    Memoization for non-activerecord connections remains, as connection managed by QC itself should have its owner. Also, there's setter too, and if ConnAdapter is set via setter, it's no longer taken from ActiveRecord.

    Potential problems with this change: when using ActiveRecord without Rails, you have to call clear_active_connections!, but at least now it's safe to call it.

# will cause it to return to AR pool, and it will become shared
# between memoized value and pool. And Rails does
# clear_active_connections! after each request
return ConnAdapter.new(ActiveRecord::Base.connection.raw_connection)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to move this part to the top (as an early return), because if rails_connection_sharing_enabled? is true, then t = Thread.current etc lines are effectively dead.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's another return before this, in order for setter to work. If thread-local :qc_conn_adapter is set before with setter, it's returned regardless of rails_connection_sharing_enabled?.

This setter is a little odd (even in master), as it sets default_conn_adapter only for current thread. After this changes, this behavior is retained. I think such setters are better to be deprecated.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. For a future reviewer, if we forget about setter, the proposed change in essence is:

def self.default_conn_adapter
  if rails_connection_sharing_enabled?
    ConnAdapter.new(ActiveRecord::Base.connection.raw_connection)
  else
    Thread.current[:qc_conn_adapter] ||= ConnAdapter.new
  end
end

@nashbridges
Copy link

  • Removed memoization of ActiveRecord::Base.connection in default_conn_adapter

This was definitely a problem for us when we upgraded from Rails 5.1 to 5.2.

Due to AR reaping an idle connection, we had hard to debug error: if a worker was idle for 300 seconds or more and then finally received a request, all AR models continued to work fine, but invoking deliver_later (ActiveJob powered by QueueClassic) resulted in a PG::ConnectionBad exception.

Current workaround for us is to disable sharing connection via setting export QC_RAILS_DATABASE=false.

@kolen thanks for looking into this!

@ukd1 ukd1 self-assigned this Oct 14, 2019
rmoriz added a commit to PSPDFKit-labs/queue_classic that referenced this pull request Feb 14, 2020
@nashbridges
Copy link

@kolen my understanding is that #312 took a bit different implementation direction, nonetheless it fixed the root cause.

@kolen kolen closed this Jul 19, 2022
@kolen
Copy link
Author

kolen commented Jul 19, 2022

Closing, seems that the issue has been fixed.

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

Successfully merging this pull request may close these issues.

ActiveRecord connection sharing: connection is returned to the pool
3 participants