janus-ar 8.0.0 → 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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/Gemfile.lock +142 -125
  4. data/README.md +153 -131
  5. data/janus-ar.gemspec +38 -36
  6. data/lib/janus-ar/active_record/connection_adapters/janus_mysql2_adapter.rb +32 -149
  7. data/lib/janus-ar/active_record/connection_adapters/janus_trilogy_adapter.rb +32 -149
  8. data/lib/janus-ar/adapter_extensions.rb +112 -0
  9. data/lib/janus-ar/client.rb +6 -6
  10. data/lib/janus-ar/context.rb +79 -80
  11. data/lib/janus-ar/db_console_config.rb +17 -17
  12. data/lib/janus-ar/logging/logger.rb +15 -15
  13. data/lib/janus-ar/logging/subscriber.rb +27 -27
  14. data/lib/janus-ar/query_director.rb +87 -54
  15. data/lib/janus-ar/railtie.rb +15 -0
  16. data/lib/janus-ar/version.rb +17 -17
  17. data/lib/janus-ar.rb +24 -22
  18. metadata +10 -30
  19. data/.bundle/config +0 -5
  20. data/.github/dependabot.yml +0 -7
  21. data/.github/workflows/ci.yml +0 -60
  22. data/.github/workflows/publish.yml +0 -25
  23. data/.gitignore +0 -2
  24. data/.rspec +0 -3
  25. data/.rubocop.yml +0 -26
  26. data/assets/.gitkeep +0 -1
  27. data/assets/janus-logo.png +0 -0
  28. data/bin/release.sh +0 -3
  29. data/spec/lib/janus-ar/active_record/connection_adapters/janus_mysql_adapter_spec.rb +0 -82
  30. data/spec/lib/janus-ar/active_record/connection_adapters/janus_trilogy_adapter_spec.rb +0 -82
  31. data/spec/lib/janus-ar/client_spec.rb +0 -7
  32. data/spec/lib/janus-ar/context_spec.rb +0 -46
  33. data/spec/lib/janus-ar/logging/logger_spec.rb +0 -31
  34. data/spec/lib/janus-ar/query_director_spec.rb +0 -59
  35. data/spec/shared_examples/a_mysql_like_server.rb +0 -85
  36. data/spec/spec_helper.rb +0 -46
@@ -1,60 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches:
6
- - main
7
- pull_request:
8
-
9
- jobs:
10
- build:
11
- runs-on: ubuntu-latest
12
- strategy:
13
- fail-fast: false
14
- matrix:
15
- ruby:
16
- - '3.2'
17
- name: Ruby ${{ matrix.ruby }}
18
- services:
19
- mysql:
20
- image: mysql:8
21
- env:
22
- MYSQL_DATABASE: test
23
- MYSQL_ROOT_PASSWORD: password
24
- MYSQL_USER: test
25
- MYSQL_PASSWORD: test_password
26
- ports:
27
- - 3306:3306
28
- options: >-
29
- --health-cmd "mysqladmin ping"
30
- --health-interval 10s
31
- --health-timeout 5s
32
- --health-retries 5
33
- steps:
34
- - uses: actions/checkout@v2
35
- - uses: ruby/setup-ruby@v1
36
- with:
37
- ruby-version: ${{ matrix.ruby }}
38
- bundler-cache: true
39
- - run: |
40
- mysql -e "CREATE USER 'replica'@'%' IDENTIFIED 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 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
46
- - run: |
47
- bundle exec rspec
48
- env:
49
- MYSQL_HOST: 127.0.0.1
50
- RAILS_ENV: test
51
- RuboCop:
52
- runs-on: ubuntu-latest
53
- steps:
54
- - uses: actions/checkout@v2
55
- - uses: ruby/setup-ruby@v1
56
- with:
57
- ruby-version: '3.2'
58
- bundler-cache: true
59
- - run: |
60
- bundle exec rubocop --parallel --color
@@ -1,25 +0,0 @@
1
- name: Publish GEM
2
-
3
- on:
4
- push:
5
- tags:
6
- - v*
7
- jobs:
8
- push:
9
- runs-on: ubuntu-latest
10
-
11
- permissions:
12
- contents: write
13
- id-token: write
14
-
15
- steps:
16
- # Set up
17
- - uses: actions/checkout@v4
18
- - name: Set up Ruby
19
- uses: ruby/setup-ruby@v1
20
- with:
21
- bundler-cache: true
22
- ruby-version: ruby
23
-
24
- # Release
25
- - uses: rubygems/release-gem@v1
data/.gitignore DELETED
@@ -1,2 +0,0 @@
1
- janus-ar-*.gem
2
- .rubocop-*
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --force-color
2
- --format documentation
3
- --require ./spec/spec_helper.rb
data/.rubocop.yml DELETED
@@ -1,26 +0,0 @@
1
- inherit_from:
2
- - https://tech.olioex.com/.support/rubocop-styling-v1.0
3
-
4
- require:
5
- - rubocop-rails
6
-
7
- Style/Documentation:
8
- Enabled: false
9
-
10
- Rails/Delegate:
11
- Enabled: false
12
-
13
- Metrics/BlockLength:
14
- Exclude:
15
- - spec/**/*
16
-
17
- Style/GlobalVars:
18
- Exclude:
19
- - 'spec/**/*'
20
-
21
- Naming/FileName:
22
- Exclude:
23
- - lib/janus-ar.rb
24
-
25
- Metrics/AbcSize:
26
- Max: 25
data/assets/.gitkeep DELETED
@@ -1 +0,0 @@
1
-
Binary file
data/bin/release.sh DELETED
@@ -1,3 +0,0 @@
1
- #!/bin/bash
2
- gem build janus-ar.gemspec
3
- gem push janus-ar-*.gem
@@ -1,82 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- RSpec.describe ActiveRecord::ConnectionAdapters::JanusMysql2Adapter 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' => 3,
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' => 3,
28
- }
29
- end
30
- let(:config) do
31
- {
32
- database:,
33
- adapter: 'janus_mysql2',
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, 'flags' => ::Janus::Client::FOUND_ROWS).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, 'flags' => ::Janus::Client::FOUND_ROWS).symbolize_keys
71
- end
72
- end
73
- end
74
-
75
- describe 'Integration tests' do
76
- describe 'Integration tests' do
77
- let(:table_name) { 'table_name_mysql2' }
78
-
79
- it_behaves_like 'a mysql like server'
80
- end
81
- end
82
- end
@@ -1,82 +0,0 @@
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
- 'found_rows' => true,
18
- }
19
- end
20
- let(:replica_config) do
21
- {
22
- 'username' => 'replica',
23
- 'password' => 'replica_password',
24
- 'host' => '127.0.0.1',
25
- 'pool' => 500,
26
- 'ssl' => true,
27
- 'ssl_mode' => 'REQUIRED',
28
- 'tls_min_version' => Trilogy::TLS_VERSION_12,
29
- 'found_rows' => true,
30
- }
31
- end
32
- let(:config) do
33
- {
34
- database:,
35
- adapter: 'janus_trilogy',
36
- janus: {
37
- 'primary' => primary_config,
38
- 'replica' => replica_config,
39
- },
40
- }
41
- end
42
-
43
- describe 'Configuration' do
44
- it 'creates primary connection as expected' do
45
- config = primary_config.dup.freeze
46
- expect(subject.config).to eq config.merge('database' => database,
47
- 'flags' => ::Janus::Client::FOUND_ROWS).symbolize_keys
48
- end
49
-
50
- it 'creates replica connection as expected' do
51
- config = replica_config.dup.freeze
52
- expect(
53
- subject.replica_connection.instance_variable_get(:@config)
54
- ).to eq config.merge('database' => database).symbolize_keys
55
- end
56
-
57
- context 'Rails sets empty database for server connection' do
58
- let(:database) { nil }
59
-
60
- it 'creates primary connection as expected' do
61
- config = primary_config.dup.freeze
62
- expect(subject.config).to eq config.merge(
63
- 'database' => nil,
64
- 'flags' => ::Janus::Client::FOUND_ROWS
65
- ).symbolize_keys
66
- end
67
-
68
- it 'creates replica connection as expected' do
69
- config = replica_config.dup.freeze
70
- expect(
71
- subject.replica_connection.instance_variable_get(:@config)
72
- ).to eq config.merge('database' => nil).symbolize_keys
73
- end
74
- end
75
- end
76
-
77
- describe 'Integration tests' do
78
- let(:table_name) { 'table_name_trilogy' }
79
-
80
- it_behaves_like 'a mysql like server'
81
- end
82
- 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,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'janus-ar/context'
4
-
5
- RSpec.describe Janus::Context do
6
- describe '#initialize' do
7
- it 'sets the primary flag and expiry' do
8
- context = described_class.new(primary: true, expiry: 60)
9
- expect(context.use_primary?).to be true
10
- expect(context.last_used_connection).to eq(:primary)
11
- end
12
- end
13
-
14
- describe '#stick_to_primary' do
15
- it 'sets the primary flag to true' do
16
- context = described_class.new
17
- context.stick_to_primary
18
- expect(context.use_primary?).to be true
19
- end
20
- end
21
-
22
- describe '#potential_write' do
23
- it 'calls stick_to_primary' do
24
- context = described_class.new
25
- expect(context).to receive(:stick_to_primary)
26
- context.potential_write
27
- end
28
- end
29
-
30
- describe '#release_all' do
31
- it 'resets the primary flag and expiry' do
32
- context = described_class.new(primary: true, expiry: 60)
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(:secondary)
43
- expect(context.last_used_connection).to eq(:secondary)
44
- end
45
- end
46
- 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,59 +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
- it {
18
- expect(described_class::WRITE_PREFIXES).to eq %w(INSERT UPDATE DELETE LOCK CREATE GRANT DROP ALTER TRUNCATE BEGIN
19
- SAVEPOINT FLUSH)
20
- }
21
-
22
- it { expect(described_class::ALL).to eq :all }
23
- it { expect(described_class::REPLICA).to eq :replica }
24
- it { expect(described_class::PRIMARY).to eq :primary }
25
- end
26
-
27
- describe '#where_to_send?' do
28
- before(:each) do
29
- Janus::Context.release_all
30
- end
31
-
32
- context 'when should send to all' do
33
- it 'returns :all' do
34
- sql = 'SET foo = bar'
35
- open_transactions = 0
36
- query_director = described_class.new(sql, open_transactions)
37
- expect(query_director.where_to_send?).to eq(:all)
38
- end
39
- end
40
-
41
- context 'when can go to replica' do
42
- it 'returns :replica' do
43
- sql = 'SELECT * FROM users'
44
- open_transactions = 0
45
- query_director = described_class.new(sql, open_transactions)
46
- expect(query_director.where_to_send?).to eq(:replica)
47
- end
48
- end
49
-
50
- context 'when should go to primary' do
51
- it 'returns :primary' do
52
- sql = 'INSERT INTO users (name) VALUES ("John")'
53
- open_transactions = 0
54
- query_director = described_class.new(sql, open_transactions)
55
- expect(query_director.where_to_send?).to eq(:primary)
56
- end
57
- end
58
- end
59
- end
@@ -1,85 +0,0 @@
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 DELETED
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'pry'
4
-
5
- require 'active_record'
6
-
7
- require './lib/janus-ar'
8
- require './lib/janus-ar/active_record/connection_adapters/janus_mysql2_adapter'
9
- require './lib/janus-ar/active_record/connection_adapters/janus_trilogy_adapter'
10
-
11
- ActiveRecord::ConnectionAdapters.register("janus_trilogy", "ActiveRecord::ConnectionAdapters::JanusTrilogyAdapter")
12
- ActiveRecord::ConnectionAdapters.register("janus_mysql2", "ActiveRecord::ConnectionAdapters::JanusMysql2Adapter")
13
-
14
- require './spec/shared_examples/a_mysql_like_server.rb'
15
-
16
- class QueryLogger
17
- def initialize
18
- @_logs = []
19
- end
20
-
21
- def flush_all
22
- @_logs = []
23
- end
24
-
25
- def log(level, message)
26
- @_logs << "#{level}: #{message}"
27
- end
28
-
29
- def error(message)
30
- log('error', message)
31
- end
32
-
33
- def queries
34
- @_logs
35
- end
36
-
37
- def debug?
38
- true
39
- end
40
-
41
- def debug(message)
42
- log('debug', message)
43
- end
44
- end
45
-
46
- $query_logger = ActiveRecord::Base.logger = QueryLogger.new