janus-ar 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +51 -0
- data/.rspec +3 -0
- data/.rubocop.yml +12 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +101 -0
- data/README.md +78 -0
- data/janus-ar.gemspec +31 -0
- data/lib/active_record/connection_adapters/janus_mysql2_adapter.rb +124 -0
- data/lib/janus/context.rb +80 -0
- data/lib/janus/logging/logger.rb +15 -0
- data/lib/janus/logging/subscriber.rb +27 -0
- data/lib/janus/version.rb +17 -0
- data/lib/janus.rb +19 -0
- data/spec/lib/janus/context_spec.rb +46 -0
- data/spec/lib/janus/logging/logger_spec.rb +31 -0
- data/spec/spec_helper.rb +3 -0
- metadata +159 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3fb7b603961432244454f93bc66d35e22466401a694c80b998ed16abbebdd5f9
|
4
|
+
data.tar.gz: eb21fbaedd97ff12c7a51f07cc54b971c8af4b2a7bbf8ed235870b810b13dddd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2c5df8a00ea7f86ef871db80b98b3ecc3ffe2cc9d74014fb71ac6b150cee316f920736deaa3ec97038f8ead55fc97a9487488d95367308c0b37ad746a9a913a6
|
7
|
+
data.tar.gz: fadc4ffb8335940a316bb4d66be416578e35976c50b2d9b66a8873d8c92f1c347c7ceb7771eeaf215f482fe0aded1b26480b0de7a710b02d37fa06af9da565eb
|
@@ -0,0 +1,51 @@
|
|
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:5.7
|
21
|
+
env:
|
22
|
+
MYSQL_ALLOW_EMPTY_PASSWORD: yes
|
23
|
+
MYSQL_DATABASE: janus_test
|
24
|
+
ports:
|
25
|
+
- 3306:3306
|
26
|
+
options: >-
|
27
|
+
--health-cmd "mysqladmin ping"
|
28
|
+
--health-interval 10s
|
29
|
+
--health-timeout 5s
|
30
|
+
--health-retries 5
|
31
|
+
steps:
|
32
|
+
- uses: actions/checkout@v2
|
33
|
+
- uses: ruby/setup-ruby@v1
|
34
|
+
with:
|
35
|
+
ruby-version: ${{ matrix.ruby }}
|
36
|
+
bundler-cache: true
|
37
|
+
- run: |
|
38
|
+
bundle exec rspec
|
39
|
+
env:
|
40
|
+
MYSQL_HOST: 127.0.0.1
|
41
|
+
RAILS_ENV: test
|
42
|
+
RuboCop:
|
43
|
+
runs-on: ubuntu-latest
|
44
|
+
steps:
|
45
|
+
- uses: actions/checkout@v2
|
46
|
+
- uses: ruby/setup-ruby@v1
|
47
|
+
with:
|
48
|
+
ruby-version: '3.2'
|
49
|
+
bundler-cache: true
|
50
|
+
- run: |
|
51
|
+
bundle exec rubocop --parallel --color
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
janus-ar (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: http://rubygems.org/
|
8
|
+
specs:
|
9
|
+
activemodel (7.1.3.2)
|
10
|
+
activesupport (= 7.1.3.2)
|
11
|
+
activerecord (7.1.3.2)
|
12
|
+
activemodel (= 7.1.3.2)
|
13
|
+
activesupport (= 7.1.3.2)
|
14
|
+
timeout (>= 0.4.0)
|
15
|
+
activesupport (7.1.3.2)
|
16
|
+
base64
|
17
|
+
bigdecimal
|
18
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
19
|
+
connection_pool (>= 2.2.5)
|
20
|
+
drb
|
21
|
+
i18n (>= 1.6, < 2)
|
22
|
+
minitest (>= 5.1)
|
23
|
+
mutex_m
|
24
|
+
tzinfo (~> 2.0)
|
25
|
+
ast (2.4.2)
|
26
|
+
base64 (0.2.0)
|
27
|
+
bigdecimal (3.1.7)
|
28
|
+
concurrent-ruby (1.2.3)
|
29
|
+
connection_pool (2.4.1)
|
30
|
+
diff-lcs (1.5.1)
|
31
|
+
drb (2.2.1)
|
32
|
+
i18n (1.14.4)
|
33
|
+
concurrent-ruby (~> 1.0)
|
34
|
+
json (2.7.1)
|
35
|
+
language_server-protocol (3.17.0.3)
|
36
|
+
minitest (5.22.3)
|
37
|
+
mutex_m (0.2.0)
|
38
|
+
mysql2 (0.5.6)
|
39
|
+
parallel (1.24.0)
|
40
|
+
parser (3.3.0.5)
|
41
|
+
ast (~> 2.4.1)
|
42
|
+
racc
|
43
|
+
racc (1.7.3)
|
44
|
+
rack (3.0.10)
|
45
|
+
rainbow (3.1.1)
|
46
|
+
rake (13.1.0)
|
47
|
+
regexp_parser (2.9.0)
|
48
|
+
rexml (3.2.6)
|
49
|
+
rspec (3.13.0)
|
50
|
+
rspec-core (~> 3.13.0)
|
51
|
+
rspec-expectations (~> 3.13.0)
|
52
|
+
rspec-mocks (~> 3.13.0)
|
53
|
+
rspec-core (3.13.0)
|
54
|
+
rspec-support (~> 3.13.0)
|
55
|
+
rspec-expectations (3.13.0)
|
56
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
57
|
+
rspec-support (~> 3.13.0)
|
58
|
+
rspec-mocks (3.13.0)
|
59
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
60
|
+
rspec-support (~> 3.13.0)
|
61
|
+
rspec-support (3.13.1)
|
62
|
+
rubocop (1.62.1)
|
63
|
+
json (~> 2.3)
|
64
|
+
language_server-protocol (>= 3.17.0)
|
65
|
+
parallel (~> 1.10)
|
66
|
+
parser (>= 3.3.0.2)
|
67
|
+
rainbow (>= 2.2.2, < 4.0)
|
68
|
+
regexp_parser (>= 1.8, < 3.0)
|
69
|
+
rexml (>= 3.2.5, < 4.0)
|
70
|
+
rubocop-ast (>= 1.31.1, < 2.0)
|
71
|
+
ruby-progressbar (~> 1.7)
|
72
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
73
|
+
rubocop-ast (1.31.2)
|
74
|
+
parser (>= 3.3.0.4)
|
75
|
+
rubocop-rails (2.24.1)
|
76
|
+
activesupport (>= 4.2.0)
|
77
|
+
rack (>= 1.1)
|
78
|
+
rubocop (>= 1.33.0, < 2.0)
|
79
|
+
rubocop-ast (>= 1.31.1, < 2.0)
|
80
|
+
ruby-progressbar (1.13.0)
|
81
|
+
timeout (0.4.1)
|
82
|
+
tzinfo (2.0.6)
|
83
|
+
concurrent-ruby (~> 1.0)
|
84
|
+
unicode-display_width (2.5.0)
|
85
|
+
|
86
|
+
PLATFORMS
|
87
|
+
arm64-darwin-23
|
88
|
+
x86_64-linux
|
89
|
+
|
90
|
+
DEPENDENCIES
|
91
|
+
activerecord (>= 7.1.0)
|
92
|
+
activesupport (>= 7.1.0)
|
93
|
+
janus-ar!
|
94
|
+
mysql2
|
95
|
+
rake
|
96
|
+
rspec (~> 3)
|
97
|
+
rubocop (~> 1.62.0)
|
98
|
+
rubocop-rails (~> 2.24.0)
|
99
|
+
|
100
|
+
BUNDLED WITH
|
101
|
+
2.4.22
|
data/README.md
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# Janus ActiveRecord
|
2
|
+
|
3
|
+
![Build Status](https://github.com/OLIOEX/janus-ar/actions/workflows/ci.yml/badge.svg)
|
4
|
+
|
5
|
+
> 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)
|
6
|
+
|
7
|
+
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.
|
8
|
+
|
9
|
+
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.
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Use the current version of the gem from [rubygems](https://rubygems.org/gems/janus-ar) in your `Gemfile`.
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'janus-ar'
|
17
|
+
```
|
18
|
+
|
19
|
+
This project assumes that your read/write endpoints are handled by a separate system (e.g. DNS).
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
After a write request during a thread the adapter will continue using the `primary` server, unless the context is specifically released.
|
24
|
+
|
25
|
+
### Configuration
|
26
|
+
|
27
|
+
Update your **database.yml** as follows:
|
28
|
+
|
29
|
+
```yml
|
30
|
+
development:
|
31
|
+
adapter: janus_mysql2
|
32
|
+
janus:
|
33
|
+
primary:
|
34
|
+
<<: *default
|
35
|
+
database: database_name
|
36
|
+
host: primary-host.local
|
37
|
+
replica:
|
38
|
+
<<: *default
|
39
|
+
password: ithappenstobedifferent
|
40
|
+
host: replica-host.local
|
41
|
+
```
|
42
|
+
|
43
|
+
### Forcing connections
|
44
|
+
|
45
|
+
A context is local to the curent thread of execution. This will allow you to stick to the primary safely in a single thread
|
46
|
+
in systems such as sidekiq, for instance.
|
47
|
+
|
48
|
+
#### Releasing stuck connections (clearing context)
|
49
|
+
|
50
|
+
If you need to clear the current context, releasing any stuck connections, all you have to do is:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
Janus::Context.release_all
|
54
|
+
```
|
55
|
+
|
56
|
+
#### Forcing connection to primary server
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
Janus::Context.stick_to_primary
|
60
|
+
```
|
61
|
+
|
62
|
+
### Logging
|
63
|
+
|
64
|
+
You can set a logger instance to ::Janus::Logging::Logger.logger and Janus.
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
Janus::Logging::Logger.logger = ::Logger.new(STDOUT)
|
68
|
+
```
|
69
|
+
|
70
|
+
### What queries goes where?
|
71
|
+
|
72
|
+
In general: Any `SELECT` statements will execute against your replica(s), anything else will go to the primary.
|
73
|
+
|
74
|
+
There are some edge cases:
|
75
|
+
* `SET` operations will be sent to all connections
|
76
|
+
* Execution of specific methods such as `connect!`, `disconnect!`, `reconnect!`, and `clear_cache!` are invoked on all underlying connections
|
77
|
+
* 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)
|
78
|
+
* Locking reads (e.g. `SELECT ... FOR UPDATE`) will always be sent to the primary
|
data/janus-ar.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
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/replca databases'
|
9
|
+
gem.summary = 'Read/Write proxy for ActiveRecord using primary/replca databases'
|
10
|
+
gem.homepage = 'https://github.com/olioex/janus-ar'
|
11
|
+
gem.licenses = ['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 = ['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 'rake'
|
28
|
+
gem.add_development_dependency 'rspec', '~> 3'
|
29
|
+
gem.add_development_dependency 'rubocop', '~> 1.62.0'
|
30
|
+
gem.add_development_dependency 'rubocop-rails', '~> 2.24.0'
|
31
|
+
end
|
@@ -0,0 +1,124 @@
|
|
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
|
+
module ConnectionAdapters
|
17
|
+
class JanusMysql2Adapter < ActiveRecord::ConnectionAdapters::Mysql2Adapter
|
18
|
+
SQL_PRIMARY_MATCHERS = [
|
19
|
+
/\A\s*select.+for update\Z/i, /select.+lock in share mode\Z/i,
|
20
|
+
/\A\s*select.+(nextval|currval|lastval|get_lock|release_lock|pg_advisory_lock|pg_advisory_unlock)\(/i,
|
21
|
+
/\A\s*show/i
|
22
|
+
].freeze
|
23
|
+
SQL_REPLICA_MATCHERS = [/\A\s*(select|with.+\)\s*select)\s/i].freeze
|
24
|
+
SQL_ALL_MATCHERS = [/\A\s*set\s/i].freeze
|
25
|
+
SQL_SKIP_ALL_MATCHERS = [/\A\s*set\s+local\s/i].freeze
|
26
|
+
|
27
|
+
def initialize(*args)
|
28
|
+
@replica_config = args[0][:janus]['replica']
|
29
|
+
args[0] = args[0][:janus]['primary']
|
30
|
+
|
31
|
+
super(*args)
|
32
|
+
@connection_parameters ||= args[0]
|
33
|
+
update_config
|
34
|
+
end
|
35
|
+
|
36
|
+
def execute(sql)
|
37
|
+
if should_send_to_all?(sql)
|
38
|
+
send_to_replica(sql, connection: :all, method: :execute)
|
39
|
+
return super(sql)
|
40
|
+
end
|
41
|
+
return send_to_replica(sql, connection: :replica, method: :execute) if can_go_to_replica?(sql)
|
42
|
+
|
43
|
+
Janus::Context.stick_to_primary if write_query?(sql)
|
44
|
+
Janus::Context.used_connection(:primary)
|
45
|
+
|
46
|
+
super(sql)
|
47
|
+
end
|
48
|
+
|
49
|
+
def execute_and_free(sql, name = nil, async: false) # :nodoc:#
|
50
|
+
if should_send_to_all?(sql)
|
51
|
+
send_to_replica(sql, name, connection: :all)
|
52
|
+
return super(sql, name, async:)
|
53
|
+
end
|
54
|
+
return send_to_replica(sql, connection: :replica) if can_go_to_replica?(sql)
|
55
|
+
|
56
|
+
Janus::Context.stick_to_primary if write_query?(sql)
|
57
|
+
Janus::Context.used_connection(:primary)
|
58
|
+
|
59
|
+
super(sql, name, async:)
|
60
|
+
end
|
61
|
+
|
62
|
+
def connect!(...)
|
63
|
+
replica_connection.connect!(...)
|
64
|
+
super
|
65
|
+
end
|
66
|
+
|
67
|
+
def reconnect!(...)
|
68
|
+
replica_connection.reconnect!(...)
|
69
|
+
super
|
70
|
+
end
|
71
|
+
|
72
|
+
def disconnect!(...)
|
73
|
+
replica_connection.disconnect!(...)
|
74
|
+
super
|
75
|
+
end
|
76
|
+
|
77
|
+
def clear_cache!(...)
|
78
|
+
replica_connection.clear_cache!(...)
|
79
|
+
super
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def should_send_to_all?(sql)
|
85
|
+
SQL_ALL_MATCHERS.any? { |matcher| sql =~ matcher } && SQL_SKIP_ALL_MATCHERS.none? { |matcher| sql =~ matcher }
|
86
|
+
end
|
87
|
+
|
88
|
+
def can_go_to_replica?(sql)
|
89
|
+
return false if Janus::Context.use_primary? ||
|
90
|
+
open_transactions.positive? ||
|
91
|
+
SQL_PRIMARY_MATCHERS.any? { |matcher| sql =~ matcher }
|
92
|
+
|
93
|
+
true
|
94
|
+
end
|
95
|
+
|
96
|
+
def send_to_replica(sql, connection: nil, method: :exec_query)
|
97
|
+
Janus::Context.used_connection(connection) if connection
|
98
|
+
if method == :execute
|
99
|
+
replica_connection.execute(sql)
|
100
|
+
else
|
101
|
+
replica_connection.exec_query(sql)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def write_query?(sql)
|
106
|
+
%w[INSERT UPDATE DELETE LOCK].include?(sql.split(' ').first)
|
107
|
+
end
|
108
|
+
|
109
|
+
def replica_connection
|
110
|
+
@replica_connection ||= ActiveRecord::ConnectionAdapters::Mysql2Adapter.new(@replica_config)
|
111
|
+
end
|
112
|
+
|
113
|
+
def update_config
|
114
|
+
@config[:flags] ||= 0
|
115
|
+
|
116
|
+
if @config[:flags].is_a? Array
|
117
|
+
@config[:flags].push 'FOUND_ROWS'
|
118
|
+
else
|
119
|
+
@config[:flags] |= ::Mysql2::Client::FOUND_ROWS
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Janus
|
4
|
+
class Context
|
5
|
+
THREAD_KEY = :janus_ar_context
|
6
|
+
|
7
|
+
# Stores the staged data with an expiration time based on the current time,
|
8
|
+
# and clears any expired entries. Returns true if any changes were made to
|
9
|
+
# the current store
|
10
|
+
def initialize(primary: false, expiry: nil)
|
11
|
+
@primary = primary
|
12
|
+
@expiry = expiry
|
13
|
+
@last_used_connection = :primary
|
14
|
+
end
|
15
|
+
|
16
|
+
def stick_to_primary
|
17
|
+
@primary = true
|
18
|
+
end
|
19
|
+
|
20
|
+
def potential_write
|
21
|
+
stick_to_primary
|
22
|
+
end
|
23
|
+
|
24
|
+
def release_all
|
25
|
+
@primary = false
|
26
|
+
@expiry = nil
|
27
|
+
@last_used_connection = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def use_primary?
|
31
|
+
@primary
|
32
|
+
end
|
33
|
+
|
34
|
+
def used_connection(connection)
|
35
|
+
@last_used_connection = connection
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :last_used_connection
|
39
|
+
|
40
|
+
class << self
|
41
|
+
def stick_to_primary
|
42
|
+
current.stick_to_primary
|
43
|
+
end
|
44
|
+
|
45
|
+
def release_all
|
46
|
+
current.release_all
|
47
|
+
end
|
48
|
+
|
49
|
+
def used_connection(connection)
|
50
|
+
current.used_connection(connection)
|
51
|
+
end
|
52
|
+
|
53
|
+
def use_primary?
|
54
|
+
current.use_primary?
|
55
|
+
end
|
56
|
+
|
57
|
+
def last_used_connection
|
58
|
+
current.last_used_connection
|
59
|
+
end
|
60
|
+
|
61
|
+
protected
|
62
|
+
|
63
|
+
def current
|
64
|
+
fetch(THREAD_KEY) { new }
|
65
|
+
end
|
66
|
+
|
67
|
+
def fetch(key)
|
68
|
+
get(key) || set(key, yield)
|
69
|
+
end
|
70
|
+
|
71
|
+
def get(key)
|
72
|
+
Thread.current.thread_variable_get(key)
|
73
|
+
end
|
74
|
+
|
75
|
+
def set(key, value)
|
76
|
+
Thread.current.thread_variable_set(key, value)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Janus
|
4
|
+
module Logging
|
5
|
+
module Subscriber
|
6
|
+
IGNORE_PAYLOAD_NAMES = %w[SCHEMA EXPLAIN].freeze
|
7
|
+
|
8
|
+
def sql(event)
|
9
|
+
name = event.payload[:name]
|
10
|
+
unless IGNORE_PAYLOAD_NAMES.include?(name)
|
11
|
+
name = [current_wrapper_name(event), name].compact.join(' ')
|
12
|
+
event.payload[:name] = name
|
13
|
+
end
|
14
|
+
super(event)
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def current_wrapper_name(_event)
|
20
|
+
connection = Janus::Context.last_used_connection
|
21
|
+
return nil unless connection
|
22
|
+
|
23
|
+
"[#{connection}]"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Janus
|
4
|
+
unless defined?(::Janus::VERSION)
|
5
|
+
module VERSION
|
6
|
+
MAJOR = 0
|
7
|
+
MINOR = 1
|
8
|
+
PATCH = 0
|
9
|
+
PRE = nil
|
10
|
+
|
11
|
+
def self.to_s
|
12
|
+
[MAJOR, MINOR, PATCH, PRE].compact.join('.')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
::Janus::VERSION
|
17
|
+
end
|
data/lib/janus.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
|
5
|
+
module Janus
|
6
|
+
autoload :Context, 'janus/context'
|
7
|
+
autoload :VERSION, 'janus/version'
|
8
|
+
|
9
|
+
module Logging
|
10
|
+
autoload :Subscriber, 'janus/logging/subscriber'
|
11
|
+
autoload :Logger, 'janus/logging/logger'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
ActiveSupport.on_load(:active_record) do
|
16
|
+
ActiveRecord::LogSubscriber.log_subscribers.each do |subscriber|
|
17
|
+
subscriber.extend Janus::Logging::Subscriber
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'janus/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
|
@@ -0,0 +1,31 @@
|
|
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
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: janus-ar
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Lloyd Watkin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-04-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 7.1.0
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 7.1.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 7.1.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 7.1.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mysql2
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
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'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 1.62.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 1.62.0
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop-rails
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 2.24.0
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 2.24.0
|
111
|
+
description: Read/Write proxy for ActiveRecord using primary/replca databases
|
112
|
+
email:
|
113
|
+
- lloyd@olioex.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- ".github/dependabot.yml"
|
119
|
+
- ".github/workflows/ci.yml"
|
120
|
+
- ".rspec"
|
121
|
+
- ".rubocop.yml"
|
122
|
+
- Gemfile
|
123
|
+
- Gemfile.lock
|
124
|
+
- README.md
|
125
|
+
- janus-ar.gemspec
|
126
|
+
- lib/active_record/connection_adapters/janus_mysql2_adapter.rb
|
127
|
+
- lib/janus.rb
|
128
|
+
- lib/janus/context.rb
|
129
|
+
- lib/janus/logging/logger.rb
|
130
|
+
- lib/janus/logging/subscriber.rb
|
131
|
+
- lib/janus/version.rb
|
132
|
+
- spec/lib/janus/context_spec.rb
|
133
|
+
- spec/lib/janus/logging/logger_spec.rb
|
134
|
+
- spec/spec_helper.rb
|
135
|
+
homepage: https://github.com/olioex/janus-ar
|
136
|
+
licenses:
|
137
|
+
- MIT
|
138
|
+
metadata:
|
139
|
+
source_code_uri: https://github.com/olioex/janus-ar
|
140
|
+
post_install_message:
|
141
|
+
rdoc_options: []
|
142
|
+
require_paths:
|
143
|
+
- lib
|
144
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
145
|
+
requirements:
|
146
|
+
- - ">="
|
147
|
+
- !ruby/object:Gem::Version
|
148
|
+
version: 3.2.0
|
149
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - ">="
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
154
|
+
requirements: []
|
155
|
+
rubygems_version: 3.4.10
|
156
|
+
signing_key:
|
157
|
+
specification_version: 4
|
158
|
+
summary: Read/Write proxy for ActiveRecord using primary/replca databases
|
159
|
+
test_files: []
|