diff --git a/CHANGELOG.md b/CHANGELOG.md index 079687d..8566978 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Fixed `db:drop`, `db:reset`, and `db:test:prepare` failing on Aurora PostgreSQL with `database "" is being accessed by other users`. The same error occurred when running `bin/rails test` after a schema change. The cause was the wrapper's topology and Blue/Green monitors, which kept their own connections open to the database being dropped. The wrapper now stops these monitors before dropping a database and when ActiveRecord clears all its connections ([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. A forked child also no longer waits on a Secrets Manager fetch that was in progress in the parent when it forked ([PR #196](https://github.com/aws/aws-advanced-ruby-driver-wrapper/pull/196)). - Fixed the AWS Secrets Manager plugin not reporting a timed-out secret fetch as a timeout. A fetch that did not finish within 60 seconds failed with a generic `SecretsManagerAuthError` (`Failed to fetch database credentials from AWS Secrets Manager`) instead of `Timed out fetching secret after 60s` ([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