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 +4 -4
- data/CHANGELOG.md +13 -4
- data/db/postgresql_structure.sql +76 -0
- data/docker-compose.yml +1 -0
- data/lib/active_record_proxy_adapters/configuration.rb +22 -4
- data/lib/active_record_proxy_adapters/log_subscriber.rb +41 -0
- data/lib/active_record_proxy_adapters/primary_replica_proxy.rb +22 -18
- data/lib/active_record_proxy_adapters/version.rb +1 -1
- metadata +4 -3
- data/Rakefile +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5312effcf68a3305eb5e7804440112433f5b6837d6b71e7d122145eac4741619
|
4
|
+
data.tar.gz: 50586657daa02e76dfffebbe1faf8df5093198471faf847cec97cd16634c0c17
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 (
|
14
|
+
- Fix CTE regex matcher (4b1d10bfd952fb1f5b102de8cc1a5bd05d25f5e9)
|
6
15
|
|
7
16
|
## [0.1.1] - 2024-11-27
|
8
17
|
|
9
|
-
- Enable RubyGems MFA (
|
18
|
+
- Enable RubyGems MFA (2a71b1f4354fb966cc0aa68231ca5837814e07ee)
|
10
19
|
|
11
20
|
## [0.1.0] - 2024-11-19
|
12
21
|
|
13
|
-
- Add PostgreSQLProxyAdapter (
|
22
|
+
- Add PostgreSQLProxyAdapter (2b3bb9f7359139519b32af3018ceb07fed8c6b33)
|
14
23
|
|
15
24
|
## [0.1.0.rc2] - 2024-10-28
|
16
25
|
|
17
|
-
- Add PostgreSQLProxyAdapter (
|
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
@@ -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
|
9
|
-
CHECKOUT_TIMEOUT
|
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
|
20
|
-
self.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
|
69
|
-
|
68
|
+
def replica_pool_unavailable?
|
69
|
+
!replica_pool
|
70
70
|
end
|
71
71
|
|
72
72
|
def replica_pool
|
73
|
-
|
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)
|
111
|
-
connection = if role == writing_role
|
112
|
-
|
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
|
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.
|
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-
|
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
|
-
-
|
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
|