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 +4 -4
- data/Gemfile +0 -2
- data/Gemfile.lock +4 -1
- data/README.md +46 -3
- data/Rakefile +0 -2
- data/janus-ar.gemspec +1 -2
- data/lib/janus-ar/active_record/connection_adapters/janus_mysql2_adapter.rb +0 -2
- data/lib/janus-ar/active_record/connection_adapters/janus_postgresql_adapter.rb +42 -0
- data/lib/janus-ar/active_record/connection_adapters/janus_trilogy_adapter.rb +0 -2
- data/lib/janus-ar/adapter_extensions.rb +16 -6
- data/lib/janus-ar/client.rb +0 -1
- data/lib/janus-ar/context.rb +0 -2
- data/lib/janus-ar/db_console_config.rb +0 -2
- data/lib/janus-ar/logging/logger.rb +0 -2
- data/lib/janus-ar/logging/subscriber.rb +0 -2
- data/lib/janus-ar/query_director.rb +9 -4
- data/lib/janus-ar/railtie.rb +0 -2
- data/lib/janus-ar/version.rb +1 -3
- data/lib/janus-ar.rb +0 -2
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1720a88365b20f91d220065128ca260baf91380779a7a1d9a6f9aa8fffc13ae8
|
|
4
|
+
data.tar.gz: 2987d7e690b1038946a489a80df29917a192c1a07b85bc16932fb0bf8c2df531
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae79f4f2c54ea96d6191a2b7597982a558027ed911d894e66bbb205041d2339e0b33bb9004ac8331162c2766ac91c922f1ebd11e125ecd18e2f30db8b8cf173c
|
|
7
|
+
data.tar.gz: 1224b98a5c03641329d47f1eee868247141cfd6299f98a1f364aed01bef7eb4c3897c4a1e3675734a04aabde3b4ca8165ddb58fe07f36c4f97691c8fcec89e17
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
janus-ar (8.1.
|
|
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
|
[](https://github.com/OLIOEX/janus-ar/actions/workflows/ci.yml)
|
|
12
12
|
[](https://badge.fury.io/rb/janus-ar)
|
|
13
13
|
|
|
14
|
-
Janus ActiveRecord is a generic primary/replica proxy for ActiveRecord 8
|
|
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
|
|
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
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'
|
|
@@ -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
|
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
|
|
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
|
-
|
|
42
|
+
send_raw_to_replica(sql, :all, ...)
|
|
45
43
|
super
|
|
46
44
|
when :replica
|
|
47
|
-
|
|
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
|
data/lib/janus-ar/client.rb
CHANGED
data/lib/janus-ar/context.rb
CHANGED
|
@@ -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
|
-
|
|
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+(
|
|
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,
|
data/lib/janus-ar/railtie.rb
CHANGED
data/lib/janus-ar/version.rb
CHANGED
data/lib/janus-ar.rb
CHANGED
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.
|
|
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
|