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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: efb046fb2816644ce048912c4c2666ae00f90fd728800f867cbe79bab99a59f3
4
- data.tar.gz: 3d193415b9063dcfb5633c8e1493c20742f8cfbf06bc0baa7efccb291c727b49
3
+ metadata.gz: 151caa80f35f53ad02ef3307a55ef0fec4f1a6f369e6afac8d063e4ea2ad5e7f
4
+ data.tar.gz: 60213ad52a363c435514038eeb65227bff7132b91a6dbed86b8917459449c39e
5
5
  SHA512:
6
- metadata.gz: 8faf6efe63b55a9f00bbd4664f862abd3f0dd729f22b4e59306515a5dcd0fefbd42077cfaafc39f33f75d23eedd11a861e9284ba38c15868c69f72956d3acbbb
7
- data.tar.gz: 91717a29aca4d6c41c71436527adb72cac844a4d19a2707eb5111d4f5f8b42b3c017990bdcc77972b48a7fa32082b02f74328a4c86b4f87cebc776b1d1641a17
6
+ metadata.gz: 7720b6fe6b2d6848d30444f77c71d2567996f2e45011c79e6c4791e043b7d9333c2d92554cdf5f6b5a48c29c6b384509a00b9f9b3b930402d47c98188744daa7
7
+ data.tar.gz: b73be535ad798afb4386d4027e4e92df00d9864a196d4cc1d30ad3a0a4e524c87ad12f67d214ef524a1641800ccf5bc2ab4911acf49c5606d867f51981a18fec
data/CHANGELOG.md CHANGED
@@ -1,7 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
- - Handle PendingMigrationConnection introduced by Rails 7.2 and backported to Rails 7.1
4
- - Stick to same connection throughout request span
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
@@ -8,7 +8,9 @@ RUN apk --update add \
8
8
  build-base \
9
9
  git \
10
10
  postgresql-dev \
11
- postgresql17-client
11
+ postgresql17-client \
12
+ mariadb-client \
13
+ mariadb-dev
12
14
  RUN gem install bundler -v 2.5.13
13
15
 
14
16
  COPY . /app
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2024 Nasdaq
3
+ Copyright (c) 2024 Nasdaq, Inc.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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 `postgresql_proxy` as the adapter for the `primary` database, and keep `postgresql` for the replica database.
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 them by using a `.configure` block:
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
- FactoryBot.create(:portal)
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 3 should go to the replica
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? (1.2ms) SELECT 1 AS one FROM "portals" WHERE "portals"."id" IS NOT NULL AND "portals"."slug" = $1 LIMIT $2 [["slug", "portal-e065948fbbee73d3b2c576b48c2b37e021115158edc6a92390d613640460e1d4"], ["LIMIT", 1]]
301
- [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", "Portal-e065948fbbee73d3b2c576b48c2b37e021115158edc6a92390d613640460e1d4"], ["LIMIT", 1]]
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
- 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
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 "active_record/connection_adapters/postgresql_proxy_adapter"
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}#{ActiveRecordProxyAdapters::PrimaryReplicaProxy::UNPROXIED_METHOD_SUFFIX}"
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
- conn.send(method_to_call, *args, **kwargs, &block)
45
- end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordProxyAdapters
4
- VERSION = "0.2.2"
4
+ VERSION = "0.3.1"
5
5
  end
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.2.2
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-01-04 00:00:00.000000000 Z
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.5.11
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: []