janus-ar 0.15.2 → 7.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,110 +1,129 @@
1
- # Janus ActiveRecord
2
-
3
- <p align="center">
4
- <img src="assets/janus-logo.png"
5
- alt="Janus Logo"
6
- style="float: left; margin: 0 auto; height: 500px;" />
7
- </p>
8
-
9
- > In ancient Roman religion and myth, Janus (/ˈdʒeɪnəs/ JAY-nəs; Latin: Ianvs [ˈi̯aːnʊs]) is the god of beginnings, gates, transitions, time, duality, doorways,[2] passages, frames, and endings. [(wikipedia)](https://en.wikipedia.org/wiki/Janus)
10
-
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
- [![Gem Version](https://badge.fury.io/rb/janus-ar.svg)](https://badge.fury.io/rb/janus-ar)
13
-
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.
17
-
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.
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
-
24
- ## Installation
25
-
26
- Use the current version of the gem from [rubygems](https://rubygems.org/gems/janus-ar) in your `Gemfile`.
27
-
28
- ```ruby
29
- gem 'janus-ar'
30
- ```
31
-
32
- This project assumes that your read/write endpoints are handled by a separate system (e.g. DNS).
33
-
34
- ## Usage
35
-
36
- After a write request during a thread the adapter will continue using the `primary` server, unless the context is specifically released.
37
-
38
- ### Configuration
39
-
40
- Update your **database.yml** as follows:
41
-
42
- ```yml
43
- development:
44
- adapter: janus_mysql2
45
- database: database_name
46
- janus:
47
- primary:
48
- <<: *default
49
- host: primary-host.local
50
- replica:
51
- <<: *default
52
- password: ithappenstobedifferent
53
- host: replica-host.local
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"
67
-
68
- ### Forcing connections
69
-
70
- A context is local to the curent thread of execution. This will allow you to stick to the primary safely in a single thread
71
- in systems such as sidekiq, for instance.
72
-
73
- #### Releasing stuck connections (clearing context)
74
-
75
- If you need to clear the current context, releasing any stuck connections, all you have to do is:
76
-
77
- ```ruby
78
- Janus::Context.release_all
79
- ```
80
-
81
- #### Forcing connection to primary server
82
-
83
- ```ruby
84
- Janus::Context.stick_to_primary
85
- ```
86
-
87
- ### Logging
88
-
89
- You can set a logger instance to `::Janus::Logging::Logger.logger`:
90
-
91
- ```ruby
92
- Janus::Logging::Logger.logger = ::Logger.new(STDOUT)
93
- ```
94
-
95
- If using `ActiveRecord` logging, Janus will append the name of the connection used to any logs e.g. `[primary]` or `[replica]`.
96
-
97
- ### What queries goes where?
98
-
99
- In general: Any `SELECT` statements will execute against your replica(s), anything else will go to the primary.
100
-
101
- There are some edge cases:
102
- * `SET` operations will be sent to all connections
103
- * Execution of specific methods such as `connect!`, `disconnect!`, `reconnect!`, and `clear_cache!` are invoked on all underlying connections
104
- * 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)
105
- * Locking reads (e.g. `SELECT ... FOR UPDATE`) will always be sent to the primary
106
-
107
-
108
- # Acknowlegements
109
-
110
- Amazing project logo by @undevelopedbruce.
1
+ # Janus ActiveRecord
2
+
3
+ <p align="center">
4
+ <img src="assets/janus-logo.png"
5
+ alt="Janus Logo"
6
+ style="float: left; margin: 0 auto; height: 500px;" />
7
+ </p>
8
+
9
+ > In ancient Roman religion and myth, Janus (/ˈdʒeɪnəs/ JAY-nəs; Latin: Ianvs [ˈi̯aːnʊs]) is the god of beginnings, gates, transitions, time, duality, doorways,[2] passages, frames, and endings. [(wikipedia)](https://en.wikipedia.org/wiki/Janus)
10
+
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
+ [![Gem Version](https://badge.fury.io/rb/janus-ar.svg)](https://badge.fury.io/rb/janus-ar)
13
+
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.
17
+
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.
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
+
24
+ ## Installation
25
+
26
+ Use the current version of the gem from [rubygems](https://rubygems.org/gems/janus-ar) in your `Gemfile`.
27
+
28
+ ```ruby
29
+ gem 'janus-ar'
30
+ ```
31
+
32
+ This project assumes that your read/write endpoints are handled by a separate system (e.g. DNS).
33
+
34
+ ## Usage
35
+
36
+ After a write request during a thread the adapter will continue using the `primary` server, unless the context is specifically released.
37
+
38
+ ## Rails 7.2+
39
+
40
+ For Rails 7.2 you'll need to manually register the database adaptor in `config/application.rb` after requiring rails but before entering the application configuration, e.g.
41
+
42
+ ```ruby
43
+ require 'rails/all'
44
+
45
+ ActiveRecord::ConnectionAdapters.register("janus_trilogy", "ActiveRecord::ConnectionAdapters::JanusTrilogyAdapter", 'janus-ar/active_record/connection_adapters/janus_trilogy_adapter')
46
+ # ...or...
47
+ ActiveRecord::ConnectionAdapters.register("janus_mysql2", "ActiveRecord::ConnectionAdapters::JanusMysql2Adapter", 'janus-ar/active_record/connection_adapters/janus_mysql2_adapter')
48
+ ```
49
+
50
+ ## Rails <= 7.1
51
+
52
+ ActiveRecord 7.1 was tested up to releases v0.15.*. After this release we only tested Rails 7.2+. This does not mean it is not compatible, just not tested.
53
+
54
+ ### Configuration
55
+
56
+ Update your **database.yml** as follows:
57
+
58
+ ```yml
59
+ development:
60
+ adapter: janus_mysql2
61
+ database: database_name
62
+ janus:
63
+ primary:
64
+ <<: *default
65
+ host: primary-host.local
66
+ replica:
67
+ <<: *default
68
+ password: ithappenstobedifferent
69
+ host: replica-host.local
70
+ ```
71
+ Note: For `trilogy` please use adapter "janus_trilogy". You'll probably need to add the following to your configuration to have it connect:
72
+
73
+ ```yml
74
+ ssl: true
75
+ ssl_mode: 'REQUIRED'
76
+ tls_min_version: 3
77
+ ```
78
+
79
+ `tls_min_version` here refers to TLS1.2.
80
+
81
+ Otherwise you will get an error like the following (see https://github.com/trilogy-libraries/trilogy/issues/26):
82
+ > trilogy_auth_recv: caching_sha2_password requires either TCP with TLS or a unix socket: TRILOGY_UNSUPPORTED"
83
+
84
+ ### Forcing connections
85
+
86
+ A context is local to the curent thread of execution. This will allow you to stick to the primary safely in a single thread
87
+ in systems such as sidekiq, for instance.
88
+
89
+ #### Releasing stuck connections (clearing context)
90
+
91
+ If you need to clear the current context, releasing any stuck connections, all you have to do is:
92
+
93
+ ```ruby
94
+ Janus::Context.release_all
95
+ ```
96
+
97
+ #### Forcing connection to primary server
98
+
99
+ ```ruby
100
+ Janus::Context.stick_to_primary
101
+ ```
102
+
103
+ ### Logging
104
+
105
+ You can set a logger instance to `::Janus::Logging::Logger.logger`:
106
+
107
+ ```ruby
108
+ Janus::Logging::Logger.logger = ::Logger.new(STDOUT)
109
+ ```
110
+
111
+ If using `ActiveRecord` logging, Janus will append the name of the connection used to any logs e.g. `[primary]` or `[replica]`.
112
+
113
+ ### What queries goes where?
114
+
115
+ In general: Any `SELECT` statements will execute against your replica(s), anything else will go to the primary.
116
+
117
+ There are some edge cases:
118
+ * `SET` operations will be sent to all connections
119
+ * Execution of specific methods such as `connect!`, `disconnect!`, `reconnect!`, and `clear_cache!` are invoked on all underlying connections
120
+ * 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)
121
+ * Locking reads (e.g. `SELECT ... FOR UPDATE`) will always be sent to the primary
122
+
123
+ # Notes
124
+
125
+ Janus does not support Rails' read/write split or sharding using `with_connection`.
126
+
127
+ # Acknowlegements
128
+
129
+ Amazing project logo by @undevelopedbruce.
data/Rakefile CHANGED
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
data/janus-ar.gemspec CHANGED
@@ -1,36 +1,36 @@
1
- # frozen_string_literal: true
2
-
3
- require File.expand_path('lib/janus/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_development_dependency 'activerecord', '>= 7.1.0'
25
- gem.add_development_dependency 'activesupport', '>= 7.1.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.63.0'
32
- gem.add_development_dependency 'rubocop-rails', '~> 2.24.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
+ 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', '~> 7.2'
25
+ gem.add_development_dependency 'activesupport', '>= 7.2.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.65.0'
32
+ gem.add_development_dependency 'rubocop-rails', '~> 2.26.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,144 +1,148 @@
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'
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 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
-
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::Mysql2Adapter.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
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, async: false, allow_retry: false, materialize_transactions: true)
55
+ case where_to_send?(sql)
56
+ when :all
57
+ send_to_replica(sql, connection: :all, method: :raw_execute)
58
+ super
59
+ when :replica
60
+ send_to_replica(sql, connection: :replica, method: :raw_execute)
61
+ else
62
+ Janus::Context.stick_to_primary if write_query?(sql)
63
+ Janus::Context.used_connection(:primary)
64
+ super
65
+ end
66
+ end
67
+
68
+ def execute(sql)
69
+ case where_to_send?(sql)
70
+ when :all
71
+ send_to_replica(sql, connection: :all, method: :execute)
72
+ super(sql)
73
+ when :replica
74
+ send_to_replica(sql, connection: :replica, method: :execute)
75
+ else
76
+ Janus::Context.stick_to_primary if write_query?(sql)
77
+ Janus::Context.used_connection(:primary)
78
+ super(sql)
79
+ end
80
+ end
81
+
82
+ def execute_and_free(sql, name = nil, async: false)
83
+ case where_to_send?(sql)
84
+ when :all
85
+ send_to_replica(sql, connection: :all, method: :execute)
86
+ super(sql, name, async:)
87
+ when :replica
88
+ send_to_replica(sql, connection: :replica, method: :execute)
89
+ else
90
+ Janus::Context.stick_to_primary if write_query?(sql)
91
+ Janus::Context.used_connection(:primary)
92
+ super(sql, name, async:)
93
+ end
94
+ end
95
+
96
+ def connect!(...)
97
+ replica_connection.connect!(...)
98
+ super
99
+ end
100
+
101
+ def reconnect!(...)
102
+ replica_connection.reconnect!(...)
103
+ super
104
+ end
105
+
106
+ def disconnect!(...)
107
+ replica_connection.disconnect!(...)
108
+ super
109
+ end
110
+
111
+ def clear_cache!(...)
112
+ replica_connection.clear_cache!(...)
113
+ super
114
+ end
115
+
116
+ def replica_connection
117
+ @replica_connection ||= ActiveRecord::ConnectionAdapters::Mysql2Adapter.new(@replica_config)
118
+ end
119
+
120
+ private
121
+
122
+ def where_to_send?(sql)
123
+ Janus::QueryDirector.new(sql, open_transactions).where_to_send?
124
+ end
125
+
126
+ def send_to_replica(sql, connection: nil, method: :exec_query)
127
+ Janus::Context.used_connection(connection) if connection
128
+ if method == :execute
129
+ replica_connection.execute(sql)
130
+ elsif method == :raw_execute
131
+ replica_connection.execute(sql)
132
+ else
133
+ replica_connection.exec_query(sql)
134
+ end
135
+ end
136
+
137
+ def update_config
138
+ @config[:flags] ||= 0
139
+
140
+ if @config[:flags].is_a? Array
141
+ @config[:flags].push FOUND_ROWS
142
+ else
143
+ @config[:flags] |= ::Janus::Client::FOUND_ROWS
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end