active_record_proxy_adapters 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: efb046fb2816644ce048912c4c2666ae00f90fd728800f867cbe79bab99a59f3
4
- data.tar.gz: 3d193415b9063dcfb5633c8e1493c20742f8cfbf06bc0baa7efccb291c727b49
3
+ metadata.gz: 1f614128aa280355a9bc8c83e29805fc36c81f8389c017489c848e075a5510c3
4
+ data.tar.gz: d28585b2d7d1f7645dfc89007890a2c35ee19c4acc6de1b850e4f3b792cc0c07
5
5
  SHA512:
6
- metadata.gz: 8faf6efe63b55a9f00bbd4664f862abd3f0dd729f22b4e59306515a5dcd0fefbd42077cfaafc39f33f75d23eedd11a861e9284ba38c15868c69f72956d3acbbb
7
- data.tar.gz: 91717a29aca4d6c41c71436527adb72cac844a4d19a2707eb5111d4f5f8b42b3c017990bdcc77972b48a7fa32082b02f74328a4c86b4f87cebc776b1d1641a17
6
+ metadata.gz: b3e36b3fe27bce5b1bdb9035fc399362f99d25062d97abc9024c02a30a8c6a328a2de3bdacda5682622c812f38f8b4a1da48d76e687131d3a6e07e44be4c6995
7
+ data.tar.gz: 053d2cfdea1db5699cd18404e8b22524a826012534eb0e077b307102021adeccde565f9e7b6417dbbe053669464a7220ab3e1e2cb3b92201cc0b296e8f23f5ce
data/CHANGELOG.md CHANGED
@@ -1,7 +1,11 @@
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
+ - Add Mysql2ProxyAdapter
4
+
5
+ ## [0.2.2, 0.1.5] - 2025-01-02
6
+
7
+ - Handle PendingMigrationConnection introduced by Rails 7.2 and backported to Rails 7.1 https://github.com/Nasdaq/active_record_proxy_adapters/commit/793562694c05d554bad6e14637b34e5f9ffd2fc5
8
+ - Stick to same connection throughout request span https://github.com/Nasdaq/active_record_proxy_adapters/commit/789742fd7a33ecd555a995e8a1e1336455caec75
5
9
 
6
10
  ## [0.2.1] - 2025-01-02
7
11
 
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/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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_record/connection_adapters/postgresql_proxy_adapter"
4
+ require "active_record/connection_adapters/mysql2_proxy_adapter"
4
5
 
5
6
  module ActiveRecordProxyAdapters
6
7
  # Module to extend ActiveRecord::Base with the connection handling methods.
@@ -31,5 +32,30 @@ module ActiveRecordProxyAdapters
31
32
  config
32
33
  )
33
34
  end
35
+
36
+ def mysql2_proxy_adapter_class
37
+ ::ActiveRecord::ConnectionAdapters::Mysql2ProxyAdapter
38
+ end
39
+
40
+ # This method is a copy and paste from Rails' mysql2_connection,
41
+ # replacing Mysql2Adapter by Mysql2ProxyAdapter
42
+ # This is required by ActiveRecord versions <= 7.2.x to establish a connection using the adapter.
43
+ def mysql2_proxy_connection(config) # rubocop:disable Metrics/MethodLength
44
+ config = config.symbolize_keys
45
+ config[:flags] ||= 0
46
+
47
+ if config[:flags].is_a? Array
48
+ config[:flags].push "FOUND_ROWS"
49
+ else
50
+ config[:flags] |= Mysql2::Client::FOUND_ROWS
51
+ end
52
+
53
+ mysql2_proxy_adapter_class.new(
54
+ mysql2_proxy_adapter_class.new_client(config),
55
+ logger,
56
+ nil,
57
+ config
58
+ )
59
+ end
34
60
  end
35
61
  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.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.2.2
4
+ version: 0.3.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: 2025-01-04 00:00:00.000000000 Z
11
+ date: 2025-01-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -67,17 +67,22 @@ files:
67
67
  - Dockerfile
68
68
  - LICENSE.txt
69
69
  - README.md
70
+ - db/mysql_structure.sql
70
71
  - db/postgresql_structure.sql
71
72
  - docker-compose.yml
72
73
  - docker/postgres_replica/cmd.sh
74
+ - lib/active_record/connection_adapters/mysql2_proxy_adapter.rb
73
75
  - lib/active_record/connection_adapters/postgresql_proxy_adapter.rb
76
+ - lib/active_record/tasks/mysql2_proxy_database_tasks.rb
74
77
  - lib/active_record/tasks/postgresql_proxy_database_tasks.rb
75
78
  - lib/active_record_proxy_adapters.rb
76
79
  - lib/active_record_proxy_adapters/active_record_context.rb
77
80
  - lib/active_record_proxy_adapters/configuration.rb
78
81
  - lib/active_record_proxy_adapters/connection_handling.rb
82
+ - lib/active_record_proxy_adapters/database_tasks.rb
79
83
  - lib/active_record_proxy_adapters/hijackable.rb
80
84
  - lib/active_record_proxy_adapters/log_subscriber.rb
85
+ - lib/active_record_proxy_adapters/mysql2_proxy.rb
81
86
  - lib/active_record_proxy_adapters/postgresql_proxy.rb
82
87
  - lib/active_record_proxy_adapters/primary_replica_proxy.rb
83
88
  - lib/active_record_proxy_adapters/railtie.rb