From 30d6a314f50938ba505019a5f7fd5d32e010587a Mon Sep 17 00:00:00 2001 From: aaron-congo Date: Mon, 5 Oct 2026 23:20:49 -0700 Subject: [PATCH] fix: let Action Cable's PostgreSQL subscription adapter run on aws_postgresql Action Cable's PostgreSQL subscription adapter refuses any raw connection that is not a PG::Connection, and aws_postgresql hands out a WrapperPgConnection, so every subscribe and broadcast (including Turbo Stream broadcasts) raised "The Active Record database must be PostgreSQL in order to use the PostgreSQL Action Cable storage adapter". The aws_postgresql adapter now registers an :action_cable load hook that prepends a verify! override to ActionCable::SubscriptionAdapter::PostgreSQL, accepting a WrapperPgConnection and leaving every other connection to Action Cable's own check. The calls Action Cable makes on the connection (exec, escape_identifier, escape_string, wait_for_notify) are forwarded to the wrapped PG::Connection. --- CHANGELOG.md | 1 + .../aws_action_cable_postgresql_support.rb | 40 +++++++++++++++++++ .../active_record/aws_postgresql_adapter.rb | 1 + .../subscription_adapter/postgresql.rb | 35 ++++++++++++++++ ...ws_action_cable_postgresql_support_spec.rb | 36 +++++++++++++++++ 5 files changed, 113 insertions(+) create mode 100644 lib/aws_advanced_ruby_driver_wrapper/active_record/aws_action_cable_postgresql_support.rb create mode 100644 spec/fixtures/action_cable_stub/action_cable/subscription_adapter/postgresql.rb create mode 100644 spec/unit/aws_action_cable_postgresql_support_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f8079f..01f2d1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Fixed the `aws_postgresql` and `aws_mysql2` ActiveRecord adapters keeping ActiveRecord's prepared statement cache after a successful failover, so every query prepared before the failover failed on that connection with `Method invoked against old connection` until the process restarted ([PR #194](https://github.com/aws/aws-advanced-ruby-driver-wrapper/pull/194), [documentation](https://aws.github.io/aws-advanced-wrapper-docs/ruby/enhanced-failover)). - Fixed `db:drop`, `db:reset`, `db:test:prepare`, and `bin/rails test` after a schema change failing on Aurora PostgreSQL with `database "" is being accessed by other users`, because the topology monitor kept its own connections to the database being dropped ([PR #195](https://github.com/aws/aws-advanced-ruby-driver-wrapper/pull/195)). - Fixed forked processes, such as Puma workers with `preload_app!`, Unicorn workers, and Resque jobs, inheriting the parent's background monitors after their threads had stopped, so the cluster topology never refreshed in the child and failover there could not find the new writer. A forked child now starts its own monitors and leaves the parent's monitoring connections open ([PR #196](https://github.com/aws/aws-advanced-ruby-driver-wrapper/pull/196)). +- Fixed Action Cable's PostgreSQL subscription adapter (`adapter: postgresql` in `cable.yml`) refusing to run on the `aws_postgresql` ActiveRecord adapter with `The Active Record database must be PostgreSQL in order to use the PostgreSQL Action Cable storage adapter`, which also stopped Turbo Stream broadcasts ([PR #197](https://github.com/aws/aws-advanced-ruby-driver-wrapper/pull/197)). ## [1.0.0] - 2026-10-05 diff --git a/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_action_cable_postgresql_support.rb b/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_action_cable_postgresql_support.rb new file mode 100644 index 0000000..903c367 --- /dev/null +++ b/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_action_cable_postgresql_support.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require_relative '../postgresql' + +module ActiveRecord + module ConnectionAdapters + # Lets Action Cable's PostgreSQL subscription adapter (cable.yml `adapter: postgresql`) run on the + # aws_postgresql adapter. Action Cable refuses any raw connection that is not a PG::Connection, and + # aws_postgresql hands out a WrapperPgConnection, which forwards the calls Action Cable makes + # (exec, escape_identifier, escape_string, wait_for_notify) to the PG::Connection it wraps. + module AwsActionCablePostgreSQLSupport + private + + def verify!(pg_conn) + super unless pg_conn.is_a?(AwsAdvancedRubyDriverWrapper::WrapperPgConnection) + end + end + end +end + +# Action Cable runs this hook when its server loads, before it loads a subscription adapter, so the +# PostgreSQL one is loaded here to patch it. Applications without Action Cable never run the hook. +ActiveSupport.on_load(:action_cable) do + require 'action_cable/subscription_adapter/postgresql' + ActionCable::SubscriptionAdapter::PostgreSQL.prepend(ActiveRecord::ConnectionAdapters::AwsActionCablePostgreSQLSupport) +end diff --git a/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb b/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb index 4b5078f..5527b57 100644 --- a/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +++ b/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb @@ -18,6 +18,7 @@ require_relative '../postgresql' require_relative '../errors' require_relative 'aws_connection_handler' +require_relative 'aws_action_cable_postgresql_support' module ActiveRecord module ConnectionAdapters diff --git a/spec/fixtures/action_cable_stub/action_cable/subscription_adapter/postgresql.rb b/spec/fixtures/action_cable_stub/action_cable/subscription_adapter/postgresql.rb new file mode 100644 index 0000000..90db008 --- /dev/null +++ b/spec/fixtures/action_cable_stub/action_cable/subscription_adapter/postgresql.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Stand-in for Action Cable's PostgreSQL subscription adapter, which is not a dependency of this gem. +# It keeps the check every supported Action Cable version (7.2 to 8.1) runs on the connection it is given. +module ActionCable + module SubscriptionAdapter + class PostgreSQL + def check(pg_conn) + verify!(pg_conn) + end + + private + + def verify!(pg_conn) + return if pg_conn.is_a?(PG::Connection) + + raise 'The Active Record database must be PostgreSQL in order to use the PostgreSQL Action Cable storage adapter' + end + end + end +end diff --git a/spec/unit/aws_action_cable_postgresql_support_spec.rb b/spec/unit/aws_action_cable_postgresql_support_spec.rb new file mode 100644 index 0000000..95f0c4d --- /dev/null +++ b/spec/unit/aws_action_cable_postgresql_support_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require_relative '../spec_helper' + +# Action Cable is not a dependency of this gem, so its PostgreSQL subscription adapter is replaced by a stand-in. +$LOAD_PATH.unshift(File.expand_path('../fixtures/action_cable_stub', __dir__)) +require 'aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter' + +RSpec.describe 'Action Cable PostgreSQL subscription adapter with aws_postgresql' do + # Action Cable runs its load hooks when its server loads. + before(:all) { ActiveSupport.run_load_hooks(:action_cable, Object.new) } + + let(:adapter) { ActionCable::SubscriptionAdapter::PostgreSQL.new } + + it 'accepts the wrapper connection that aws_postgresql hands out as its raw connection' do + expect { adapter.check(AwsAdvancedRubyDriverWrapper::WrapperPgConnection.allocate) }.not_to raise_error + end + + it 'still rejects connections that are not PostgreSQL' do + expect { adapter.check(Object.new) }.to raise_error(RuntimeError, /must be PostgreSQL/) + end +end