janus-ar 0.13.0 → 0.15.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +9 -4
- data/.rubocop.yml +1 -1
- data/Gemfile.lock +5 -3
- data/README.md +19 -1
- data/janus-ar.gemspec +1 -0
- data/lib/active_record/connection_adapters/janus_mysql2_adapter.rb +38 -44
- data/lib/active_record/connection_adapters/janus_trilogy_adapter.rb +144 -0
- data/lib/janus/client.rb +6 -0
- data/lib/janus/query_director.rb +54 -0
- data/lib/janus/version.rb +1 -1
- data/lib/janus.rb +2 -0
- data/spec/lib/active_record/connection_adapters/janus_mysql_adapter_spec.rb +7 -76
- data/spec/lib/active_record/connection_adapters/janus_trilogy_adapter_spec.rb +80 -0
- data/spec/lib/janus/client_spec.rb +7 -0
- data/spec/lib/janus/query_director_spec.rb +58 -0
- data/spec/shared_examples/a_mysql_like_server.rb +85 -0
- data/spec/spec_helper.rb +3 -0
- metadata +23 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f7fe09a7bb9899b86750ef7c83ca352a04852106381730a71b83da85b79115d2
|
4
|
+
data.tar.gz: 5f3f27e26208e28544bd942423718e498a281613c6cc68127444558f327283b7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b525df1821bdf670e572052cdaf4f886560e66e07f4a93f95cf1f0f527cd05f8159d70d70cea62754ce4adc0939aef6bdd03afc0606fedea8e777d949bca13f4
|
7
|
+
data.tar.gz: 2c4f60b4105630a5384e0591beb9786a6ad8c81acb0b77f00d0aa2c9219b2b8929c86240eb3b0b0bd32ad9199d58cc4453d678c610d3d6c1ceee2d8ecbbf14c9
|
data/.github/workflows/ci.yml
CHANGED
@@ -17,12 +17,12 @@ jobs:
|
|
17
17
|
name: Ruby ${{ matrix.ruby }}
|
18
18
|
services:
|
19
19
|
mysql:
|
20
|
-
image: mysql:
|
20
|
+
image: mysql:8
|
21
21
|
env:
|
22
22
|
MYSQL_DATABASE: test
|
23
23
|
MYSQL_ROOT_PASSWORD: password
|
24
|
-
MYSQL_USER:
|
25
|
-
MYSQL_PASSWORD:
|
24
|
+
MYSQL_USER: test
|
25
|
+
MYSQL_PASSWORD: test_password
|
26
26
|
ports:
|
27
27
|
- 3306:3306
|
28
28
|
options: >-
|
@@ -37,7 +37,12 @@ jobs:
|
|
37
37
|
ruby-version: ${{ matrix.ruby }}
|
38
38
|
bundler-cache: true
|
39
39
|
- run: |
|
40
|
-
mysql -u root -p${{ env.MYSQL_PASSWORD || 'password' }} -h 127.0.0.1
|
40
|
+
mysql -e "CREATE USER 'replica'@'%' IDENTIFIED WITH mysql_native_password BY 'replica_password';" -u root -p${{ env.MYSQL_PASSWORD || 'password' }} -h 127.0.0.1
|
41
|
+
mysql -e "GRANT SELECT ON test.* TO 'replica'@'%'" -u root -p${{ env.MYSQL_PASSWORD || 'password' }} -h 127.0.0.1
|
42
|
+
|
43
|
+
mysql -e "CREATE USER 'primary'@'%' IDENTIFIED WITH mysql_native_password BY 'primary_password';" -u root -p${{ env.MYSQL_PASSWORD || 'password' }} -h 127.0.0.1
|
44
|
+
mysql -e "GRANT ALL PRIVILEGES ON *.* TO 'primary'@'%';" -u root -p${{ env.MYSQL_PASSWORD || 'password' }} -h 127.0.0.1
|
45
|
+
mysql -e "FLUSH PRIVILEGES;" -u root -p${{ env.MYSQL_PASSWORD || 'password' }} -h 127.0.0.1
|
41
46
|
- run: |
|
42
47
|
bundle exec rspec
|
43
48
|
env:
|
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
janus-ar (0.
|
4
|
+
janus-ar (0.14.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: http://rubygems.org/
|
@@ -39,7 +39,7 @@ GEM
|
|
39
39
|
mutex_m (0.2.0)
|
40
40
|
mysql2 (0.5.6)
|
41
41
|
parallel (1.24.0)
|
42
|
-
parser (3.3.0
|
42
|
+
parser (3.3.1.0)
|
43
43
|
ast (~> 2.4.1)
|
44
44
|
racc
|
45
45
|
pry (0.14.2)
|
@@ -64,7 +64,7 @@ GEM
|
|
64
64
|
diff-lcs (>= 1.2.0, < 2.0)
|
65
65
|
rspec-support (~> 3.13.0)
|
66
66
|
rspec-support (3.13.1)
|
67
|
-
rubocop (1.63.
|
67
|
+
rubocop (1.63.4)
|
68
68
|
json (~> 2.3)
|
69
69
|
language_server-protocol (>= 3.17.0)
|
70
70
|
parallel (~> 1.10)
|
@@ -100,6 +100,7 @@ GEM
|
|
100
100
|
rubocop (>= 0.90.0)
|
101
101
|
ruby-progressbar (1.13.0)
|
102
102
|
timeout (0.4.1)
|
103
|
+
trilogy (2.8.0)
|
103
104
|
tzinfo (2.0.6)
|
104
105
|
concurrent-ruby (~> 1.0)
|
105
106
|
unicode-display_width (2.5.0)
|
@@ -121,6 +122,7 @@ DEPENDENCIES
|
|
121
122
|
rubocop-rails (~> 2.24.0)
|
122
123
|
rubocop-rspec
|
123
124
|
rubocop-thread_safety
|
125
|
+
trilogy
|
124
126
|
|
125
127
|
BUNDLED WITH
|
126
128
|
2.4.22
|
data/README.md
CHANGED
@@ -11,10 +11,16 @@
|
|
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 generic primary/replica proxy for ActiveRecord 7.1+ and MySQL. It handles the switching of connections between primary and replica database servers. It comes with an ActiveRecord database adapter implementation.
|
14
|
+
Janus ActiveRecord is generic primary/replica proxy for ActiveRecord 7.1+ 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.
|
15
|
+
|
16
|
+
Note: Trilogy support is experimental at this stage.
|
15
17
|
|
16
18
|
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
19
|
|
20
|
+
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).
|
21
|
+
|
22
|
+
Notes: GEM is currently tested with MySQL 8, Ruby 3.2, ActiveRecord 7.1+
|
23
|
+
|
18
24
|
## Installation
|
19
25
|
|
20
26
|
Use the current version of the gem from [rubygems](https://rubygems.org/gems/janus-ar) in your `Gemfile`.
|
@@ -46,6 +52,18 @@ development:
|
|
46
52
|
password: ithappenstobedifferent
|
47
53
|
host: replica-host.local
|
48
54
|
```
|
55
|
+
Note: For `trilogy` please use adapter "janus_trilogy". You'll probably need to add the following to your configuration to have it connect:
|
56
|
+
|
57
|
+
```yml
|
58
|
+
ssl: true
|
59
|
+
ssl_mode: 'REQUIRED'
|
60
|
+
tls_min_version: 3
|
61
|
+
```
|
62
|
+
|
63
|
+
`tls_min_version` here refers to TLS1.2.
|
64
|
+
|
65
|
+
Otherwise you will get an error like the following (see https://github.com/trilogy-libraries/trilogy/issues/26):
|
66
|
+
> trilogy_auth_recv: caching_sha2_password requires either TCP with TLS or a unix socket: TRILOGY_UNSUPPORTED"
|
49
67
|
|
50
68
|
### Forcing connections
|
51
69
|
|
data/janus-ar.gemspec
CHANGED
@@ -24,6 +24,7 @@ Gem::Specification.new do |gem|
|
|
24
24
|
gem.add_development_dependency 'activerecord', '>= 7.1.0'
|
25
25
|
gem.add_development_dependency 'activesupport', '>= 7.1.0'
|
26
26
|
gem.add_development_dependency 'mysql2'
|
27
|
+
gem.add_development_dependency 'trilogy'
|
27
28
|
gem.add_development_dependency 'pry'
|
28
29
|
gem.add_development_dependency 'rake'
|
29
30
|
gem.add_development_dependency 'rspec', '~> 3'
|
@@ -24,15 +24,6 @@ module ActiveRecord
|
|
24
24
|
module ConnectionAdapters
|
25
25
|
class JanusMysql2Adapter < ActiveRecord::ConnectionAdapters::Mysql2Adapter
|
26
26
|
FOUND_ROWS = 'FOUND_ROWS'
|
27
|
-
SQL_PRIMARY_MATCHERS = [
|
28
|
-
/\A\s*select.+for update\Z/i, /select.+lock in share mode\Z/i,
|
29
|
-
/\A\s*select.+(nextval|currval|lastval|get_lock|release_lock|pg_advisory_lock|pg_advisory_unlock)\(/i,
|
30
|
-
/\A\s*show/i
|
31
|
-
].freeze
|
32
|
-
SQL_REPLICA_MATCHERS = [/\A\s*(select|with.+\)\s*select)\s/i].freeze
|
33
|
-
SQL_ALL_MATCHERS = [/\A\s*set\s/i].freeze
|
34
|
-
SQL_SKIP_ALL_MATCHERS = [/\A\s*set\s+local\s/i].freeze
|
35
|
-
WRITE_PREFIXES = %w(INSERT UPDATE DELETE LOCK CREATE GRANT DROP ALTER).freeze
|
36
27
|
|
37
28
|
attr_reader :config
|
38
29
|
|
@@ -56,30 +47,46 @@ module ActiveRecord
|
|
56
47
|
update_config
|
57
48
|
end
|
58
49
|
|
50
|
+
def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
|
51
|
+
case where_to_send?(sql)
|
52
|
+
when :all
|
53
|
+
send_to_replica(sql, connection: :all, method: :raw_execute)
|
54
|
+
super
|
55
|
+
when :replica
|
56
|
+
send_to_replica(sql, connection: :replica, method: :raw_execute)
|
57
|
+
else
|
58
|
+
Janus::Context.stick_to_primary if write_query?(sql)
|
59
|
+
Janus::Context.used_connection(:primary)
|
60
|
+
super
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
59
64
|
def execute(sql)
|
60
|
-
|
65
|
+
case where_to_send?(sql)
|
66
|
+
when :all
|
61
67
|
send_to_replica(sql, connection: :all, method: :execute)
|
62
|
-
|
68
|
+
super(sql)
|
69
|
+
when :replica
|
70
|
+
send_to_replica(sql, connection: :replica, method: :execute)
|
71
|
+
else
|
72
|
+
Janus::Context.stick_to_primary if write_query?(sql)
|
73
|
+
Janus::Context.used_connection(:primary)
|
74
|
+
super(sql)
|
63
75
|
end
|
64
|
-
return send_to_replica(sql, connection: :replica, method: :execute) if can_go_to_replica?(sql)
|
65
|
-
|
66
|
-
Janus::Context.stick_to_primary if write_query?(sql)
|
67
|
-
Janus::Context.used_connection(:primary)
|
68
|
-
|
69
|
-
super(sql)
|
70
76
|
end
|
71
77
|
|
72
78
|
def execute_and_free(sql, name = nil, async: false)
|
73
|
-
|
74
|
-
|
75
|
-
|
79
|
+
case where_to_send?(sql)
|
80
|
+
when :all
|
81
|
+
send_to_replica(sql, connection: :all, method: :execute)
|
82
|
+
super(sql, name, async:)
|
83
|
+
when :replica
|
84
|
+
send_to_replica(sql, connection: :replica, method: :execute)
|
85
|
+
else
|
86
|
+
Janus::Context.stick_to_primary if write_query?(sql)
|
87
|
+
Janus::Context.used_connection(:primary)
|
88
|
+
super(sql, name, async:)
|
76
89
|
end
|
77
|
-
return send_to_replica(sql, connection: :replica) if can_go_to_replica?(sql)
|
78
|
-
|
79
|
-
Janus::Context.stick_to_primary if write_query?(sql)
|
80
|
-
Janus::Context.used_connection(:primary)
|
81
|
-
|
82
|
-
super(sql, name, async:)
|
83
90
|
end
|
84
91
|
|
85
92
|
def connect!(...)
|
@@ -108,41 +115,28 @@ module ActiveRecord
|
|
108
115
|
|
109
116
|
private
|
110
117
|
|
111
|
-
def
|
112
|
-
|
113
|
-
end
|
114
|
-
|
115
|
-
def can_go_to_replica?(sql)
|
116
|
-
!should_go_to_primary?(sql)
|
117
|
-
end
|
118
|
-
|
119
|
-
def should_go_to_primary?(sql)
|
120
|
-
Janus::Context.use_primary? ||
|
121
|
-
write_query?(sql) ||
|
122
|
-
open_transactions.positive? ||
|
123
|
-
SQL_PRIMARY_MATCHERS.any? { |matcher| sql =~ matcher }
|
118
|
+
def where_to_send?(sql)
|
119
|
+
Janus::QueryDirector.new(sql, open_transactions).where_to_send?
|
124
120
|
end
|
125
121
|
|
126
122
|
def send_to_replica(sql, connection: nil, method: :exec_query)
|
127
123
|
Janus::Context.used_connection(connection) if connection
|
128
124
|
if method == :execute
|
129
125
|
replica_connection.execute(sql)
|
126
|
+
elsif method == :raw_execute
|
127
|
+
replica_connection.execute(sql)
|
130
128
|
else
|
131
129
|
replica_connection.exec_query(sql)
|
132
130
|
end
|
133
131
|
end
|
134
132
|
|
135
|
-
def write_query?(sql)
|
136
|
-
WRITE_PREFIXES.include?(sql.upcase.split(' ').first)
|
137
|
-
end
|
138
|
-
|
139
133
|
def update_config
|
140
134
|
@config[:flags] ||= 0
|
141
135
|
|
142
136
|
if @config[:flags].is_a? Array
|
143
137
|
@config[:flags].push FOUND_ROWS
|
144
138
|
else
|
145
|
-
@config[:flags] |= ::
|
139
|
+
@config[:flags] |= ::Janus::Client::FOUND_ROWS
|
146
140
|
end
|
147
141
|
end
|
148
142
|
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
4
|
+
require 'active_record/connection_adapters/trilogy_adapter'
|
5
|
+
require_relative '../../janus'
|
6
|
+
|
7
|
+
module ActiveRecord
|
8
|
+
module ConnectionHandling
|
9
|
+
def janus_trilogy_connection(config)
|
10
|
+
ActiveRecord::ConnectionAdapters::JanusTrilogyAdapter.new(config)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module ActiveRecord
|
16
|
+
class Base
|
17
|
+
def self.janus_trilogy_adapter_class
|
18
|
+
ActiveRecord::ConnectionAdapters::JanusTrilogyAdapter
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module ActiveRecord
|
24
|
+
module ConnectionAdapters
|
25
|
+
class JanusTrilogyAdapter < ActiveRecord::ConnectionAdapters::TrilogyAdapter
|
26
|
+
FOUND_ROWS = 'FOUND_ROWS'
|
27
|
+
|
28
|
+
attr_reader :config
|
29
|
+
|
30
|
+
class << self
|
31
|
+
def dbconsole(config, options = {})
|
32
|
+
connection_config = Janus::DbConsoleConfig.new(config)
|
33
|
+
|
34
|
+
super(connection_config, options)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize(*args)
|
39
|
+
args[0][:janus]['replica']['database'] = args[0][:database]
|
40
|
+
args[0][:janus]['primary']['database'] = args[0][:database]
|
41
|
+
|
42
|
+
@replica_config = args[0][:janus]['replica'].symbolize_keys
|
43
|
+
args[0] = args[0][:janus]['primary'].symbolize_keys
|
44
|
+
|
45
|
+
super(*args)
|
46
|
+
@connection_parameters ||= args[0]
|
47
|
+
update_config
|
48
|
+
end
|
49
|
+
|
50
|
+
def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
|
51
|
+
case where_to_send?(sql)
|
52
|
+
when :all
|
53
|
+
send_to_replica(sql, connection: :all, method: :execute)
|
54
|
+
super
|
55
|
+
when :replica
|
56
|
+
send_to_replica(sql, connection: :replica, method: :execute)
|
57
|
+
else
|
58
|
+
Janus::Context.stick_to_primary if write_query?(sql)
|
59
|
+
Janus::Context.used_connection(:primary)
|
60
|
+
super
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def execute(sql)
|
65
|
+
case where_to_send?(sql)
|
66
|
+
when :all
|
67
|
+
send_to_replica(sql, connection: :all, method: :execute)
|
68
|
+
super(sql)
|
69
|
+
when :replica
|
70
|
+
send_to_replica(sql, connection: :replica, method: :execute)
|
71
|
+
else
|
72
|
+
Janus::Context.stick_to_primary if write_query?(sql)
|
73
|
+
Janus::Context.used_connection(:primary)
|
74
|
+
super(sql)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def execute_and_free(sql, name = nil, async: false)
|
79
|
+
case where_to_send?(sql)
|
80
|
+
when :all
|
81
|
+
send_to_replica(sql, connection: :all, method: :execute)
|
82
|
+
super(sql, name, async:)
|
83
|
+
when :replica
|
84
|
+
send_to_replica(sql, connection: :replica, method: :execute)
|
85
|
+
else
|
86
|
+
Janus::Context.stick_to_primary if write_query?(sql)
|
87
|
+
Janus::Context.used_connection(:primary)
|
88
|
+
super(sql, name, async:)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def connect!(...)
|
93
|
+
replica_connection.connect!(...)
|
94
|
+
super
|
95
|
+
end
|
96
|
+
|
97
|
+
def reconnect!(...)
|
98
|
+
replica_connection.reconnect!(...)
|
99
|
+
super
|
100
|
+
end
|
101
|
+
|
102
|
+
def disconnect!(...)
|
103
|
+
replica_connection.disconnect!(...)
|
104
|
+
super
|
105
|
+
end
|
106
|
+
|
107
|
+
def clear_cache!(...)
|
108
|
+
replica_connection.clear_cache!(...)
|
109
|
+
super
|
110
|
+
end
|
111
|
+
|
112
|
+
def replica_connection
|
113
|
+
@replica_connection ||= ActiveRecord::ConnectionAdapters::TrilogyAdapter.new(@replica_config)
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def where_to_send?(sql)
|
119
|
+
Janus::QueryDirector.new(sql, open_transactions).where_to_send?
|
120
|
+
end
|
121
|
+
|
122
|
+
def send_to_replica(sql, connection: nil, method: :exec_query)
|
123
|
+
Janus::Context.used_connection(connection) if connection
|
124
|
+
if method == :execute
|
125
|
+
replica_connection.execute(sql)
|
126
|
+
elsif method == :raw_execute
|
127
|
+
replica_connection.execute(sql)
|
128
|
+
else
|
129
|
+
replica_connection.exec_query(sql)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def update_config
|
134
|
+
@config[:flags] ||= 0
|
135
|
+
|
136
|
+
if @config[:flags].is_a? Array
|
137
|
+
@config[:flags].push FOUND_ROWS
|
138
|
+
else
|
139
|
+
@config[:flags] |= ::Janus::Client::FOUND_ROWS
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
data/lib/janus/client.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Janus
|
3
|
+
class QueryDirector
|
4
|
+
ALL = :all
|
5
|
+
REPLICA = :replica
|
6
|
+
PRIMARY = :primary
|
7
|
+
|
8
|
+
SQL_PRIMARY_MATCHERS = [
|
9
|
+
/\A\s*select.+for update\Z/i, /select.+lock in share mode\Z/i,
|
10
|
+
/\A\s*select.+(nextval|currval|lastval|get_lock|release_lock|pg_advisory_lock|pg_advisory_unlock)\(/i,
|
11
|
+
/\A\s*show/i
|
12
|
+
].freeze
|
13
|
+
SQL_REPLICA_MATCHERS = [/\A\s*(select|with.+\)\s*select)\s/i].freeze
|
14
|
+
SQL_ALL_MATCHERS = [/\A\s*set\s/i].freeze
|
15
|
+
SQL_SKIP_ALL_MATCHERS = [/\A\s*set\s+local\s/i].freeze
|
16
|
+
WRITE_PREFIXES = %w(INSERT UPDATE DELETE LOCK CREATE GRANT DROP ALTER TRUNCATE FLUSH).freeze
|
17
|
+
|
18
|
+
def initialize(sql, open_transactions)
|
19
|
+
@_sql = sql
|
20
|
+
@_open_transactions = open_transactions
|
21
|
+
end
|
22
|
+
|
23
|
+
def where_to_send?
|
24
|
+
if should_send_to_all?
|
25
|
+
ALL
|
26
|
+
elsif can_go_to_replica?
|
27
|
+
REPLICA
|
28
|
+
else
|
29
|
+
PRIMARY
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def should_send_to_all?
|
36
|
+
SQL_ALL_MATCHERS.any? { |matcher| @_sql =~ matcher } && SQL_SKIP_ALL_MATCHERS.none? { |matcher| @_sql =~ matcher }
|
37
|
+
end
|
38
|
+
|
39
|
+
def can_go_to_replica?
|
40
|
+
!should_go_to_primary?
|
41
|
+
end
|
42
|
+
|
43
|
+
def should_go_to_primary?
|
44
|
+
Janus::Context.use_primary? ||
|
45
|
+
write_query? ||
|
46
|
+
@_open_transactions.positive? ||
|
47
|
+
SQL_PRIMARY_MATCHERS.any? { |matcher| @_sql =~ matcher }
|
48
|
+
end
|
49
|
+
|
50
|
+
def write_query?
|
51
|
+
WRITE_PREFIXES.include?(@_sql.upcase.split(' ').first)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/janus/version.rb
CHANGED
data/lib/janus.rb
CHANGED
@@ -4,19 +4,6 @@ RSpec.describe ActiveRecord::ConnectionAdapters::JanusMysql2Adapter do
|
|
4
4
|
subject { described_class.new(config) }
|
5
5
|
|
6
6
|
it { expect(described_class::FOUND_ROWS).to eq 'FOUND_ROWS' }
|
7
|
-
it { expect(described_class::SQL_SKIP_ALL_MATCHERS).to eq [/\A\s*set\s+local\s/i] }
|
8
|
-
it {
|
9
|
-
expect(described_class::SQL_PRIMARY_MATCHERS).to eq(
|
10
|
-
[
|
11
|
-
/\A\s*select.+for update\Z/i, /select.+lock in share mode\Z/i,
|
12
|
-
/\A\s*select.+(nextval|currval|lastval|get_lock|release_lock|pg_advisory_lock|pg_advisory_unlock)\(/i,
|
13
|
-
/\A\s*show/i
|
14
|
-
]
|
15
|
-
)
|
16
|
-
}
|
17
|
-
it { expect(described_class::SQL_REPLICA_MATCHERS).to eq([/\A\s*(select|with.+\)\s*select)\s/i]) }
|
18
|
-
it { expect(described_class::SQL_ALL_MATCHERS).to eq([/\A\s*set\s/i]) }
|
19
|
-
it { expect(described_class::WRITE_PREFIXES).to eq %w(INSERT UPDATE DELETE LOCK CREATE GRANT DROP ALTER) }
|
20
7
|
|
21
8
|
let(:database) { 'test' }
|
22
9
|
let(:primary_config) do
|
@@ -49,14 +36,14 @@ RSpec.describe ActiveRecord::ConnectionAdapters::JanusMysql2Adapter do
|
|
49
36
|
it 'creates primary connection as expected' do
|
50
37
|
config = primary_config.dup.freeze
|
51
38
|
expect(subject.config).to eq config.merge('database' => database,
|
52
|
-
'flags' => ::
|
39
|
+
'flags' => ::Janus::Client::FOUND_ROWS).symbolize_keys
|
53
40
|
end
|
54
41
|
|
55
42
|
it 'creates replica connection as expected' do
|
56
43
|
config = replica_config.dup.freeze
|
57
44
|
expect(
|
58
45
|
subject.replica_connection.instance_variable_get(:@config)
|
59
|
-
).to eq config.merge('database' => database, 'flags' => ::
|
46
|
+
).to eq config.merge('database' => database, 'flags' => ::Janus::Client::FOUND_ROWS).symbolize_keys
|
60
47
|
end
|
61
48
|
|
62
49
|
context 'Rails sets empty database for server connection' do
|
@@ -66,7 +53,7 @@ RSpec.describe ActiveRecord::ConnectionAdapters::JanusMysql2Adapter do
|
|
66
53
|
config = primary_config.dup.freeze
|
67
54
|
expect(subject.config).to eq config.merge(
|
68
55
|
'database' => nil,
|
69
|
-
'flags' => ::
|
56
|
+
'flags' => ::Janus::Client::FOUND_ROWS
|
70
57
|
).symbolize_keys
|
71
58
|
end
|
72
59
|
|
@@ -74,72 +61,16 @@ RSpec.describe ActiveRecord::ConnectionAdapters::JanusMysql2Adapter do
|
|
74
61
|
config = replica_config.dup.freeze
|
75
62
|
expect(
|
76
63
|
subject.replica_connection.instance_variable_get(:@config)
|
77
|
-
).to eq config.merge('database' => nil, 'flags' => ::
|
64
|
+
).to eq config.merge('database' => nil, 'flags' => ::Janus::Client::FOUND_ROWS).symbolize_keys
|
78
65
|
end
|
79
66
|
end
|
80
67
|
end
|
81
68
|
|
82
69
|
describe 'Integration tests' do
|
83
|
-
|
70
|
+
describe 'Integration tests' do
|
71
|
+
let(:table_name) { 'table_name_mysql2' }
|
84
72
|
|
85
|
-
|
86
|
-
$query_logger.flush_all
|
87
|
-
ActiveRecord::Base.establish_connection(config)
|
88
|
-
end
|
89
|
-
|
90
|
-
after(:each) do
|
91
|
-
ActiveRecord::Base.connection.execute(<<-SQL
|
92
|
-
SELECT CONCAT('DROP TABLE IF EXISTS `', table_name, '`;')
|
93
|
-
FROM information_schema.tables
|
94
|
-
WHERE table_schema = '#{database}';
|
95
|
-
SQL
|
96
|
-
).to_a.map { |row| ActiveRecord::Base.connection.execute(row[0]) }
|
97
|
-
end
|
98
|
-
|
99
|
-
it 'can list tables' do
|
100
|
-
expect(ActiveRecord::Base.connection.execute('SHOW TABLES;').to_a).to eq []
|
101
|
-
end
|
102
|
-
|
103
|
-
it 'can create table' do
|
104
|
-
create_test_table
|
105
|
-
expect(ActiveRecord::Base.connection.execute('SHOW TABLES;').to_a).to eq [%w(test_table)]
|
106
|
-
end
|
107
|
-
|
108
|
-
describe 'SELECT' do
|
109
|
-
it 'reads from `replica` by default' do
|
110
|
-
create_test_table
|
111
|
-
Janus::Context.release_all
|
112
|
-
$query_logger.flush_all
|
113
|
-
ActiveRecord::Base.connection.execute('SELECT * FROM test_table;')
|
114
|
-
expect($query_logger.queries.first).to include '[replica]'
|
115
|
-
end
|
116
|
-
|
117
|
-
it 'will read from primary after a write operation' do
|
118
|
-
create_test_table
|
119
|
-
$query_logger.flush_all
|
120
|
-
ActiveRecord::Base.connection.execute('SELECT * FROM test_table;')
|
121
|
-
expect($query_logger.queries.first).to include '[primary]'
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
describe 'INSERT' do
|
126
|
-
let(:insert_query) { 'INSERT INTO test_table SET `id` = 5;' }
|
127
|
-
|
128
|
-
before(:each) do
|
129
|
-
create_test_table
|
130
|
-
$query_logger.flush_all
|
131
|
-
Janus::Context.release_all
|
132
|
-
end
|
133
|
-
|
134
|
-
it 'sends INSERT query to primary' do
|
135
|
-
ActiveRecord::Base.connection.execute(insert_query)
|
136
|
-
expect($query_logger.queries.first).to include '[primary]'
|
137
|
-
end
|
138
|
-
|
139
|
-
it 'ignores case when directing queries' do
|
140
|
-
ActiveRecord::Base.connection.execute(insert_query.downcase)
|
141
|
-
expect($query_logger.queries.first).to include '[primary]'
|
142
|
-
end
|
73
|
+
it_behaves_like 'a mysql like server'
|
143
74
|
end
|
144
75
|
end
|
145
76
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe ActiveRecord::ConnectionAdapters::JanusTrilogyAdapter do
|
4
|
+
subject { described_class.new(config) }
|
5
|
+
|
6
|
+
it { expect(described_class::FOUND_ROWS).to eq 'FOUND_ROWS' }
|
7
|
+
|
8
|
+
let(:database) { 'test' }
|
9
|
+
let(:primary_config) do
|
10
|
+
{
|
11
|
+
'username' => 'primary',
|
12
|
+
'password' => 'primary_password',
|
13
|
+
'host' => '127.0.0.1',
|
14
|
+
'ssl' => true,
|
15
|
+
'ssl_mode' => 'REQUIRED',
|
16
|
+
'tls_min_version' => Trilogy::TLS_VERSION_12,
|
17
|
+
}
|
18
|
+
end
|
19
|
+
let(:replica_config) do
|
20
|
+
{
|
21
|
+
'username' => 'replica',
|
22
|
+
'password' => 'replica_password',
|
23
|
+
'host' => '127.0.0.1',
|
24
|
+
'pool' => 500,
|
25
|
+
'ssl' => true,
|
26
|
+
'ssl_mode' => 'REQUIRED',
|
27
|
+
'tls_min_version' => Trilogy::TLS_VERSION_12,
|
28
|
+
}
|
29
|
+
end
|
30
|
+
let(:config) do
|
31
|
+
{
|
32
|
+
database:,
|
33
|
+
adapter: 'janus_trilogy',
|
34
|
+
janus: {
|
35
|
+
'primary' => primary_config,
|
36
|
+
'replica' => replica_config,
|
37
|
+
},
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'Configuration' do
|
42
|
+
it 'creates primary connection as expected' do
|
43
|
+
config = primary_config.dup.freeze
|
44
|
+
expect(subject.config).to eq config.merge('database' => database,
|
45
|
+
'flags' => ::Janus::Client::FOUND_ROWS).symbolize_keys
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'creates replica connection as expected' do
|
49
|
+
config = replica_config.dup.freeze
|
50
|
+
expect(
|
51
|
+
subject.replica_connection.instance_variable_get(:@config)
|
52
|
+
).to eq config.merge('database' => database).symbolize_keys
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'Rails sets empty database for server connection' do
|
56
|
+
let(:database) { nil }
|
57
|
+
|
58
|
+
it 'creates primary connection as expected' do
|
59
|
+
config = primary_config.dup.freeze
|
60
|
+
expect(subject.config).to eq config.merge(
|
61
|
+
'database' => nil,
|
62
|
+
'flags' => ::Janus::Client::FOUND_ROWS
|
63
|
+
).symbolize_keys
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'creates replica connection as expected' do
|
67
|
+
config = replica_config.dup.freeze
|
68
|
+
expect(
|
69
|
+
subject.replica_connection.instance_variable_get(:@config)
|
70
|
+
).to eq config.merge('database' => nil).symbolize_keys
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe 'Integration tests' do
|
76
|
+
let(:table_name) { 'table_name_trilogy' }
|
77
|
+
|
78
|
+
it_behaves_like 'a mysql like server'
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe Janus::QueryDirector do
|
4
|
+
describe 'Constants' do
|
5
|
+
it { expect(described_class::SQL_SKIP_ALL_MATCHERS).to eq [/\A\s*set\s+local\s/i] }
|
6
|
+
it {
|
7
|
+
expect(described_class::SQL_PRIMARY_MATCHERS).to eq(
|
8
|
+
[
|
9
|
+
/\A\s*select.+for update\Z/i, /select.+lock in share mode\Z/i,
|
10
|
+
/\A\s*select.+(nextval|currval|lastval|get_lock|release_lock|pg_advisory_lock|pg_advisory_unlock)\(/i,
|
11
|
+
/\A\s*show/i
|
12
|
+
]
|
13
|
+
)
|
14
|
+
}
|
15
|
+
it { expect(described_class::SQL_REPLICA_MATCHERS).to eq([/\A\s*(select|with.+\)\s*select)\s/i]) }
|
16
|
+
it { expect(described_class::SQL_ALL_MATCHERS).to eq([/\A\s*set\s/i]) }
|
17
|
+
it {
|
18
|
+
expect(described_class::WRITE_PREFIXES).to eq %w(INSERT UPDATE DELETE LOCK CREATE GRANT DROP ALTER TRUNCATE FLUSH)
|
19
|
+
}
|
20
|
+
|
21
|
+
it { expect(described_class::ALL).to eq :all }
|
22
|
+
it { expect(described_class::REPLICA).to eq :replica }
|
23
|
+
it { expect(described_class::PRIMARY).to eq :primary }
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#where_to_send?' do
|
27
|
+
before(:each) do
|
28
|
+
Janus::Context.release_all
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'when should send to all' do
|
32
|
+
it 'returns :all' do
|
33
|
+
sql = 'SET foo = bar'
|
34
|
+
open_transactions = 0
|
35
|
+
query_director = described_class.new(sql, open_transactions)
|
36
|
+
expect(query_director.where_to_send?).to eq(:all)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'when can go to replica' do
|
41
|
+
it 'returns :replica' do
|
42
|
+
sql = 'SELECT * FROM users'
|
43
|
+
open_transactions = 0
|
44
|
+
query_director = described_class.new(sql, open_transactions)
|
45
|
+
expect(query_director.where_to_send?).to eq(:replica)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'when should go to primary' do
|
50
|
+
it 'returns :primary' do
|
51
|
+
sql = 'INSERT INTO users (name) VALUES ("John")'
|
52
|
+
open_transactions = 0
|
53
|
+
query_director = described_class.new(sql, open_transactions)
|
54
|
+
expect(query_director.where_to_send?).to eq(:primary)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
RSpec.shared_examples 'a mysql like server' do
|
3
|
+
let(:create_test_table) { ActiveRecord::Base.connection.execute("CREATE TABLE `#{table_name}` (id INT);") }
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
$query_logger.flush_all
|
7
|
+
ActiveRecord::Base.establish_connection(config)
|
8
|
+
end
|
9
|
+
|
10
|
+
after(:each) do
|
11
|
+
ActiveRecord::Base.connection.execute(<<-SQL
|
12
|
+
SELECT CONCAT('DROP TABLE IF EXISTS `', table_name, '`;')
|
13
|
+
FROM information_schema.tables
|
14
|
+
WHERE table_schema = '#{database}';
|
15
|
+
SQL
|
16
|
+
).to_a.map { |row| ActiveRecord::Base.connection.execute(row[0]) }
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'can list tables' do
|
20
|
+
expect(ActiveRecord::Base.connection.execute('SHOW TABLES;').to_a).to eq []
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'can create table' do
|
24
|
+
create_test_table
|
25
|
+
expect(ActiveRecord::Base.connection.execute('SHOW TABLES;').to_a).to eq [[table_name]]
|
26
|
+
end
|
27
|
+
|
28
|
+
describe 'SELECT' do
|
29
|
+
it 'reads from `replica` by default' do
|
30
|
+
create_test_table
|
31
|
+
Janus::Context.release_all
|
32
|
+
$query_logger.flush_all
|
33
|
+
ActiveRecord::Base.connection.execute("SELECT * FROM `#{table_name}`;")
|
34
|
+
expect($query_logger.queries.first).to include '[replica]'
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'will read from primary after a write operation' do
|
38
|
+
create_test_table
|
39
|
+
$query_logger.flush_all
|
40
|
+
ActiveRecord::Base.connection.execute("SELECT * FROM `#{table_name}`;")
|
41
|
+
expect($query_logger.queries.first).to include '[primary]'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe 'INSERT' do
|
46
|
+
let(:insert_query) { "INSERT INTO `#{table_name}` SET `id` = 5;" }
|
47
|
+
|
48
|
+
before(:each) do
|
49
|
+
create_test_table
|
50
|
+
$query_logger.flush_all
|
51
|
+
Janus::Context.release_all
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'sends INSERT query to primary' do
|
55
|
+
ActiveRecord::Base.connection.execute(insert_query)
|
56
|
+
expect($query_logger.queries.first).to include '[primary]'
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'ignores case when directing queries' do
|
60
|
+
ActiveRecord::Base.connection.execute(insert_query.downcase)
|
61
|
+
expect($query_logger.queries.first).to include '[primary]'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe 'UPDATE' do
|
66
|
+
before(:each) do
|
67
|
+
create_test_table
|
68
|
+
5.times { |i| ActiveRecord::Base.connection.execute("INSERT INTO `#{table_name}` SET `id` = #{i};") }
|
69
|
+
$query_logger.flush_all
|
70
|
+
Janus::Context.release_all
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'continues to direct after bulk update' do
|
74
|
+
ActiveRecord::Base.connection.execute("UPDATE `#{table_name}` SET `id` = `id` + 2;")
|
75
|
+
expect($query_logger.queries.first).to include '[primary]'
|
76
|
+
expect(Janus::Context.last_used_connection).to eq :primary
|
77
|
+
ActiveRecord::Base.connection.execute("SELECT * FROM `#{table_name}`;")
|
78
|
+
expect($query_logger.queries.last).to include '[primary]'
|
79
|
+
Janus::Context.release_all
|
80
|
+
ActiveRecord::Base.connection.execute("SELECT * FROM `#{table_name}`;")
|
81
|
+
expect($query_logger.queries.last).to include '[replica]'
|
82
|
+
expect(Janus::Context.last_used_connection).to eq :replica
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -6,6 +6,9 @@ require 'active_record'
|
|
6
6
|
|
7
7
|
require './lib/janus'
|
8
8
|
require './lib/active_record/connection_adapters/janus_mysql2_adapter'
|
9
|
+
require './lib/active_record/connection_adapters/janus_trilogy_adapter'
|
10
|
+
|
11
|
+
require './spec/shared_examples/a_mysql_like_server.rb'
|
9
12
|
|
10
13
|
class QueryLogger
|
11
14
|
def initialize
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: janus-ar
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.15.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lloyd Watkin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-04-
|
11
|
+
date: 2024-04-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: trilogy
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: pry
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -189,15 +203,22 @@ files:
|
|
189
203
|
- bin/release.sh
|
190
204
|
- janus-ar.gemspec
|
191
205
|
- lib/active_record/connection_adapters/janus_mysql2_adapter.rb
|
206
|
+
- lib/active_record/connection_adapters/janus_trilogy_adapter.rb
|
192
207
|
- lib/janus.rb
|
208
|
+
- lib/janus/client.rb
|
193
209
|
- lib/janus/context.rb
|
194
210
|
- lib/janus/db_console_config.rb
|
195
211
|
- lib/janus/logging/logger.rb
|
196
212
|
- lib/janus/logging/subscriber.rb
|
213
|
+
- lib/janus/query_director.rb
|
197
214
|
- lib/janus/version.rb
|
198
215
|
- spec/lib/active_record/connection_adapters/janus_mysql_adapter_spec.rb
|
216
|
+
- spec/lib/active_record/connection_adapters/janus_trilogy_adapter_spec.rb
|
217
|
+
- spec/lib/janus/client_spec.rb
|
199
218
|
- spec/lib/janus/context_spec.rb
|
200
219
|
- spec/lib/janus/logging/logger_spec.rb
|
220
|
+
- spec/lib/janus/query_director_spec.rb
|
221
|
+
- spec/shared_examples/a_mysql_like_server.rb
|
201
222
|
- spec/spec_helper.rb
|
202
223
|
homepage: https://github.com/olioex/janus-ar
|
203
224
|
licenses:
|