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.
- checksums.yaml +4 -4
- data/bin/openc3cli +105 -13
- data/bin/pipinstall +38 -6
- data/data/config/command_modifiers.yaml +1 -0
- data/data/config/item_modifiers.yaml +2 -1
- data/data/config/microservice.yaml +12 -1
- data/data/config/parameter_modifiers.yaml +49 -7
- data/data/config/table_parameter_modifiers.yaml +3 -1
- data/data/config/target.yaml +11 -0
- data/data/config/target_config.yaml +6 -2
- data/lib/openc3/accessors/template_accessor.rb +9 -0
- data/lib/openc3/api/cmd_api.rb +2 -1
- data/lib/openc3/api/metrics_api.rb +11 -1
- data/lib/openc3/api/tlm_api.rb +21 -6
- data/lib/openc3/core_ext/faraday.rb +1 -1
- data/lib/openc3/interfaces/interface.rb +1 -6
- data/lib/openc3/io/json_api.rb +1 -1
- data/lib/openc3/logs/log_writer.rb +3 -1
- data/lib/openc3/microservices/decom_common.rb +128 -0
- data/lib/openc3/microservices/decom_microservice.rb +27 -96
- data/lib/openc3/microservices/interface_decom_common.rb +28 -10
- data/lib/openc3/microservices/interface_microservice.rb +16 -9
- data/lib/openc3/microservices/log_microservice.rb +1 -1
- data/lib/openc3/microservices/microservice.rb +3 -2
- data/lib/openc3/microservices/queue_microservice.rb +1 -1
- data/lib/openc3/microservices/scope_cleanup_microservice.rb +60 -46
- data/lib/openc3/microservices/text_log_microservice.rb +1 -2
- data/lib/openc3/models/cvt_model.rb +24 -13
- data/lib/openc3/models/db_sharded_model.rb +110 -0
- data/lib/openc3/models/interface_model.rb +9 -0
- data/lib/openc3/models/interface_status_model.rb +33 -3
- data/lib/openc3/models/metric_model.rb +96 -37
- data/lib/openc3/models/microservice_model.rb +7 -0
- data/lib/openc3/models/microservice_status_model.rb +30 -3
- data/lib/openc3/models/plugin_model.rb +9 -1
- data/lib/openc3/models/python_package_model.rb +1 -1
- data/lib/openc3/models/reaction_model.rb +27 -9
- data/lib/openc3/models/reingest_job_model.rb +153 -0
- data/lib/openc3/models/scope_model.rb +3 -2
- data/lib/openc3/models/script_status_model.rb +4 -20
- data/lib/openc3/models/target_model.rb +113 -100
- data/lib/openc3/models/trigger_model.rb +24 -7
- data/lib/openc3/packets/packet_config.rb +4 -1
- data/lib/openc3/script/api_shared.rb +39 -2
- data/lib/openc3/script/calendar.rb +32 -10
- data/lib/openc3/script/extract.rb +46 -13
- data/lib/openc3/script/script.rb +2 -2
- data/lib/openc3/script/script_runner.rb +4 -4
- data/lib/openc3/script/telemetry.rb +3 -3
- data/lib/openc3/script/web_socket_api.rb +29 -22
- data/lib/openc3/system/system.rb +20 -3
- data/lib/openc3/topics/command_decom_topic.rb +4 -2
- data/lib/openc3/topics/command_topic.rb +8 -5
- data/lib/openc3/topics/decom_interface_topic.rb +31 -11
- data/lib/openc3/topics/interface_topic.rb +88 -27
- data/lib/openc3/topics/limits_event_topic.rb +62 -41
- data/lib/openc3/topics/router_topic.rb +61 -21
- data/lib/openc3/topics/system_events_topic.rb +18 -1
- data/lib/openc3/topics/telemetry_decom_topic.rb +2 -1
- data/lib/openc3/topics/telemetry_topic.rb +4 -2
- data/lib/openc3/topics/topic.rb +77 -5
- data/lib/openc3/utilities/aws_bucket.rb +2 -0
- data/lib/openc3/utilities/cli_generator.rb +3 -2
- data/lib/openc3/utilities/ctrf.rb +231 -0
- data/lib/openc3/utilities/metric.rb +15 -1
- data/lib/openc3/utilities/questdb_client.rb +177 -40
- data/lib/openc3/utilities/reingest_job.rb +377 -0
- data/lib/openc3/utilities/ruby_lex_utils.rb +2 -0
- data/lib/openc3/utilities/store_autoload.rb +78 -52
- data/lib/openc3/utilities/store_queued.rb +20 -12
- data/lib/openc3/version.rb +5 -5
- data/templates/plugin/plugin.gemspec +13 -1
- data/templates/tool_angular/package.json +2 -2
- data/templates/tool_react/package.json +1 -1
- data/templates/tool_svelte/package.json +1 -1
- data/templates/tool_vue/package.json +3 -4
- data/templates/tool_vue/src/router.js +2 -2
- data/templates/widget/package.json +2 -2
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
@@ -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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
|
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)
|