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
data/janus-ar.gemspec CHANGED
@@ -1,36 +1,38 @@
1
- # frozen_string_literal: true
2
-
3
- require File.expand_path('lib/janus-ar/version.rb', __dir__)
4
-
5
- Gem::Specification.new do |gem|
6
- gem.authors = ['Lloyd Watkin']
7
- gem.email = ['lloyd@olioex.com']
8
- gem.description = 'Read/Write proxy for ActiveRecord using primary/replica databases'
9
- gem.summary = 'Read/Write proxy for ActiveRecord using primary/replica databases'
10
- gem.homepage = 'https://github.com/olioex/janus-ar'
11
- gem.licenses = %w(MIT)
12
- gem.metadata = {
13
- 'source_code_uri' => 'https://github.com/olioex/janus-ar',
14
- }
15
-
16
- gem.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
17
- gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
18
- gem.name = 'janus-ar'
19
- gem.require_paths = %w(lib)
20
- gem.version = Janus::VERSION
21
-
22
- gem.required_ruby_version = '>= 3.2.0'
23
-
24
- gem.add_dependency 'activerecord', '>= 8.0', '< 9.0'
25
- gem.add_development_dependency 'activesupport', '>= 8.0'
26
- gem.add_development_dependency 'mysql2'
27
- gem.add_development_dependency 'trilogy'
28
- gem.add_development_dependency 'pry'
29
- gem.add_development_dependency 'rake'
30
- gem.add_development_dependency 'rspec', '~> 3'
31
- gem.add_development_dependency 'rubocop', '~> 1.69.2'
32
- gem.add_development_dependency 'rubocop-rails', '~> 2.28.0'
33
- gem.add_development_dependency 'rubocop-rspec'
34
- gem.add_development_dependency 'rubocop-thread_safety'
35
- gem.add_development_dependency 'rubocop-performance'
36
- end
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path('lib/janus-ar/version.rb', __dir__)
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.authors = ['Lloyd Watkin']
7
+ gem.email = ['lloyd@olioex.com']
8
+ gem.description = 'Read/Write proxy for ActiveRecord using primary/replica databases'
9
+ gem.summary = 'Read/Write proxy for ActiveRecord using primary/replica databases'
10
+ gem.homepage = 'https://github.com/olioex/janus-ar'
11
+ gem.licenses = %w(MIT)
12
+ gem.metadata = {
13
+ 'source_code_uri' => 'https://github.com/olioex/janus-ar',
14
+ }
15
+
16
+ ignored = %r{^(bin|spec|assets|gemfiles|\.github)/|^\.}
17
+ gem.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR).grep_v(ignored)
18
+ # The gem ships no executables; bin/ holds repository maintenance scripts.
19
+ gem.executables = []
20
+ gem.name = 'janus-ar'
21
+ gem.require_paths = %w(lib)
22
+ gem.version = Janus::VERSION
23
+
24
+ gem.required_ruby_version = '>= 3.2.0'
25
+
26
+ gem.add_dependency 'activerecord', '>= 8.0', '< 9.0'
27
+ gem.add_development_dependency 'activesupport', '>= 8.0'
28
+ gem.add_development_dependency 'mysql2'
29
+ gem.add_development_dependency 'trilogy'
30
+ gem.add_development_dependency 'pry'
31
+ gem.add_development_dependency 'rake'
32
+ gem.add_development_dependency 'rspec', '~> 3'
33
+ gem.add_development_dependency 'rubocop', '~> 1.90.0'
34
+ gem.add_development_dependency 'rubocop-rails', '~> 2.37.0'
35
+ gem.add_development_dependency 'rubocop-rspec'
36
+ gem.add_development_dependency 'rubocop-thread_safety'
37
+ gem.add_development_dependency 'rubocop-performance'
38
+ end
@@ -1,149 +1,32 @@
1
- # frozen_string_literal: true
2
-
3
- require 'active_record/connection_adapters/abstract_adapter'
4
- require 'active_record/connection_adapters/mysql2_adapter'
5
- require_relative '../../../janus-ar'
6
-
7
- module ActiveRecord
8
- module ConnectionHandling
9
- def janus_mysql2_connection(config)
10
- ActiveRecord::ConnectionAdapters::JanusMysql2Adapter.new(config)
11
- end
12
- end
13
- end
14
-
15
- module ActiveRecord
16
- class Base
17
- def self.janus_mysql2_adapter_class
18
- ActiveRecord::ConnectionAdapters::JanusMysql2Adapter
19
- end
20
- end
21
- end
22
-
23
- module ActiveRecord
24
- module ConnectionAdapters
25
- class JanusMysql2Adapter < ActiveRecord::ConnectionAdapters::Mysql2Adapter
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']
43
- args[0] = args[0][:janus]['primary']
44
-
45
- super(*args)
46
- @connection_parameters ||= args[0]
47
- update_config
48
- end
49
-
50
- def with_connection(_args = {})
51
- self
52
- end
53
-
54
- def raw_execute(sql, name = nil, binds = [], prepare: false, async: false, allow_retry: false,
55
- materialize_transactions: true, batch: false)
56
- case where_to_send?(sql)
57
- when :all
58
- send_to_replica(sql, connection: :all, method: :raw_execute)
59
- super
60
- when :replica
61
- send_to_replica(sql, connection: :replica, method: :raw_execute)
62
- else
63
- Janus::Context.stick_to_primary if write_query?(sql)
64
- Janus::Context.used_connection(:primary)
65
- super
66
- end
67
- end
68
-
69
- def execute(sql)
70
- case where_to_send?(sql)
71
- when :all
72
- send_to_replica(sql, connection: :all, method: :execute)
73
- super(sql)
74
- when :replica
75
- send_to_replica(sql, connection: :replica, method: :execute)
76
- else
77
- Janus::Context.stick_to_primary if write_query?(sql)
78
- Janus::Context.used_connection(:primary)
79
- super(sql)
80
- end
81
- end
82
-
83
- def execute_and_free(sql, name = nil, async: false)
84
- case where_to_send?(sql)
85
- when :all
86
- send_to_replica(sql, connection: :all, method: :execute)
87
- super(sql, name, async:)
88
- when :replica
89
- send_to_replica(sql, connection: :replica, method: :execute)
90
- else
91
- Janus::Context.stick_to_primary if write_query?(sql)
92
- Janus::Context.used_connection(:primary)
93
- super(sql, name, async:)
94
- end
95
- end
96
-
97
- def connect!(...)
98
- replica_connection.connect!(...)
99
- super
100
- end
101
-
102
- def reconnect!(...)
103
- replica_connection.reconnect!(...)
104
- super
105
- end
106
-
107
- def disconnect!(...)
108
- replica_connection.disconnect!(...)
109
- super
110
- end
111
-
112
- def clear_cache!(...)
113
- replica_connection.clear_cache!(...)
114
- super
115
- end
116
-
117
- def replica_connection
118
- @replica_connection ||= ActiveRecord::ConnectionAdapters::Mysql2Adapter.new(@replica_config)
119
- end
120
-
121
- private
122
-
123
- def where_to_send?(sql)
124
- Janus::QueryDirector.new(sql, open_transactions).where_to_send?
125
- end
126
-
127
- def send_to_replica(sql, connection: nil, method: :exec_query)
128
- Janus::Context.used_connection(connection) if connection
129
- if method == :execute
130
- replica_connection.execute(sql)
131
- elsif method == :raw_execute
132
- replica_connection.execute(sql)
133
- else
134
- replica_connection.exec_query(sql)
135
- end
136
- end
137
-
138
- def update_config
139
- @config[:flags] ||= 0
140
-
141
- if @config[:flags].is_a? Array
142
- @config[:flags].push FOUND_ROWS
143
- else
144
- @config[:flags] |= ::Janus::Client::FOUND_ROWS
145
- end
146
- end
147
- end
148
- end
149
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/connection_adapters/abstract_adapter'
4
+ require 'active_record/connection_adapters/mysql2_adapter'
5
+ require_relative '../../../janus-ar'
6
+ require_relative '../../adapter_extensions'
7
+
8
+ module ActiveRecord
9
+ module ConnectionHandling
10
+ def janus_mysql2_connection(config)
11
+ ActiveRecord::ConnectionAdapters::JanusMysql2Adapter.new(config)
12
+ end
13
+ end
14
+
15
+ class Base
16
+ def self.janus_mysql2_adapter_class
17
+ ActiveRecord::ConnectionAdapters::JanusMysql2Adapter
18
+ end
19
+ end
20
+
21
+ module ConnectionAdapters
22
+ class JanusMysql2Adapter < ActiveRecord::ConnectionAdapters::Mysql2Adapter
23
+ include Janus::AdapterExtensions
24
+
25
+ private
26
+
27
+ def replica_adapter_class
28
+ ActiveRecord::ConnectionAdapters::Mysql2Adapter
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,149 +1,32 @@
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-ar'
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 = nil, binds = [], prepare: false, async: false, allow_retry: false,
51
- materialize_transactions: true, batch: false)
52
- case where_to_send?(sql)
53
- when :all
54
- send_to_replica(sql, connection: :all, method: :execute)
55
- super
56
- when :replica
57
- send_to_replica(sql, connection: :replica, method: :execute)
58
- else
59
- Janus::Context.stick_to_primary if write_query?(sql)
60
- Janus::Context.used_connection(:primary)
61
- super
62
- end
63
- end
64
-
65
- def execute(sql)
66
- case where_to_send?(sql)
67
- when :all
68
- send_to_replica(sql, connection: :all, method: :execute)
69
- super(sql)
70
- when :replica
71
- send_to_replica(sql, connection: :replica, method: :execute)
72
- else
73
- Janus::Context.stick_to_primary if write_query?(sql)
74
- Janus::Context.used_connection(:primary)
75
- super(sql)
76
- end
77
- end
78
-
79
- def execute_and_free(sql, name = nil, async: false)
80
- case where_to_send?(sql)
81
- when :all
82
- send_to_replica(sql, connection: :all, method: :execute)
83
- super(sql, name, async:)
84
- when :replica
85
- send_to_replica(sql, connection: :replica, method: :execute)
86
- else
87
- Janus::Context.stick_to_primary if write_query?(sql)
88
- Janus::Context.used_connection(:primary)
89
- super(sql, name, async:)
90
- end
91
- end
92
-
93
- def with_connection(_args = {})
94
- self
95
- end
96
-
97
- def connect!(...)
98
- replica_connection.connect!(...)
99
- super
100
- end
101
-
102
- def reconnect!(...)
103
- replica_connection.reconnect!(...)
104
- super
105
- end
106
-
107
- def disconnect!(...)
108
- replica_connection.disconnect!(...)
109
- super
110
- end
111
-
112
- def clear_cache!(...)
113
- replica_connection.clear_cache!(...)
114
- super
115
- end
116
-
117
- def replica_connection
118
- @replica_connection ||= ActiveRecord::ConnectionAdapters::TrilogyAdapter.new(@replica_config)
119
- end
120
-
121
- private
122
-
123
- def where_to_send?(sql)
124
- Janus::QueryDirector.new(sql, open_transactions).where_to_send?
125
- end
126
-
127
- def send_to_replica(sql, connection: nil, method: :exec_query)
128
- Janus::Context.used_connection(connection) if connection
129
- if method == :execute
130
- replica_connection.execute(sql)
131
- elsif method == :raw_execute
132
- replica_connection.execute(sql)
133
- else
134
- replica_connection.exec_query(sql)
135
- end
136
- end
137
-
138
- def update_config
139
- @config[:flags] ||= 0
140
-
141
- if @config[:flags].is_a? Array
142
- @config[:flags].push FOUND_ROWS
143
- else
144
- @config[:flags] |= ::Janus::Client::FOUND_ROWS
145
- end
146
- end
147
- end
148
- end
149
- end
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-ar'
6
+ require_relative '../../adapter_extensions'
7
+
8
+ module ActiveRecord
9
+ module ConnectionHandling
10
+ def janus_trilogy_connection(config)
11
+ ActiveRecord::ConnectionAdapters::JanusTrilogyAdapter.new(config)
12
+ end
13
+ end
14
+
15
+ class Base
16
+ def self.janus_trilogy_adapter_class
17
+ ActiveRecord::ConnectionAdapters::JanusTrilogyAdapter
18
+ end
19
+ end
20
+
21
+ module ConnectionAdapters
22
+ class JanusTrilogyAdapter < ActiveRecord::ConnectionAdapters::TrilogyAdapter
23
+ include Janus::AdapterExtensions
24
+
25
+ private
26
+
27
+ def replica_adapter_class
28
+ ActiveRecord::ConnectionAdapters::TrilogyAdapter
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Janus
4
+ # Behaviour shared by the Janus MySQL2 and Trilogy adapters.
5
+ #
6
+ # Each Janus adapter subclasses the matching ActiveRecord adapter and owns the
7
+ # *primary* connection (reached via `super`). This module routes every
8
+ # statement to the primary, a lazily created replica connection, or both, and
9
+ # keeps Janus::Context up to date.
10
+ #
11
+ # Including adapters only need to implement #replica_adapter_class.
12
+ module AdapterExtensions
13
+ def self.included(base)
14
+ base.extend(ClassMethods)
15
+ end
16
+
17
+ module ClassMethods
18
+ def dbconsole(config, options = {})
19
+ super(Janus::DbConsoleConfig.new(config), options)
20
+ end
21
+ end
22
+
23
+ attr_reader :config
24
+
25
+ def initialize(*args)
26
+ config = args[0]
27
+ config[:janus]['replica']['database'] = config[:database]
28
+ config[:janus]['primary']['database'] = config[:database]
29
+
30
+ @replica_config = config[:janus]['replica'].symbolize_keys
31
+ args[0] = config[:janus]['primary'].symbolize_keys
32
+
33
+ super
34
+ @connection_parameters ||= args[0]
35
+ end
36
+
37
+ # The argument lists below intentionally use anonymous splats and a bare
38
+ # `super`: ActiveRecord's `raw_execute`/`execute` signatures differ between
39
+ # versions, so we forward whatever we are given unchanged rather than
40
+ # restating (and pinning ourselves to) the current signature.
41
+ def raw_execute(sql, *, **)
42
+ case where_to_send?(sql)
43
+ when :all
44
+ send_to_replica(sql, :all)
45
+ super
46
+ when :replica
47
+ send_to_replica(sql, :replica)
48
+ else
49
+ mark_primary(sql)
50
+ super
51
+ end
52
+ end
53
+
54
+ def execute(sql, *, **)
55
+ case where_to_send?(sql)
56
+ when :all
57
+ send_to_replica(sql, :all)
58
+ super
59
+ when :replica
60
+ send_to_replica(sql, :replica)
61
+ else
62
+ mark_primary(sql)
63
+ super
64
+ end
65
+ end
66
+
67
+ def connect!(...)
68
+ replica_connection.connect!(...)
69
+ super
70
+ end
71
+
72
+ def reconnect!(...)
73
+ replica_connection.reconnect!(...)
74
+ super
75
+ end
76
+
77
+ def discard!(...)
78
+ replica_connection.discard!(...)
79
+ super
80
+ end
81
+
82
+ def disconnect!(...)
83
+ replica_connection.disconnect!(...)
84
+ super
85
+ end
86
+
87
+ def clear_cache!(...)
88
+ replica_connection.clear_cache!(...)
89
+ super
90
+ end
91
+
92
+ def replica_connection
93
+ @replica_connection ||= replica_adapter_class.new(@replica_config)
94
+ end
95
+
96
+ private
97
+
98
+ def mark_primary(sql)
99
+ Janus::Context.stick_to_primary if write_query?(sql)
100
+ Janus::Context.used_connection(:primary)
101
+ end
102
+
103
+ def where_to_send?(sql)
104
+ Janus::QueryDirector.new(sql, open_transactions).where_to_send?
105
+ end
106
+
107
+ def send_to_replica(sql, connection)
108
+ Janus::Context.used_connection(connection)
109
+ replica_connection.execute(sql)
110
+ end
111
+ end
112
+ end
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
- module Janus
3
- class Client
4
- FOUND_ROWS = 2
5
- end
6
- end
1
+ # frozen_string_literal: true
2
+ module Janus
3
+ class Client
4
+ FOUND_ROWS = 2
5
+ end
6
+ end