janus-ar 7.2.2 → 8.0.1
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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +67 -59
- data/.gitignore +1 -0
- data/.rubocop.yml +29 -26
- data/Gemfile.lock +142 -124
- data/README.md +130 -129
- data/bin/release.sh +1 -0
- data/gemfiles/activerecord_8_0.gemfile +7 -0
- data/gemfiles/activerecord_8_1.gemfile +7 -0
- data/janus-ar.gemspec +36 -36
- data/lib/janus-ar/active_record/connection_adapters/janus_mysql2_adapter.rb +32 -147
- data/lib/janus-ar/active_record/connection_adapters/janus_trilogy_adapter.rb +32 -148
- data/lib/janus-ar/adapter_extensions.rb +107 -0
- data/lib/janus-ar/context.rb +79 -80
- data/lib/janus-ar/query_director.rb +81 -54
- data/lib/janus-ar/railtie.rb +15 -0
- data/lib/janus-ar/version.rb +17 -17
- data/lib/janus-ar.rb +24 -22
- data/spec/lib/janus-ar/active_record/connection_adapters/janus_mysql_adapter_spec.rb +78 -82
- data/spec/lib/janus-ar/active_record/connection_adapters/janus_trilogy_adapter_spec.rb +77 -82
- data/spec/lib/janus-ar/context_spec.rb +118 -46
- data/spec/lib/janus-ar/db_console_config_spec.rb +28 -0
- data/spec/lib/janus-ar/logging/subscriber_spec.rb +58 -0
- data/spec/lib/janus-ar/query_director_spec.rb +141 -59
- data/spec/shared_examples/a_mysql_like_server.rb +70 -0
- metadata +25 -16
|
@@ -1,148 +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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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, 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: :execute)
|
|
54
|
-
super
|
|
55
|
-
when :replica
|
|
56
|
-
send_to_replica(sql, connection: :replica, method: :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, allow_retry: false)
|
|
79
|
-
case where_to_send?(sql)
|
|
80
|
-
when :all
|
|
81
|
-
send_to_replica(sql, connection: :all, method: :execute)
|
|
82
|
-
super(sql, name, async:, allow_retry:)
|
|
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:, allow_retry:)
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def with_connection(_args = {})
|
|
93
|
-
self
|
|
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::TrilogyAdapter.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
|
|
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,107 @@
|
|
|
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 disconnect!(...)
|
|
78
|
+
replica_connection.disconnect!(...)
|
|
79
|
+
super
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def clear_cache!(...)
|
|
83
|
+
replica_connection.clear_cache!(...)
|
|
84
|
+
super
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def replica_connection
|
|
88
|
+
@replica_connection ||= replica_adapter_class.new(@replica_config)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def mark_primary(sql)
|
|
94
|
+
Janus::Context.stick_to_primary if write_query?(sql)
|
|
95
|
+
Janus::Context.used_connection(:primary)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def where_to_send?(sql)
|
|
99
|
+
Janus::QueryDirector.new(sql, open_transactions).where_to_send?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def send_to_replica(sql, connection)
|
|
103
|
+
Janus::Context.used_connection(connection)
|
|
104
|
+
replica_connection.execute(sql)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/janus-ar/context.rb
CHANGED
|
@@ -1,80 +1,79 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def
|
|
35
|
-
@
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def
|
|
46
|
-
current.
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def
|
|
50
|
-
current.
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def
|
|
54
|
-
current.
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def
|
|
58
|
-
current.
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/isolated_execution_state'
|
|
4
|
+
|
|
5
|
+
module Janus
|
|
6
|
+
# Per-execution state that records whether the current unit of work has been
|
|
7
|
+
# pinned to the primary (e.g. after a write, so subsequent reads stay
|
|
8
|
+
# consistent). State is stored in ActiveSupport::IsolatedExecutionState so it
|
|
9
|
+
# follows the application's configured isolation level (thread or fiber),
|
|
10
|
+
# matching ActiveRecord itself.
|
|
11
|
+
#
|
|
12
|
+
# Because pooled threads/fibers are reused across requests and jobs, the
|
|
13
|
+
# context MUST be released between units of work or a thread that performed a
|
|
14
|
+
# single write would keep routing every later read to the primary. The Rails
|
|
15
|
+
# integration (see Janus::Railtie) does this automatically; outside Rails,
|
|
16
|
+
# call Janus::Context.release_all yourself (e.g. in a Sidekiq middleware).
|
|
17
|
+
class Context
|
|
18
|
+
STATE_KEY = :janus_ar_context
|
|
19
|
+
|
|
20
|
+
def initialize(primary: false)
|
|
21
|
+
@primary = primary
|
|
22
|
+
@last_used_connection = :primary
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def stick_to_primary
|
|
26
|
+
@primary = true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def release_all
|
|
30
|
+
@primary = false
|
|
31
|
+
@last_used_connection = nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def use_primary?
|
|
35
|
+
@primary
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def used_connection(connection)
|
|
39
|
+
@last_used_connection = connection
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
attr_reader :last_used_connection
|
|
43
|
+
|
|
44
|
+
class << self
|
|
45
|
+
def stick_to_primary
|
|
46
|
+
current.stick_to_primary
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def release_all
|
|
50
|
+
current.release_all
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def used_connection(connection)
|
|
54
|
+
current.used_connection(connection)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def use_primary?
|
|
58
|
+
current.use_primary?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def last_used_connection
|
|
62
|
+
current.last_used_connection
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Release the context at the start of every unit of work wrapped by the
|
|
66
|
+
# given ActiveSupport executor (web requests, ActiveJob and
|
|
67
|
+
# Sidekiq-on-Rails jobs all run inside it).
|
|
68
|
+
def install_reset_hook(executor)
|
|
69
|
+
executor.to_run { Janus::Context.release_all }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
protected
|
|
73
|
+
|
|
74
|
+
def current
|
|
75
|
+
ActiveSupport::IsolatedExecutionState[STATE_KEY] ||= new
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -1,54 +1,81 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
module Janus
|
|
3
|
-
class QueryDirector
|
|
4
|
-
ALL = :all
|
|
5
|
-
REPLICA = :replica
|
|
6
|
-
PRIMARY = :primary
|
|
7
|
-
|
|
8
|
-
SQL_PRIMARY_MATCHERS = [
|
|
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
|
-
].freeze
|
|
13
|
-
SQL_REPLICA_MATCHERS = [/\A\s*(select|with.+\)\s*select)\s/i].freeze
|
|
14
|
-
SQL_ALL_MATCHERS = [/\A\s*set\s/i].freeze
|
|
15
|
-
SQL_SKIP_ALL_MATCHERS = [/\A\s*set\s+local\s/i].freeze
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def
|
|
40
|
-
!
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module Janus
|
|
3
|
+
class QueryDirector
|
|
4
|
+
ALL = :all
|
|
5
|
+
REPLICA = :replica
|
|
6
|
+
PRIMARY = :primary
|
|
7
|
+
|
|
8
|
+
SQL_PRIMARY_MATCHERS = [
|
|
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
|
+
].freeze
|
|
13
|
+
SQL_REPLICA_MATCHERS = [/\A\s*(select|with.+\)\s*select)\s/i].freeze
|
|
14
|
+
SQL_ALL_MATCHERS = [/\A\s*set\s/i].freeze
|
|
15
|
+
SQL_SKIP_ALL_MATCHERS = [/\A\s*set\s+local\s/i].freeze
|
|
16
|
+
|
|
17
|
+
# Leading whitespace and SQL comments are stripped before matching so that an
|
|
18
|
+
# annotated statement (e.g. `/* app:web */ INSERT ...`) is classified by the
|
|
19
|
+
# statement itself rather than by the comment.
|
|
20
|
+
LEADING_NOISE = %r{\A(?:\s+|/\*.*?\*/|--[^\n]*(?:\n|\z)|\#[^\n]*(?:\n|\z))+}m
|
|
21
|
+
|
|
22
|
+
def initialize(sql, open_transactions)
|
|
23
|
+
@_sql = sql
|
|
24
|
+
@_open_transactions = open_transactions
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def where_to_send?
|
|
28
|
+
if should_send_to_all?
|
|
29
|
+
ALL
|
|
30
|
+
elsif can_go_to_replica?
|
|
31
|
+
REPLICA
|
|
32
|
+
else
|
|
33
|
+
PRIMARY
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def should_send_to_all?
|
|
40
|
+
match_any?(SQL_ALL_MATCHERS) && !match_any?(SQL_SKIP_ALL_MATCHERS)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A replica may only serve a statement we positively recognise as a read and
|
|
44
|
+
# that nothing else forces onto the primary. Everything we do not recognise
|
|
45
|
+
# as a read defaults to the primary, which is the safe direction for a
|
|
46
|
+
# write/read proxy: a misrouted read only costs a little primary load, while
|
|
47
|
+
# a misrouted write is an error (or worse) against a read-only replica.
|
|
48
|
+
#
|
|
49
|
+
# Because this is only reached for a confirmed read, there is no need to also
|
|
50
|
+
# test for a write here.
|
|
51
|
+
def can_go_to_replica?
|
|
52
|
+
read_query? && !should_go_to_primary?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def read_query?
|
|
56
|
+
match_any?(SQL_REPLICA_MATCHERS)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def should_go_to_primary?
|
|
60
|
+
Janus::Context.use_primary? ||
|
|
61
|
+
@_open_transactions.positive? ||
|
|
62
|
+
match_any?(SQL_PRIMARY_MATCHERS)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def match_any?(matchers)
|
|
66
|
+
matchers.any? { |matcher| normalized_sql.match?(matcher) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Avoid copying the statement when there is no leading comment/whitespace to
|
|
70
|
+
# strip, which is the common case for ActiveRecord-generated SQL.
|
|
71
|
+
def normalized_sql
|
|
72
|
+
@normalized_sql ||= strip_leading_noise
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def strip_leading_noise
|
|
76
|
+
return @_sql unless LEADING_NOISE.match?(@_sql)
|
|
77
|
+
|
|
78
|
+
@_sql.sub(LEADING_NOISE, '')
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/railtie'
|
|
4
|
+
|
|
5
|
+
module Janus
|
|
6
|
+
class Railtie < ::Rails::Railtie
|
|
7
|
+
# Clear Janus' per-request primary stickiness at the start of every unit of
|
|
8
|
+
# work wrapped by the Rails executor (web requests, ActiveJob and
|
|
9
|
+
# Sidekiq-on-Rails jobs). Without this, a pooled thread that performs a
|
|
10
|
+
# write keeps routing reads to the primary for the rest of its life.
|
|
11
|
+
initializer 'janus.clear_context_per_execution' do |app|
|
|
12
|
+
Janus::Context.install_reset_hook(app.executor)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/janus-ar/version.rb
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Janus
|
|
4
|
-
unless defined?(::Janus::VERSION)
|
|
5
|
-
module VERSION
|
|
6
|
-
MAJOR =
|
|
7
|
-
MINOR =
|
|
8
|
-
PATCH =
|
|
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
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Janus
|
|
4
|
+
unless defined?(::Janus::VERSION)
|
|
5
|
+
module VERSION
|
|
6
|
+
MAJOR = 8
|
|
7
|
+
MINOR = 0
|
|
8
|
+
PATCH = 1
|
|
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-ar.rb
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'active_support'
|
|
4
|
-
|
|
5
|
-
module Janus
|
|
6
|
-
autoload :Context, 'janus-ar/context'
|
|
7
|
-
autoload :Client, 'janus-ar/client'
|
|
8
|
-
autoload :QueryDirector, 'janus-ar/query_director'
|
|
9
|
-
autoload :VERSION, 'janus-ar/version'
|
|
10
|
-
autoload :DbConsoleConfig, 'janus-ar/db_console_config'
|
|
11
|
-
|
|
12
|
-
module Logging
|
|
13
|
-
autoload :Subscriber, 'janus-ar/logging/subscriber'
|
|
14
|
-
autoload :Logger, 'janus-ar/logging/logger'
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
ActiveSupport.on_load(:active_record) do
|
|
19
|
-
ActiveRecord::LogSubscriber.log_subscribers.each do |subscriber|
|
|
20
|
-
subscriber.extend Janus::Logging::Subscriber
|
|
21
|
-
end
|
|
22
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support'
|
|
4
|
+
|
|
5
|
+
module Janus
|
|
6
|
+
autoload :Context, 'janus-ar/context'
|
|
7
|
+
autoload :Client, 'janus-ar/client'
|
|
8
|
+
autoload :QueryDirector, 'janus-ar/query_director'
|
|
9
|
+
autoload :VERSION, 'janus-ar/version'
|
|
10
|
+
autoload :DbConsoleConfig, 'janus-ar/db_console_config'
|
|
11
|
+
|
|
12
|
+
module Logging
|
|
13
|
+
autoload :Subscriber, 'janus-ar/logging/subscriber'
|
|
14
|
+
autoload :Logger, 'janus-ar/logging/logger'
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
ActiveSupport.on_load(:active_record) do
|
|
19
|
+
ActiveRecord::LogSubscriber.log_subscribers.each do |subscriber|
|
|
20
|
+
subscriber.extend Janus::Logging::Subscriber
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
require 'janus-ar/railtie' if defined?(Rails::Railtie)
|