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,69 +1,105 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'base_connection_handler'
4
+ require_relative 'sql_strategy'
4
5
 
5
6
  module ConsoleKit
6
7
  module Connections
7
- # Handles SQL connections
8
8
  class SqlConnectionHandler < BaseConnectionHandler
9
+ backend :sql,
10
+ display_name: 'SQL',
11
+ context_attribute: :tenant_shard,
12
+ constants_key: :shard,
13
+ detail_label: 'Shard'
14
+
15
+ DEFAULT_BASE_CLASS = 'ApplicationRecord'
16
+ MISSING_BASE_CLASS = 'the configured sql_base_class %<name>s could not be resolved. Check the class name and ' \
17
+ 'that the class is loaded'
18
+
9
19
  class << self
20
+ def target_error(value) = identifier_error(value)
21
+
10
22
  def sql_version(conn)
11
23
  conn.select_value('SELECT version()')
12
- rescue StandardError
24
+ rescue StandardError => e
25
+ raise e if ConsoleKit.programming_error?(e)
26
+
13
27
  nil
14
28
  end
15
29
 
16
30
  def base_class_name = ConsoleKit.configuration.sql_base_class
17
31
  end
18
32
 
19
- def connect
20
- shard = context_attribute(:tenant_shard).presence&.to_sym
21
- Output.print_info("#{connection_message(shard)} via #{base_class}")
22
- disconnect_existing_pool
23
- shard ? base_class.establish_connection(shard) : base_class.establish_connection
33
+ def available? = resolved_base_class.present?
34
+
35
+ def unavailable_reason
36
+ name = self.class.base_class_name
37
+ return nil if name.to_s == DEFAULT_BASE_CLASS || resolved_base_class.present?
38
+
39
+ format(MISSING_BASE_CLASS, name: name.inspect)
40
+ end
41
+
42
+ def prepare(target)
43
+ validate_target!(target)
44
+ unless strategy.switchable?
45
+ raise UnsupportedBackendError, "#{display_name} base class #{base_class} cannot switch connections."
46
+ end
47
+ return if strategy.resolvable?(normalize(target))
48
+
49
+ raise ConfigurationError,
50
+ "ConsoleKit: SQL shard #{scrub(target.inspect)} is not a registered shard or database configuration."
24
51
  end
25
52
 
26
- def available? = self.class.base_class_name.to_s.safe_constantize.present?
53
+ def snapshot = strategy.snapshot
27
54
 
28
- def diagnostics
29
- return unavailable_diagnostics('SQL') unless available?
55
+ def connect!(target)
56
+ shard = normalize(target)
57
+ Output.print_info("#{connection_message(shard)} via #{base_class}")
58
+ strategy.apply(shard)
59
+ end
60
+
61
+ def verify!(target)
62
+ expected, actual = strategy.identity(normalize(target))
63
+ return true if expected.to_s == actual.to_s
30
64
 
31
- perform_diagnostics
32
- rescue StandardError => e
33
- error_diagnostics('SQL', e)
65
+ raise verification_error(expected, actual)
34
66
  end
35
67
 
68
+ def restore(state) = strategy.restore(state)
69
+
70
+ def diagnostic_identity = strategy.pool_details
71
+
36
72
  private
37
73
 
38
- def perform_diagnostics
39
- conn = base_class.connection
40
- latency = measure_latency { conn.execute('SELECT 1') }
41
- build_sql_diagnostics(conn, latency)
74
+ def basic_diagnostics
75
+ details = strategy.pool_details
76
+ { name: display_name, status: details.empty? ? :unknown : :connected, latency_ms: nil, details: details }
42
77
  end
43
78
 
44
- def disconnect_existing_pool
45
- pool = base_class.try(:connection_pool)
46
- pool&.disconnect!
79
+ def full_diagnostics
80
+ conn = base_class.connection
81
+ latency = measure_latency { conn.execute('SELECT 1') }
82
+ { name: display_name, status: :connected, latency_ms: latency, details: full_details(conn) }
47
83
  end
48
84
 
49
- def build_sql_diagnostics(conn, latency)
85
+ def full_details(conn)
50
86
  {
51
- name: 'SQL',
52
- status: :connected,
53
- latency_ms: latency,
54
- details: {
55
- adapter: conn.adapter_name,
56
- pool_size: base_class.connection_pool.size,
57
- version: self.class.sql_version(conn).to_s.truncate(50)
58
- }
87
+ adapter: conn.adapter_name,
88
+ pool_size: base_class.connection_pool.size,
89
+ version: self.class.sql_version(conn).to_s.truncate(50)
59
90
  }
60
91
  end
61
92
 
93
+ def strategy = @strategy ||= SqlStrategy.new(base_class)
94
+ def normalize(target) = target.presence&.to_sym
95
+
96
+ def resolved_base_class = self.class.base_class_name.to_s.safe_constantize
97
+
62
98
  def base_class
63
99
  @base_class ||= begin
64
100
  name = self.class.base_class_name
65
101
  klass = name.to_s.safe_constantize
66
- klass || raise(Error, "ConsoleKit: sql_base_class '#{name}' could not be found.")
102
+ klass || raise(ConfigurationError, "ConsoleKit: sql_base_class '#{name}' could not be found.")
67
103
  end
68
104
  end
69
105
 
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+ require_relative '../output'
5
+ require_relative '../instrumentation'
6
+
7
+ module ConsoleKit
8
+ module Connections
9
+ class ShardFrame
10
+ KEY = :console_kit_sql_connected_to_frame
11
+ STOLEN = 'ConsoleKit: a `connected_to` block removed the shard frame ConsoleKit had committed, so shard ' \
12
+ '%<shard>p was not live while that block was open. Re-applying it now.'
13
+ REASSERTED = 'console_kit.sql_frame_reasserted'
14
+
15
+ attr_reader :base_class
16
+
17
+ def initialize(base_class) = @base_class = base_class
18
+
19
+ def apply(shard)
20
+ base_class.connecting_to(shard: shard || base_class.default_shard, role: base_class.current_role)
21
+ place(shard)
22
+ end
23
+
24
+ def live_shard
25
+ reassert if taken?
26
+ base_class.current_shard
27
+ end
28
+
29
+ def applied_shard
30
+ return nil unless on?
31
+
32
+ owned[:shard] || base_class.default_shard
33
+ end
34
+
35
+ def forget_unless_on
36
+ Thread.current.thread_variable_set(KEY, nil) if owned && !on?
37
+ end
38
+
39
+ def on? = !index_in.nil?
40
+
41
+ private
42
+
43
+ def stack = base_class.try(:connected_to_stack)
44
+ def taken? = !owned.nil? && !on?
45
+
46
+ def reassert
47
+ shard = owned[:shard]
48
+ Instrumentation.increment(REASSERTED)
49
+ Output.print_warning(format(STOLEN, shard: shard || base_class.default_shard))
50
+ apply(shard)
51
+ end
52
+
53
+ def place(shard)
54
+ current = stack
55
+ index = index_in
56
+ current[index] = current.pop if index
57
+ remember(current, index || (current.size - 1), shard)
58
+ end
59
+
60
+ def index_in
61
+ current = stack
62
+ entry = owned
63
+ return nil unless entry && current
64
+
65
+ index = entry[:index]
66
+ index if current[index].equal?(entry[:frame])
67
+ end
68
+
69
+ def owned
70
+ entry = Thread.current.thread_variable_get(KEY)
71
+ entry if entry && entry[:base].equal?(base_class)
72
+ end
73
+
74
+ def remember(current, index, shard)
75
+ Thread.current.thread_variable_set(
76
+ KEY, { base: base_class, frame: current[index], index: index, shard: shard }
77
+ )
78
+ end
79
+ end
80
+
81
+ class PoolSlot
82
+ attr_reader :base_class
83
+
84
+ def initialize(base_class) = @base_class = base_class
85
+
86
+ def absent? = lookup.nil?
87
+
88
+ def remove
89
+ return if absent?
90
+ return handler.remove_connection_pool(spec, role: role, shard: shard) if removable_handler?
91
+
92
+ base_class.remove_connection if base_class.respond_to?(:remove_connection)
93
+ end
94
+
95
+ private
96
+
97
+ def handler = base_class.try(:connection_handler)
98
+ def spec = base_class.try(:connection_specification_name)
99
+ def role = base_class.try(:current_role)
100
+ def shard = base_class.try(:current_shard)
101
+ def removable_handler? = handler.respond_to?(:remove_connection_pool) && spec
102
+
103
+ def lookup
104
+ return handler.retrieve_connection_pool(spec, role: role, shard: shard) if retrievable_handler?
105
+
106
+ base_class.try(:connection_pool)
107
+ rescue StandardError => e
108
+ raise e if ConsoleKit.programming_error?(e)
109
+
110
+ nil
111
+ end
112
+
113
+ def retrievable_handler? = handler.respond_to?(:retrieve_connection_pool) && spec
114
+ end
115
+
116
+ class SqlStrategy
117
+ NATIVE_METHODS = %i[connecting_to connected_to_stack default_shard current_shard current_role].freeze
118
+
119
+ attr_reader :base_class
120
+
121
+ def initialize(base_class) = @base_class = base_class
122
+
123
+ def switchable? = base_class.respond_to?(:establish_connection) || native_capable?
124
+
125
+ def native?(shard)
126
+ return false unless native_capable?
127
+ return !connected_to_stack.to_a.empty? if shard.nil?
128
+
129
+ !shard_pool(shard).nil?
130
+ end
131
+
132
+ def resolvable?(shard)
133
+ return true if !shard || native?(shard)
134
+
135
+ configs = env_configs
136
+ configs.empty? || configs.any? { |cfg| config_name(cfg).to_s == shard.to_s }
137
+ end
138
+
139
+ def snapshot
140
+ name = current_db_config_name
141
+ {
142
+ shard: frame.applied_shard || base_class.try(:current_shard),
143
+ role: base_class.try(:current_role),
144
+ stack_depth: connected_to_stack&.size,
145
+ db_config_name: name,
146
+ pool_absent: !name && pool.absent?
147
+ }
148
+ end
149
+
150
+ def apply(shard) = native?(shard) ? apply_native(shard) : apply_fallback(shard)
151
+
152
+ def restore(state)
153
+ return unless state
154
+
155
+ unwind_stack(state[:stack_depth])
156
+ restore_shard(state[:shard])
157
+ state[:pool_absent] ? pool.remove : reestablish(state[:db_config_name])
158
+ end
159
+
160
+ def identity(shard)
161
+ return [shard || base_class.default_shard, frame.live_shard] if native?(shard)
162
+
163
+ [expected_db_config_name(shard), current_db_config_name]
164
+ end
165
+
166
+ def pool_details
167
+ describe_pool(base_class.try(:connection_pool))
168
+ rescue StandardError => e
169
+ raise e if ConsoleKit.programming_error?(e)
170
+
171
+ {}
172
+ end
173
+
174
+ private
175
+
176
+ def describe_pool(live_pool)
177
+ return {} unless live_pool
178
+
179
+ { adapter: live_pool.try(:db_config).try(:adapter), pool_size: live_pool.try(:size),
180
+ config: current_db_config_name, shard: base_class.try(:current_shard) }.compact
181
+ end
182
+
183
+ def native_capable? = NATIVE_METHODS.all? { |method| base_class.respond_to?(method) }
184
+ def connected_to_stack = base_class.try(:connected_to_stack)
185
+
186
+ def frame = @frame ||= ShardFrame.new(base_class)
187
+ def pool = @pool ||= PoolSlot.new(base_class)
188
+ def apply_native(shard) = frame.apply(shard)
189
+
190
+ def restore_shard(shard)
191
+ return if !shard || !native_capable? || (frame.applied_shard || base_class.current_shard) == shard
192
+
193
+ apply_native(shard)
194
+ end
195
+
196
+ def apply_fallback(shard)
197
+ desired = expected_db_config_name(shard)
198
+ return if desired && desired.to_s == current_db_config_name.to_s
199
+
200
+ shard ? base_class.establish_connection(shard.to_sym) : base_class.establish_connection
201
+ end
202
+
203
+ def unwind_stack(depth)
204
+ stack = connected_to_stack
205
+ return unless stack && depth
206
+
207
+ stack.pop while stack.size > depth
208
+ frame.forget_unless_on
209
+ end
210
+
211
+ def reestablish(name)
212
+ return if !name || name.to_s == current_db_config_name.to_s
213
+
214
+ base_class.establish_connection(name.to_sym)
215
+ end
216
+
217
+ def shard_pool(shard)
218
+ handler = base_class.try(:connection_handler)
219
+ spec_name = base_class.try(:connection_specification_name)
220
+ return nil unless spec_name && handler.respond_to?(:retrieve_connection_pool)
221
+
222
+ handler.retrieve_connection_pool(spec_name, role: base_class.current_role, shard: shard.to_sym)
223
+ end
224
+
225
+ def expected_db_config_name(shard) = shard ? shard.to_s : config_name(env_configs.first)
226
+
227
+ def env_configs
228
+ configs = base_class.try(:configurations)
229
+ env = current_db_config.try(:env_name)
230
+ return [] unless env && configs.respond_to?(:configs_for)
231
+
232
+ configs.configs_for(env_name: env)
233
+ end
234
+
235
+ def current_db_config
236
+ base_class.try(:connection_pool).try(:db_config)
237
+ rescue StandardError => e
238
+ raise e if ConsoleKit.programming_error?(e)
239
+
240
+ nil
241
+ end
242
+
243
+ def current_db_config_name = config_name(current_db_config)
244
+
245
+ def config_name(config) = config&.name
246
+ end
247
+ end
248
+ end
@@ -1,43 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'table_formatter'
4
-
5
3
  module ConsoleKit
6
4
  module Connections
7
- # Renders diagnostic data into a Unicode box-drawing table
8
5
  module TableRenderer
6
+ HEADERS = %w[Service Status Latency Details].freeze
7
+ STATUS = {
8
+ connected: "\u2713 Connected",
9
+ error: "\u2717 Error",
10
+ unavailable: "\u2014 N/A"
11
+ }.freeze
12
+
9
13
  class << self
10
14
  def render(rows)
11
- headers = %w[Service Status Latency Details]
12
- table_rows = rows.map { |row| TableFormatter.format_row(row) }
13
- widths = calculate_widths(headers, table_rows)
15
+ table_rows = rows.map { |row| format_row(row) }
16
+ widths = ([HEADERS] + table_rows).transpose.map { |column| column.map(&:length).max }
14
17
 
15
- build_table(headers, table_rows, widths)
18
+ build_table(table_rows, widths)
16
19
  end
17
20
 
18
21
  private
19
22
 
20
- def calculate_widths(headers, rows)
21
- all_rows = [headers] + rows
22
- headers.each_index.map do |index|
23
- column_max_width(all_rows, index)
24
- end
23
+ def format_row(diag)
24
+ latency = diag[:latency_ms]
25
+ [
26
+ diag[:name],
27
+ STATUS.fetch(diag[:status], '? Unknown'),
28
+ latency ? "#{latency}ms" : "\u2014",
29
+ format_details(diag[:details])
30
+ ]
25
31
  end
26
32
 
27
- def column_max_width(rows, index)
28
- rows.map { |row| row[index].length }.max
33
+ def format_details(details)
34
+ return '' unless details&.any?
35
+
36
+ details.compact.map { |key, value| "#{key}: #{value}" }.join(', ')
29
37
  end
30
38
 
31
- def build_table(headers, rows, widths)
32
- lines = [table_top(widths), table_line(headers, widths), table_mid(widths)]
39
+ def build_table(rows, widths)
40
+ lines = [rule(widths, "\u250C\u252C\u2510"), table_line(HEADERS, widths),
41
+ rule(widths, "\u251C\u253C\u2524")]
33
42
  rows.each { |row| lines << table_line(row, widths) }
34
- lines << table_bottom(widths)
43
+ lines << rule(widths, "\u2514\u2534\u2518")
35
44
  lines.join("\n")
36
45
  end
37
46
 
38
- def table_top(widths) = "\u250C#{widths.map { |width| "\u2500" * (width + 2) }.join("\u252C")}\u2510"
39
- def table_mid(widths) = "\u251C#{widths.map { |width| "\u2500" * (width + 2) }.join("\u253C")}\u2524"
40
- def table_bottom(widths) = "\u2514#{widths.map { |width| "\u2500" * (width + 2) }.join("\u2534")}\u2518"
47
+ def rule(widths, corners)
48
+ left, join, right = corners.chars
49
+ "#{left}#{widths.map { |width| "\u2500" * (width + 2) }.join(join)}#{right}"
50
+ end
41
51
 
42
52
  def table_line(cells, widths)
43
53
  content = cells.each_with_index.map { |cell, index| " #{cell.ljust(widths[index])} " }.join("\u2502")
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ConsoleKit
4
- # Helper methods available in the Rails console
5
4
  module ConsoleHelpers
6
5
  def switch_tenant
7
6
  ConsoleKit.reset_current_tenant
@@ -9,56 +8,43 @@ module ConsoleKit
9
8
  end
10
9
 
11
10
  def tenant_info
12
- tenant = ConsoleKit::Setup.current_tenant
13
- return no_tenant_warning unless tenant
11
+ tenant = ConsoleKit::StateStore.tenant_key
12
+ return ConsoleKit::Output.print_warning('No tenant is currently configured.') unless tenant
14
13
 
15
- display_tenant_info(tenant)
14
+ constants = ConsoleKit.configuration.tenants[tenant]&.[](:constants) || {}
15
+ ConsoleHelpers.print_tenant_details(tenant, constants)
16
16
  nil
17
17
  end
18
18
 
19
- def dashboard
20
- ConsoleKit::Connections::Dashboard.display
19
+ def dashboard(level: :basic)
20
+ ConsoleKit::Connections::Dashboard.display(level: level)
21
21
  self
22
22
  end
23
23
 
24
24
  def tenants
25
25
  names = ConsoleKit.configuration.tenants&.keys || []
26
- print_available_tenants(names)
27
- names
28
- end
29
-
30
- DETAIL_LABELS = {
31
- 'Partner' => :partner_code, 'Shard' => :shard, 'Mongo DB' => :mongo_db,
32
- 'Redis DB' => :redis_db, 'ES Prefix' => :elasticsearch_prefix, 'Environment' => :environment
33
- }.freeze
34
-
35
- private
36
-
37
- def no_tenant_warning
38
- ConsoleKit::Output.print_warning('No tenant is currently configured.')
39
- self
40
- end
41
-
42
- def display_tenant_info(tenant)
43
- constants = ConsoleKit.configuration.tenants[tenant]&.[](:constants) || {}
44
- ConsoleHelpers.print_tenant_details(tenant, constants)
45
- self
46
- end
47
-
48
- def print_available_tenants(names)
49
26
  ConsoleKit::Output.print_list(names, header: 'Available Tenants')
50
- self
27
+ names
51
28
  end
52
29
 
53
30
  class << self
54
31
  def print_tenant_details(tenant, constants)
55
32
  ConsoleKit::Output.print_header("Tenant: #{tenant}")
56
- DETAIL_LABELS.each do |label, key|
33
+ detail_labels.each do |label, key|
57
34
  next unless constants.key?(key)
58
35
 
59
36
  ConsoleKit::Output.print_info(" #{label.ljust(13)}#{constants[key]}")
60
37
  end
61
38
  end
39
+
40
+ private
41
+
42
+ def detail_labels
43
+ backend_labels = ConsoleKit::Connections::BaseConnectionHandler.registry.to_h do |handler|
44
+ [handler.detail_label, handler.constants_key]
45
+ end
46
+ { 'Partner' => :partner_code }.merge(backend_labels).merge('Environment' => :environment)
47
+ end
62
48
  end
63
49
  end
64
50
  end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors'
4
+ require_relative 'tenant_state'
5
+ require_relative 'instrumentation'
6
+ require_relative 'connections/diagnostic_helpers'
7
+
8
+ module ConsoleKit
9
+ module Diagnostics
10
+ LEVELS = %i[basic full].freeze
11
+ DEFAULT_TIMEOUT = 2
12
+ CACHE_TTL_SECONDS = 2.0
13
+ EVENT = 'console_kit.diagnostics'
14
+ TIMEOUT_COUNTER = 'console_kit.diagnostics_timeout'
15
+
16
+ class << self
17
+ def run(level: :basic, timeout: DEFAULT_TIMEOUT)
18
+ validate_level!(level)
19
+ available_handlers.map { |handler| cached(handler, level, timeout) }
20
+ end
21
+
22
+ def clear_cache! = Cache.clear!
23
+
24
+ def validate_level!(level)
25
+ return level if LEVELS.include?(level)
26
+
27
+ raise ConfigurationError,
28
+ "ConsoleKit: unknown diagnostics level #{level.inspect}. Expected one of #{LEVELS.inspect}."
29
+ end
30
+
31
+ private
32
+
33
+ def available_handlers
34
+ Connections::ConnectionManager.available_handlers(ConsoleKit.configuration.context_class)
35
+ end
36
+
37
+ def cached(handler, level, timeout)
38
+ Cache.fetch_row(handler, level) { handler.safe_diagnostics(timeout: timeout, level: level) }
39
+ end
40
+ end
41
+
42
+ module Cache
43
+ STORE_KEY = :console_kit_diagnostics_cache
44
+ CACHED_LEVEL = :full
45
+
46
+ class << self
47
+ def fetch_row(handler, level)
48
+ return yield unless level == CACHED_LEVEL
49
+
50
+ key = [StateStore.tenant_key, level, handler.backend_key]
51
+ identity = identity_of(handler)
52
+ read(key, identity) || write(key, identity, yield)
53
+ end
54
+
55
+ def clear!
56
+ Thread.current.thread_variable_set(STORE_KEY, nil)
57
+ end
58
+
59
+ private
60
+
61
+ def store
62
+ thread = Thread.current
63
+ thread.thread_variable_get(STORE_KEY) || thread.thread_variable_set(STORE_KEY, {})
64
+ end
65
+
66
+ def read(key, identity)
67
+ entry = store[key]
68
+ return nil unless entry
69
+
70
+ unless current?(entry, identity)
71
+ store.delete(key)
72
+ return nil
73
+ end
74
+
75
+ entry[:row]
76
+ end
77
+
78
+ def write(key, identity, row)
79
+ return row if row[:status] == :error
80
+
81
+ purge_expired!
82
+ store[key] = { row: row, expires_at: now + CACHE_TTL_SECONDS, state: state, identity: identity }
83
+ row
84
+ end
85
+
86
+ def identity_of(handler)
87
+ handler.diagnostic_identity
88
+ rescue StandardError => e
89
+ raise e if ConsoleKit.programming_error?(e)
90
+
91
+ Object.new
92
+ end
93
+
94
+ def purge_expired!
95
+ store.delete_if { |_key, entry| !fresh?(entry) }
96
+ end
97
+
98
+ def fresh?(entry) = entry[:expires_at] > now && entry[:state].equal?(state)
99
+
100
+ def current?(entry, identity) = fresh?(entry) && entry[:identity] == identity
101
+
102
+ def state = StateStore.stored
103
+ def now = Connections::DiagnosticHelpers.clock_time
104
+ end
105
+ end
106
+
107
+ module Runner
108
+ class << self
109
+ def call(handler, timeout: DEFAULT_TIMEOUT, level: :basic)
110
+ Diagnostics.validate_level!(level)
111
+ started = Connections::DiagnosticHelpers.clock_time
112
+ outcome = execute(handler, level)
113
+ report_overrun(started, timeout)
114
+ resolve(handler, outcome)
115
+ end
116
+
117
+ def execute(handler, level)
118
+ Instrumentation.instrument(EVENT, backend: handler.backend_key, level: level) do
119
+ handler.diagnostics(level: level)
120
+ end
121
+ rescue StandardError, ScriptError => e
122
+ e
123
+ end
124
+
125
+ private
126
+
127
+ def resolve(handler, outcome) = outcome.is_a?(Hash) ? outcome : failed_row(handler, outcome)
128
+
129
+ def report_overrun(started, timeout)
130
+ return if Connections::DiagnosticHelpers.clock_time - started <= timeout
131
+
132
+ Instrumentation.increment(TIMEOUT_COUNTER)
133
+ end
134
+
135
+ def failed_row(handler, error)
136
+ raise error if ConsoleKit.programming_error?(error)
137
+
138
+ Connections::DiagnosticHelpers.error_diagnostics(handler.display_name, error)
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end