active_record_proxy_adapters 0.2.1 → 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: f5fc4961ba1a8bce59f560d529b55eb965cd27971d92772f95f57b7a21bce30c
4
- data.tar.gz: 9c5c16ed9546f3be6df5e191e20483bb2177473b6c934ac589035a75771189e9
3
+ metadata.gz: 1f614128aa280355a9bc8c83e29805fc36c81f8389c017489c848e075a5510c3
4
+ data.tar.gz: d28585b2d7d1f7645dfc89007890a2c35ee19c4acc6de1b850e4f3b792cc0c07
5
5
  SHA512:
6
- metadata.gz: 8a97f02e6e212666c43490ebebe13957a9226103fb59ab2142de1905a10cc6357372314736f79fb86e141b5efdff0bfc1078a89e4205a52e53c80daf6df6b073
7
- data.tar.gz: b8d947651861fbbe6227c5ef6cc670c72a73bd58126c228d1cc6fe253327531e6b1ef299b51ea69402bd986380134be5d39308fdaaa9fc075f127b22ee37ee8d
6
+ metadata.gz: b3e36b3fe27bce5b1bdb9035fc399362f99d25062d97abc9024c02a30a8c6a328a2de3bdacda5682622c812f38f8b4a1da48d76e687131d3a6e07e44be4c6995
7
+ data.tar.gz: 053d2cfdea1db5699cd18404e8b22524a826012534eb0e077b307102021adeccde565f9e7b6417dbbe053669464a7220ab3e1e2cb3b92201cc0b296e8f23f5ce
data/CHANGELOG.md CHANGED
@@ -1,30 +1,41 @@
1
1
  ## [Unreleased]
2
2
 
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
9
+
3
10
  ## [0.2.1] - 2025-01-02
4
11
 
5
- - Fix replica connection pool getter when specific connection name is not found (847e150dd21c5bc619745ee1d9d8fcaa9b8f2eea)
12
+ - Fix replica connection pool getter when specific connection name is not found https://github.com/Nasdaq/active_record_proxy_adapters/commit/847e150dd21c5bc619745ee1d9d8fcaa9b8f2eea
6
13
 
7
14
  ## [0.2.0] - 2024-12-24
8
15
 
9
- - Add custom log subscriber to tag queries based on the adapter being used (68b8c1f4191388eb957bf12e0f84289da667e940)
16
+ - Add custom log subscriber to tag queries based on the adapter being used https://github.com/Nasdaq/active_record_proxy_adapters/commit/68b8c1f4191388eb957bf12e0f84289da667e940
17
+
18
+ ## [0.1.4] - 2025-01-02
19
+
20
+ - Fix replica connection pool getter when specific connection name is not found https://github.com/Nasdaq/active_record_proxy_adapters/commit/88b32a282b54d420e652f638656dbcf063ac8796
10
21
 
11
22
  ## [0.1.3] - 2024-12-24
12
23
 
13
- - Fix replica connection pool getter when database configurations have multiple replicas (ea5a33997da45ac073f166b3fbd2d12426053cd6)
14
- - Retrieve replica pool without checking out a connection (6470ef58e851082ae1f7a860ecdb5b451ef903c8)
24
+ - Fix replica connection pool getter when database configurations have multiple replicas https://github.com/Nasdaq/active_record_proxy_adapters/commit/ea5a33997da45ac073f166b3fbd2d12426053cd6
25
+ - Retrieve replica pool without checking out a connection https://github.com/Nasdaq/active_record_proxy_adapters/commit/6470ef58e851082ae1f7a860ecdb5b451ef903c8
15
26
 
16
27
  ## [0.1.2] - 2024-12-16
17
28
 
18
- - Fix CTE regex matcher (4b1d10bfd952fb1f5b102de8cc1a5bd05d25f5e9)
29
+ - Fix CTE regex matcher https://github.com/Nasdaq/active_record_proxy_adapters/commit/4b1d10bfd952fb1f5b102de8cc1a5bd05d25f5e9
19
30
 
20
31
  ## [0.1.1] - 2024-11-27
21
32
 
22
- - Enable RubyGems MFA (2a71b1f4354fb966cc0aa68231ca5837814e07ee)
33
+ - Enable RubyGems MFA https://github.com/Nasdaq/active_record_proxy_adapters/commit/2a71b1f4354fb966cc0aa68231ca5837814e07ee
23
34
 
24
35
  ## [0.1.0] - 2024-11-19
25
36
 
26
- - Add PostgreSQLProxyAdapter (2b3bb9f7359139519b32af3018ceb07fed8c6b33)
37
+ - Add PostgreSQLProxyAdapter https://github.com/Nasdaq/active_record_proxy_adapters/commit/2b3bb9f7359139519b32af3018ceb07fed8c6b33
27
38
 
28
39
  ## [0.1.0.rc2] - 2024-10-28
29
40
 
30
- - Add PostgreSQLProxyAdapter (2b3bb9f7359139519b32af3018ceb07fed8c6b33)
41
+ - Add PostgreSQLProxyAdapter https://github.com/Nasdaq/active_record_proxy_adapters/commit/2b3bb9f7359139519b32af3018ceb07fed8c6b33
data/Dockerfile CHANGED
@@ -8,7 +8,9 @@ RUN apk --update add \
8
8
  build-base \
9
9
  git \
10
10
  postgresql-dev \
11
- postgresql-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
@@ -1,5 +1,7 @@
1
1
  # ActiveRecordProxyAdapters
2
2
 
3
+ [![Run Test Suite](https://github.com/Nasdaq/active_record_proxy_adapters/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/Nasdaq/active_record_proxy_adapters/actions/workflows/test.yml)
4
+
3
5
  A set of ActiveRecord adapters that leverage Rails native multiple database setup to allow automatic connection switching from _one_ primary pool to _one_ replica pool at the database statement level.
4
6
 
5
7
  ## Why do I need this?
@@ -31,8 +33,19 @@ If bundler is not being used to manage dependencies, install the gem by executin
31
33
 
32
34
  ### On Rails
33
35
 
34
- 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
+
38
+ Currently supported adapters:
39
+
40
+ - `postgresql`
41
+ - `mysql2`
42
+
43
+ Coming soon:
44
+ - `trilogy`
45
+ - `sqlite`
46
+
35
47
 
48
+ #### PostgreSQL
36
49
  ```yaml
37
50
  # config/database.yml
38
51
  development:
@@ -46,6 +59,20 @@ development:
46
59
  # your replica credentials here
47
60
  ```
48
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
+
49
76
  ```ruby
50
77
  # app/models/application_record.rb
51
78
  class ApplicationRecord < ActiveRecord::Base
@@ -88,7 +115,7 @@ end
88
115
 
89
116
  ## Configuration
90
117
 
91
- 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:
92
119
 
93
120
  ```ruby
94
121
  # config/initializers/active_record_proxy_adapters.rb
@@ -237,6 +264,7 @@ end
237
264
 
238
265
  # app/models/portal.rb
239
266
  class Portal < ApplicationRecord
267
+ validates :name, uniqueness: true
240
268
  end
241
269
 
242
270
  # in rails console -e test
@@ -244,13 +272,17 @@ ActiveRecord::Base.logger.formatter = proc do |_severity, _time, _progname, msg|
244
272
  "[#{Time.current.iso8601} THREAD #{Thread.current[:name]}] #{msg}\n"
245
273
  end
246
274
 
275
+ ActiveRecordProxyAdapters.configure do |config|
276
+ config.proxy_delay = 2.seconds
277
+ end
278
+
247
279
  def read_your_own_writes
248
280
  proc do
249
281
  Portal.all.count # should go to the replica
250
- FactoryBot.create(:portal)
282
+ Portal.create(name: 'Read your own write')
251
283
 
252
284
  5.times do
253
- 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
254
286
  sleep(3)
255
287
  end
256
288
  end
@@ -295,9 +327,8 @@ irb(main):051:0> test_multithread_queries
295
327
  [2024-12-24T13:52:40-05:00 THREAD USE REPLICA] [PostgreSQL Replica] Portal Count (1.4ms) SELECT COUNT(*) FROM "portals"
296
328
  [2024-12-24T13:52:40-05:00 THREAD READ YOUR OWN WRITES] [PostgreSQL Replica] Portal Count (0.4ms) SELECT COUNT(*) FROM "portals"
297
329
  [2024-12-24T13:52:40-05:00 THREAD READ YOUR OWN WRITES] [PostgreSQLProxy Primary] TRANSACTION (0.5ms) BEGIN
298
- [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]]
299
- [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]]
300
- [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"]]
301
332
  [2024-12-24T13:52:40-05:00 THREAD READ YOUR OWN WRITES] [PostgreSQLProxy Primary] TRANSACTION (0.7ms) COMMIT
302
333
  [2024-12-24T13:52:40-05:00 THREAD READ YOUR OWN WRITES] [PostgreSQLProxy Primary] Portal Count (0.6ms) SELECT COUNT(*) FROM "portals"
303
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
+
@@ -1,6 +1,7 @@
1
1
  SET statement_timeout = 0;
2
2
  SET lock_timeout = 0;
3
3
  SET idle_in_transaction_session_timeout = 0;
4
+ SET transaction_timeout = 0;
4
5
  SET client_encoding = 'UTF8';
5
6
  SET standard_conforming_strings = on;
6
7
  SELECT pg_catalog.set_config('search_path', '', false);
@@ -9,16 +10,18 @@ SET xmloption = content;
9
10
  SET client_min_messages = warning;
10
11
  SET row_security = off;
11
12
 
12
- --
13
- -- Name: public; Type: SCHEMA; Schema: -; Owner: -
14
- --
13
+ SET default_tablespace = '';
15
14
 
16
- -- *not* creating schema, since initdb creates it
15
+ SET default_table_access_method = heap;
17
16
 
17
+ --
18
+ -- Name: schema_migrations; Type: TABLE; Schema: public; Owner: -
19
+ --
18
20
 
19
- SET default_tablespace = '';
21
+ CREATE TABLE public.schema_migrations (
22
+ version character varying(255) NOT NULL
23
+ );
20
24
 
21
- SET default_table_access_method = heap;
22
25
 
23
26
  --
24
27
  -- Name: users; Type: TABLE; Schema: public; Owner: -
@@ -68,6 +71,13 @@ ALTER TABLE ONLY public.users
68
71
  ADD CONSTRAINT users_pkey PRIMARY KEY (id);
69
72
 
70
73
 
74
+ --
75
+ -- Name: unique_schema_migrations; Type: INDEX; Schema: public; Owner: -
76
+ --
77
+
78
+ CREATE UNIQUE INDEX unique_schema_migrations ON public.schema_migrations USING btree (version);
79
+
80
+
71
81
  --
72
82
  -- PostgreSQL database dump complete
73
83
  --
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
@@ -62,7 +47,7 @@ module ActiveRecordProxyAdapters
62
47
 
63
48
  attr_reader :primary_connection, :last_write_at, :active_record_context
64
49
 
65
- delegate :connection_handler, :connected_to_stack, to: :connection_class
50
+ delegate :connection_handler, to: :connection_class
66
51
  delegate :reading_role, :writing_role, to: :active_record_context
67
52
 
68
53
  def replica_pool_unavailable?
@@ -120,11 +105,25 @@ module ActiveRecordProxyAdapters
120
105
  [reading_role, writing_role].include?(role) ? role : nil
121
106
  end
122
107
 
108
+ def connected_to_stack
109
+ return connection_class.connected_to_stack if connection_class.respond_to?(:connected_to_stack)
110
+
111
+ # handle Rails 7.2+ pending migrations Connection
112
+ return [{ role: writing_role }] if pending_migration_connection?
113
+
114
+ []
115
+ end
116
+
117
+ def pending_migration_connection?
118
+ active_record_context.active_record_v7_1_or_greater? &&
119
+ connection_class.name == "ActiveRecord::PendingMigrationConnection"
120
+ end
121
+
123
122
  def connection_for(role, sql_string)
124
123
  connection = primary_connection if role == writing_role || replica_pool_unavailable?
125
124
  connection ||= checkout_replica_connection
126
125
 
127
- result = yield(connection)
126
+ result = connected_to(role:) { yield connection }
128
127
 
129
128
  update_primary_latest_write_timestamp if !replica_connection?(connection) && write_statement?(sql_string)
130
129
 
@@ -133,6 +132,12 @@ module ActiveRecordProxyAdapters
133
132
  replica_connection?(connection) && replica_pool.checkin(connection)
134
133
  end
135
134
 
135
+ def connected_to(role:, &block)
136
+ return block.call unless connection_class.respond_to?(:connected_to)
137
+
138
+ connection_class.connected_to(role:, &block)
139
+ end
140
+
136
141
  def replica_connection?(connection)
137
142
  connection && connection != primary_connection
138
143
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordProxyAdapters
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -1,4 +1,4 @@
1
- FROM docker.io/postgres:14-alpine
1
+ FROM docker.io/postgres:17-alpine
2
2
 
3
3
  ARG REPLICA_USER=replicator
4
4
  ARG REPLICA_PASSWORD=replicator
@@ -1,4 +1,4 @@
1
- FROM docker.io/postgres:14-alpine
1
+ FROM docker.io/postgres:17-alpine
2
2
 
3
3
  ENV PRIMARY_DATABASE_HOST=localhost
4
4
  ENV PRIMARY_DATABASE_PORT=5432
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.1
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-02 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