active_record_proxy_adapters 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b23e66bd5df6d979b9efad29a52139552950f29774935cc91fffdbc87805a4c
4
- data.tar.gz: 5b3206ee7420635f2f76432a1bbe728e161a9ea434db42da22ac5290ae561e3d
3
+ metadata.gz: 5312effcf68a3305eb5e7804440112433f5b6837d6b71e7d122145eac4741619
4
+ data.tar.gz: 50586657daa02e76dfffebbe1faf8df5093198471faf847cec97cd16634c0c17
5
5
  SHA512:
6
- metadata.gz: cb98f62ce8a62ef0d71364491d1686186c527adc0c6a8612fd5ee118710fe9360166f14ae06b348856548c8b507ef90edf413113e93a9afa56c82e84c267df1e
7
- data.tar.gz: 75f30a3ce9473415be91a51acc9fc39a5aacc31b6623943a007b3fb0c41e330752c6f6b470ee759d141b6c84feb96d3b24a015939bb0b053d82ac50a22adb447
6
+ metadata.gz: 707b80a3618328639126d13f3f76bb8123105019b3e30c75f15c66a66a88d49aa5de4301fe70b25ff5356ef37e54ebff896f4cab0160cadbb79d08ae9cf0dc4e
7
+ data.tar.gz: 6b84984e33cf87abe9a12f5553bccffc32e2a9563a0c859c488cdae4be4c3a019ffce01242648620c67bb85a16dc61d982f0be8ecba83943f01c718e067743dc
data/CHANGELOG.md CHANGED
@@ -1,17 +1,26 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2024-12-24
4
+
5
+ - Add custom log subscriber to tag queries based on the adapter being used (68b8c1f4191388eb957bf12e0f84289da667e940)
6
+
7
+ ## [0.1.3] - 2024-12-24
8
+
9
+ - Fix replica connection pool getter when database configurations have multiple replicas (ea5a33997da45ac073f166b3fbd2d12426053cd6)
10
+ - Retrieve replica pool without checking out a connection (6470ef58e851082ae1f7a860ecdb5b451ef903c8)
11
+
3
12
  ## [0.1.2] - 2024-12-16
4
13
 
5
- Fix CTE regex matcher (4b1d10b)
14
+ - Fix CTE regex matcher (4b1d10bfd952fb1f5b102de8cc1a5bd05d25f5e9)
6
15
 
7
16
  ## [0.1.1] - 2024-11-27
8
17
 
9
- - Enable RubyGems MFA (2a71b1f)
18
+ - Enable RubyGems MFA (2a71b1f4354fb966cc0aa68231ca5837814e07ee)
10
19
 
11
20
  ## [0.1.0] - 2024-11-19
12
21
 
13
- - Add PostgreSQLProxyAdapter (2b3bb9f)
22
+ - Add PostgreSQLProxyAdapter (2b3bb9f7359139519b32af3018ceb07fed8c6b33)
14
23
 
15
24
  ## [0.1.0.rc2] - 2024-10-28
16
25
 
17
- - Add PostgreSQLProxyAdapter (2b3bb9f)
26
+ - Add PostgreSQLProxyAdapter (2b3bb9f7359139519b32af3018ceb07fed8c6b33)
@@ -0,0 +1,76 @@
1
+ SET statement_timeout = 0;
2
+ SET lock_timeout = 0;
3
+ SET idle_in_transaction_session_timeout = 0;
4
+ SET client_encoding = 'UTF8';
5
+ SET standard_conforming_strings = on;
6
+ SELECT pg_catalog.set_config('search_path', '', false);
7
+ SET check_function_bodies = false;
8
+ SET xmloption = content;
9
+ SET client_min_messages = warning;
10
+ SET row_security = off;
11
+
12
+ --
13
+ -- Name: public; Type: SCHEMA; Schema: -; Owner: -
14
+ --
15
+
16
+ -- *not* creating schema, since initdb creates it
17
+
18
+
19
+ SET default_tablespace = '';
20
+
21
+ SET default_table_access_method = heap;
22
+
23
+ --
24
+ -- Name: users; Type: TABLE; Schema: public; Owner: -
25
+ --
26
+
27
+ CREATE TABLE public.users (
28
+ id integer NOT NULL,
29
+ name text NOT NULL,
30
+ email text NOT NULL,
31
+ created_at timestamp without time zone DEFAULT now() NOT NULL,
32
+ updated_at timestamp without time zone DEFAULT now() NOT NULL
33
+ );
34
+
35
+
36
+ --
37
+ -- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: -
38
+ --
39
+
40
+ CREATE SEQUENCE public.users_id_seq
41
+ AS integer
42
+ START WITH 1
43
+ INCREMENT BY 1
44
+ NO MINVALUE
45
+ NO MAXVALUE
46
+ CACHE 1;
47
+
48
+
49
+ --
50
+ -- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
51
+ --
52
+
53
+ ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id;
54
+
55
+
56
+ --
57
+ -- Name: users id; Type: DEFAULT; Schema: public; Owner: -
58
+ --
59
+
60
+ ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass);
61
+
62
+
63
+ --
64
+ -- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: -
65
+ --
66
+
67
+ ALTER TABLE ONLY public.users
68
+ ADD CONSTRAINT users_pkey PRIMARY KEY (id);
69
+
70
+
71
+ --
72
+ -- PostgreSQL database dump complete
73
+ --
74
+
75
+ SET search_path TO "$user", public;
76
+
data/docker-compose.yml CHANGED
@@ -50,6 +50,7 @@ services:
50
50
  - POSTGRES_LOG_STATEMENT=${POSTGRES_LOG_STATEMENT:-}
51
51
  - REPLICA_USER=replicator
52
52
  - REPLICA_PASSWORD=replicator
53
+ container_name: postgres_primary
53
54
  environment:
54
55
  POSTGRES_DB: postgres
55
56
  POSTGRES_USER: postgres_primary_test
@@ -5,8 +5,10 @@ require "active_support/core_ext/integer/time"
5
5
  module ActiveRecordProxyAdapters
6
6
  # Provides a global configuration object to configure how the proxy should behave.
7
7
  class Configuration
8
- PROXY_DELAY = 2.seconds.freeze
9
- CHECKOUT_TIMEOUT = 2.seconds.freeze
8
+ PROXY_DELAY = 2.seconds.freeze
9
+ CHECKOUT_TIMEOUT = 2.seconds.freeze
10
+ LOG_SUBSCRIBER_PRIMARY_PREFIX = proc { |event| "#{event.payload[:connection].class::ADAPTER_NAME} Primary" }.freeze
11
+ LOG_SUBSCRIBER_REPLICA_PREFIX = proc { |event| "#{event.payload[:connection].class::ADAPTER_NAME} Replica" }.freeze
10
12
 
11
13
  # @return [ActiveSupport::Duration] How long the proxy should reroute all read requests to the primary database
12
14
  # since the latest write. Defaults to PROXY_DELAY.
@@ -15,9 +17,25 @@ module ActiveRecordProxyAdapters
15
17
  # Defaults to CHECKOUT_TIMEOUT.
16
18
  attr_accessor :checkout_timeout
17
19
 
20
+ # @return [Proc] Prefix for the log subscriber when the primary database is used.
21
+ attr_reader :log_subscriber_primary_prefix
22
+
23
+ # @return [Proc] Prefix for the log subscriber when the replica database is used.
24
+ attr_reader :log_subscriber_replica_prefix
25
+
18
26
  def initialize
19
- self.proxy_delay = PROXY_DELAY
20
- self.checkout_timeout = CHECKOUT_TIMEOUT
27
+ self.proxy_delay = PROXY_DELAY
28
+ self.checkout_timeout = CHECKOUT_TIMEOUT
29
+ self.log_subscriber_primary_prefix = LOG_SUBSCRIBER_PRIMARY_PREFIX
30
+ self.log_subscriber_replica_prefix = LOG_SUBSCRIBER_REPLICA_PREFIX
31
+ end
32
+
33
+ def log_subscriber_primary_prefix=(prefix)
34
+ @log_subscriber_primary_prefix = prefix.is_a?(Proc) ? prefix : proc { prefix.to_s }
35
+ end
36
+
37
+ def log_subscriber_replica_prefix=(prefix)
38
+ @log_subscriber_replica_prefix = prefix.is_a?(Proc) ? prefix : proc { prefix.to_s }
21
39
  end
22
40
  end
23
41
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordProxyAdapters
4
+ class LogSubscriber < ActiveRecord::LogSubscriber # rubocop:disable Style/Documentation
5
+ attach_to :active_record
6
+
7
+ IGNORE_PAYLOAD_NAMES = %w[SCHEMA EXPLAIN].freeze
8
+
9
+ def sql(event)
10
+ payload = event.payload
11
+ name = payload[:name]
12
+ unless IGNORE_PAYLOAD_NAMES.include?(name)
13
+ name = [database_instance_prefix_for(event), name].compact.join(" ")
14
+ payload[:name] = name
15
+ end
16
+ super
17
+ end
18
+
19
+ protected
20
+
21
+ def database_instance_prefix_for(event)
22
+ connection = event.payload[:connection]
23
+ config = connection.instance_variable_get(:@config)
24
+ prefix = if config[:replica] || config["replica"]
25
+ log_subscriber_replica_prefix
26
+ else
27
+ log_subscriber_primary_prefix
28
+ end
29
+
30
+ "[#{prefix.call(event)}]"
31
+ end
32
+
33
+ private
34
+
35
+ delegate :log_subscriber_primary_prefix, :log_subscriber_replica_prefix, to: :config
36
+
37
+ def config
38
+ ActiveRecordProxyAdapters.config
39
+ end
40
+ end
41
+ end
@@ -62,15 +62,19 @@ module ActiveRecordProxyAdapters
62
62
 
63
63
  attr_reader :primary_connection, :last_write_at, :active_record_context
64
64
 
65
- delegate :connected_to_stack, to: :connection_class
65
+ delegate :connection_handler, :connected_to_stack, to: :connection_class
66
66
  delegate :reading_role, :writing_role, to: :active_record_context
67
67
 
68
- def connection_class
69
- active_record_context.connection_class_for(primary_connection)
68
+ def replica_pool_unavailable?
69
+ !replica_pool
70
70
  end
71
71
 
72
72
  def replica_pool
73
- ActiveRecord::Base.connected_to(role: reading_role) { ActiveRecord::Base.connection_pool }
73
+ connection_handler.retrieve_connection_pool(connection_class.name, role: reading_role)
74
+ end
75
+
76
+ def connection_class
77
+ active_record_context.connection_class_for(primary_connection)
74
78
  end
75
79
 
76
80
  def coerce_query_to_string(sql_or_arel)
@@ -107,21 +111,12 @@ module ActiveRecordProxyAdapters
107
111
  [reading_role, writing_role].include?(role) ? role : nil
108
112
  end
109
113
 
110
- def connection_for(role, sql_string) # rubocop:disable Metrics/MethodLength
111
- connection = if role == writing_role
112
- primary_connection
113
- else
114
- begin
115
- replica_pool.checkout(checkout_timeout)
116
- # rescue NoDatabaseError to avoid crashing when running db:create rake task
117
- # rescue ConnectionNotEstablished to handle connectivity issues in the replica
118
- # (for example, replication delay)
119
- rescue ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished
120
- primary_connection
121
- end
122
- end
114
+ def connection_for(role, sql_string)
115
+ connection = primary_connection if role == writing_role || replica_pool_unavailable?
116
+ connection ||= checkout_replica_connection
123
117
 
124
118
  result = yield(connection)
119
+
125
120
  update_primary_latest_write_timestamp if !replica_connection?(connection) && write_statement?(sql_string)
126
121
 
127
122
  result
@@ -130,7 +125,16 @@ module ActiveRecordProxyAdapters
130
125
  end
131
126
 
132
127
  def replica_connection?(connection)
133
- connection != primary_connection
128
+ connection && connection != primary_connection
129
+ end
130
+
131
+ def checkout_replica_connection
132
+ replica_pool.checkout(checkout_timeout)
133
+ # rescue NoDatabaseError to avoid crashing when running db:create rake task
134
+ # rescue ConnectionNotEstablished to handle connectivity issues in the replica
135
+ # (for example, replication delay)
136
+ rescue ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished
137
+ primary_connection
134
138
  end
135
139
 
136
140
  # @return [TrueClass] if there has been a write within the last {#proxy_delay} seconds
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordProxyAdapters
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record_proxy_adapters
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Cruz
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-12-17 00:00:00.000000000 Z
11
+ date: 2024-12-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -67,7 +67,7 @@ files:
67
67
  - Dockerfile
68
68
  - LICENSE.txt
69
69
  - README.md
70
- - Rakefile
70
+ - db/postgresql_structure.sql
71
71
  - docker-compose.yml
72
72
  - docker/postgres_replica/cmd.sh
73
73
  - lib/active_record/connection_adapters/postgresql_proxy_adapter.rb
@@ -77,6 +77,7 @@ files:
77
77
  - lib/active_record_proxy_adapters/configuration.rb
78
78
  - lib/active_record_proxy_adapters/connection_handling.rb
79
79
  - lib/active_record_proxy_adapters/hijackable.rb
80
+ - lib/active_record_proxy_adapters/log_subscriber.rb
80
81
  - lib/active_record_proxy_adapters/postgresql_proxy.rb
81
82
  - lib/active_record_proxy_adapters/primary_replica_proxy.rb
82
83
  - lib/active_record_proxy_adapters/railtie.rb
data/Rakefile DELETED
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
-
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- require "rubocop/rake_task"
9
-
10
- RuboCop::RakeTask.new
11
-
12
- task default: %i[spec rubocop]
13
-
14
- namespace :coverage do
15
- desc "Collates all result sets generated by the different test runners"
16
- task :report do
17
- require "simplecov"
18
-
19
- SimpleCov.collate Dir["coverage/**/.resultset.json"] do
20
- add_group "PostgreSQL" do |src_file|
21
- [/postgresql/, /postgre_sql/].any? { |pattern| pattern.match?(src_file.filename) }
22
- end
23
- end
24
- end
25
- end