openc3 7.0.1 → 7.1.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +50 -3
  3. data/data/config/interface_modifiers.yaml +3 -1
  4. data/data/config/item_modifiers.yaml +1 -1
  5. data/data/config/microservice.yaml +15 -2
  6. data/data/config/parameter_modifiers.yaml +49 -7
  7. data/data/config/plugins.yaml +1 -0
  8. data/data/config/target.yaml +11 -0
  9. data/data/config/target_config.yaml +6 -2
  10. data/lib/openc3/api/api.rb +1 -0
  11. data/lib/openc3/api/calendar_api.rb +183 -0
  12. data/lib/openc3/api/cmd_api.rb +2 -1
  13. data/lib/openc3/api/metrics_api.rb +11 -1
  14. data/lib/openc3/api/tlm_api.rb +21 -6
  15. data/lib/openc3/core_ext/faraday.rb +1 -1
  16. data/lib/openc3/io/json_api.rb +1 -1
  17. data/lib/openc3/logs/log_writer.rb +3 -1
  18. data/lib/openc3/microservices/decom_common.rb +128 -0
  19. data/lib/openc3/microservices/decom_microservice.rb +30 -97
  20. data/lib/openc3/microservices/interface_decom_common.rb +6 -2
  21. data/lib/openc3/microservices/interface_microservice.rb +10 -8
  22. data/lib/openc3/microservices/log_microservice.rb +1 -1
  23. data/lib/openc3/microservices/microservice.rb +3 -2
  24. data/lib/openc3/microservices/queue_microservice.rb +1 -1
  25. data/lib/openc3/microservices/scope_cleanup_microservice.rb +60 -46
  26. data/lib/openc3/microservices/text_log_microservice.rb +1 -2
  27. data/lib/openc3/models/cvt_model.rb +24 -13
  28. data/lib/openc3/models/db_sharded_model.rb +110 -0
  29. data/lib/openc3/models/interface_model.rb +9 -0
  30. data/lib/openc3/models/interface_status_model.rb +33 -3
  31. data/lib/openc3/models/metric_model.rb +96 -37
  32. data/lib/openc3/models/microservice_model.rb +7 -0
  33. data/lib/openc3/models/microservice_status_model.rb +30 -3
  34. data/lib/openc3/models/plugin_model.rb +20 -8
  35. data/lib/openc3/models/queue_model.rb +36 -46
  36. data/lib/openc3/models/reingest_job_model.rb +153 -0
  37. data/lib/openc3/models/scope_model.rb +3 -2
  38. data/lib/openc3/models/script_status_model.rb +4 -20
  39. data/lib/openc3/models/target_model.rb +113 -100
  40. data/lib/openc3/models/trigger_model.rb +1 -1
  41. data/lib/openc3/packets/packet_config.rb +4 -1
  42. data/lib/openc3/packets/parsers/xtce_parser.rb +23 -1
  43. data/lib/openc3/script/script.rb +6 -4
  44. data/lib/openc3/script/script_runner.rb +4 -4
  45. data/lib/openc3/script/telemetry.rb +3 -3
  46. data/lib/openc3/script/web_socket_api.rb +29 -22
  47. data/lib/openc3/system/system.rb +20 -3
  48. data/lib/openc3/topics/command_decom_topic.rb +4 -2
  49. data/lib/openc3/topics/command_topic.rb +9 -5
  50. data/lib/openc3/topics/decom_interface_topic.rb +15 -10
  51. data/lib/openc3/topics/interface_topic.rb +71 -29
  52. data/lib/openc3/topics/limits_event_topic.rb +62 -41
  53. data/lib/openc3/topics/router_topic.rb +61 -21
  54. data/lib/openc3/topics/system_events_topic.rb +18 -1
  55. data/lib/openc3/topics/telemetry_decom_topic.rb +3 -1
  56. data/lib/openc3/topics/telemetry_topic.rb +4 -2
  57. data/lib/openc3/topics/topic.rb +77 -5
  58. data/lib/openc3/utilities/aws_bucket.rb +2 -0
  59. data/lib/openc3/utilities/cli_generator.rb +10 -2
  60. data/lib/openc3/utilities/metric.rb +15 -1
  61. data/lib/openc3/utilities/questdb_client.rb +173 -37
  62. data/lib/openc3/utilities/reingest_job.rb +377 -0
  63. data/lib/openc3/utilities/ruby_lex_utils.rb +2 -0
  64. data/lib/openc3/utilities/running_script.rb +8 -10
  65. data/lib/openc3/utilities/store_autoload.rb +78 -52
  66. data/lib/openc3/utilities/store_queued.rb +20 -12
  67. data/lib/openc3/version.rb +5 -5
  68. data/templates/microservice/microservices/TEMPLATE/microservice.py +9 -0
  69. data/templates/plugin/plugin.gemspec +13 -1
  70. data/templates/tool_angular/package.json +2 -2
  71. data/templates/tool_react/package.json +1 -1
  72. data/templates/tool_svelte/package.json +1 -1
  73. data/templates/tool_vue/package.json +3 -3
  74. data/templates/tool_vue/src/router.js +2 -2
  75. data/templates/widget/package.json +2 -2
  76. metadata +8 -3
@@ -0,0 +1,128 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2026 OpenC3, Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This program is distributed in the hope that it will be useful,
7
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
8
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
9
+ # See LICENSE.md for more details.
10
+ #
11
+ # This file may also be used under the terms of a commercial license
12
+ # if purchased from OpenC3, Inc.
13
+
14
+ require 'openc3/system/system'
15
+ require 'openc3/microservices/interface_microservice'
16
+ require 'openc3/topics/telemetry_decom_topic'
17
+ require 'openc3/models/target_model'
18
+
19
+ module OpenC3
20
+ # Shared decom pipeline used by DecomMicroservice (live telemetry) and
21
+ # ReingestJob (historical raw log replay). The reingest path passes
22
+ # check_limits: false so historical data does not re-fire limits events.
23
+ module DecomCommon
24
+ extend self
25
+
26
+ # Decommutate a Packet and publish it on the TelemetryDecomTopic. This is the
27
+ # step that lands data in the CVT and in the Python TsdbMicroservice → QuestDB.
28
+ #
29
+ # @param packet [Packet] A fully buffered Packet. Caller sets received_time,
30
+ # received_count, stored, extra, buffer.
31
+ # @param scope [String] Scope name.
32
+ # @param target_names [Array<String>] Used when a subpacket must be re-identified.
33
+ # @param logger [Logger] Destination for warnings.
34
+ # @param name [String] Identifier used in subpacket warning messages
35
+ # (microservice name, or "REINGEST:<job_id>").
36
+ # @param check_limits [Boolean] When false, skips the Packet#check_limits call.
37
+ # Reingest passes false so historical data does not re-fire limits events.
38
+ # @param metric [Metric, nil] Optional; when set, records decom_duration_seconds.
39
+ # @param error_callback [Proc, nil] Called as error_callback.call(exception) when
40
+ # Packet#process or Packet#check_limits raises. The microservice uses this to
41
+ # bump its decom_error_total metric.
42
+ # @return [Integer] Number of (sub)packets published.
43
+ def decom_and_publish(packet, scope:, target_names:, logger:, name:,
44
+ check_limits: true, metric: nil, error_callback: nil)
45
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC) if metric
46
+ published = 0
47
+
48
+ packet_and_subpackets = packet.subpacketize
49
+ packet_and_subpackets.each do |packet_or_subpacket|
50
+ if packet_or_subpacket.subpacket
51
+ packet_or_subpacket = handle_subpacket(packet, packet_or_subpacket,
52
+ target_names: target_names,
53
+ scope: scope,
54
+ logger: logger,
55
+ name: name)
56
+ end
57
+
58
+ begin
59
+ packet_or_subpacket.process
60
+ rescue Exception => e
61
+ error_callback&.call(e)
62
+ logger.error e.message
63
+ end
64
+
65
+ packet_or_subpacket.check_limits(System.limits_set) if check_limits
66
+
67
+ TelemetryDecomTopic.write_packet(packet_or_subpacket, scope: scope)
68
+ published += 1
69
+ end
70
+
71
+ if metric
72
+ diff = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
73
+ metric.set(name: 'decom_duration_seconds', value: diff, type: 'gauge', unit: 'seconds')
74
+ end
75
+
76
+ published
77
+ end
78
+
79
+ # Identify a subpacket and (except for stored telemetry) update the CVT.
80
+ # Extracted from DecomMicroservice so reingest can handle subpackets too.
81
+ def handle_subpacket(packet, subpacket, target_names:, scope:, logger:, name:)
82
+ subpacket.received_time = packet.received_time
83
+ subpacket.stored = packet.stored
84
+ subpacket.extra = packet.extra
85
+
86
+ if subpacket.stored
87
+ identified_subpacket = System.telemetry.identify_and_define_packet(subpacket, target_names, subpackets: true)
88
+ else
89
+ if subpacket.identified?
90
+ begin
91
+ identified_subpacket = System.telemetry.update!(subpacket.target_name,
92
+ subpacket.packet_name,
93
+ subpacket.buffer)
94
+ rescue RuntimeError
95
+ logger.warn "#{name}: Received unknown identified subpacket: #{subpacket.target_name} #{subpacket.packet_name}"
96
+ subpacket.target_name = nil
97
+ subpacket.packet_name = nil
98
+ identified_subpacket = System.telemetry.identify!(subpacket.buffer,
99
+ target_names, subpackets: true)
100
+ end
101
+ else
102
+ identified_subpacket = System.telemetry.identify!(subpacket.buffer,
103
+ target_names, subpackets: true)
104
+ end
105
+ end
106
+
107
+ if identified_subpacket
108
+ identified_subpacket.received_time = subpacket.received_time
109
+ identified_subpacket.stored = subpacket.stored
110
+ identified_subpacket.extra = subpacket.extra
111
+ subpacket = identified_subpacket
112
+ else
113
+ unknown_subpacket = System.telemetry.update!('UNKNOWN', 'UNKNOWN', subpacket.buffer)
114
+ unknown_subpacket.received_time = subpacket.received_time
115
+ unknown_subpacket.stored = subpacket.stored
116
+ unknown_subpacket.extra = subpacket.extra
117
+ subpacket = unknown_subpacket
118
+ num_bytes_to_print = [InterfaceMicroservice::UNKNOWN_BYTES_TO_PRINT, subpacket.length].min
119
+ data = subpacket.buffer(false)[0..(num_bytes_to_print - 1)]
120
+ prefix = data.each_byte.map { |byte| sprintf("%02X", byte) }.join()
121
+ logger.warn "#{name} #{subpacket.target_name} packet length: #{subpacket.length} starting with: #{prefix}"
122
+ end
123
+
124
+ TargetModel.sync_tlm_packet_counts(subpacket, target_names, scope: scope)
125
+ subpacket
126
+ end
127
+ end
128
+ end
@@ -18,6 +18,7 @@
18
18
  require 'time'
19
19
  require 'thread'
20
20
  require 'openc3/microservices/microservice'
21
+ require 'openc3/microservices/decom_common'
21
22
  require 'openc3/microservices/interface_decom_common'
22
23
  require 'openc3/microservices/interface_microservice'
23
24
  require 'openc3/topics/telemetry_decom_topic'
@@ -90,14 +91,18 @@ module OpenC3
90
91
  if @name =~ /__DECOM__/
91
92
  @topics << "#{@scope}__DECOMINTERFACE__{#{@target_names[0]}}"
92
93
  end
93
- Topic.update_topic_offsets(@topics)
94
+ @limits_event_topic = "#{@scope}__openc3_limits_events"
95
+ @topics << @limits_event_topic
96
+ Topic.update_topic_offsets(@topics, db_shard: @db_shard)
97
+ # Initialize before assigning limits_change_callback - sync_system below
98
+ # can fire the callback synchronously for any persisted-disabled items.
99
+ @limits_response_queue = Queue.new
100
+ @limits_response_thread = nil
94
101
  System.telemetry.limits_change_callback = method(:limits_change_callback)
95
102
  LimitsEventTopic.sync_system(scope: @scope)
96
103
  @error_count = 0
97
104
  @metric.set(name: 'decom_total', value: @count, type: 'counter')
98
105
  @metric.set(name: 'decom_error_total', value: @error_count, type: 'counter')
99
- @limits_response_queue = Queue.new
100
- @limits_response_thread = nil
101
106
  end
102
107
 
103
108
  def run
@@ -110,10 +115,13 @@ module OpenC3
110
115
 
111
116
  begin
112
117
  OpenC3.in_span("read_topics") do
113
- Topic.read_topics(@topics) do |topic, msg_id, msg_hash, redis|
118
+ Topic.read_topics(@topics, db_shard: @db_shard) do |topic, msg_id, msg_hash, redis|
114
119
  break if @cancel_thread
115
120
  if topic == @microservice_topic
116
121
  microservice_cmd(topic, msg_id, msg_hash, redis)
122
+ elsif topic == @limits_event_topic
123
+ event = JSON.parse(msg_hash['event'], allow_nan: true, create_additions: true)
124
+ LimitsEventTopic.process_event(event)
117
125
  elsif topic =~ /__DECOMINTERFACE/
118
126
  if msg_hash.key?('inject_tlm')
119
127
  handle_inject_tlm_with_ack(msg_hash['inject_tlm'], msg_id)
@@ -134,7 +142,6 @@ module OpenC3
134
142
  @count += 1
135
143
  end
136
144
  end
137
- LimitsEventTopic.sync_system_thread_body(scope: @scope)
138
145
  rescue => e
139
146
  @error_count += 1
140
147
  @metric.set(name: 'decom_error_total', value: @error_count, type: 'counter')
@@ -153,8 +160,6 @@ module OpenC3
153
160
  delta = Time.now.to_f - msgid_seconds_from_epoch
154
161
  @metric.set(name: 'decom_topic_delta_seconds', value: delta, type: 'gauge', unit: 'seconds', help: 'Delta time between data written to stream and decom start')
155
162
 
156
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
157
-
158
163
  #######################################
159
164
  # Build packet object from topic data
160
165
  #######################################
@@ -172,100 +177,23 @@ module OpenC3
172
177
  end
173
178
  packet.buffer = msg_hash["buffer"]
174
179
 
175
- ################################################################################
176
- # Break packet into subpackets (if necessary)
177
- # Subpackets are typically channelized data
178
- ################################################################################
179
- packet_and_subpackets = packet.subpacketize
180
-
181
- packet_and_subpackets.each do |packet_or_subpacket|
182
- if packet_or_subpacket.subpacket
183
- packet_or_subpacket = handle_subpacket(packet, packet_or_subpacket)
184
- end
185
-
186
- #####################################################################################
187
- # Run Processors
188
- # This must be before the full decom so that processor derived values are available
189
- #####################################################################################
190
- begin
191
- packet_or_subpacket.process # Run processors
192
- rescue Exception => e
180
+ DecomCommon.decom_and_publish(
181
+ packet,
182
+ scope: @scope,
183
+ target_names: @target_names,
184
+ logger: @logger,
185
+ name: @name,
186
+ check_limits: true,
187
+ metric: @metric,
188
+ error_callback: ->(e) {
193
189
  @error_count += 1
194
190
  @metric.set(name: 'decom_error_total', value: @error_count, type: 'counter')
195
191
  @error = e
196
- @logger.error e.message
197
- end
198
-
199
- #############################################################################
200
- # Process all the limits and call the limits_change_callback (as necessary)
201
- # This must be before the full decom so that limits states are available
202
- #############################################################################
203
- packet_or_subpacket.check_limits(System.limits_set)
204
-
205
- # This is what actually decommutates the packet and updates the CVT
206
- TelemetryDecomTopic.write_packet(packet_or_subpacket, scope: @scope)
207
- end
208
-
209
- diff = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start # seconds as a float
210
- @metric.set(name: 'decom_duration_seconds', value: diff, type: 'gauge', unit: 'seconds')
192
+ },
193
+ )
211
194
  end
212
195
  end
213
196
 
214
- def handle_subpacket(packet, subpacket)
215
- # Subpacket received time always = packet.received_time
216
- # Use packet_time appropriately if another timestamp is needed
217
- subpacket.received_time = packet.received_time
218
- subpacket.stored = packet.stored
219
- subpacket.extra = packet.extra
220
-
221
- if subpacket.stored
222
- # Stored telemetry does not update the current value table
223
- identified_subpacket = System.telemetry.identify_and_define_packet(subpacket, @target_names, subpackets: true)
224
- else
225
- # Identify and update subpacket
226
- if subpacket.identified?
227
- begin
228
- # Preidentifed subpacket - place it into the current value table
229
- identified_subpacket = System.telemetry.update!(subpacket.target_name,
230
- subpacket.packet_name,
231
- subpacket.buffer)
232
- rescue RuntimeError
233
- # Subpacket identified but we don't know about it
234
- # Clear packet_name and target_name and try to identify
235
- @logger.warn "#{@name}: Received unknown identified subpacket: #{subpacket.target_name} #{subpacket.packet_name}"
236
- subpacket.target_name = nil
237
- subpacket.packet_name = nil
238
- identified_subpacket = System.telemetry.identify!(subpacket.buffer,
239
- @target_names, subpackets: true)
240
- end
241
- else
242
- # Packet needs to be identified
243
- identified_subpacket = System.telemetry.identify!(subpacket.buffer,
244
- @target_names, subpackets: true)
245
- end
246
- end
247
-
248
- if identified_subpacket
249
- identified_subpacket.received_time = subpacket.received_time
250
- identified_subpacket.stored = subpacket.stored
251
- identified_subpacket.extra = subpacket.extra
252
- subpacket = identified_subpacket
253
- else
254
- unknown_subpacket = System.telemetry.update!('UNKNOWN', 'UNKNOWN', subpacket.buffer)
255
- unknown_subpacket.received_time = subpacket.received_time
256
- unknown_subpacket.stored = subpacket.stored
257
- unknown_subpacket.extra = subpacket.extra
258
- subpacket = unknown_subpacket
259
- num_bytes_to_print = [InterfaceMicroservice::UNKNOWN_BYTES_TO_PRINT, subpacket.length].min
260
- data = subpacket.buffer(false)[0..(num_bytes_to_print - 1)]
261
- prefix = data.each_byte.map { | byte | sprintf("%02X", byte) }.join()
262
- @logger.warn "#{@name} #{subpacket.target_name} packet length: #{subpacket.length} starting with: #{prefix}"
263
- end
264
-
265
- TargetModel.sync_tlm_packet_counts(subpacket, @target_names, scope: @scope)
266
- return subpacket
267
- end
268
-
269
197
  # Called when an item in any packet changes limits states.
270
198
  #
271
199
  # @param packet [Packet] Packet which has had an item change limits state
@@ -285,7 +213,12 @@ module OpenC3
285
213
  if value
286
214
  message = "#{packet.target_name} #{packet.packet_name} #{item.name} = #{value} is #{item.limits.state}"
287
215
  if item.limits.values
288
- values = item.limits.values[System.limits_set]
216
+ selected_limits_set = if item.limits.values.has_key?(System.limits_set)
217
+ System.limits_set
218
+ else
219
+ :DEFAULT
220
+ end
221
+ values = item.limits.values[selected_limits_set]
289
222
  # Check if the state is RED_LOW, YELLOW_LOW, YELLOW_HIGH, RED_HIGH, GREEN_LOW, GREEN_HIGH
290
223
  if LIMITS_STATE_INDEX[item.limits.state]
291
224
  # Directly index into the values and return the value
@@ -335,4 +268,4 @@ if __FILE__ == $0
335
268
  OpenC3::DecomMicroservice.run
336
269
  OpenC3::ThreadManager.instance.shutdown
337
270
  OpenC3::ThreadManager.instance.join
338
- end
271
+ end
@@ -31,6 +31,8 @@ module OpenC3
31
31
  packet.write(name.to_s, value, type)
32
32
  end
33
33
  end
34
+ stored = inject_tlm_hash.fetch('stored', false)
35
+ packet.stored = stored.to_s.downcase == 'true'
34
36
  packet.received_time = Time.now.sys
35
37
  packet.received_count = TargetModel.increment_telemetry_count(packet.target_name, packet.packet_name, 1, scope: @scope)
36
38
  TelemetryTopic.write_packet(packet, scope: @scope)
@@ -63,6 +65,7 @@ module OpenC3
63
65
  cmd_params = build_cmd_hash['cmd_params']
64
66
  range_check = build_cmd_hash['range_check']
65
67
  raw = build_cmd_hash['raw']
68
+ db_shard = Store.db_shard_for_target(target_name, scope: @scope)
66
69
  ack_topic = "{#{@scope}__ACKCMD}TARGET__#{target_name}"
67
70
  begin
68
71
  command = System.commands.build_cmd(target_name, cmd_name, cmd_params, range_check, raw)
@@ -84,13 +87,14 @@ module OpenC3
84
87
  result: error.message
85
88
  }
86
89
  end
87
- Topic.write_topic(ack_topic, msg_hash)
90
+ Topic.write_topic(ack_topic, msg_hash, db_shard: db_shard)
88
91
  end
89
92
 
90
93
  def handle_get_tlm_buffer(get_tlm_buffer_json, msg_id)
91
94
  get_tlm_buffer_hash = JSON.parse(get_tlm_buffer_json, allow_nan: true, create_additions: true)
92
95
  target_name = get_tlm_buffer_hash['target_name']
93
96
  packet_name = get_tlm_buffer_hash['packet_name']
97
+ db_shard = Store.db_shard_for_target(target_name, scope: @scope)
94
98
  ack_topic = "{#{@scope}__ACKCMD}TARGET__#{target_name}"
95
99
  begin
96
100
  packet = System.telemetry.packet(target_name, packet_name)
@@ -115,7 +119,7 @@ module OpenC3
115
119
  result: error.message
116
120
  }
117
121
  end
118
- Topic.write_topic(ack_topic, msg_hash)
122
+ Topic.write_topic(ack_topic, msg_hash, db_shard: db_shard)
119
123
  end
120
124
  end
121
125
  end
@@ -42,10 +42,11 @@ module OpenC3
42
42
  class InterfaceCmdHandlerThread
43
43
  include InterfaceDecomCommon
44
44
 
45
- def initialize(interface, tlm, logger: nil, metric: nil, scope:)
45
+ def initialize(interface, tlm, logger: nil, metric: nil, db_shard: 0, scope:)
46
46
  @interface = interface
47
47
  @tlm = tlm
48
48
  @scope = scope
49
+ @db_shard = db_shard.to_i
49
50
  scope_model = ScopeModel.get_model(name: @scope)
50
51
  if scope_model
51
52
  @critical_commanding = scope_model.critical_commanding
@@ -81,7 +82,7 @@ module OpenC3
81
82
  end
82
83
 
83
84
  def run
84
- InterfaceTopic.receive_commands(@interface, scope: @scope) do |topic, msg_id, msg_hash, _redis|
85
+ InterfaceTopic.receive_commands(@interface, scope: @scope, db_shard: @db_shard) do |topic, msg_id, msg_hash, _redis|
85
86
  OpenC3.with_context(msg_hash) do
86
87
  release_critical = false
87
88
  critical_model = nil
@@ -105,7 +106,7 @@ module OpenC3
105
106
  @metric.set(name: 'interface_directive_total', value: @directive_count, type: 'counter') if @metric
106
107
  if msg_hash['shutdown']
107
108
  @logger.info "#{@interface.name}: Shutdown requested"
108
- InterfaceTopic.clear_topics(InterfaceTopic.topics(@interface, scope: @scope))
109
+ InterfaceTopic.clear_topics(InterfaceTopic.topics(@interface, scope: @scope), db_shard: @db_shard)
109
110
  return
110
111
  end
111
112
  if msg_hash['connect']
@@ -379,10 +380,11 @@ module OpenC3
379
380
  end
380
381
 
381
382
  class RouterTlmHandlerThread
382
- def initialize(router, tlm, logger: nil, metric: nil, scope:)
383
+ def initialize(router, tlm, logger: nil, metric: nil, db_shard: 0, scope:)
383
384
  @router = router
384
385
  @tlm = tlm
385
386
  @scope = scope
387
+ @db_shard = db_shard.to_i
386
388
  @logger = logger
387
389
  @logger = Logger unless @logger
388
390
  @metric = metric
@@ -412,7 +414,7 @@ module OpenC3
412
414
  end
413
415
 
414
416
  def run
415
- RouterTopic.receive_telemetry(@router, scope: @scope) do |topic, msg_id, msg_hash, _redis|
417
+ RouterTopic.receive_telemetry(@router, scope: @scope, db_shard: @db_shard) do |topic, msg_id, msg_hash, _redis|
416
418
  msgid_seconds_from_epoch = msg_id.split('-')[0].to_i / 1000.0
417
419
  delta = Time.now.to_f - msgid_seconds_from_epoch
418
420
  @metric.set(name: 'router_topic_delta_seconds', value: delta, type: 'gauge', unit: 'seconds', help: 'Delta time between data written to stream and router tlm start') if @metric
@@ -424,7 +426,7 @@ module OpenC3
424
426
 
425
427
  if msg_hash['shutdown']
426
428
  @logger.info "#{@router.name}: Shutdown requested"
427
- RouterTopic.clear_topics(RouterTopic.topics(@router, scope: @scope))
429
+ RouterTopic.clear_topics(RouterTopic.topics(@router, scope: @scope), db_shard: @db_shard)
428
430
  return
429
431
  end
430
432
  if msg_hash['connect']
@@ -590,9 +592,9 @@ module OpenC3
590
592
  @connection_failed_messages = []
591
593
  @connection_lost_messages = []
592
594
  if @interface_or_router == 'INTERFACE'
593
- @handler_thread = InterfaceCmdHandlerThread.new(@interface, self, logger: @logger, metric: @metric, scope: @scope)
595
+ @handler_thread = InterfaceCmdHandlerThread.new(@interface, self, logger: @logger, metric: @metric, db_shard: @db_shard, scope: @scope)
594
596
  else
595
- @handler_thread = RouterTlmHandlerThread.new(@interface, self, logger: @logger, metric: @metric, scope: @scope)
597
+ @handler_thread = RouterTlmHandlerThread.new(@interface, self, logger: @logger, metric: @metric, db_shard: @db_shard, scope: @scope)
596
598
  end
597
599
  @handler_thread.start
598
600
  end
@@ -63,7 +63,7 @@ module OpenC3
63
63
  while true
64
64
  break if @cancel_thread
65
65
 
66
- Topic.read_topics(@topics) do |topic, msg_id, msg_hash, redis|
66
+ Topic.read_topics(@topics, db_shard: @db_shard) do |topic, msg_id, msg_hash, redis|
67
67
  break if @cancel_thread
68
68
  if topic == @microservice_topic
69
69
  microservice_cmd(topic, msg_id, msg_hash, redis)
@@ -127,6 +127,7 @@ module OpenC3
127
127
  @logger.info("Microservice initialized with config:\n#{@config}")
128
128
  @topics ||= []
129
129
  @microservice_topic = "MICROSERVICE__#{@name}"
130
+ @db_shard = (@config['db_shard'] || 0).to_i
130
131
 
131
132
  # Get configuration for any targets
132
133
  @target_names = @config["target_names"]
@@ -259,10 +260,10 @@ module OpenC3
259
260
  else
260
261
  raise "Invalid topics given to microservice_cmd: #{topics}"
261
262
  end
262
- Topic.trim_topic(topic, msg_id)
263
+ Topic.trim_topic(topic, msg_id, db_shard: @db_shard)
263
264
  return true
264
265
  end
265
- Topic.trim_topic(topic, msg_id)
266
+ Topic.trim_topic(topic, msg_id, db_shard: @db_shard)
266
267
  return false
267
268
  end
268
269
  end
@@ -19,7 +19,7 @@ require 'openc3/api/api'
19
19
 
20
20
  module OpenC3
21
21
  saved_verbose = $VERBOSE
22
- $VERBOSE = false
22
+ $VERBOSE = nil
23
23
  module Script
24
24
  private
25
25
  # Override the prompt_for_hazardous method to always return true since there is no user to prompt
@@ -12,6 +12,7 @@
12
12
  # if purchased from OpenC3, Inc.
13
13
 
14
14
  require 'openc3/models/scope_model'
15
+ require 'openc3/models/target_model'
15
16
  require 'openc3/microservices/cleanup_microservice'
16
17
 
17
18
  module OpenC3
@@ -62,63 +63,76 @@ ORDER BY
62
63
  super(areas, bucket)
63
64
  end
64
65
 
65
- # Always check TSDB health
66
+ # Always check TSDB health across all db_shards
66
67
  if @scope == 'DEFAULT'
68
+ # Collect all unique db_shard numbers from targets
69
+ db_shards = Set.new([0]) # Always check db_shard 0
67
70
  begin
68
- conn = OpenC3::QuestDBClient.connection
69
- result = conn.exec(TSDB_HEALTH_QUERY)
70
- columns = result.fields
71
- rows = result.values
72
-
73
- table_name_column = columns.index("table_name")
74
- wal_pending_row_count_column = columns.index("wal_pending_row_count")
75
- status_column = columns.index("status")
76
- lag_txns_column = columns.index("lag_txns")
77
-
78
- rows.each do |values|
79
- table_name = values[table_name_column]
80
- wal_pending_row_count = values[wal_pending_row_count_column].to_i
81
- status = values[status_column]
82
- lag_txns = values[lag_txns_column].to_i
83
-
84
- if status != 'OK'
85
- @logger.error("QuestDB: #{table_name} in bad state: #{status}")
86
-
87
- if status == 'SUSPENDED'
88
- # Try to automatically unsuspend
89
- @logger.info("QuestDB: Attempting to unsuspend: #{table_name}")
90
- conn.exec("ALTER TABLE #{table_name} RESUME WAL;")
71
+ targets = OpenC3::TargetModel.all(scope: @scope)
72
+ targets.each_value { |target| db_shards << target['db_shard'].to_i if target['db_shard'] }
73
+ rescue => e
74
+ @logger.error("QuestDB: Error getting target db_shards: #{e.formatted}")
75
+ end
76
+
77
+ db_shards.each do |db_shard|
78
+ begin
79
+ conn = OpenC3::QuestDBClient.connection(db_shard: db_shard)
80
+ result = conn.exec(TSDB_HEALTH_QUERY)
81
+ columns = result.fields
82
+ rows = result.values
83
+
84
+ table_name_column = columns.index("table_name")
85
+ wal_pending_row_count_column = columns.index("wal_pending_row_count")
86
+ status_column = columns.index("status")
87
+ lag_txns_column = columns.index("lag_txns")
88
+
89
+ rows.each do |values|
90
+ table_name = values[table_name_column]
91
+ # Prefix with db_shard to avoid key collisions across db_shards
92
+ tracking_key = "s#{db_shard}__#{table_name}"
93
+ wal_pending_row_count = values[wal_pending_row_count_column].to_i
94
+ status = values[status_column]
95
+ lag_txns = values[lag_txns_column].to_i
96
+
97
+ if status != 'OK'
98
+ @logger.error("QuestDB db_shard #{db_shard}: #{table_name} in bad state: #{status}")
99
+
100
+ if status == 'SUSPENDED'
101
+ # Try to automatically unsuspend
102
+ @logger.info("QuestDB db_shard #{db_shard}: Attempting to unsuspend: #{table_name}")
103
+ conn.exec("ALTER TABLE \"#{table_name}\" RESUME WAL;")
104
+ end
91
105
  end
92
- end
93
106
 
94
- @wal_pending_row_count[table_name] ||= []
95
- @wal_pending_row_count[table_name] << wal_pending_row_count
96
- @lag_txns[table_name] ||= []
97
- @lag_txns[table_name] << lag_txns
107
+ @wal_pending_row_count[tracking_key] ||= []
108
+ @wal_pending_row_count[tracking_key] << wal_pending_row_count
109
+ @lag_txns[tracking_key] ||= []
110
+ @lag_txns[tracking_key] << lag_txns
98
111
 
99
- if @wal_pending_row_count[table_name].length > GROWTH_NUM_SAMPLE_PERIODS
100
- if detect_growth(@wal_pending_row_count[table_name], GROWTH_NUM_SAMPLE_PERIODS)
101
- # Crossed threshold of sample periods of growth
102
- @logger.error("QuestDB: #{table_name} has growing wal_pending_row_count: #{wal_pending_row_count}")
112
+ if @wal_pending_row_count[tracking_key].length > GROWTH_NUM_SAMPLE_PERIODS
113
+ if detect_growth(@wal_pending_row_count[tracking_key], GROWTH_NUM_SAMPLE_PERIODS)
114
+ # Crossed threshold of sample periods of growth
115
+ @logger.error("QuestDB db_shard #{db_shard}: #{table_name} has growing wal_pending_row_count: #{wal_pending_row_count}")
116
+ end
117
+
118
+ # Leave the last GROWTH_NUM_SAMPLE_PERIODS samples
119
+ @wal_pending_row_count[tracking_key] = @wal_pending_row_count[tracking_key][-GROWTH_NUM_SAMPLE_PERIODS..-1]
103
120
  end
104
121
 
105
- # Leave the last GROWTH_NUM_SAMPLE_PERIODS samples
106
- @wal_pending_row_count[table_name] = @wal_pending_row_count[table_name][-GROWTH_NUM_SAMPLE_PERIODS..-1]
107
- end
122
+ if @lag_txns[tracking_key].length > GROWTH_NUM_SAMPLE_PERIODS
123
+ if detect_growth(@lag_txns[tracking_key], GROWTH_NUM_SAMPLE_PERIODS)
124
+ # Crossed threshold of sample periods of growth
125
+ @logger.error("QuestDB db_shard #{db_shard}: #{table_name} has growing lag_txns: #{lag_txns}")
126
+ end
108
127
 
109
- if @lag_txns[table_name].length > GROWTH_NUM_SAMPLE_PERIODS
110
- if detect_growth(@lag_txns[table_name], GROWTH_NUM_SAMPLE_PERIODS)
111
- # Crossed threshold of sample periods of growth
112
- @logger.error("QuestDB: #{table_name} has growing lag_txns: #{lag_txns}")
128
+ # Leave the last GROWTH_NUM_SAMPLE_PERIODS samples
129
+ @lag_txns[tracking_key] = @lag_txns[tracking_key][-GROWTH_NUM_SAMPLE_PERIODS..-1]
113
130
  end
114
-
115
- # Leave the last GROWTH_NUM_SAMPLE_PERIODS samples
116
- @lag_txns[table_name] = @lag_txns[table_name][-GROWTH_NUM_SAMPLE_PERIODS..-1]
117
131
  end
132
+ rescue => e
133
+ OpenC3::QuestDBClient.disconnect(db_shard: db_shard)
134
+ @logger.error("QuestDB db_shard #{db_shard} Error: #{e.formatted}")
118
135
  end
119
- rescue => e
120
- OpenC3::QuestDBClient.disconnect
121
- @logger.error("QuestDB Error: #{e.formatted}")
122
136
  end
123
137
  end
124
138
  end
@@ -55,8 +55,7 @@ module OpenC3
55
55
  while true
56
56
  break if @cancel_thread
57
57
 
58
- # Read each topic separately to support multiple redis shards
59
- # This is needed with Redis cluster because the topics will likely be on different shards and we don't use {} in the topic names
58
+ # Read each topic separately to support multiple redis db_shards
60
59
  individual_topics.each do |individual_topic|
61
60
  break if @cancel_thread
62
61
  # 500ms timeout - To support completing within 1 second with two topics (DEFAULT and NOSCOPE)