console_kit 1.4.0 → 1.5.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/lib/console_kit/configuration.rb +15 -21
- data/lib/console_kit/configuration_validator.rb +179 -0
- data/lib/console_kit/connections/base_connection_handler.rb +139 -25
- data/lib/console_kit/connections/connection_manager.rb +42 -8
- data/lib/console_kit/connections/dashboard.rb +3 -11
- data/lib/console_kit/connections/diagnostic_helpers.rb +24 -5
- data/lib/console_kit/connections/elasticsearch_connection_handler.rb +87 -32
- data/lib/console_kit/connections/elasticsearch_prefix_registry.rb +70 -0
- data/lib/console_kit/connections/mongo_connection_handler.rb +51 -46
- data/lib/console_kit/connections/mongoid_support.rb +56 -0
- data/lib/console_kit/connections/redis_client_adapter.rb +92 -0
- data/lib/console_kit/connections/redis_connection_handler.rb +97 -41
- data/lib/console_kit/connections/sql_connection_handler.rb +66 -30
- data/lib/console_kit/connections/sql_strategy.rb +248 -0
- data/lib/console_kit/connections/table_renderer.rb +30 -20
- data/lib/console_kit/console_helpers.rb +17 -31
- data/lib/console_kit/diagnostics.rb +143 -0
- data/lib/console_kit/errors.rb +93 -0
- data/lib/console_kit/instrumentation.rb +57 -0
- data/lib/console_kit/output.rb +5 -14
- data/lib/console_kit/prompt.rb +17 -29
- data/lib/console_kit/railtie.rb +2 -3
- data/lib/console_kit/setup.rb +66 -13
- data/lib/console_kit/setup_ui.rb +1 -4
- data/lib/console_kit/tenant_configurator/context_wrapper.rb +51 -23
- data/lib/console_kit/tenant_configurator.rb +40 -78
- data/lib/console_kit/tenant_orchestrator.rb +31 -27
- data/lib/console_kit/tenant_plan.rb +68 -0
- data/lib/console_kit/tenant_rollback.rb +77 -0
- data/lib/console_kit/tenant_selector.rb +3 -6
- data/lib/console_kit/tenant_state.rb +73 -0
- data/lib/console_kit/tenant_switch.rb +124 -0
- data/lib/console_kit/version.rb +1 -1
- data/lib/console_kit.rb +51 -24
- data/lib/generators/console_kit/install_generator.rb +1 -12
- data/lib/generators/console_kit/templates/console_kit.rb +4 -0
- metadata +15 -4
- data/lib/console_kit/connections/table_formatter.rb +0 -37
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'base_connection_handler'
|
|
4
|
+
require_relative 'elasticsearch_prefix_registry'
|
|
4
5
|
|
|
5
6
|
module ConsoleKit
|
|
6
7
|
module Connections
|
|
7
|
-
# Handles Elasticsearch connections
|
|
8
8
|
class ElasticsearchConnectionHandler < BaseConnectionHandler
|
|
9
|
+
backend :elasticsearch,
|
|
10
|
+
display_name: 'Elasticsearch',
|
|
11
|
+
context_attribute: :tenant_elasticsearch_prefix,
|
|
12
|
+
constants_key: :elasticsearch_prefix,
|
|
13
|
+
detail_label: 'ES Prefix'
|
|
14
|
+
|
|
15
|
+
UNSUPPORTED = 'Elasticsearch does not expose index_name_prefix= in this version.'
|
|
16
|
+
UNVERIFIABLE = 'Elasticsearch exposes no index_name_prefix reader in this version, so the process-wide prefix ' \
|
|
17
|
+
'cannot be read back and a switch could not be verified or rolled back. Give each tenant its ' \
|
|
18
|
+
'own index naming instead.'
|
|
19
|
+
NOT_A_NAME = 'expected a String or Symbol'
|
|
20
|
+
|
|
9
21
|
class << self
|
|
10
22
|
def elasticsearch_available?
|
|
11
23
|
return false unless defined?(Elasticsearch::Model)
|
|
@@ -16,56 +28,99 @@ module ConsoleKit
|
|
|
16
28
|
false
|
|
17
29
|
end
|
|
18
30
|
|
|
19
|
-
def
|
|
20
|
-
return unless
|
|
31
|
+
def target_error(value)
|
|
32
|
+
return NOT_A_NAME unless value.nil? || value.is_a?(String) || value.is_a?(Symbol)
|
|
21
33
|
|
|
22
|
-
|
|
34
|
+
ElasticsearchPrefixRegistry.prefix_error(value.presence&.to_s)
|
|
23
35
|
end
|
|
24
36
|
end
|
|
25
37
|
|
|
26
|
-
def
|
|
27
|
-
|
|
38
|
+
def available? = self.class.elasticsearch_available?
|
|
39
|
+
def isolation_model = :process_global
|
|
40
|
+
def thread_isolated? = false
|
|
41
|
+
|
|
42
|
+
def prepare(target)
|
|
43
|
+
validate_target!(target)
|
|
44
|
+
settable = registry.settable?
|
|
45
|
+
raise UnsupportedBackendError, UNSUPPORTED if normalize(target) && !settable
|
|
46
|
+
return unless settable
|
|
47
|
+
|
|
48
|
+
raise UnsupportedBackendError, UNVERIFIABLE unless registry.readable?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def snapshot = { global: registry.global, thread: registry.current }
|
|
52
|
+
|
|
53
|
+
def connect!(target)
|
|
54
|
+
prefix = normalize(target)
|
|
28
55
|
Output.print_info(switch_message(prefix))
|
|
29
|
-
|
|
30
|
-
|
|
56
|
+
report_conflicts(prefix)
|
|
57
|
+
apply_global(prefix)
|
|
58
|
+
registry.record(prefix)
|
|
31
59
|
end
|
|
32
60
|
|
|
33
|
-
def
|
|
61
|
+
def verify!(target)
|
|
62
|
+
expected = normalize(target)
|
|
63
|
+
actual = effective_prefix
|
|
64
|
+
return true if actual == expected
|
|
34
65
|
|
|
35
|
-
|
|
36
|
-
|
|
66
|
+
raise verification_error(expected, actual)
|
|
67
|
+
end
|
|
37
68
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
69
|
+
def restore(state)
|
|
70
|
+
apply_global(state[:global])
|
|
71
|
+
registry.record(state[:thread])
|
|
41
72
|
end
|
|
42
73
|
|
|
74
|
+
def effective_prefix = registry.readable? ? registry.global : registry.current
|
|
75
|
+
|
|
76
|
+
def diagnostic_identity = effective_prefix
|
|
77
|
+
|
|
43
78
|
private
|
|
44
79
|
|
|
45
|
-
def
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
health = client.cluster.health
|
|
53
|
-
build_elasticsearch_diagnostics(health['cluster_name'], health['status'], latency)
|
|
80
|
+
def registry = ElasticsearchPrefixRegistry
|
|
81
|
+
|
|
82
|
+
def basic_diagnostics
|
|
83
|
+
{
|
|
84
|
+
name: display_name, status: :connected, latency_ms: nil,
|
|
85
|
+
details: { prefix: effective_prefix, isolation: isolation_model }
|
|
86
|
+
}
|
|
54
87
|
end
|
|
55
88
|
|
|
56
|
-
def
|
|
89
|
+
def full_diagnostics
|
|
90
|
+
client = Elasticsearch::Model.client
|
|
91
|
+
latency = measure_latency { ping!(client) }
|
|
92
|
+
health = client.cluster.health
|
|
57
93
|
{
|
|
58
|
-
name:
|
|
59
|
-
|
|
60
|
-
latency_ms: latency,
|
|
61
|
-
details: {
|
|
62
|
-
prefix: context_attribute(:tenant_elasticsearch_prefix),
|
|
63
|
-
cluster: cluster,
|
|
64
|
-
health: status
|
|
65
|
-
}
|
|
94
|
+
name: display_name, status: :connected, latency_ms: latency,
|
|
95
|
+
details: { prefix: effective_prefix, cluster: health['cluster_name'], health: health['status'] }
|
|
66
96
|
}
|
|
67
97
|
end
|
|
68
98
|
|
|
99
|
+
def ping!(client)
|
|
100
|
+
client.ping
|
|
101
|
+
rescue StandardError => e
|
|
102
|
+
raise ConnectionError.new("#{display_name} cluster unreachable: #{scrub(e.message)}",
|
|
103
|
+
backend: display_name, operation: :diagnostics)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def apply_global(prefix)
|
|
107
|
+
raise UnsupportedBackendError, UNSUPPORTED if prefix && !registry.settable?
|
|
108
|
+
|
|
109
|
+
registry.global = prefix
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def report_conflicts(prefix)
|
|
113
|
+
others = registry.unreported_conflicts(prefix)
|
|
114
|
+
return if others.empty?
|
|
115
|
+
|
|
116
|
+
Output.print_warning(
|
|
117
|
+
"#{display_name} index prefix is process-global: this thread wants #{prefix.inspect} while other " \
|
|
118
|
+
"live threads hold #{others.map(&:inspect).join(', ')}. The last writer wins for the whole process."
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def normalize(target) = target.presence&.to_s
|
|
123
|
+
|
|
69
124
|
def switch_message(prefix)
|
|
70
125
|
prefix ? "Setting Elasticsearch index prefix: #{prefix}" : 'Resetting Elasticsearch index prefix to default'
|
|
71
126
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConsoleKit
|
|
4
|
+
module Connections
|
|
5
|
+
class ElasticsearchPrefixRegistry
|
|
6
|
+
THREAD_KEY = :console_kit_elasticsearch_prefix
|
|
7
|
+
REPORTED_KEY = :console_kit_elasticsearch_prefix_conflict
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def prefix_error(prefix)
|
|
11
|
+
return unless prefix
|
|
12
|
+
return 'must be lowercase' if prefix.match?(/[[:upper:]]/)
|
|
13
|
+
return 'must not begin with _, - or +' if prefix.match?(/\A[_\-+]/)
|
|
14
|
+
return 'must not contain whitespace' if prefix.match?(/\s/)
|
|
15
|
+
|
|
16
|
+
'must not contain \\ / * ? " < > | , or #' if prefix.match?(%r{[\\/*?"<>|,#]})
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def model = defined?(Elasticsearch::Model) ? Elasticsearch::Model : nil
|
|
20
|
+
def settable? = model.respond_to?(:index_name_prefix=)
|
|
21
|
+
def readable? = model.respond_to?(:index_name_prefix)
|
|
22
|
+
def global = readable? ? model.index_name_prefix.presence&.to_s : nil
|
|
23
|
+
|
|
24
|
+
def global=(prefix)
|
|
25
|
+
model.index_name_prefix = prefix if settable?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def current
|
|
29
|
+
synchronize do
|
|
30
|
+
prune
|
|
31
|
+
entries[Thread.current]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def record(prefix)
|
|
36
|
+
synchronize { store(Thread.current, prefix) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def conflicts(prefix)
|
|
40
|
+
synchronize do
|
|
41
|
+
prune
|
|
42
|
+
entries.reject { |thread, held| thread.equal?(Thread.current) || held == prefix }.values.uniq
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def unreported_conflicts(prefix)
|
|
47
|
+
thread = Thread.current
|
|
48
|
+
others = conflicts(prefix)
|
|
49
|
+
return [] if others.empty? || thread[REPORTED_KEY] == [prefix, others]
|
|
50
|
+
|
|
51
|
+
thread[REPORTED_KEY] = [prefix, others]
|
|
52
|
+
others
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def store(thread, prefix)
|
|
58
|
+
prune
|
|
59
|
+
prefix ? (entries[thread] = prefix) : entries.delete(thread)
|
|
60
|
+
thread[THREAD_KEY] = prefix
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def entries = @entries ||= {}
|
|
64
|
+
def mutex = @mutex ||= Mutex.new
|
|
65
|
+
def synchronize(&) = mutex.synchronize(&)
|
|
66
|
+
def prune = entries.select! { |thread, _prefix| thread.alive? }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -1,74 +1,79 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'base_connection_handler'
|
|
4
|
+
require_relative 'mongoid_support'
|
|
4
5
|
|
|
5
6
|
module ConsoleKit
|
|
6
7
|
module Connections
|
|
7
|
-
# Handles MongoDB connections
|
|
8
8
|
class MongoConnectionHandler < BaseConnectionHandler
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
UNSUPPORTED_OVERRIDE = 'Mongoid.%<setter>s, which this target needs, is not available in this Mongoid version.'
|
|
10
|
+
UNVERIFIABLE = 'state cannot be read back in this Mongoid version, so a switch could not be verified or ' \
|
|
11
|
+
'rolled back. Give each tenant its own Mongoid client instead.'
|
|
12
|
+
|
|
13
|
+
backend :mongo,
|
|
14
|
+
display_name: 'MongoDB',
|
|
15
|
+
context_attribute: :tenant_mongo_db,
|
|
16
|
+
constants_key: :mongo_db,
|
|
17
|
+
detail_label: 'Mongo DB'
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def target_error(value) = identifier_error(value)
|
|
14
21
|
end
|
|
15
22
|
|
|
16
|
-
def available? =
|
|
23
|
+
def available? = mongoid.available?
|
|
24
|
+
|
|
25
|
+
def prepare(target)
|
|
26
|
+
validate_target!(target)
|
|
27
|
+
setter = mongoid.setter_for(target)
|
|
28
|
+
raise UnsupportedBackendError, unsupported_message(setter) unless mongoid.settable?(setter)
|
|
29
|
+
raise UnsupportedBackendError, "#{display_name} #{UNVERIFIABLE}" unless mongoid.readable?
|
|
30
|
+
end
|
|
17
31
|
|
|
18
|
-
def
|
|
19
|
-
return unavailable_diagnostics('MongoDB') unless available?
|
|
32
|
+
def snapshot = mongoid.overrides
|
|
20
33
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
def connect!(target) = mongoid.override(target)
|
|
35
|
+
|
|
36
|
+
def verify!(target)
|
|
37
|
+
if target.nil?
|
|
38
|
+
verify_reset!
|
|
39
|
+
elsif mongoid.named_client?(target)
|
|
40
|
+
verify_match!(target, mongoid.client_override)
|
|
41
|
+
else
|
|
42
|
+
verify_match!(target, mongoid.database_name)
|
|
43
|
+
end
|
|
24
44
|
end
|
|
25
45
|
|
|
46
|
+
def restore(state) = mongoid.restore(state)
|
|
47
|
+
|
|
26
48
|
private
|
|
27
49
|
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
build_mongo_diagnostics(db.name, info['version'], latency)
|
|
50
|
+
def mongoid = MongoidSupport
|
|
51
|
+
|
|
52
|
+
def basic_diagnostics
|
|
53
|
+
{ name: display_name, status: :connected, latency_ms: nil, details: { database: mongoid.database_name } }
|
|
33
54
|
end
|
|
34
55
|
|
|
35
|
-
def
|
|
56
|
+
def full_diagnostics
|
|
57
|
+
latency = measure_latency { mongoid.ping }
|
|
36
58
|
{
|
|
37
|
-
name:
|
|
38
|
-
|
|
39
|
-
latency_ms: latency,
|
|
40
|
-
details: { database: name, version: version }
|
|
59
|
+
name: display_name, status: :connected, latency_ms: latency,
|
|
60
|
+
details: { database: mongoid.database_name, version: mongoid.server_version }
|
|
41
61
|
}
|
|
42
62
|
end
|
|
43
63
|
|
|
44
|
-
def
|
|
45
|
-
override = context_attribute(:tenant_mongo_db).presence
|
|
46
|
-
client = Mongoid.default_client
|
|
47
|
-
(override ? client.use(override) : client).database
|
|
48
|
-
end
|
|
64
|
+
def unsupported_message(setter) = "#{display_name} #{format(UNSUPPORTED_OVERRIDE, setter: setter)}"
|
|
49
65
|
|
|
50
|
-
def
|
|
51
|
-
if
|
|
52
|
-
Output.print_info('Resetting MongoDB client to default')
|
|
53
|
-
reset_overrides
|
|
54
|
-
elsif named_client?(db)
|
|
55
|
-
Output.print_info("Switching to MongoDB client: #{db}")
|
|
56
|
-
Mongoid.override_client(db)
|
|
57
|
-
else
|
|
58
|
-
Output.print_info("Switching to MongoDB database: #{db}")
|
|
59
|
-
Mongoid.override_database(db)
|
|
60
|
-
end
|
|
61
|
-
end
|
|
66
|
+
def verify_match!(target, actual)
|
|
67
|
+
return if actual.to_s == target.to_s
|
|
62
68
|
|
|
63
|
-
|
|
64
|
-
Mongoid.override_client(nil) if Mongoid.respond_to?(:override_client)
|
|
65
|
-
Mongoid.override_database(nil)
|
|
69
|
+
raise verification_error(target, actual)
|
|
66
70
|
end
|
|
67
71
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
def verify_reset!
|
|
73
|
+
overrides = mongoid.overrides
|
|
74
|
+
return if overrides.values.none?
|
|
75
|
+
|
|
76
|
+
raise verification_error(nil, overrides)
|
|
72
77
|
end
|
|
73
78
|
end
|
|
74
79
|
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConsoleKit
|
|
4
|
+
module Connections
|
|
5
|
+
module MongoidSupport
|
|
6
|
+
class << self
|
|
7
|
+
def available? = !!defined?(Mongoid)
|
|
8
|
+
|
|
9
|
+
def settable?(setter) = Mongoid.respond_to?(setter)
|
|
10
|
+
|
|
11
|
+
def readable?
|
|
12
|
+
return false unless Mongoid.respond_to?(:default_client) && threaded_readable?(:database_override)
|
|
13
|
+
|
|
14
|
+
!Mongoid.respond_to?(:override_client) || threaded_readable?(:client_override)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def setter_for(target) = named_client?(target) ? :override_client : :override_database
|
|
18
|
+
|
|
19
|
+
def named_client?(name)
|
|
20
|
+
return false unless defined?(Mongoid::Config) && Mongoid::Config.respond_to?(:clients)
|
|
21
|
+
|
|
22
|
+
Mongoid::Config.clients.key?(name.to_s)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def client_override = threaded_readable?(:client_override) ? Mongoid::Threaded.client_override : nil
|
|
26
|
+
|
|
27
|
+
def overrides = { client: client_override, database: database_override }
|
|
28
|
+
|
|
29
|
+
def override(target)
|
|
30
|
+
return restore({}) if target.nil?
|
|
31
|
+
|
|
32
|
+
Mongoid.public_send(setter_for(target), target)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def restore(state)
|
|
36
|
+
Mongoid.override_client(state[:client]) if Mongoid.respond_to?(:override_client)
|
|
37
|
+
Mongoid.override_database(state[:database])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def database_name = database.name
|
|
41
|
+
def ping = database.command(ping: 1)
|
|
42
|
+
def server_version = database.command(buildInfo: 1).first['version']
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def database = Mongoid.default_client.database
|
|
47
|
+
|
|
48
|
+
def database_override = threaded_readable?(:database_override) ? Mongoid::Threaded.database_override : nil
|
|
49
|
+
|
|
50
|
+
def threaded_readable?(reader)
|
|
51
|
+
defined?(Mongoid::Threaded) && Mongoid::Threaded.respond_to?(reader)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../errors'
|
|
4
|
+
|
|
5
|
+
module ConsoleKit
|
|
6
|
+
module Connections
|
|
7
|
+
class RedisClientAdapter
|
|
8
|
+
DIGITS = /\A\d+\z/
|
|
9
|
+
MEMO_KEY = :console_kit_redis_isolation
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def db_index(value)
|
|
13
|
+
return value if value.is_a?(Integer) && !value.negative?
|
|
14
|
+
return nil unless value.is_a?(String) && value.match?(DIGITS)
|
|
15
|
+
|
|
16
|
+
value.to_i
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def db_index_error(value)
|
|
20
|
+
'expected a non-negative Integer or a digit String' unless value.nil? || db_index(value)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def read_db(target)
|
|
24
|
+
return target.db if target.respond_to?(:db)
|
|
25
|
+
|
|
26
|
+
config = target.config if target.respond_to?(:config)
|
|
27
|
+
return config.db if config.respond_to?(:db)
|
|
28
|
+
|
|
29
|
+
info = target.connection if target.respond_to?(:connection)
|
|
30
|
+
info[:db] if info.respond_to?(:[])
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def client = resolve
|
|
35
|
+
|
|
36
|
+
def selectable? = client.respond_to?(:select)
|
|
37
|
+
def db_readable? = !current_db.nil?
|
|
38
|
+
def isolation_model = @isolation_model ||= remembered_isolation
|
|
39
|
+
|
|
40
|
+
def current_db
|
|
41
|
+
Integer(client.then { |target| target && self.class.read_db(target) }, exception: false)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def movable_to?(db) = selectable? && ![nil, db].include?(current_db)
|
|
45
|
+
|
|
46
|
+
def select(db)
|
|
47
|
+
return unless selectable?
|
|
48
|
+
|
|
49
|
+
client.select(db)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def resolve
|
|
55
|
+
return nil unless defined?(::Redis) && ::Redis.respond_to?(:current)
|
|
56
|
+
|
|
57
|
+
::Redis.current
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
raise e if ConsoleKit.programming_error?(e)
|
|
60
|
+
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def remembered_isolation
|
|
65
|
+
here = resolve
|
|
66
|
+
memo = Thread.current[MEMO_KEY]
|
|
67
|
+
return memo[:model] if memo && memo[:client].equal?(here)
|
|
68
|
+
|
|
69
|
+
model = probe_isolation(here)
|
|
70
|
+
Thread.current[MEMO_KEY] = { client: here, model: model } unless model == :unknown
|
|
71
|
+
model
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def probe_isolation(here)
|
|
75
|
+
return :none unless here && resolve.equal?(here)
|
|
76
|
+
|
|
77
|
+
elsewhere = resolve_elsewhere
|
|
78
|
+
return :unknown unless elsewhere
|
|
79
|
+
|
|
80
|
+
elsewhere.equal?(here) ? :process_global : :scoped
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def resolve_elsewhere
|
|
84
|
+
Thread.new { resolve }.value
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
raise e if ConsoleKit.programming_error?(e)
|
|
87
|
+
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -1,75 +1,131 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'base_connection_handler'
|
|
4
|
+
require_relative 'redis_client_adapter'
|
|
4
5
|
|
|
5
6
|
module ConsoleKit
|
|
6
7
|
module Connections
|
|
7
|
-
# Handles Redis connections
|
|
8
8
|
class RedisConnectionHandler < BaseConnectionHandler
|
|
9
|
+
backend :redis,
|
|
10
|
+
display_name: 'Redis',
|
|
11
|
+
context_attribute: :tenant_redis_db,
|
|
12
|
+
constants_key: :redis_db,
|
|
13
|
+
detail_label: 'Redis DB'
|
|
14
|
+
|
|
9
15
|
DEFAULT_REDIS_DB = 0
|
|
10
16
|
|
|
17
|
+
PROCESS_GLOBAL_WARNING = 'Redis DB selection is process-wide with this client, so it is NOT isolated per ' \
|
|
18
|
+
'thread. Threads on different tenants share one logical DB.'
|
|
19
|
+
|
|
11
20
|
class << self
|
|
12
|
-
|
|
21
|
+
attr_accessor :isolation_warned
|
|
13
22
|
|
|
14
|
-
def
|
|
15
|
-
Output.print_warning("Redis DB #{db_index} configured but auto-select not supported with RedisClient. " \
|
|
16
|
-
'Ensure your Redis configuration sets the correct DB.')
|
|
17
|
-
end
|
|
23
|
+
def target_error(value) = RedisClientAdapter.db_index_error(value)
|
|
18
24
|
end
|
|
19
25
|
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
def available? = !!defined?(Redis) || !!defined?(RedisClient)
|
|
27
|
+
|
|
28
|
+
def isolation_model = adapter.isolation_model
|
|
29
|
+
def thread_isolated? = isolation_model == :scoped
|
|
30
|
+
|
|
31
|
+
def diagnostic_identity = adapter.current_db
|
|
32
|
+
|
|
33
|
+
def prepare(target)
|
|
34
|
+
validate_target!(target)
|
|
35
|
+
db = coerce_db(target)
|
|
36
|
+
return if target.nil? || (adapter.selectable? && adapter.db_readable?)
|
|
37
|
+
|
|
38
|
+
raise UnsupportedBackendError, unsupported_message(db)
|
|
24
39
|
end
|
|
25
40
|
|
|
26
|
-
def
|
|
41
|
+
def snapshot = { db: adapter.current_db }
|
|
27
42
|
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
return
|
|
43
|
+
def connect!(target)
|
|
44
|
+
db = coerce_db(target)
|
|
45
|
+
return unless adapter.movable_to?(db)
|
|
31
46
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
Output.print_info(switch_message(db))
|
|
48
|
+
warn_process_global
|
|
49
|
+
apply(db)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def verify!(target)
|
|
53
|
+
expected = coerce_db(target)
|
|
54
|
+
actual = adapter.current_db
|
|
55
|
+
return true if (actual || DEFAULT_REDIS_DB) == expected
|
|
56
|
+
|
|
57
|
+
raise verification_error(expected, actual)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def restore(state)
|
|
61
|
+
db = state && state[:db]
|
|
62
|
+
return unless db && adapter.current_db != db
|
|
63
|
+
|
|
64
|
+
apply(db)
|
|
35
65
|
end
|
|
36
66
|
|
|
37
67
|
private
|
|
38
68
|
|
|
39
|
-
def
|
|
69
|
+
def basic_diagnostics
|
|
70
|
+
{
|
|
71
|
+
name: display_name, status: adapter.selectable? ? :connected : :unknown, latency_ms: nil,
|
|
72
|
+
details: { db: resolved_db, isolation: isolation_model }
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def full_diagnostics
|
|
77
|
+
redis = adapter.client
|
|
78
|
+
return basic_diagnostics unless redis
|
|
79
|
+
|
|
40
80
|
latency = measure_latency { redis.ping }
|
|
41
|
-
|
|
42
|
-
build_redis_diagnostics(info['redis_version'], info['used_memory_human'], latency)
|
|
81
|
+
connected_diagnostics(latency, redis.info)
|
|
43
82
|
end
|
|
44
83
|
|
|
45
|
-
def
|
|
84
|
+
def connected_diagnostics(latency, info)
|
|
46
85
|
{
|
|
47
|
-
name:
|
|
48
|
-
|
|
49
|
-
latency_ms: latency,
|
|
50
|
-
details: {
|
|
51
|
-
db: context_attribute(:tenant_redis_db) || DEFAULT_REDIS_DB,
|
|
52
|
-
version: version,
|
|
53
|
-
memory: memory
|
|
54
|
-
}
|
|
86
|
+
name: display_name, status: :connected, latency_ms: latency,
|
|
87
|
+
details: { db: resolved_db, version: info['redis_version'], memory: info['used_memory_human'] }
|
|
55
88
|
}
|
|
56
89
|
end
|
|
57
90
|
|
|
58
|
-
def
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
91
|
+
def apply(db)
|
|
92
|
+
adapter.select(db)
|
|
93
|
+
rescue ConsoleKit::Error
|
|
94
|
+
raise
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
raise ConnectionError.new("#{display_name} SELECT #{db} failed: #{scrub(e.message)}",
|
|
97
|
+
backend: display_name, operation: :connect)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def warn_process_global
|
|
101
|
+
return unless isolation_model == :process_global
|
|
102
|
+
|
|
103
|
+
handler_class = self.class
|
|
104
|
+
return if handler_class.isolation_warned
|
|
105
|
+
|
|
106
|
+
handler_class.isolation_warned = true
|
|
107
|
+
Output.print_warning(PROCESS_GLOBAL_WARNING)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def coerce_db(target)
|
|
111
|
+
return DEFAULT_REDIS_DB if target.nil?
|
|
112
|
+
|
|
113
|
+
RedisClientAdapter.db_index(target) ||
|
|
114
|
+
raise(ConfigurationError, "ConsoleKit: Redis DB #{scrub(target.inspect)} is not a non-negative integer.")
|
|
68
115
|
end
|
|
69
116
|
|
|
70
|
-
def
|
|
71
|
-
|
|
117
|
+
def unsupported_message(db)
|
|
118
|
+
"#{display_name} DB #{db} was requested but this client exposes no connection that can be selected and " \
|
|
119
|
+
"verified without a round-trip (isolation model: #{isolation_model}). " \
|
|
120
|
+
'Point each tenant at its own Redis URL instead.'
|
|
72
121
|
end
|
|
122
|
+
|
|
123
|
+
def switch_message(db)
|
|
124
|
+
db == DEFAULT_REDIS_DB ? 'Resetting Redis connection to default' : "Switching to Redis DB: #{db}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def resolved_db = adapter.current_db || coerce_db(target)
|
|
128
|
+
def adapter = @adapter ||= RedisClientAdapter.new
|
|
73
129
|
end
|
|
74
130
|
end
|
|
75
131
|
end
|