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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/console_kit/configuration.rb +15 -21
  3. data/lib/console_kit/configuration_validator.rb +179 -0
  4. data/lib/console_kit/connections/base_connection_handler.rb +139 -25
  5. data/lib/console_kit/connections/connection_manager.rb +42 -8
  6. data/lib/console_kit/connections/dashboard.rb +3 -11
  7. data/lib/console_kit/connections/diagnostic_helpers.rb +24 -5
  8. data/lib/console_kit/connections/elasticsearch_connection_handler.rb +87 -32
  9. data/lib/console_kit/connections/elasticsearch_prefix_registry.rb +70 -0
  10. data/lib/console_kit/connections/mongo_connection_handler.rb +51 -46
  11. data/lib/console_kit/connections/mongoid_support.rb +56 -0
  12. data/lib/console_kit/connections/redis_client_adapter.rb +92 -0
  13. data/lib/console_kit/connections/redis_connection_handler.rb +97 -41
  14. data/lib/console_kit/connections/sql_connection_handler.rb +66 -30
  15. data/lib/console_kit/connections/sql_strategy.rb +248 -0
  16. data/lib/console_kit/connections/table_renderer.rb +30 -20
  17. data/lib/console_kit/console_helpers.rb +17 -31
  18. data/lib/console_kit/diagnostics.rb +143 -0
  19. data/lib/console_kit/errors.rb +93 -0
  20. data/lib/console_kit/instrumentation.rb +57 -0
  21. data/lib/console_kit/output.rb +5 -14
  22. data/lib/console_kit/prompt.rb +17 -29
  23. data/lib/console_kit/railtie.rb +2 -3
  24. data/lib/console_kit/setup.rb +66 -13
  25. data/lib/console_kit/setup_ui.rb +1 -4
  26. data/lib/console_kit/tenant_configurator/context_wrapper.rb +51 -23
  27. data/lib/console_kit/tenant_configurator.rb +40 -78
  28. data/lib/console_kit/tenant_orchestrator.rb +31 -27
  29. data/lib/console_kit/tenant_plan.rb +68 -0
  30. data/lib/console_kit/tenant_rollback.rb +77 -0
  31. data/lib/console_kit/tenant_selector.rb +3 -6
  32. data/lib/console_kit/tenant_state.rb +73 -0
  33. data/lib/console_kit/tenant_switch.rb +124 -0
  34. data/lib/console_kit/version.rb +1 -1
  35. data/lib/console_kit.rb +51 -24
  36. data/lib/generators/console_kit/install_generator.rb +1 -12
  37. data/lib/generators/console_kit/templates/console_kit.rb +4 -0
  38. metadata +15 -4
  39. 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 apply_prefix(prefix)
20
- return unless defined?(Elasticsearch::Model)
31
+ def target_error(value)
32
+ return NOT_A_NAME unless value.nil? || value.is_a?(String) || value.is_a?(Symbol)
21
33
 
22
- Elasticsearch::Model.try(:index_name_prefix=, prefix)
34
+ ElasticsearchPrefixRegistry.prefix_error(value.presence&.to_s)
23
35
  end
24
36
  end
25
37
 
26
- def connect
27
- prefix = context_attribute(:tenant_elasticsearch_prefix).presence
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
- Thread.current[:console_kit_elasticsearch_prefix] = prefix
30
- self.class.apply_prefix(prefix)
56
+ report_conflicts(prefix)
57
+ apply_global(prefix)
58
+ registry.record(prefix)
31
59
  end
32
60
 
33
- def available? = self.class.elasticsearch_available?
61
+ def verify!(target)
62
+ expected = normalize(target)
63
+ actual = effective_prefix
64
+ return true if actual == expected
34
65
 
35
- def diagnostics
36
- return unavailable_diagnostics('Elasticsearch') unless available?
66
+ raise verification_error(expected, actual)
67
+ end
37
68
 
38
- perform_diagnostics
39
- rescue StandardError => e
40
- error_diagnostics('Elasticsearch', e)
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 perform_diagnostics
46
- client = Elasticsearch::Model.client
47
- latency = measure_latency do
48
- client.ping
49
- rescue StandardError
50
- nil
51
- end
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 build_elasticsearch_diagnostics(cluster, status, latency)
89
+ def full_diagnostics
90
+ client = Elasticsearch::Model.client
91
+ latency = measure_latency { ping!(client) }
92
+ health = client.cluster.health
57
93
  {
58
- name: 'Elasticsearch',
59
- status: :connected,
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
- def connect
10
- db = context_attribute(:tenant_mongo_db).presence
11
- switch_mongo(db)
12
- rescue NoMethodError
13
- Output.print_warning('Mongoid client override is not available in this version of Mongoid.')
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? = defined?(Mongoid)
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 diagnostics
19
- return unavailable_diagnostics('MongoDB') unless available?
32
+ def snapshot = mongoid.overrides
20
33
 
21
- perform_diagnostics
22
- rescue StandardError => e
23
- error_diagnostics('MongoDB', e)
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 perform_diagnostics
29
- db = tenant_database
30
- latency = measure_latency { db.command(ping: 1) }
31
- info = db.command(buildInfo: 1).first
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 build_mongo_diagnostics(name, version, latency)
56
+ def full_diagnostics
57
+ latency = measure_latency { mongoid.ping }
36
58
  {
37
- name: 'MongoDB',
38
- status: :connected,
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 tenant_database
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 switch_mongo(db)
51
- if db.nil?
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
- def reset_overrides
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 named_client?(name)
69
- Mongoid::Config.clients.key?(name.to_s)
70
- rescue StandardError
71
- false
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
- def redis_client = Redis.try(:current)
21
+ attr_accessor :isolation_warned
13
22
 
14
- def warn_no_auto_select(db_index)
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 connect
21
- db_index = context_attribute(:tenant_redis_db) || DEFAULT_REDIS_DB
22
- Output.print_info(switch_message(db_index))
23
- select_redis_db(db_index)
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 available? = defined?(Redis)
41
+ def snapshot = { db: adapter.current_db }
27
42
 
28
- def diagnostics
29
- redis = self.class.redis_client if available?
30
- return unavailable_diagnostics('Redis') unless redis
43
+ def connect!(target)
44
+ db = coerce_db(target)
45
+ return unless adapter.movable_to?(db)
31
46
 
32
- perform_diagnostics(redis)
33
- rescue StandardError => e
34
- error_diagnostics('Redis', e)
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 perform_diagnostics(redis)
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
- info = redis.info
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 build_redis_diagnostics(version, memory, latency)
84
+ def connected_diagnostics(latency, info)
46
85
  {
47
- name: 'Redis',
48
- status: :connected,
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 select_redis_db(db_index)
59
- klass = self.class
60
- redis = klass.redis_client
61
- if redis
62
- redis.select(db_index)
63
- elsif defined?(RedisClient) && db_index != DEFAULT_REDIS_DB
64
- klass.warn_no_auto_select(db_index)
65
- end
66
- rescue NoMethodError
67
- Output.print_warning('Redis.current is not available (deprecated in Redis v5+).')
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 switch_message(db_index)
71
- db_index ? "Switching to Redis DB: #{db_index}" : 'Resetting Redis connection to default'
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