openc3 7.0.0 → 7.1.0

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +105 -13
  3. data/bin/pipinstall +38 -6
  4. data/data/config/command_modifiers.yaml +1 -0
  5. data/data/config/item_modifiers.yaml +2 -1
  6. data/data/config/microservice.yaml +12 -1
  7. data/data/config/parameter_modifiers.yaml +49 -7
  8. data/data/config/table_parameter_modifiers.yaml +3 -1
  9. data/data/config/target.yaml +11 -0
  10. data/data/config/target_config.yaml +6 -2
  11. data/lib/openc3/accessors/template_accessor.rb +9 -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/interfaces/interface.rb +1 -6
  17. data/lib/openc3/io/json_api.rb +1 -1
  18. data/lib/openc3/logs/log_writer.rb +3 -1
  19. data/lib/openc3/microservices/decom_common.rb +128 -0
  20. data/lib/openc3/microservices/decom_microservice.rb +27 -96
  21. data/lib/openc3/microservices/interface_decom_common.rb +28 -10
  22. data/lib/openc3/microservices/interface_microservice.rb +16 -9
  23. data/lib/openc3/microservices/log_microservice.rb +1 -1
  24. data/lib/openc3/microservices/microservice.rb +3 -2
  25. data/lib/openc3/microservices/queue_microservice.rb +1 -1
  26. data/lib/openc3/microservices/scope_cleanup_microservice.rb +60 -46
  27. data/lib/openc3/microservices/text_log_microservice.rb +1 -2
  28. data/lib/openc3/models/cvt_model.rb +24 -13
  29. data/lib/openc3/models/db_sharded_model.rb +110 -0
  30. data/lib/openc3/models/interface_model.rb +9 -0
  31. data/lib/openc3/models/interface_status_model.rb +33 -3
  32. data/lib/openc3/models/metric_model.rb +96 -37
  33. data/lib/openc3/models/microservice_model.rb +7 -0
  34. data/lib/openc3/models/microservice_status_model.rb +30 -3
  35. data/lib/openc3/models/plugin_model.rb +9 -1
  36. data/lib/openc3/models/python_package_model.rb +1 -1
  37. data/lib/openc3/models/reaction_model.rb +27 -9
  38. data/lib/openc3/models/reingest_job_model.rb +153 -0
  39. data/lib/openc3/models/scope_model.rb +3 -2
  40. data/lib/openc3/models/script_status_model.rb +4 -20
  41. data/lib/openc3/models/target_model.rb +113 -100
  42. data/lib/openc3/models/trigger_model.rb +24 -7
  43. data/lib/openc3/packets/packet_config.rb +4 -1
  44. data/lib/openc3/script/api_shared.rb +39 -2
  45. data/lib/openc3/script/calendar.rb +32 -10
  46. data/lib/openc3/script/extract.rb +46 -13
  47. data/lib/openc3/script/script.rb +2 -2
  48. data/lib/openc3/script/script_runner.rb +4 -4
  49. data/lib/openc3/script/telemetry.rb +3 -3
  50. data/lib/openc3/script/web_socket_api.rb +29 -22
  51. data/lib/openc3/system/system.rb +20 -3
  52. data/lib/openc3/topics/command_decom_topic.rb +4 -2
  53. data/lib/openc3/topics/command_topic.rb +8 -5
  54. data/lib/openc3/topics/decom_interface_topic.rb +31 -11
  55. data/lib/openc3/topics/interface_topic.rb +88 -27
  56. data/lib/openc3/topics/limits_event_topic.rb +62 -41
  57. data/lib/openc3/topics/router_topic.rb +61 -21
  58. data/lib/openc3/topics/system_events_topic.rb +18 -1
  59. data/lib/openc3/topics/telemetry_decom_topic.rb +2 -1
  60. data/lib/openc3/topics/telemetry_topic.rb +4 -2
  61. data/lib/openc3/topics/topic.rb +77 -5
  62. data/lib/openc3/utilities/aws_bucket.rb +2 -0
  63. data/lib/openc3/utilities/cli_generator.rb +3 -2
  64. data/lib/openc3/utilities/ctrf.rb +231 -0
  65. data/lib/openc3/utilities/metric.rb +15 -1
  66. data/lib/openc3/utilities/questdb_client.rb +177 -40
  67. data/lib/openc3/utilities/reingest_job.rb +377 -0
  68. data/lib/openc3/utilities/ruby_lex_utils.rb +2 -0
  69. data/lib/openc3/utilities/store_autoload.rb +78 -52
  70. data/lib/openc3/utilities/store_queued.rb +20 -12
  71. data/lib/openc3/version.rb +5 -5
  72. data/templates/plugin/plugin.gemspec +13 -1
  73. data/templates/tool_angular/package.json +2 -2
  74. data/templates/tool_react/package.json +1 -1
  75. data/templates/tool_svelte/package.json +1 -1
  76. data/templates/tool_vue/package.json +3 -4
  77. data/templates/tool_vue/src/router.js +2 -2
  78. data/templates/widget/package.json +2 -2
  79. 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,7 +91,9 @@ 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)
94
97
  System.telemetry.limits_change_callback = method(:limits_change_callback)
95
98
  LimitsEventTopic.sync_system(scope: @scope)
96
99
  @error_count = 0
@@ -110,13 +113,16 @@ module OpenC3
110
113
 
111
114
  begin
112
115
  OpenC3.in_span("read_topics") do
113
- Topic.read_topics(@topics) do |topic, msg_id, msg_hash, redis|
116
+ Topic.read_topics(@topics, db_shard: @db_shard) do |topic, msg_id, msg_hash, redis|
114
117
  break if @cancel_thread
115
118
  if topic == @microservice_topic
116
119
  microservice_cmd(topic, msg_id, msg_hash, redis)
120
+ elsif topic == @limits_event_topic
121
+ event = JSON.parse(msg_hash['event'], allow_nan: true, create_additions: true)
122
+ LimitsEventTopic.process_event(event)
117
123
  elsif topic =~ /__DECOMINTERFACE/
118
124
  if msg_hash.key?('inject_tlm')
119
- handle_inject_tlm(msg_hash['inject_tlm'])
125
+ handle_inject_tlm_with_ack(msg_hash['inject_tlm'], msg_id)
120
126
  next
121
127
  end
122
128
  if msg_hash.key?('build_cmd')
@@ -134,7 +140,6 @@ module OpenC3
134
140
  @count += 1
135
141
  end
136
142
  end
137
- LimitsEventTopic.sync_system_thread_body(scope: @scope)
138
143
  rescue => e
139
144
  @error_count += 1
140
145
  @metric.set(name: 'decom_error_total', value: @error_count, type: 'counter')
@@ -153,8 +158,6 @@ module OpenC3
153
158
  delta = Time.now.to_f - msgid_seconds_from_epoch
154
159
  @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
160
 
156
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
157
-
158
161
  #######################################
159
162
  # Build packet object from topic data
160
163
  #######################################
@@ -172,98 +175,21 @@ module OpenC3
172
175
  end
173
176
  packet.buffer = msg_hash["buffer"]
174
177
 
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
178
+ DecomCommon.decom_and_publish(
179
+ packet,
180
+ scope: @scope,
181
+ target_names: @target_names,
182
+ logger: @logger,
183
+ name: @name,
184
+ check_limits: true,
185
+ metric: @metric,
186
+ error_callback: ->(e) {
193
187
  @error_count += 1
194
188
  @metric.set(name: 'decom_error_total', value: @error_count, type: 'counter')
195
189
  @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')
211
- end
212
- end
213
-
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
190
+ },
191
+ )
246
192
  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
193
  end
268
194
 
269
195
  # Called when an item in any packet changes limits states.
@@ -285,7 +211,12 @@ module OpenC3
285
211
  if value
286
212
  message = "#{packet.target_name} #{packet.packet_name} #{item.name} = #{value} is #{item.limits.state}"
287
213
  if item.limits.values
288
- values = item.limits.values[System.limits_set]
214
+ selected_limits_set = if item.limits.values.has_key?(System.limits_set)
215
+ System.limits_set
216
+ else
217
+ :DEFAULT
218
+ end
219
+ values = item.limits.values[selected_limits_set]
289
220
  # Check if the state is RED_LOW, YELLOW_LOW, YELLOW_HIGH, RED_HIGH, GREEN_LOW, GREEN_HIGH
290
221
  if LIMITS_STATE_INDEX[item.limits.state]
291
222
  # Directly index into the values and return the value
@@ -335,4 +266,4 @@ if __FILE__ == $0
335
266
  OpenC3::DecomMicroservice.run
336
267
  OpenC3::ThreadManager.instance.shutdown
337
268
  OpenC3::ThreadManager.instance.join
338
- end
269
+ end
@@ -31,13 +31,31 @@ 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)
37
- # If the inject_tlm parameters are bad we rescue so
38
- # interface_microservice and decom_microservice can continue
39
- rescue => e
40
- @logger.error "inject_tlm error due to #{e.message}"
39
+ end
40
+
41
+ def handle_inject_tlm_with_ack(inject_tlm_json, msg_id)
42
+ inject_tlm_hash = JSON.parse(inject_tlm_json, allow_nan: true, create_additions: true)
43
+ target_name = inject_tlm_hash['target_name']
44
+ ack_topic = "{#{@scope}__ACKCMD}TARGET__#{target_name}"
45
+ begin
46
+ handle_inject_tlm(inject_tlm_json)
47
+ msg_hash = {
48
+ id: msg_id,
49
+ result: 'SUCCESS'
50
+ }
51
+ rescue => error
52
+ @logger.error "inject_tlm error due to #{error.message}"
53
+ msg_hash = {
54
+ id: msg_id,
55
+ result: error.message
56
+ }
57
+ end
58
+ Topic.write_topic(ack_topic, msg_hash)
41
59
  end
42
60
 
43
61
  def handle_build_cmd(build_cmd_json, msg_id)
@@ -47,6 +65,7 @@ module OpenC3
47
65
  cmd_params = build_cmd_hash['cmd_params']
48
66
  range_check = build_cmd_hash['range_check']
49
67
  raw = build_cmd_hash['raw']
68
+ db_shard = Store.db_shard_for_target(target_name, scope: @scope)
50
69
  ack_topic = "{#{@scope}__ACKCMD}TARGET__#{target_name}"
51
70
  begin
52
71
  command = System.commands.build_cmd(target_name, cmd_name, cmd_params, range_check, raw)
@@ -65,17 +84,17 @@ module OpenC3
65
84
  rescue => error
66
85
  msg_hash = {
67
86
  id: msg_id,
68
- result: 'ERROR',
69
- message: error.message
87
+ result: error.message
70
88
  }
71
89
  end
72
- Topic.write_topic(ack_topic, msg_hash)
90
+ Topic.write_topic(ack_topic, msg_hash, db_shard: db_shard)
73
91
  end
74
92
 
75
93
  def handle_get_tlm_buffer(get_tlm_buffer_json, msg_id)
76
94
  get_tlm_buffer_hash = JSON.parse(get_tlm_buffer_json, allow_nan: true, create_additions: true)
77
95
  target_name = get_tlm_buffer_hash['target_name']
78
96
  packet_name = get_tlm_buffer_hash['packet_name']
97
+ db_shard = Store.db_shard_for_target(target_name, scope: @scope)
79
98
  ack_topic = "{#{@scope}__ACKCMD}TARGET__#{target_name}"
80
99
  begin
81
100
  packet = System.telemetry.packet(target_name, packet_name)
@@ -97,11 +116,10 @@ module OpenC3
97
116
  rescue => error
98
117
  msg_hash = {
99
118
  id: msg_id,
100
- result: 'ERROR',
101
- message: error.message
119
+ result: error.message
102
120
  }
103
121
  end
104
- Topic.write_topic(ack_topic, msg_hash)
122
+ Topic.write_topic(ack_topic, msg_hash, db_shard: db_shard)
105
123
  end
106
124
  end
107
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']
@@ -179,7 +180,12 @@ module OpenC3
179
180
  next 'SUCCESS'
180
181
  end
181
182
  if msg_hash.key?('inject_tlm')
182
- handle_inject_tlm(msg_hash['inject_tlm'])
183
+ begin
184
+ handle_inject_tlm(msg_hash['inject_tlm'])
185
+ rescue => e
186
+ @logger.error "#{@interface.name}: inject_tlm: #{e.formatted}"
187
+ next e.message
188
+ end
183
189
  next 'SUCCESS'
184
190
  end
185
191
  if msg_hash.key?('release_critical')
@@ -374,10 +380,11 @@ module OpenC3
374
380
  end
375
381
 
376
382
  class RouterTlmHandlerThread
377
- def initialize(router, tlm, logger: nil, metric: nil, scope:)
383
+ def initialize(router, tlm, logger: nil, metric: nil, db_shard: 0, scope:)
378
384
  @router = router
379
385
  @tlm = tlm
380
386
  @scope = scope
387
+ @db_shard = db_shard.to_i
381
388
  @logger = logger
382
389
  @logger = Logger unless @logger
383
390
  @metric = metric
@@ -407,7 +414,7 @@ module OpenC3
407
414
  end
408
415
 
409
416
  def run
410
- 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|
411
418
  msgid_seconds_from_epoch = msg_id.split('-')[0].to_i / 1000.0
412
419
  delta = Time.now.to_f - msgid_seconds_from_epoch
413
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
@@ -419,7 +426,7 @@ module OpenC3
419
426
 
420
427
  if msg_hash['shutdown']
421
428
  @logger.info "#{@router.name}: Shutdown requested"
422
- RouterTopic.clear_topics(RouterTopic.topics(@router, scope: @scope))
429
+ RouterTopic.clear_topics(RouterTopic.topics(@router, scope: @scope), db_shard: @db_shard)
423
430
  return
424
431
  end
425
432
  if msg_hash['connect']
@@ -585,9 +592,9 @@ module OpenC3
585
592
  @connection_failed_messages = []
586
593
  @connection_lost_messages = []
587
594
  if @interface_or_router == 'INTERFACE'
588
- @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)
589
596
  else
590
- @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)
591
598
  end
592
599
  @handler_thread.start
593
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)