active_record_proxy_adapters 0.3.6 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +4 -0
  3. data/.rubocop.yml +18 -0
  4. data/Appraisals +25 -0
  5. data/CHANGELOG.md +49 -0
  6. data/CODE_OF_CONDUCT.md +132 -0
  7. data/Dockerfile +21 -0
  8. data/db/mysql_structure.sql +41 -0
  9. data/db/postgresql_structure.sql +86 -0
  10. data/docker/postgres_replica/cmd.sh +9 -0
  11. data/docker-compose.yml +98 -0
  12. data/gemfiles/rails_7.0.gemfile +25 -0
  13. data/gemfiles/rails_7.0.gemfile.lock +122 -0
  14. data/gemfiles/rails_7.1.gemfile +21 -0
  15. data/gemfiles/rails_7.1.gemfile.lock +131 -0
  16. data/gemfiles/rails_7.2.gemfile +21 -0
  17. data/gemfiles/rails_7.2.gemfile.lock +129 -0
  18. data/gemfiles/rails_8.0.gemfile +21 -0
  19. data/gemfiles/rails_8.0.gemfile.lock +131 -0
  20. data/lib/active_record/connection_adapters/mysql2_proxy_adapter.rb +3 -1
  21. data/lib/active_record/connection_adapters/postgresql_proxy_adapter.rb +4 -1
  22. data/lib/active_record/connection_adapters/trilogy_proxy_adapter.rb +41 -0
  23. data/lib/active_record/tasks/trilogy_proxy_database_tasks.rb +19 -0
  24. data/lib/active_record_proxy_adapters/active_record_context.rb +0 -18
  25. data/lib/active_record_proxy_adapters/connection_handling/mysql2.rb +29 -23
  26. data/lib/active_record_proxy_adapters/connection_handling/postgresql.rb +27 -21
  27. data/lib/active_record_proxy_adapters/connection_handling/trilogy.rb +44 -0
  28. data/lib/active_record_proxy_adapters/connection_handling.rb +1 -7
  29. data/lib/active_record_proxy_adapters/hijackable.rb +3 -0
  30. data/lib/active_record_proxy_adapters/primary_replica_proxy.rb +8 -41
  31. data/lib/active_record_proxy_adapters/railtie.rb +1 -4
  32. data/lib/active_record_proxy_adapters/trilogy_proxy.rb +9 -0
  33. data/lib/active_record_proxy_adapters/version.rb +1 -1
  34. data/postgres_primary.dockerfile +34 -0
  35. data/postgres_replica.dockerfile +15 -0
  36. data/sig/active_record_proxy_adapters.rbs +4 -0
  37. metadata +28 -5
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "active_record/connection_adapters/trilogy_proxy_adapter"
5
+ rescue LoadError
6
+ # trilogy not available
7
+ return
8
+ end
9
+
10
+ module ActiveRecordProxyAdapters
11
+ module Trilogy
12
+ # Module to extend ActiveRecord::Base with the connection handling methods.
13
+ # Required to make adapter work in ActiveRecord versions <= 7.2.x
14
+ module ConnectionHandling
15
+ def trilogy_proxy_adapter_class
16
+ ActiveRecord::ConnectionAdapters::TrilogyProxyAdapter
17
+ end
18
+
19
+ def trilogy_proxy_connection(config) # rubocop:disable Metrics/MethodLength
20
+ configuration = config.dup
21
+
22
+ # Set FOUND_ROWS capability on the connection so UPDATE queries returns number of rows
23
+ # matched rather than number of rows updated.
24
+ configuration[:found_rows] = true
25
+
26
+ options = [
27
+ configuration[:host],
28
+ configuration[:port],
29
+ configuration[:database],
30
+ configuration[:username],
31
+ configuration[:password],
32
+ configuration[:socket],
33
+ 0
34
+ ]
35
+
36
+ trilogy_proxy_adapter_class.new nil, logger, options, configuration
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ ActiveSupport.on_load(:active_record) do
43
+ ActiveRecord::Base.extend(ActiveRecordProxyAdapters::Trilogy::ConnectionHandling)
44
+ end
@@ -2,10 +2,4 @@
2
2
 
3
3
  require "active_record_proxy_adapters/connection_handling/postgresql"
4
4
  require "active_record_proxy_adapters/connection_handling/mysql2"
5
-
6
- module ActiveRecordProxyAdapters
7
- # Module to extend ActiveRecord::Base with the connection handling methods.
8
- # Required to make adapter work in ActiveRecord versions <= 7.2.x
9
- module ConnectionHandling
10
- end
11
- end
5
+ require "active_record_proxy_adapters/connection_handling/trilogy"
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_record/tasks/postgresql_proxy_database_tasks"
4
+ require "active_record/connection_adapters/postgresql_adapter"
5
+
3
6
  module ActiveRecordProxyAdapters
4
7
  # Defines mixins to delegate specific methods from the proxy to the adapter.
5
8
  module Hijackable
@@ -18,30 +18,19 @@ module ActiveRecordProxyAdapters
18
18
  /\A\s*select.+for update\Z/i, /select.+lock in share mode\Z/i,
19
19
  /\A\s*select.+(nextval|currval|lastval|get_lock|release_lock|pg_advisory_lock|pg_advisory_unlock)\(/i
20
20
  ].map(&:freeze).freeze
21
-
22
- CTE_MATCHER = /\A\s*WITH\s+(?<CTE>\S+\s+AS\s+\(\s?[\s\S]*\))/i
23
21
  # All queries that match these patterns should be sent to the replica database
24
- SQL_REPLICA_MATCHERS = [
25
- /\A\s*(select)\s/i,
26
- /#{CTE_MATCHER.source}\s*select/i
27
- ].map(&:freeze).freeze
22
+ SQL_REPLICA_MATCHERS = [/\A\s*(select|with\s[\s\S]*\)\s*select)\s/i].map(&:freeze).freeze
28
23
  # All queries that match these patterns should be sent to all databases
29
24
  SQL_ALL_MATCHERS = [/\A\s*set\s/i].map(&:freeze).freeze
30
25
  # Local sets queries should not be sent to all datbases
31
26
  SQL_SKIP_ALL_MATCHERS = [/\A\s*set\s+local\s/i].map(&:freeze).freeze
32
27
  # These patterns define which database statments are considered write statments, so we can shortly re-route all
33
28
  # requests to the primary database so the replica has time to replicate
34
- WRITE_STATEMENT_MATCHERS = [
35
- /\ABEGIN/i,
36
- /\ACOMMIT/i,
37
- /INSERT\s[\s\S]*INTO\s[\s\S]*/i,
38
- /UPDATE\s[\s\S]*/i,
39
- /DELETE\s[\s\S]*FROM\s[\s\S]*/i,
40
- /DROP\s/i
41
- ].map(&:freeze).freeze
29
+ WRITE_STATEMENT_MATCHERS = [/\ABEGIN/i, /\ACOMMIT/i, /INSERT\sINTO\s/i, /UPDATE\s/i, /DELETE\sFROM\s/i,
30
+ /DROP\s/i].map(&:freeze).freeze
42
31
 
43
32
  # Abstract adapter methods that should be proxied.
44
- hijack_method(*ActiveRecordContext.hijackable_methods)
33
+ hijack_method :execute, :exec_query
45
34
 
46
35
  def self.hijacked_methods
47
36
  @hijacked_methods.to_a
@@ -61,22 +50,6 @@ module ActiveRecordProxyAdapters
61
50
  delegate :connection_handler, to: :connection_class
62
51
  delegate :reading_role, :writing_role, to: :active_record_context
63
52
 
64
- # We need to call .verify! to ensure `configure_connection` is called on the instance before attempting to use it.
65
- # This is necessary because the connection may have been lazily initialized and is an unintended side effect from a
66
- # change in Rails to defer connection verification: https://github.com/rails/rails/pull/44576
67
- # verify! cannot be called before the object is initialized and because of how the proxy hooks into the connection
68
- # instance, it has to be done lazily (hence the memoization). Ideally, we shouldn't have to worry about this at all
69
- # But there is tight coupling between methods in ActiveRecord::ConnectionAdapters::AbstractAdapter and
70
- # its descendants which will require significant refactoring to be decoupled.
71
- # See https://github.com/rails/rails/issues/51780
72
- def verified_primary_connection
73
- @verified_primary_connection ||= begin
74
- connected_to(role: writing_role) { primary_connection.verify! }
75
-
76
- primary_connection
77
- end
78
- end
79
-
80
53
  def replica_pool_unavailable?
81
54
  !replica_pool
82
55
  end
@@ -147,8 +120,7 @@ module ActiveRecordProxyAdapters
147
120
  end
148
121
 
149
122
  def connection_for(role, sql_string)
150
- connection = verified_primary_connection if role == writing_role || replica_pool_unavailable?
151
-
123
+ connection = primary_connection if role == writing_role || replica_pool_unavailable?
152
124
  connection ||= checkout_replica_connection
153
125
 
154
126
  result = connected_to(role:) { yield connection }
@@ -182,21 +154,16 @@ module ActiveRecordProxyAdapters
182
154
  # @return [TrueClass] if there has been a write within the last {#proxy_delay} seconds
183
155
  # @return [TrueClass] if sql_string matches a write statement (i.e. INSERT, UPDATE, DELETE, SELECT FOR UPDATE)
184
156
  # @return [FalseClass] if sql_string matches a read statement (i.e. SELECT)
185
- def need_primary?(sql_string) # rubocop:disable Metrics/CyclomaticComplexity
186
- return true if recent_write_to_primary?
157
+ def need_primary?(sql_string)
158
+ return true if recent_write_to_primary?
159
+
187
160
  return true if in_transaction?
188
- return true if cte_for_write?(sql_string)
189
161
  return true if SQL_PRIMARY_MATCHERS.any?(&match_sql?(sql_string))
190
162
  return false if SQL_REPLICA_MATCHERS.any?(&match_sql?(sql_string))
191
163
 
192
164
  true
193
165
  end
194
166
 
195
- def cte_for_write?(sql_string)
196
- CTE_MATCHER.match?(sql_string) &&
197
- WRITE_STATEMENT_MATCHERS.any?(&match_sql?(sql_string))
198
- end
199
-
200
167
  def need_all?(sql_string)
201
168
  return false if SQL_SKIP_ALL_MATCHERS.any?(&match_sql?(sql_string))
202
169
 
@@ -5,10 +5,7 @@ require "active_support"
5
5
  module ActiveRecordProxyAdapters
6
6
  # Hooks into rails boot process to extend ActiveRecord with the proxy adapter.
7
7
  class Railtie < Rails::Railtie
8
- ActiveSupport.on_load :active_record do
9
- require "active_record_proxy_adapters/connection_handling"
10
- ActiveRecord::Base.extend(ActiveRecordProxyAdapters::ConnectionHandling)
11
- end
8
+ require "active_record_proxy_adapters/connection_handling"
12
9
 
13
10
  config.to_prepare do
14
11
  Rails.autoloaders.each do |autoloader|
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record_proxy_adapters/mysql2_proxy"
4
+
5
+ module ActiveRecordProxyAdapters
6
+ # Proxy to the Mysql2Proxy, allowing the use of the ActiveRecordProxyAdapters::PrimaryReplicaProxy.
7
+ class TrilogyProxy < Mysql2Proxy
8
+ end
9
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordProxyAdapters
4
- VERSION = "0.3.6"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -0,0 +1,34 @@
1
+ FROM docker.io/postgres:17-alpine
2
+
3
+ ARG REPLICA_USER=replicator
4
+ ARG REPLICA_PASSWORD=replicator
5
+ ARG REPLICATION_SLOT_NAME=replication_slot
6
+ ARG INIT_SQL=00_init.sql
7
+ ARG POSTGRES_LOGGING_COLLECTOR=
8
+ ARG POSTGRES_LOG_DESTINATION=
9
+ ARG POSTGRES_LOG_STATEMENT=
10
+ ENV CONF_SAMPLE="/usr/local/share/postgresql/postgresql.conf.sample"
11
+
12
+ WORKDIR /docker-entrypoint-initdb.d
13
+
14
+ USER root
15
+
16
+ RUN touch $INIT_SQL
17
+ RUN chown -R postgres:postgres $INIT_SQL
18
+ RUN echo "CREATE USER ${REPLICA_USER} WITH REPLICATION ENCRYPTED PASSWORD '${REPLICA_PASSWORD}';" > $INIT_SQL
19
+ RUN echo "SELECT pg_create_physical_replication_slot('${REPLICATION_SLOT_NAME}');" >> $INIT_SQL
20
+
21
+ # Enable logging collector if given
22
+ RUN if [[ ! -z "${POSTGRES_LOGGING_COLLECTOR}" ]]; then sed -i "s/#\(logging_collector = \)off\(.*\)/\1${POSTGRES_LOGGING_COLLECTOR}\2/" ${CONF_SAMPLE}; fi
23
+
24
+ # Override default log destination if given
25
+ RUN if [[ ! -z "${POSTGRES_LOG_DESTINATION}" ]]; then sed -i "s/#\(log_destination = \)'stderr'\(.*\)/\1'${POSTGRES_LOG_DESTINATION}'\2/" ${CONF_SAMPLE}; fi
26
+
27
+ # Override log statement if given
28
+ RUN if [[ ! -z "${POSTGRES_LOG_STATEMENT}" ]]; then sed -i "s/#\(log_statement = \)'none'\(.*\)/\1'${POSTGRES_LOG_STATEMENT}'\2/" ${CONF_SAMPLE}; fi
29
+
30
+ WORKDIR /
31
+
32
+ USER postgres
33
+
34
+ CMD ["postgres", "-c", "wal_level=replica", "-c", "hot_standby=on", "-c", "max_wal_senders=10", "-c", "max_replication_slots=10", "-c", "hot_standby_feedback=on" ]
@@ -0,0 +1,15 @@
1
+ FROM docker.io/postgres:17-alpine
2
+
3
+ ENV PRIMARY_DATABASE_HOST=localhost
4
+ ENV PRIMARY_DATABASE_PORT=5432
5
+ ENV PRIMARY_REPLICATION_SLOT=replication_slot
6
+
7
+
8
+ COPY docker/postgres_replica/cmd.sh cmd.sh
9
+
10
+ USER root
11
+ RUN chown -R postgres:postgres cmd.sh
12
+ USER postgres
13
+ RUN chmod u+x cmd.sh
14
+
15
+ CMD [ "./cmd.sh" ]
@@ -0,0 +1,4 @@
1
+ module ActiveRecordProxyAdapters
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata CHANGED
@@ -1,13 +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.3.6
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Cruz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-28 00:00:00.000000000 Z
10
+ date: 2025-02-25 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activerecord
@@ -57,22 +57,41 @@ email:
57
57
  - matt.cruz@nasdaq.com
58
58
  executables: []
59
59
  extensions: []
60
- extra_rdoc_files:
61
- - README.md
62
- - LICENSE.txt
60
+ extra_rdoc_files: []
63
61
  files:
62
+ - ".rspec"
63
+ - ".rubocop.yml"
64
+ - Appraisals
65
+ - CHANGELOG.md
66
+ - CODE_OF_CONDUCT.md
67
+ - Dockerfile
64
68
  - LICENSE.txt
65
69
  - README.md
70
+ - db/mysql_structure.sql
71
+ - db/postgresql_structure.sql
72
+ - docker-compose.yml
73
+ - docker/postgres_replica/cmd.sh
74
+ - gemfiles/rails_7.0.gemfile
75
+ - gemfiles/rails_7.0.gemfile.lock
76
+ - gemfiles/rails_7.1.gemfile
77
+ - gemfiles/rails_7.1.gemfile.lock
78
+ - gemfiles/rails_7.2.gemfile
79
+ - gemfiles/rails_7.2.gemfile.lock
80
+ - gemfiles/rails_8.0.gemfile
81
+ - gemfiles/rails_8.0.gemfile.lock
66
82
  - lib/active_record/connection_adapters/mysql2_proxy_adapter.rb
67
83
  - lib/active_record/connection_adapters/postgresql_proxy_adapter.rb
84
+ - lib/active_record/connection_adapters/trilogy_proxy_adapter.rb
68
85
  - lib/active_record/tasks/mysql2_proxy_database_tasks.rb
69
86
  - lib/active_record/tasks/postgresql_proxy_database_tasks.rb
87
+ - lib/active_record/tasks/trilogy_proxy_database_tasks.rb
70
88
  - lib/active_record_proxy_adapters.rb
71
89
  - lib/active_record_proxy_adapters/active_record_context.rb
72
90
  - lib/active_record_proxy_adapters/configuration.rb
73
91
  - lib/active_record_proxy_adapters/connection_handling.rb
74
92
  - lib/active_record_proxy_adapters/connection_handling/mysql2.rb
75
93
  - lib/active_record_proxy_adapters/connection_handling/postgresql.rb
94
+ - lib/active_record_proxy_adapters/connection_handling/trilogy.rb
76
95
  - lib/active_record_proxy_adapters/database_tasks.rb
77
96
  - lib/active_record_proxy_adapters/hijackable.rb
78
97
  - lib/active_record_proxy_adapters/log_subscriber.rb
@@ -80,7 +99,11 @@ files:
80
99
  - lib/active_record_proxy_adapters/postgresql_proxy.rb
81
100
  - lib/active_record_proxy_adapters/primary_replica_proxy.rb
82
101
  - lib/active_record_proxy_adapters/railtie.rb
102
+ - lib/active_record_proxy_adapters/trilogy_proxy.rb
83
103
  - lib/active_record_proxy_adapters/version.rb
104
+ - postgres_primary.dockerfile
105
+ - postgres_replica.dockerfile
106
+ - sig/active_record_proxy_adapters.rbs
84
107
  homepage: https://github.com/Nasdaq/active_record_proxy_adapters
85
108
  licenses:
86
109
  - MIT