janus-ar 8.1.0 → 8.1.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: cd05e24e1306fce67609a9e991fddad6c4d7014ce58f696a01eb64317838e837
4
- data.tar.gz: d07db4e1d21a6363ed9f0f1ea41736b1d71d1ae0fd6c419f34f15e5f74e8825f
3
+ metadata.gz: 1720a88365b20f91d220065128ca260baf91380779a7a1d9a6f9aa8fffc13ae8
4
+ data.tar.gz: 2987d7e690b1038946a489a80df29917a192c1a07b85bc16932fb0bf8c2df531
5
5
  SHA512:
6
- metadata.gz: 9bc3a5029f1b51374051a2c6f8ccc743192780bae4eaaafe18ea6578d9fb77826760ae95a6714bb5ea8f3cd128ef51a3247d39416ae78b8af7b5554ee652ab64
7
- data.tar.gz: 0fc71666876765b84cc70ddc4ffc016cd175ae8594fb68dd0a442b047a27aced86e5597ebb1fcf3cebc10d00e81081f8ff63122fcacc4eefa680a89c0fdc4ca2
6
+ metadata.gz: ae79f4f2c54ea96d6191a2b7597982a558027ed911d894e66bbb205041d2339e0b33bb9004ac8331162c2766ac91c922f1ebd11e125ecd18e2f30db8b8cf173c
7
+ data.tar.gz: 1224b98a5c03641329d47f1eee868247141cfd6299f98a1f364aed01bef7eb4c3897c4a1e3675734a04aabde3b4ca8165ddb58fe07f36c4f97691c8fcec89e17
data/Gemfile CHANGED
@@ -1,4 +1,2 @@
1
- # frozen_string_literal: true
2
-
3
1
  source 'https://rubygems.org'
4
2
  gemspec
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- janus-ar (8.1.0)
4
+ janus-ar (8.1.1)
5
5
  activerecord (>= 8.0, < 9.0)
6
6
 
7
7
  GEM
@@ -51,6 +51,8 @@ GEM
51
51
  parser (3.3.12.0)
52
52
  ast (~> 2.4.1)
53
53
  racc
54
+ pg (1.6.3-arm64-darwin)
55
+ pg (1.6.3-x86_64-linux)
54
56
  prism (1.9.0)
55
57
  pry (0.16.0)
56
58
  coderay (~> 1.1)
@@ -128,6 +130,7 @@ DEPENDENCIES
128
130
  activesupport (>= 8.0)
129
131
  janus-ar!
130
132
  mysql2
133
+ pg
131
134
  pry
132
135
  rake
133
136
  rspec (~> 3)
data/README.md CHANGED
@@ -11,13 +11,13 @@
11
11
  [![CI](https://github.com/OLIOEX/janus-ar/actions/workflows/ci.yml/badge.svg)](https://github.com/OLIOEX/janus-ar/actions/workflows/ci.yml)
12
12
  [![Gem Version](https://badge.fury.io/rb/janus-ar.svg)](https://badge.fury.io/rb/janus-ar)
13
13
 
14
- Janus ActiveRecord is a generic primary/replica proxy for ActiveRecord 8 and MySQL (via `mysql2` and `trilogy`). It handles the switching of connections between primary and replica database servers. It comes with an ActiveRecord database adapter implementation.
14
+ Janus ActiveRecord is a generic primary/replica proxy for ActiveRecord 8, supporting MySQL (via `mysql2` and `trilogy`) and PostgreSQL (via `pg`). It handles the switching of connections between primary and replica database servers. It comes with an ActiveRecord database adapter implementation.
15
15
 
16
16
  Janus is heavily inspired by [Makara](https://github.com/instacart/makara) from TaskRabbit and then Instacart. Unfortunately this project is unmaintained and broke for us with Rails 7.1. This is an attempt to start afresh on the project. It is definitely not as fully featured as Makara at this stage.
17
17
 
18
18
  Learn more about its origins: [https://tech.olioex.com/ruby/2024/04/16/introducing-janus.html](https://tech.olioex.com/ruby/2024/04/16/introducing-janus.html).
19
19
 
20
- Notes: the gem requires ActiveRecord `>= 8.0, < 9.0` and Ruby `>= 3.2`, and is tested against MySQL 8.
20
+ Notes: the gem requires ActiveRecord `>= 8.0, < 9.0` and Ruby `>= 3.2`, and is tested against MySQL 8 and PostgreSQL 16 and 17.
21
21
 
22
22
  ## Installation
23
23
 
@@ -45,6 +45,8 @@ require 'rails/all'
45
45
  ActiveRecord::ConnectionAdapters.register("janus_trilogy", "ActiveRecord::ConnectionAdapters::JanusTrilogyAdapter", 'janus-ar/active_record/connection_adapters/janus_trilogy_adapter')
46
46
  # ...or...
47
47
  ActiveRecord::ConnectionAdapters.register("janus_mysql2", "ActiveRecord::ConnectionAdapters::JanusMysql2Adapter", 'janus-ar/active_record/connection_adapters/janus_mysql2_adapter')
48
+ # ...or...
49
+ ActiveRecord::ConnectionAdapters.register("janus_postgresql", "ActiveRecord::ConnectionAdapters::JanusPostgreSQLAdapter", 'janus-ar/active_record/connection_adapters/janus_postgresql_adapter')
48
50
  ```
49
51
 
50
52
  #### Rails <= 7.1
@@ -81,6 +83,45 @@ Note: For `trilogy` please use adapter "janus_trilogy". You'll probably need to
81
83
  Otherwise you will get an error like the following (see https://github.com/trilogy-libraries/trilogy/issues/26):
82
84
  > trilogy_auth_recv: caching_sha2_password requires either TCP with TLS or a unix socket: TRILOGY_UNSUPPORTED"
83
85
 
86
+ #### PostgreSQL
87
+
88
+ Use adapter `janus_postgresql`, and add the `pg` gem to your `Gemfile`:
89
+
90
+ ```yml
91
+ development:
92
+ adapter: janus_postgresql
93
+ database: database_name
94
+ janus:
95
+ primary:
96
+ <<: *default
97
+ host: primary-host.local
98
+ username: app
99
+ password: primary_password
100
+ replica:
101
+ <<: *default
102
+ host: replica-host.local
103
+ username: app_readonly
104
+ password: replica_password
105
+ ```
106
+
107
+ Anything the adapter reads out of its own configuration — `pool`, `prepared_statements`,
108
+ `insert_returning`, `variables`, `schema_search_path`, SSL settings and so on — must go
109
+ inside the `primary:` and `replica:` blocks rather than alongside `adapter:`, because each
110
+ connection is built from its own block.
111
+
112
+ Two things worth knowing about the PostgreSQL adapter specifically:
113
+
114
+ * Unlike MySQL, PostgreSQL never inlines bind values into the statement: reads
115
+ reach the replica as `$1` placeholders plus a separate parameter list, and
116
+ prepared statements are cached per connection. The adapter forwards binds and
117
+ the prepare flag to the replica, so each connection maintains its own
118
+ statement cache.
119
+ * Type OIDs are resolved against whichever connection served the lookup. This is
120
+ correct for a physical (streaming) replica, where OIDs are identical to the
121
+ primary's by construction. If you point Janus at a logical replica whose custom
122
+ types, enums or extensions were created independently, the OIDs can diverge and
123
+ results may be cast with the wrong type.
124
+
84
125
  ### Forcing connections
85
126
 
86
127
  A context is local to the current unit of work (thread or fiber, following ActiveRecord's configured isolation level). This allows you to stick to the primary safely within a single request or job, in systems such as Sidekiq for instance.
@@ -119,7 +160,9 @@ There are some edge cases:
119
160
  * `SET` operations will be sent to all connections
120
161
  * Execution of specific methods such as `connect!`, `disconnect!`, `reconnect!`, and `clear_cache!` are invoked on all underlying connections
121
162
  * Calls inside a transaction will always be sent to the primary (otherwise changes from within the transaction could not be read back on most transaction isolation levels)
122
- * Locking reads (e.g. `SELECT ... FOR UPDATE`, `FOR UPDATE SKIP LOCKED`, `FOR SHARE`, `LOCK IN SHARE MODE`, `GET_LOCK(...)`) will always be sent to the primary
163
+ * Locking reads (e.g. `SELECT ... FOR UPDATE`, `FOR UPDATE SKIP LOCKED`, `FOR SHARE`, `FOR NO KEY UPDATE`, `FOR KEY SHARE`, `LOCK IN SHARE MODE`) will always be sent to the primary
164
+ * So will reads that call a locking or sequence function — `GET_LOCK(...)`, `IS_FREE_LOCK(...)`, `nextval(...)`, `setval(...)` and PostgreSQL's advisory lock family (`pg_advisory_lock`, `pg_try_advisory_xact_lock_shared`, `pg_advisory_unlock_all`, and so on)
165
+ * PostgreSQL cursor statements (`DECLARE`, `FETCH`, `MOVE`, `CLOSE`) go to the primary, so a cursor is always fetched on the connection that declared it
123
166
 
124
167
  # Notes
125
168
 
data/Rakefile CHANGED
@@ -1,3 +1 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'bundler/gem_tasks'
data/janus-ar.gemspec CHANGED
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require File.expand_path('lib/janus-ar/version.rb', __dir__)
4
2
 
5
3
  Gem::Specification.new do |gem|
@@ -26,6 +24,7 @@ Gem::Specification.new do |gem|
26
24
  gem.add_dependency 'activerecord', '>= 8.0', '< 9.0'
27
25
  gem.add_development_dependency 'activesupport', '>= 8.0'
28
26
  gem.add_development_dependency 'mysql2'
27
+ gem.add_development_dependency 'pg'
29
28
  gem.add_development_dependency 'trilogy'
30
29
  gem.add_development_dependency 'pry'
31
30
  gem.add_development_dependency 'rake'
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'active_record/connection_adapters/abstract_adapter'
4
2
  require 'active_record/connection_adapters/mysql2_adapter'
5
3
  require_relative '../../../janus-ar'
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/connection_adapters/abstract_adapter'
4
+ require 'active_record/connection_adapters/postgresql_adapter'
5
+ require_relative '../../../janus-ar'
6
+ require_relative '../../adapter_extensions'
7
+
8
+ module ActiveRecord
9
+ module ConnectionHandling
10
+ def janus_postgresql_connection(config)
11
+ ActiveRecord::ConnectionAdapters::JanusPostgreSQLAdapter.new(config)
12
+ end
13
+ end
14
+
15
+ class Base
16
+ def self.janus_postgresql_adapter_class
17
+ ActiveRecord::ConnectionAdapters::JanusPostgreSQLAdapter
18
+ end
19
+ end
20
+
21
+ module ConnectionAdapters
22
+ class JanusPostgreSQLAdapter < ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
23
+ include Janus::AdapterExtensions
24
+
25
+ private
26
+
27
+ def replica_adapter_class
28
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
29
+ end
30
+
31
+ # PostgreSQL compiles statements to `$1` placeholders and passes the values
32
+ # separately, so the replica needs the whole call, not just the SQL.
33
+ #
34
+ # `raw_execute` is private on a stock adapter, hence the `send`. It is the
35
+ # right target rather than the public `execute`: the statement has already
36
+ # been through `preprocess_query`, which we do not want applied twice.
37
+ def forward_raw_execute(sql, ...)
38
+ replica_connection.send(:raw_execute, sql, ...)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'active_record/connection_adapters/abstract_adapter'
4
2
  require 'active_record/connection_adapters/trilogy_adapter'
5
3
  require_relative '../../../janus-ar'
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Janus
4
2
  # Behaviour shared by the Janus MySQL2 and Trilogy adapters.
5
3
  #
@@ -34,17 +32,17 @@ module Janus
34
32
  @connection_parameters ||= args[0]
35
33
  end
36
34
 
37
- # The argument lists below intentionally use anonymous splats and a bare
35
+ # The argument lists below intentionally use argument forwarding and a bare
38
36
  # `super`: ActiveRecord's `raw_execute`/`execute` signatures differ between
39
37
  # versions, so we forward whatever we are given unchanged rather than
40
38
  # restating (and pinning ourselves to) the current signature.
41
- def raw_execute(sql, *, **)
39
+ def raw_execute(sql, ...)
42
40
  case where_to_send?(sql)
43
41
  when :all
44
- send_to_replica(sql, :all)
42
+ send_raw_to_replica(sql, :all, ...)
45
43
  super
46
44
  when :replica
47
- send_to_replica(sql, :replica)
45
+ send_raw_to_replica(sql, :replica, ...)
48
46
  else
49
47
  mark_primary(sql)
50
48
  super
@@ -108,5 +106,17 @@ module Janus
108
106
  Janus::Context.used_connection(connection)
109
107
  replica_connection.execute(sql)
110
108
  end
109
+
110
+ def send_raw_to_replica(sql, connection, ...)
111
+ Janus::Context.used_connection(connection)
112
+ forward_raw_execute(sql, ...)
113
+ end
114
+
115
+ # The MySQL adapters inline bind values into the SQL string, so replaying the
116
+ # statement alone is enough. Adapters that keep binds separate must override
117
+ # this to carry them across.
118
+ def forward_raw_execute(sql, *, **)
119
+ replica_connection.execute(sql)
120
+ end
111
121
  end
112
122
  end
@@ -1,4 +1,3 @@
1
- # frozen_string_literal: true
2
1
  module Janus
3
2
  class Client
4
3
  FOUND_ROWS = 2
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'active_support/isolated_execution_state'
4
2
 
5
3
  module Janus
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Janus
4
2
  class DbConsoleConfig
5
3
  def initialize(config)
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Janus
4
2
  module Logging
5
3
  class Logger
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Janus
4
2
  module Logging
5
3
  module Subscriber
@@ -1,17 +1,22 @@
1
- # frozen_string_literal: true
2
1
  module Janus
3
2
  class QueryDirector
4
3
  ALL = :all
5
4
  REPLICA = :replica
6
5
  PRIMARY = :primary
7
6
 
7
+ # Regexp fragments, not literal names: PostgreSQL's advisory lock family has
8
+ # too many members to list.
8
9
  LOCK_FUNCTIONS = %w(
9
- nextval currval lastval get_lock release_lock is_free_lock is_used_lock
10
- pg_advisory_lock pg_advisory_unlock
10
+ nextval currval lastval setval get_lock release_lock is_free_lock is_used_lock
11
+ pg_(?:try_)?advisory_(?:xact_)?(?:un)?lock(?:_shared|_all)?
11
12
  ).freeze
12
13
 
14
+ # A string rather than a Regexp: interpolating a Regexp below would wrap it in
15
+ # a group that resets the /im flags, so `FOR UPDATE` would stop matching.
16
+ ROW_LOCK_STRENGTHS = 'no\s+key\s+update|key\s+share|update|share'
17
+
13
18
  SQL_PRIMARY_MATCHERS = [
14
- /\A\s*select\b.*\bfor\s+(update|share)\b/im,
19
+ /\A\s*select\b.*\bfor\s+(?:#{ROW_LOCK_STRENGTHS})\b/im,
15
20
  /\A\s*select\b.*\block\s+in\s+share\s+mode\b/im,
16
21
  /\A\s*select\b.*\b(#{LOCK_FUNCTIONS.join('|')})\s*\(/im,
17
22
  /\A\s*show\b/i,
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'rails/railtie'
4
2
 
5
3
  module Janus
@@ -1,11 +1,9 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Janus
4
2
  unless defined?(::Janus::VERSION)
5
3
  module VERSION
6
4
  MAJOR = 8
7
5
  MINOR = 1
8
- PATCH = 0
6
+ PATCH = 1
9
7
  PRE = nil
10
8
 
11
9
  def self.to_s
data/lib/janus-ar.rb CHANGED
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'active_support'
4
2
 
5
3
  module Janus
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: janus-ar
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.1.0
4
+ version: 8.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lloyd Watkin
@@ -57,6 +57,20 @@ dependencies:
57
57
  - - ">="
58
58
  - !ruby/object:Gem::Version
59
59
  version: '0'
60
+ - !ruby/object:Gem::Dependency
61
+ name: pg
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
60
74
  - !ruby/object:Gem::Dependency
61
75
  name: trilogy
62
76
  requirement: !ruby/object:Gem::Requirement
@@ -199,6 +213,7 @@ files:
199
213
  - janus-ar.gemspec
200
214
  - lib/janus-ar.rb
201
215
  - lib/janus-ar/active_record/connection_adapters/janus_mysql2_adapter.rb
216
+ - lib/janus-ar/active_record/connection_adapters/janus_postgresql_adapter.rb
202
217
  - lib/janus-ar/active_record/connection_adapters/janus_trilogy_adapter.rb
203
218
  - lib/janus-ar/adapter_extensions.rb
204
219
  - lib/janus-ar/client.rb