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.
- checksums.yaml +4 -4
- data/bin/openc3cli +50 -3
- data/data/config/interface_modifiers.yaml +3 -1
- data/data/config/item_modifiers.yaml +1 -1
- data/data/config/microservice.yaml +15 -2
- data/data/config/parameter_modifiers.yaml +49 -7
- data/data/config/plugins.yaml +1 -0
- data/data/config/target.yaml +11 -0
- data/data/config/target_config.yaml +6 -2
- data/lib/openc3/api/api.rb +1 -0
- data/lib/openc3/api/calendar_api.rb +183 -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/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 +30 -97
- data/lib/openc3/microservices/interface_decom_common.rb +6 -2
- data/lib/openc3/microservices/interface_microservice.rb +10 -8
- 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 +20 -8
- data/lib/openc3/models/queue_model.rb +36 -46
- 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 +1 -1
- data/lib/openc3/packets/packet_config.rb +4 -1
- data/lib/openc3/packets/parsers/xtce_parser.rb +23 -1
- data/lib/openc3/script/script.rb +6 -4
- 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 +9 -5
- data/lib/openc3/topics/decom_interface_topic.rb +15 -10
- data/lib/openc3/topics/interface_topic.rb +71 -29
- 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 +3 -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 +10 -2
- data/lib/openc3/utilities/metric.rb +15 -1
- data/lib/openc3/utilities/questdb_client.rb +173 -37
- data/lib/openc3/utilities/reingest_job.rb +377 -0
- data/lib/openc3/utilities/ruby_lex_utils.rb +2 -0
- data/lib/openc3/utilities/running_script.rb +8 -10
- 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/microservice/microservices/TEMPLATE/microservice.py +9 -0
- 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 -3
- 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,14 +91,18 @@ 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)
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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')
|
|
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
|
-
|
|
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
|
|
@@ -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)
|