active_record_proxy_adapters 0.2.2 → 0.3.1
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 +10 -2
- data/Dockerfile +3 -1
- data/LICENSE.txt +1 -1
- data/README.md +36 -7
- data/db/mysql_structure.sql +41 -0
- data/docker-compose.yml +23 -3
- data/lib/active_record/connection_adapters/mysql2_proxy_adapter.rb +39 -0
- data/lib/active_record/tasks/mysql2_proxy_database_tasks.rb +19 -0
- data/lib/active_record/tasks/postgresql_proxy_database_tasks.rb +3 -29
- data/lib/active_record_proxy_adapters/connection_handling/mysql2.rb +39 -0
- data/lib/active_record_proxy_adapters/connection_handling/postgresql.rb +40 -0
- data/lib/active_record_proxy_adapters/connection_handling.rb +2 -26
- data/lib/active_record_proxy_adapters/database_tasks.rb +39 -0
- data/lib/active_record_proxy_adapters/hijackable.rb +32 -3
- data/lib/active_record_proxy_adapters/mysql2_proxy.rb +9 -0
- data/lib/active_record_proxy_adapters/postgresql_proxy.rb +0 -2
- data/lib/active_record_proxy_adapters/primary_replica_proxy.rb +5 -20
- data/lib/active_record_proxy_adapters/version.rb +1 -1
- metadata +10 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 151caa80f35f53ad02ef3307a55ef0fec4f1a6f369e6afac8d063e4ea2ad5e7f
|
4
|
+
data.tar.gz: 60213ad52a363c435514038eeb65227bff7132b91a6dbed86b8917459449c39e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7720b6fe6b2d6848d30444f77c71d2567996f2e45011c79e6c4791e043b7d9333c2d92554cdf5f6b5a48c29c6b384509a00b9f9b3b930402d47c98188744daa7
|
7
|
+
data.tar.gz: b73be535ad798afb4386d4027e4e92df00d9864a196d4cc1d30ad3a0a4e524c87ad12f67d214ef524a1641800ccf5bc2ab4911acf49c5606d867f51981a18fec
|
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
-
-
|
4
|
-
|
3
|
+
- Fix Active Record adapters dependency loading
|
4
|
+
|
5
|
+
## [0.3.0] - 2025-01-17
|
6
|
+
|
7
|
+
- Add Mysql2ProxyAdapter https://github.com/Nasdaq/active_record_proxy_adapters/commit/7481b79dc93114f9b3b40faa8f3eecce90fe9104
|
8
|
+
|
9
|
+
## [0.2.2, 0.1.5] - 2025-01-02
|
10
|
+
|
11
|
+
- Handle PendingMigrationConnection introduced by Rails 7.2 and backported to Rails 7.1 https://github.com/Nasdaq/active_record_proxy_adapters/commit/793562694c05d554bad6e14637b34e5f9ffd2fc5
|
12
|
+
- Stick to same connection throughout request span https://github.com/Nasdaq/active_record_proxy_adapters/commit/789742fd7a33ecd555a995e8a1e1336455caec75
|
5
13
|
|
6
14
|
## [0.2.1] - 2025-01-02
|
7
15
|
|
data/Dockerfile
CHANGED
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -33,8 +33,19 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
33
33
|
|
34
34
|
### On Rails
|
35
35
|
|
36
|
-
In `config/database.yml`, use `
|
36
|
+
In `config/database.yml`, use `{your_database_adapter}_proxy` as the adapter for the `primary` database, and keep `{your_database_adapter}` for the replica database.
|
37
37
|
|
38
|
+
Currently supported adapters:
|
39
|
+
|
40
|
+
- `postgresql`
|
41
|
+
- `mysql2`
|
42
|
+
|
43
|
+
Coming soon:
|
44
|
+
- `trilogy`
|
45
|
+
- `sqlite`
|
46
|
+
|
47
|
+
|
48
|
+
#### PostgreSQL
|
38
49
|
```yaml
|
39
50
|
# config/database.yml
|
40
51
|
development:
|
@@ -48,6 +59,20 @@ development:
|
|
48
59
|
# your replica credentials here
|
49
60
|
```
|
50
61
|
|
62
|
+
#### MySQL
|
63
|
+
```yaml
|
64
|
+
# config/database.yml
|
65
|
+
development:
|
66
|
+
primary:
|
67
|
+
adapter: mysql2_proxy
|
68
|
+
# your primary credentials here
|
69
|
+
|
70
|
+
primary_replica:
|
71
|
+
adapter: mysql2
|
72
|
+
replica: true
|
73
|
+
# your replica credentials here
|
74
|
+
```
|
75
|
+
|
51
76
|
```ruby
|
52
77
|
# app/models/application_record.rb
|
53
78
|
class ApplicationRecord < ActiveRecord::Base
|
@@ -90,7 +115,7 @@ end
|
|
90
115
|
|
91
116
|
## Configuration
|
92
117
|
|
93
|
-
The gem comes preconfigured out of the box. However, if default configuration does not suit your needs, you can modify
|
118
|
+
The gem comes preconfigured out of the box. However, if default configuration does not suit your needs, you can modify it by using a `.configure` block:
|
94
119
|
|
95
120
|
```ruby
|
96
121
|
# config/initializers/active_record_proxy_adapters.rb
|
@@ -239,6 +264,7 @@ end
|
|
239
264
|
|
240
265
|
# app/models/portal.rb
|
241
266
|
class Portal < ApplicationRecord
|
267
|
+
validates :name, uniqueness: true
|
242
268
|
end
|
243
269
|
|
244
270
|
# in rails console -e test
|
@@ -246,13 +272,17 @@ ActiveRecord::Base.logger.formatter = proc do |_severity, _time, _progname, msg|
|
|
246
272
|
"[#{Time.current.iso8601} THREAD #{Thread.current[:name]}] #{msg}\n"
|
247
273
|
end
|
248
274
|
|
275
|
+
ActiveRecordProxyAdapters.configure do |config|
|
276
|
+
config.proxy_delay = 2.seconds
|
277
|
+
end
|
278
|
+
|
249
279
|
def read_your_own_writes
|
250
280
|
proc do
|
251
281
|
Portal.all.count # should go to the replica
|
252
|
-
|
282
|
+
Portal.create(name: 'Read your own write')
|
253
283
|
|
254
284
|
5.times do
|
255
|
-
Portal.all.count # first one goes the primary, last
|
285
|
+
Portal.all.count # first one goes the primary, last 4 should go to the replica
|
256
286
|
sleep(3)
|
257
287
|
end
|
258
288
|
end
|
@@ -297,9 +327,8 @@ irb(main):051:0> test_multithread_queries
|
|
297
327
|
[2024-12-24T13:52:40-05:00 THREAD USE REPLICA] [PostgreSQL Replica] Portal Count (1.4ms) SELECT COUNT(*) FROM "portals"
|
298
328
|
[2024-12-24T13:52:40-05:00 THREAD READ YOUR OWN WRITES] [PostgreSQL Replica] Portal Count (0.4ms) SELECT COUNT(*) FROM "portals"
|
299
329
|
[2024-12-24T13:52:40-05:00 THREAD READ YOUR OWN WRITES] [PostgreSQLProxy Primary] TRANSACTION (0.5ms) BEGIN
|
300
|
-
[2024-12-24T13:52:40-05:00 THREAD READ YOUR OWN WRITES] [PostgreSQLProxy Primary] Portal Exists? (
|
301
|
-
[2024-12-24T13:52:40-05:00 THREAD READ YOUR OWN WRITES] [PostgreSQLProxy Primary] Portal
|
302
|
-
[2024-12-24T13:52:40-05:00 THREAD READ YOUR OWN WRITES] [PostgreSQLProxy Primary] Portal Create (0.8ms) INSERT INTO "portals" ("name", "slug", "logo", "created_at", "updated_at", "visible") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id" [["name", "Portal-e065948fbbee73d3b2c576b48c2b37e021115158edc6a92390d613640460e1d4"], ["slug", "portal-e065948fbbee73d3b2c576b48c2b37e021115158edc6a92390d613640460e1d4"], ["logo", nil], ["created_at", "2024-12-24 18:52:40.428383"], ["updated_at", "2024-12-24 18:52:40.428383"], ["visible", true]]
|
330
|
+
[2024-12-24T13:52:40-05:00 THREAD READ YOUR OWN WRITES] [PostgreSQLProxy Primary] Portal Exists? (0.4ms) SELECT 1 AS one FROM "portals" WHERE "portals"."name" = $1 LIMIT $2 [["name", "Read your own write"], ["LIMIT", 1]]
|
331
|
+
[2024-12-24T13:52:40-05:00 THREAD READ YOUR OWN WRITES] [PostgreSQLProxy Primary] Portal Create (0.8ms) INSERT INTO "portals" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["name", "Read your own write"], ["created_at", "2024-12-24 18:52:40.428383"], ["updated_at", "2024-12-24 18:52:40.428383"]]
|
303
332
|
[2024-12-24T13:52:40-05:00 THREAD READ YOUR OWN WRITES] [PostgreSQLProxy Primary] TRANSACTION (0.7ms) COMMIT
|
304
333
|
[2024-12-24T13:52:40-05:00 THREAD READ YOUR OWN WRITES] [PostgreSQLProxy Primary] Portal Count (0.6ms) SELECT COUNT(*) FROM "portals"
|
305
334
|
[2024-12-24T13:52:41-05:00 THREAD USE REPLICA] [PostgreSQL Replica] Portal Count (4.4ms) SELECT COUNT(*) FROM "portals"
|
@@ -0,0 +1,41 @@
|
|
1
|
+
/*M!999999\- enable the sandbox mode */
|
2
|
+
|
3
|
+
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
4
|
+
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
5
|
+
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
6
|
+
/*!40101 SET NAMES utf8mb4 */;
|
7
|
+
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
8
|
+
/*!40103 SET TIME_ZONE='+00:00' */;
|
9
|
+
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
10
|
+
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
11
|
+
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
12
|
+
/*M!100616 SET @OLD_NOTE_VERBOSITY=@@NOTE_VERBOSITY, NOTE_VERBOSITY=0 */;
|
13
|
+
DROP TABLE IF EXISTS `schema_migrations`;
|
14
|
+
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
15
|
+
/*!40101 SET character_set_client = utf8 */;
|
16
|
+
CREATE TABLE `schema_migrations` (
|
17
|
+
`version` varchar(255) NOT NULL
|
18
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
19
|
+
/*!40101 SET character_set_client = @saved_cs_client */;
|
20
|
+
DROP TABLE IF EXISTS `users`;
|
21
|
+
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
22
|
+
/*!40101 SET character_set_client = utf8 */;
|
23
|
+
CREATE TABLE `users` (
|
24
|
+
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
25
|
+
`name` text NOT NULL,
|
26
|
+
`email` text NOT NULL,
|
27
|
+
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
28
|
+
`updated_at` timestamp NULL DEFAULT current_timestamp(),
|
29
|
+
PRIMARY KEY (`id`)
|
30
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci;
|
31
|
+
/*!40101 SET character_set_client = @saved_cs_client */;
|
32
|
+
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
33
|
+
|
34
|
+
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
35
|
+
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
36
|
+
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
37
|
+
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
38
|
+
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
39
|
+
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
40
|
+
/*M!100616 SET NOTE_VERBOSITY=@OLD_NOTE_VERBOSITY */;
|
41
|
+
|
data/docker-compose.yml
CHANGED
@@ -31,12 +31,22 @@ services:
|
|
31
31
|
PG_REPLICA_PASSWORD: postgres_primary_test
|
32
32
|
PG_REPLICA_HOST: postgres_replica
|
33
33
|
PG_REPLICA_PORT: 5432
|
34
|
+
MYSQL_PRIMARY_USER: root
|
35
|
+
MYSQL_PRIMARY_PASSWORD: mysql
|
36
|
+
MYSQL_PRIMARY_HOST: mysql_primary
|
37
|
+
MYSQL_PRIMARY_PORT: 3306
|
38
|
+
MYSQL_REPLICA_USER: root
|
39
|
+
MYSQL_REPLICA_PASSWORD: mysql
|
40
|
+
MYSQL_REPLICA_HOST: mysql_primary
|
41
|
+
MYSQL_REPLICA_PORT: 3306
|
34
42
|
depends_on:
|
35
43
|
- postgres_primary
|
36
44
|
- postgres_replica
|
45
|
+
- mariadb
|
37
46
|
networks:
|
38
47
|
- app
|
39
48
|
- postgres
|
49
|
+
- mariadb
|
40
50
|
volumes:
|
41
51
|
- .:/app
|
42
52
|
postgres_primary:
|
@@ -70,9 +80,19 @@ services:
|
|
70
80
|
PRIMARY_DATABASE_HOST: postgres_primary
|
71
81
|
depends_on:
|
72
82
|
- postgres_primary
|
83
|
+
# TODO: create mysql replica images
|
84
|
+
mariadb:
|
85
|
+
image: mariadb:11.4
|
86
|
+
container_name: mysql_primary
|
87
|
+
environment:
|
88
|
+
MARIADB_ROOT_PASSWORD: mysql
|
89
|
+
MARIADB_DATABASE: mysql
|
90
|
+
ports:
|
91
|
+
- "3306:3306"
|
92
|
+
networks:
|
93
|
+
- mariadb
|
94
|
+
|
73
95
|
networks:
|
74
96
|
app:
|
75
97
|
postgres:
|
76
|
-
|
77
|
-
volumes:
|
78
|
-
postgres_primary:
|
98
|
+
mariadb:
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record/tasks/mysql2_proxy_database_tasks"
|
4
|
+
require "active_record/connection_adapters/mysql2_adapter"
|
5
|
+
require "active_record_proxy_adapters/active_record_context"
|
6
|
+
require "active_record_proxy_adapters/hijackable"
|
7
|
+
require "active_record_proxy_adapters/mysql2_proxy"
|
8
|
+
|
9
|
+
module ActiveRecord
|
10
|
+
module ConnectionAdapters
|
11
|
+
# This adapter is a proxy to the original Mysql2Adapter, allowing the use of the
|
12
|
+
# ActiveRecordProxyAdapters::PrimaryReplicaProxy.
|
13
|
+
class Mysql2ProxyAdapter < Mysql2Adapter
|
14
|
+
include ActiveRecordProxyAdapters::Hijackable
|
15
|
+
|
16
|
+
ADAPTER_NAME = "Mysql2Proxy"
|
17
|
+
|
18
|
+
delegate_to_proxy :execute, :exec_query
|
19
|
+
|
20
|
+
def initialize(...)
|
21
|
+
@proxy = ActiveRecordProxyAdapters::Mysql2Proxy.new(self)
|
22
|
+
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :proxy
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
if ActiveRecordProxyAdapters::ActiveRecordContext.active_record_v7_2_or_greater?
|
34
|
+
ActiveRecord::ConnectionAdapters.register(
|
35
|
+
"mysql2_proxy",
|
36
|
+
"ActiveRecord::ConnectionAdapters::Mysql2ProxyAdapter",
|
37
|
+
"active_record/connection_adapters/mysql2_proxy_adapter"
|
38
|
+
)
|
39
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record_proxy_adapters/database_tasks"
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module Tasks
|
7
|
+
# Defines the mysql tasks for dropping, creating, loading schema and dumping schema.
|
8
|
+
# Bypasses all the proxy logic to send all requests to primary.
|
9
|
+
class Mysql2ProxyDatabaseTasks < MySQLDatabaseTasks
|
10
|
+
include ActiveRecordProxyAdapters::DatabaseTasks
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Allow proxy adapter to run rake tasks, i.e. db:drop, db:create, db:schema:load db:migrate, etc...
|
16
|
+
ActiveRecord::Tasks::DatabaseTasks.register_task(
|
17
|
+
/mysql2_proxy/,
|
18
|
+
"ActiveRecord::Tasks::Mysql2ProxyDatabaseTasks"
|
19
|
+
)
|
@@ -1,39 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "active_record_proxy_adapters/database_tasks"
|
4
|
+
|
3
5
|
module ActiveRecord
|
4
6
|
module Tasks
|
5
7
|
# Defines the postgresql tasks for dropping, creating, loading schema and dumping schema.
|
6
8
|
# Bypasses all the proxy logic to send all requests to primary.
|
7
9
|
class PostgreSQLProxyDatabaseTasks < PostgreSQLDatabaseTasks
|
8
|
-
|
9
|
-
sticking_to_primary { super }
|
10
|
-
end
|
11
|
-
|
12
|
-
def drop(...)
|
13
|
-
sticking_to_primary { super }
|
14
|
-
end
|
15
|
-
|
16
|
-
def structure_dump(...)
|
17
|
-
sticking_to_primary { super }
|
18
|
-
end
|
19
|
-
|
20
|
-
def structure_load(...)
|
21
|
-
sticking_to_primary { super }
|
22
|
-
end
|
23
|
-
|
24
|
-
def purge(...)
|
25
|
-
sticking_to_primary { super }
|
26
|
-
end
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
def sticking_to_primary(&)
|
31
|
-
ActiveRecord::Base.connected_to(role: context.writing_role, &)
|
32
|
-
end
|
33
|
-
|
34
|
-
def context
|
35
|
-
ActiveRecordProxyAdapters::ActiveRecordContext.new
|
36
|
-
end
|
10
|
+
include ActiveRecordProxyAdapters::DatabaseTasks
|
37
11
|
end
|
38
12
|
end
|
39
13
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "active_record/connection_adapters/mysql2_proxy_adapter"
|
5
|
+
rescue LoadError
|
6
|
+
# mysql2 not available
|
7
|
+
return
|
8
|
+
end
|
9
|
+
|
10
|
+
module ActiveRecordProxyAdapters
|
11
|
+
# Module to extend ActiveRecord::Base with the connection handling methods.
|
12
|
+
# Required to make adapter work in ActiveRecord versions <= 7.2.x
|
13
|
+
module ConnectionHandling
|
14
|
+
def mysql2_proxy_adapter_class
|
15
|
+
::ActiveRecord::ConnectionAdapters::Mysql2ProxyAdapter
|
16
|
+
end
|
17
|
+
|
18
|
+
# This method is a copy and paste from Rails' mysql2_connection,
|
19
|
+
# replacing Mysql2Adapter by Mysql2ProxyAdapter
|
20
|
+
# This is required by ActiveRecord versions <= 7.2.x to establish a connection using the adapter.
|
21
|
+
def mysql2_proxy_connection(config) # rubocop:disable Metrics/MethodLength
|
22
|
+
config = config.symbolize_keys
|
23
|
+
config[:flags] ||= 0
|
24
|
+
|
25
|
+
if config[:flags].is_a? Array
|
26
|
+
config[:flags].push "FOUND_ROWS"
|
27
|
+
else
|
28
|
+
config[:flags] |= Mysql2::Client::FOUND_ROWS
|
29
|
+
end
|
30
|
+
|
31
|
+
mysql2_proxy_adapter_class.new(
|
32
|
+
mysql2_proxy_adapter_class.new_client(config),
|
33
|
+
logger,
|
34
|
+
nil,
|
35
|
+
config
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "active_record/connection_adapters/postgresql_proxy_adapter"
|
5
|
+
rescue LoadError
|
6
|
+
# Postgres not available
|
7
|
+
return
|
8
|
+
end
|
9
|
+
|
10
|
+
module ActiveRecordProxyAdapters
|
11
|
+
# Module to extend ActiveRecord::Base with the connection handling methods.
|
12
|
+
# Required to make adapter work in ActiveRecord versions <= 7.2.x
|
13
|
+
module ConnectionHandling
|
14
|
+
def postgresql_proxy_adapter_class
|
15
|
+
::ActiveRecord::ConnectionAdapters::PostgreSQLProxyAdapter
|
16
|
+
end
|
17
|
+
|
18
|
+
# This method is a copy and paste from Rails' postgresql_connection,
|
19
|
+
# replacing PostgreSQLAdapter by PostgreSQLProxyAdapter
|
20
|
+
# This is required by ActiveRecord versions <= 7.2.x to establish a connection using the adapter.
|
21
|
+
def postgresql_proxy_connection(config) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
22
|
+
conn_params = config.symbolize_keys.compact
|
23
|
+
|
24
|
+
# Map ActiveRecords param names to PGs.
|
25
|
+
conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
|
26
|
+
conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
|
27
|
+
|
28
|
+
# Forward only valid config params to PG::Connection.connect.
|
29
|
+
valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl]
|
30
|
+
conn_params.slice!(*valid_conn_param_keys)
|
31
|
+
|
32
|
+
postgresql_proxy_adapter_class.new(
|
33
|
+
postgresql_proxy_adapter_class.new_client(conn_params),
|
34
|
+
logger,
|
35
|
+
conn_params,
|
36
|
+
config
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -1,35 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
3
|
+
require "active_record_proxy_adapters/connection_handling/postgresql"
|
4
|
+
require "active_record_proxy_adapters/connection_handling/mysql2"
|
4
5
|
|
5
6
|
module ActiveRecordProxyAdapters
|
6
7
|
# Module to extend ActiveRecord::Base with the connection handling methods.
|
7
8
|
# Required to make adapter work in ActiveRecord versions <= 7.2.x
|
8
9
|
module ConnectionHandling
|
9
|
-
def postgresql_proxy_adapter_class
|
10
|
-
::ActiveRecord::ConnectionAdapters::PostgreSQLProxyAdapter
|
11
|
-
end
|
12
|
-
|
13
|
-
# This method is a copy and paste from Rails' postgresql_connection,
|
14
|
-
# replacing PostgreSQLAdapter by PostgreSQLProxyAdapter
|
15
|
-
# This is required by ActiveRecord versions <= 7.2.x to establish a connection using the adapter.
|
16
|
-
def postgresql_proxy_connection(config) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
17
|
-
conn_params = config.symbolize_keys.compact
|
18
|
-
|
19
|
-
# Map ActiveRecords param names to PGs.
|
20
|
-
conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
|
21
|
-
conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
|
22
|
-
|
23
|
-
# Forward only valid config params to PG::Connection.connect.
|
24
|
-
valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl]
|
25
|
-
conn_params.slice!(*valid_conn_param_keys)
|
26
|
-
|
27
|
-
postgresql_proxy_adapter_class.new(
|
28
|
-
postgresql_proxy_adapter_class.new_client(conn_params),
|
29
|
-
logger,
|
30
|
-
conn_params,
|
31
|
-
config
|
32
|
-
)
|
33
|
-
end
|
34
10
|
end
|
35
11
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordProxyAdapters
|
4
|
+
module DatabaseTasks # rubocop:disable Style/Documentation
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
def create(...)
|
9
|
+
sticking_to_primary { super }
|
10
|
+
end
|
11
|
+
|
12
|
+
def drop(...)
|
13
|
+
sticking_to_primary { super }
|
14
|
+
end
|
15
|
+
|
16
|
+
def structure_dump(...)
|
17
|
+
sticking_to_primary { super }
|
18
|
+
end
|
19
|
+
|
20
|
+
def structure_load(...)
|
21
|
+
sticking_to_primary { super }
|
22
|
+
end
|
23
|
+
|
24
|
+
def purge(...)
|
25
|
+
sticking_to_primary { super }
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def sticking_to_primary(&)
|
31
|
+
ActiveRecord::Base.connected_to(role: context.writing_role, &)
|
32
|
+
end
|
33
|
+
|
34
|
+
def context
|
35
|
+
ActiveRecordProxyAdapters::ActiveRecordContext.new
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -2,14 +2,13 @@
|
|
2
2
|
|
3
3
|
require "active_record/tasks/postgresql_proxy_database_tasks"
|
4
4
|
require "active_record/connection_adapters/postgresql_adapter"
|
5
|
-
require "active_record_proxy_adapters/primary_replica_proxy"
|
6
5
|
|
7
6
|
module ActiveRecordProxyAdapters
|
8
7
|
# Defines mixins to delegate specific methods from the proxy to the adapter.
|
9
8
|
module Hijackable
|
10
9
|
extend ActiveSupport::Concern
|
11
10
|
|
12
|
-
class_methods do
|
11
|
+
class_methods do # rubocop:disable Metrics/BlockLength
|
13
12
|
# Renames the methods from the original Adapter using the proxy suffix (_unproxied)
|
14
13
|
# and delegate the original method name to the proxy.
|
15
14
|
# Example: delegate_to_proxy(:execute) creates a method `execute_unproxied`,
|
@@ -32,10 +31,40 @@ module ActiveRecordProxyAdapters
|
|
32
31
|
delegate(*method_names, to: :proxy)
|
33
32
|
end
|
34
33
|
|
34
|
+
# Defines which methods should be hijacked from the original adapter and use the proxy
|
35
|
+
# @param method_names [Array<Symbol>] the list of method names from the adapter
|
36
|
+
def hijack_method(*method_names) # rubocop:disable Metrics/MethodLength
|
37
|
+
@hijacked_methods ||= Set.new
|
38
|
+
@hijacked_methods += Set.new(method_names)
|
39
|
+
|
40
|
+
method_names.each do |method_name|
|
41
|
+
define_method(method_name) do |*args, **kwargs, &block|
|
42
|
+
proxy_bypass_method = "#{method_name}#{unproxied_method_suffix}"
|
43
|
+
sql_string = coerce_query_to_string(args.first)
|
44
|
+
|
45
|
+
appropriate_connection(sql_string) do |conn|
|
46
|
+
method_to_call = conn == primary_connection ? proxy_bypass_method : method_name
|
47
|
+
|
48
|
+
conn.send(method_to_call, *args, **kwargs, &block)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def unproxied_method_suffix
|
55
|
+
"_unproxied"
|
56
|
+
end
|
57
|
+
|
35
58
|
private
|
36
59
|
|
37
60
|
def proxy_method_name_for(method_name)
|
38
|
-
:"#{method_name}#{
|
61
|
+
:"#{method_name}#{unproxied_method_suffix}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
included do
|
66
|
+
def unproxied_method_suffix
|
67
|
+
self.class.unproxied_method_suffix
|
39
68
|
end
|
40
69
|
end
|
41
70
|
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record_proxy_adapters/primary_replica_proxy"
|
4
|
+
|
5
|
+
module ActiveRecordProxyAdapters
|
6
|
+
# Proxy to the original Mysql2Adapter, allowing the use of the ActiveRecordProxyAdapters::PrimaryReplicaProxy.
|
7
|
+
class Mysql2Proxy < PrimaryReplicaProxy
|
8
|
+
end
|
9
|
+
end
|
@@ -7,8 +7,6 @@ module ActiveRecordProxyAdapters
|
|
7
7
|
# Proxy to the original PostgreSQLAdapter, allowing the use of the ActiveRecordProxyAdapters::PrimaryReplicaProxy.
|
8
8
|
class PostgreSQLProxy < PrimaryReplicaProxy
|
9
9
|
# ActiveRecord::PostgreSQLAdapter methods that should be proxied.
|
10
|
-
hijack_method :execute, :exec_query
|
11
|
-
|
12
10
|
hijack_method :exec_no_cache, :exec_cache unless ActiveRecordContext.active_record_v8_0_or_greater?
|
13
11
|
end
|
14
12
|
end
|
@@ -5,11 +5,14 @@ require "active_support/core_ext/module/delegation"
|
|
5
5
|
require "active_support/core_ext/object/blank"
|
6
6
|
require "concurrent-ruby"
|
7
7
|
require "active_record_proxy_adapters/active_record_context"
|
8
|
+
require "active_record_proxy_adapters/hijackable"
|
8
9
|
|
9
10
|
module ActiveRecordProxyAdapters
|
10
11
|
# This is the base class for all proxies. It defines the methods that should be proxied
|
11
12
|
# and the logic to determine which database to use.
|
12
13
|
class PrimaryReplicaProxy # rubocop:disable Metrics/ClassLength
|
14
|
+
include Hijackable
|
15
|
+
|
13
16
|
# All queries that match these patterns should be sent to the primary database
|
14
17
|
SQL_PRIMARY_MATCHERS = [
|
15
18
|
/\A\s*select.+for update\Z/i, /select.+lock in share mode\Z/i,
|
@@ -25,27 +28,9 @@ module ActiveRecordProxyAdapters
|
|
25
28
|
# requests to the primary database so the replica has time to replicate
|
26
29
|
WRITE_STATEMENT_MATCHERS = [/\ABEGIN/i, /\ACOMMIT/i, /INSERT\sINTO\s/i, /UPDATE\s/i, /DELETE\sFROM\s/i,
|
27
30
|
/DROP\s/i].map(&:freeze).freeze
|
28
|
-
UNPROXIED_METHOD_SUFFIX = "_unproxied"
|
29
|
-
|
30
|
-
# Defines which methods should be hijacked from the original adapter and use the proxy
|
31
|
-
# @param method_names [Array<Symbol>] the list of method names from the adapter
|
32
|
-
def self.hijack_method(*method_names) # rubocop:disable Metrics/MethodLength
|
33
|
-
@hijacked_methods ||= Set.new
|
34
|
-
@hijacked_methods += Set.new(method_names)
|
35
|
-
|
36
|
-
method_names.each do |method_name|
|
37
|
-
define_method(method_name) do |*args, **kwargs, &block|
|
38
|
-
proxy_bypass_method = "#{method_name}#{UNPROXIED_METHOD_SUFFIX}"
|
39
|
-
sql_string = coerce_query_to_string(args.first)
|
40
|
-
|
41
|
-
appropriate_connection(sql_string) do |conn|
|
42
|
-
method_to_call = conn == primary_connection ? proxy_bypass_method : method_name
|
43
31
|
|
44
|
-
|
45
|
-
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
32
|
+
# Abstract adapter methods that should be proxied.
|
33
|
+
hijack_method :execute, :exec_query
|
49
34
|
|
50
35
|
def self.hijacked_methods
|
51
36
|
@hijacked_methods.to_a
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
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.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matt Cruz
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date: 2025-
|
10
|
+
date: 2025-02-12 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: activerecord
|
@@ -67,17 +66,24 @@ files:
|
|
67
66
|
- Dockerfile
|
68
67
|
- LICENSE.txt
|
69
68
|
- README.md
|
69
|
+
- db/mysql_structure.sql
|
70
70
|
- db/postgresql_structure.sql
|
71
71
|
- docker-compose.yml
|
72
72
|
- docker/postgres_replica/cmd.sh
|
73
|
+
- lib/active_record/connection_adapters/mysql2_proxy_adapter.rb
|
73
74
|
- lib/active_record/connection_adapters/postgresql_proxy_adapter.rb
|
75
|
+
- lib/active_record/tasks/mysql2_proxy_database_tasks.rb
|
74
76
|
- lib/active_record/tasks/postgresql_proxy_database_tasks.rb
|
75
77
|
- lib/active_record_proxy_adapters.rb
|
76
78
|
- lib/active_record_proxy_adapters/active_record_context.rb
|
77
79
|
- lib/active_record_proxy_adapters/configuration.rb
|
78
80
|
- lib/active_record_proxy_adapters/connection_handling.rb
|
81
|
+
- lib/active_record_proxy_adapters/connection_handling/mysql2.rb
|
82
|
+
- lib/active_record_proxy_adapters/connection_handling/postgresql.rb
|
83
|
+
- lib/active_record_proxy_adapters/database_tasks.rb
|
79
84
|
- lib/active_record_proxy_adapters/hijackable.rb
|
80
85
|
- lib/active_record_proxy_adapters/log_subscriber.rb
|
86
|
+
- lib/active_record_proxy_adapters/mysql2_proxy.rb
|
81
87
|
- lib/active_record_proxy_adapters/postgresql_proxy.rb
|
82
88
|
- lib/active_record_proxy_adapters/primary_replica_proxy.rb
|
83
89
|
- lib/active_record_proxy_adapters/railtie.rb
|
@@ -94,7 +100,6 @@ metadata:
|
|
94
100
|
source_code_uri: https://github.com/Nasdaq/active_record_proxy_adapters
|
95
101
|
changelog_uri: https://github.com/Nasdaq/active_record_proxy_adapters/blob/main/CHANGELOG.md
|
96
102
|
rubygems_mfa_required: 'true'
|
97
|
-
post_install_message:
|
98
103
|
rdoc_options: []
|
99
104
|
require_paths:
|
100
105
|
- lib
|
@@ -109,8 +114,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
109
114
|
- !ruby/object:Gem::Version
|
110
115
|
version: '0'
|
111
116
|
requirements: []
|
112
|
-
rubygems_version: 3.
|
113
|
-
signing_key:
|
117
|
+
rubygems_version: 3.6.2
|
114
118
|
specification_version: 4
|
115
119
|
summary: Read replica proxy adapters for ActiveRecord!
|
116
120
|
test_files: []
|