janus-ar 8.0.1 → 8.1.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.
@@ -1,78 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- RSpec.describe ActiveRecord::ConnectionAdapters::JanusMysql2Adapter do
4
- subject { described_class.new(config) }
5
-
6
- let(:database) { 'test' }
7
- let(:primary_config) do
8
- {
9
- 'username' => 'primary',
10
- 'password' => 'primary_password',
11
- 'host' => '127.0.0.1',
12
- 'ssl' => true,
13
- 'ssl_mode' => 'REQUIRED',
14
- 'tls_min_version' => 3,
15
- }
16
- end
17
- let(:replica_config) do
18
- {
19
- 'username' => 'replica',
20
- 'password' => 'replica_password',
21
- 'host' => '127.0.0.1',
22
- 'pool' => 500,
23
- 'ssl' => true,
24
- 'ssl_mode' => 'REQUIRED',
25
- 'tls_min_version' => 3,
26
- }
27
- end
28
- let(:config) do
29
- {
30
- database:,
31
- adapter: 'janus_mysql2',
32
- janus: {
33
- 'primary' => primary_config,
34
- 'replica' => replica_config,
35
- },
36
- }
37
- end
38
-
39
- describe 'Configuration' do
40
- it 'creates primary connection as expected' do
41
- config = primary_config.dup.freeze
42
- expect(subject.config).to eq config.merge('database' => database,
43
- 'flags' => ::Janus::Client::FOUND_ROWS).symbolize_keys
44
- end
45
-
46
- it 'creates replica connection as expected' do
47
- config = replica_config.dup.freeze
48
- expect(
49
- subject.replica_connection.instance_variable_get(:@config)
50
- ).to eq config.merge('database' => database, 'flags' => ::Janus::Client::FOUND_ROWS).symbolize_keys
51
- end
52
-
53
- context 'Rails sets empty database for server connection' do
54
- let(:database) { nil }
55
-
56
- it 'creates primary connection as expected' do
57
- config = primary_config.dup.freeze
58
- expect(subject.config).to eq config.merge(
59
- 'database' => nil,
60
- 'flags' => ::Janus::Client::FOUND_ROWS
61
- ).symbolize_keys
62
- end
63
-
64
- it 'creates replica connection as expected' do
65
- config = replica_config.dup.freeze
66
- expect(
67
- subject.replica_connection.instance_variable_get(:@config)
68
- ).to eq config.merge('database' => nil, 'flags' => ::Janus::Client::FOUND_ROWS).symbolize_keys
69
- end
70
- end
71
- end
72
-
73
- describe 'Integration tests' do
74
- let(:table_name) { 'table_name_mysql2' }
75
-
76
- it_behaves_like 'a mysql like server'
77
- end
78
- end
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- RSpec.describe ActiveRecord::ConnectionAdapters::JanusTrilogyAdapter do
4
- subject { described_class.new(config) }
5
-
6
- let(:database) { 'test' }
7
- let(:primary_config) do
8
- {
9
- 'username' => 'primary',
10
- 'password' => 'primary_password',
11
- 'host' => '127.0.0.1',
12
- 'ssl' => true,
13
- 'ssl_mode' => 'REQUIRED',
14
- 'tls_min_version' => Trilogy::TLS_VERSION_12,
15
- }
16
- end
17
- let(:replica_config) do
18
- {
19
- 'username' => 'replica',
20
- 'password' => 'replica_password',
21
- 'host' => '127.0.0.1',
22
- 'pool' => 500,
23
- 'ssl' => true,
24
- 'ssl_mode' => 'REQUIRED',
25
- 'tls_min_version' => Trilogy::TLS_VERSION_12,
26
- }
27
- end
28
- let(:config) do
29
- {
30
- database:,
31
- adapter: 'janus_trilogy',
32
- janus: {
33
- 'primary' => primary_config,
34
- 'replica' => replica_config,
35
- },
36
- }
37
- end
38
-
39
- describe 'Configuration' do
40
- # Trilogy enables FOUND_ROWS via the `found_rows` option (not mysql2-style
41
- # flags), and ActiveRecord's TrilogyAdapter forces it on. We assert it is
42
- # present even though the supplied config omits it.
43
- it 'creates primary connection as expected' do
44
- config = primary_config.dup.freeze
45
- expect(subject.config).to eq config.merge('database' => database, 'found_rows' => true).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, 'found_rows' => true).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('database' => nil, 'found_rows' => true).symbolize_keys
61
- end
62
-
63
- it 'creates replica connection as expected' do
64
- config = replica_config.dup.freeze
65
- expect(
66
- subject.replica_connection.instance_variable_get(:@config)
67
- ).to eq config.merge('database' => nil, 'found_rows' => true).symbolize_keys
68
- end
69
- end
70
- end
71
-
72
- describe 'Integration tests' do
73
- let(:table_name) { 'table_name_trilogy' }
74
-
75
- it_behaves_like 'a mysql like server'
76
- end
77
- end
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'janus-ar/client'
4
-
5
- RSpec.describe Janus::Client do
6
- it { expect(described_class::FOUND_ROWS).to eq 2 }
7
- end
@@ -1,118 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'janus-ar/context'
4
- require 'active_support/executor'
5
-
6
- RSpec.describe Janus::Context do
7
- after(:each) { described_class.release_all }
8
-
9
- describe '#initialize' do
10
- it 'defaults to the replica and a primary last-used connection' do
11
- context = described_class.new
12
- expect(context.use_primary?).to be false
13
- expect(context.last_used_connection).to eq(:primary)
14
- end
15
-
16
- it 'can be created pinned to the primary' do
17
- expect(described_class.new(primary: true).use_primary?).to be true
18
- end
19
- end
20
-
21
- describe '#stick_to_primary' do
22
- it 'sets the primary flag to true' do
23
- context = described_class.new
24
- context.stick_to_primary
25
- expect(context.use_primary?).to be true
26
- end
27
- end
28
-
29
- describe '#release_all' do
30
- it 'resets the primary flag and the last used connection' do
31
- context = described_class.new(primary: true)
32
- context.used_connection(:primary)
33
- context.release_all
34
- expect(context.use_primary?).to be false
35
- expect(context.last_used_connection).to be_nil
36
- end
37
- end
38
-
39
- describe '#used_connection' do
40
- it 'sets the last used connection' do
41
- context = described_class.new
42
- context.used_connection(:replica)
43
- expect(context.last_used_connection).to eq(:replica)
44
- end
45
- end
46
-
47
- describe 'class-level access' do
48
- it 'sticks and releases the current execution state' do
49
- described_class.stick_to_primary
50
- expect(described_class.use_primary?).to be true
51
- described_class.release_all
52
- expect(described_class.use_primary?).to be false
53
- end
54
-
55
- it 'isolates state between threads' do
56
- described_class.stick_to_primary
57
- other = Thread.new { described_class.use_primary? }.value
58
- expect(other).to be false
59
- expect(described_class.use_primary?).to be true
60
- end
61
-
62
- it 'isolates the last used connection between threads' do
63
- described_class.used_connection(:replica)
64
- other = Thread.new do
65
- described_class.used_connection(:primary)
66
- described_class.last_used_connection
67
- end.value
68
- expect(other).to eq(:primary)
69
- expect(described_class.last_used_connection).to eq(:replica)
70
- end
71
-
72
- it 'is safe to release a context that was never touched' do
73
- Thread.new do
74
- expect { described_class.release_all }.not_to raise_error
75
- expect(described_class.use_primary?).to be false
76
- end.join
77
- end
78
- end
79
-
80
- describe '.install_reset_hook' do
81
- let(:executor) { Class.new(ActiveSupport::Executor) }
82
-
83
- it 'releases the context at the start of every executor run' do
84
- described_class.install_reset_hook(executor)
85
- described_class.stick_to_primary
86
-
87
- stuck_inside = nil
88
- executor.wrap { stuck_inside = described_class.use_primary? }
89
-
90
- expect(stuck_inside).to be false
91
- end
92
-
93
- it 'stops stickiness leaking from a previous run into the next' do
94
- described_class.install_reset_hook(executor)
95
-
96
- executor.wrap { described_class.stick_to_primary }
97
-
98
- leaked = nil
99
- executor.wrap { leaked = described_class.use_primary? }
100
- expect(leaked).to be false
101
- end
102
-
103
- it 'still starts the next run clean after the previous run raised' do
104
- described_class.install_reset_hook(executor)
105
-
106
- expect do
107
- executor.wrap do
108
- described_class.stick_to_primary
109
- raise 'boom'
110
- end
111
- end.to raise_error('boom')
112
-
113
- leaked = nil
114
- executor.wrap { leaked = described_class.use_primary? }
115
- expect(leaked).to be false
116
- end
117
- end
118
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'janus-ar/db_console_config'
4
-
5
- RSpec.describe Janus::DbConsoleConfig do
6
- subject(:console_config) { described_class.new(db_config) }
7
-
8
- let(:db_config) do
9
- instance_double(
10
- ActiveRecord::DatabaseConfigurations::HashConfig,
11
- configuration_hash: {
12
- database: 'my_database',
13
- janus: {
14
- 'primary' => { 'host' => 'primary.local', 'username' => 'primary' },
15
- 'replica' => { 'host' => 'replica.local', 'username' => 'replica' },
16
- },
17
- }
18
- )
19
- end
20
-
21
- it 'exposes the replica configuration with symbol keys so dbconsole connects to a replica' do
22
- expect(console_config.configuration_hash).to eq(host: 'replica.local', username: 'replica')
23
- end
24
-
25
- it 'exposes the database name from the top-level config' do
26
- expect(console_config.database).to eq('my_database')
27
- end
28
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- RSpec.describe Janus::Logging::Logger do
4
- describe '.log' do
5
- let(:logger) { double('logger') }
6
-
7
- before do
8
- described_class.logger = logger
9
- end
10
-
11
- it 'logs the message with the specified format' do
12
- expect(logger).to receive(:send).with(:info, '[Janus] Test message')
13
- described_class.log('Test message', :info)
14
- end
15
-
16
- it 'does not log the message if logger is not set' do
17
- described_class.logger = nil
18
- expect(logger).not_to receive(:send)
19
- described_class.log('Test message', :info)
20
- end
21
- end
22
-
23
- describe '.logger=' do
24
- let(:logger) { double('logger') }
25
-
26
- it 'sets the logger' do
27
- described_class.logger = logger
28
- expect(described_class.logger).to eq(logger)
29
- end
30
- end
31
- end
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'janus-ar/context'
4
- require 'janus-ar/logging/subscriber'
5
-
6
- RSpec.describe Janus::Logging::Subscriber do
7
- # A minimal stand-in for ActiveRecord::LogSubscriber: it records the event it
8
- # is given so we can assert on the (possibly rewritten) payload name.
9
- let(:base_class) do
10
- Class.new do
11
- attr_reader :received_event
12
-
13
- def sql(event)
14
- @received_event = event
15
- end
16
- end
17
- end
18
- let(:subscriber) { base_class.new.extend(described_class) }
19
- let(:event) { instance_double(ActiveSupport::Notifications::Event, payload: { name: 'User Load' }) }
20
-
21
- after(:each) { Janus::Context.release_all }
22
-
23
- it 'tags the log name with the last used connection' do
24
- Janus::Context.used_connection(:replica)
25
-
26
- subscriber.sql(event)
27
-
28
- expect(event.payload[:name]).to eq('[replica] User Load')
29
- expect(subscriber.received_event).to eq(event)
30
- end
31
-
32
- it 'reflects the primary connection' do
33
- Janus::Context.used_connection(:primary)
34
-
35
- subscriber.sql(event)
36
-
37
- expect(event.payload[:name]).to eq('[primary] User Load')
38
- end
39
-
40
- it 'leaves the name unchanged when no connection has been used yet' do
41
- Janus::Context.release_all
42
-
43
- subscriber.sql(event)
44
-
45
- expect(event.payload[:name]).to eq('User Load')
46
- end
47
-
48
- Janus::Logging::Subscriber::IGNORE_PAYLOAD_NAMES.each do |ignored|
49
- it "does not tag #{ignored} statements" do
50
- Janus::Context.used_connection(:replica)
51
- ignored_event = instance_double(ActiveSupport::Notifications::Event, payload: { name: ignored })
52
-
53
- subscriber.sql(ignored_event)
54
-
55
- expect(ignored_event.payload[:name]).to eq(ignored)
56
- end
57
- end
58
- end
@@ -1,141 +0,0 @@
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
-
18
- it { expect(described_class::ALL).to eq :all }
19
- it { expect(described_class::REPLICA).to eq :replica }
20
- it { expect(described_class::PRIMARY).to eq :primary }
21
- end
22
-
23
- describe '#where_to_send?' do
24
- let(:open_transactions) { 0 }
25
-
26
- before(:each) { Janus::Context.release_all }
27
-
28
- context 'with reads' do
29
- {
30
- 'plain select' => 'SELECT * FROM users',
31
- 'lower case select' => 'select * from users',
32
- 'CTE select' => 'WITH recent AS (SELECT * FROM users) SELECT * FROM recent',
33
- 'leading whitespace' => "\n\t SELECT 1",
34
- }.each do |label, query|
35
- it "routes a #{label} to the replica" do
36
- expect(described_class.new(query, 0).where_to_send?).to eq(:replica)
37
- end
38
- end
39
- end
40
-
41
- context 'with writes' do
42
- # Every one of these used to be sent to the replica because the router
43
- # defaulted unknown statements there. They are genuine writes and must
44
- # reach the primary.
45
- %w(
46
- INSERT UPDATE DELETE REPLACE RENAME CALL LOAD OPTIMIZE ANALYZE REPAIR
47
- CREATE DROP TRUNCATE ALTER
48
- ).each do |verb|
49
- it "routes #{verb} to the primary" do
50
- sql = "#{verb} something that is not a read"
51
- expect(described_class.new(sql, 0).where_to_send?).to eq(:primary)
52
- end
53
- end
54
-
55
- it 'routes a write annotated with a leading comment to the primary' do
56
- sql = '/* app:web,controller:orders */ INSERT INTO orders (id) VALUES (1)'
57
- expect(described_class.new(sql, 0).where_to_send?).to eq(:primary)
58
- end
59
-
60
- it 'routes a read annotated with a leading comment to the replica' do
61
- sql = '/* app:web */ SELECT * FROM orders'
62
- expect(described_class.new(sql, 0).where_to_send?).to eq(:replica)
63
- end
64
- end
65
-
66
- context 'with comments and whitespace around the statement' do
67
- {
68
- 'a line (--) comment then a read' => "-- audit trail\nSELECT * FROM orders",
69
- 'a hash (#) comment then a read' => "# cache key\nSELECT * FROM orders",
70
- 'several stacked leading comments then a read' => "/* a */ /* b */\n\t SELECT 1",
71
- 'tabs and newlines then a read' => "\n\t SELECT 1",
72
- }.each do |label, query|
73
- it "routes #{label} to the replica" do
74
- expect(described_class.new(query, 0).where_to_send?).to eq(:replica)
75
- end
76
- end
77
-
78
- {
79
- 'a line (--) comment then a write' => "-- triggered by job 42\nUPDATE orders SET state = 1",
80
- 'a hash (#) comment then a write' => "# backfill\nDELETE FROM orders",
81
- 'a block comment then a write' => "/* migration */ ALTER TABLE orders ADD COLUMN x INT",
82
- }.each do |label, query|
83
- it "routes #{label} to the primary" do
84
- expect(described_class.new(query, 0).where_to_send?).to eq(:primary)
85
- end
86
- end
87
- end
88
-
89
- context 'with locking reads' do
90
- it 'routes SELECT ... FOR UPDATE to the primary' do
91
- expect(described_class.new('SELECT * FROM users FOR UPDATE', 0).where_to_send?).to eq(:primary)
92
- end
93
-
94
- it 'routes SELECT ... LOCK IN SHARE MODE to the primary' do
95
- expect(described_class.new('SELECT * FROM users LOCK IN SHARE MODE', 0).where_to_send?).to eq(:primary)
96
- end
97
-
98
- it 'routes advisory lock reads to the primary' do
99
- expect(described_class.new("SELECT get_lock('x', 0)", 0).where_to_send?).to eq(:primary)
100
- end
101
- end
102
-
103
- context 'with SHOW' do
104
- it 'routes SHOW statements to the primary' do
105
- expect(described_class.new('SHOW TABLES', 0).where_to_send?).to eq(:primary)
106
- end
107
- end
108
-
109
- context 'with SET' do
110
- it 'routes SET to all connections' do
111
- expect(described_class.new('SET sql_mode = ?', 0).where_to_send?).to eq(:all)
112
- end
113
-
114
- it 'does not broadcast SET LOCAL to all connections' do
115
- expect(described_class.new('SET LOCAL sql_mode = ?', 0).where_to_send?).to eq(:primary)
116
- end
117
- end
118
-
119
- context 'when the context is stuck to the primary' do
120
- before(:each) { Janus::Context.stick_to_primary }
121
-
122
- it 'routes an otherwise replica-bound read to the primary' do
123
- expect(described_class.new('SELECT * FROM users', 0).where_to_send?).to eq(:primary)
124
- end
125
- end
126
-
127
- context 'when inside a transaction' do
128
- let(:open_transactions) { 1 }
129
-
130
- it 'routes reads to the primary so they can see uncommitted writes' do
131
- expect(described_class.new('SELECT * FROM users', open_transactions).where_to_send?).to eq(:primary)
132
- end
133
- end
134
-
135
- context 'with an unrecognised / empty statement' do
136
- it 'defaults blank input to the primary' do
137
- expect(described_class.new(' ', 0).where_to_send?).to eq(:primary)
138
- end
139
- end
140
- end
141
- end