openc3 7.4.1 → 7.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/pipinstall +18 -5
- data/bin/pipuninstall +1 -1
- data/bin/uvinstall +36 -8
- data/data/config/default_plugins.yaml +43 -0
- data/data/config/interface_modifiers.yaml +22 -7
- data/data/config/item_modifiers.yaml +14 -4
- data/data/config/plugins.yaml +1 -1
- data/data/config/widgets.yaml +1 -1
- data/lib/openc3/api/cmd_api.rb +10 -13
- data/lib/openc3/api/interface_api.rb +1 -2
- data/lib/openc3/api/limits_api.rb +149 -160
- data/lib/openc3/api/metrics_api.rb +26 -1
- data/lib/openc3/api/router_api.rb +1 -2
- data/lib/openc3/api/tlm_api.rb +225 -71
- data/lib/openc3/config/config_parser.rb +3 -1
- data/lib/openc3/core_ext/tempfile.rb +2 -0
- data/lib/openc3/interfaces/file_interface.rb +6 -1
- data/lib/openc3/interfaces/http_client_interface.rb +14 -6
- data/lib/openc3/interfaces/interface.rb +27 -3
- data/lib/openc3/interfaces/read_queue_interface.rb +331 -0
- data/lib/openc3/interfaces/stream_interface.rb +22 -3
- data/lib/openc3/interfaces/tcpip_server_interface.rb +13 -0
- data/lib/openc3/interfaces/udp_interface.rb +16 -2
- data/lib/openc3/interfaces.rb +1 -0
- data/lib/openc3/logs/log_writer.rb +14 -4
- data/lib/openc3/microservices/decom_microservice.rb +30 -5
- data/lib/openc3/microservices/interface_decom_common.rb +3 -1
- data/lib/openc3/microservices/interface_microservice.rb +146 -50
- data/lib/openc3/microservices/log_microservice.rb +1 -3
- data/lib/openc3/microservices/microservice.rb +25 -1
- data/lib/openc3/microservices/plugin_microservice.rb +32 -0
- data/lib/openc3/microservices/text_log_microservice.rb +1 -3
- data/lib/openc3/models/cvt_model.rb +7 -2
- data/lib/openc3/models/interface_model.rb +1 -1
- data/lib/openc3/models/interface_status_model.rb +6 -0
- data/lib/openc3/models/metric_model.rb +52 -4
- data/lib/openc3/models/microservice_model.rb +7 -3
- data/lib/openc3/models/plugin_model.rb +12 -12
- data/lib/openc3/models/python_package_model.rb +3 -13
- data/lib/openc3/models/target_model.rb +92 -15
- data/lib/openc3/operators/microservice_operator.rb +5 -0
- data/lib/openc3/operators/operator.rb +69 -0
- data/lib/openc3/packets/packet.rb +10 -4
- data/lib/openc3/packets/packet_config.rb +12 -1
- data/lib/openc3/packets/packet_item.rb +40 -22
- data/lib/openc3/packets/parsers/state_parser.rb +1 -1
- data/lib/openc3/packets/structure_item.rb +3 -2
- data/lib/openc3/script/api_shared.rb +62 -86
- data/lib/openc3/script/extract.rb +263 -20
- data/lib/openc3/script/plugins.rb +4 -11
- data/lib/openc3/script/script.rb +1 -1
- data/lib/openc3/script/storage.rb +81 -10
- data/lib/openc3/system/system.rb +9 -6
- data/lib/openc3/topics/command_decom_topic.rb +1 -1
- data/lib/openc3/topics/command_topic.rb +2 -1
- data/lib/openc3/topics/limits_event_topic.rb +90 -3
- data/lib/openc3/topics/router_topic.rb +7 -2
- data/lib/openc3/topics/system_events_topic.rb +9 -6
- data/lib/openc3/topics/telemetry_decom_topic.rb +1 -1
- data/lib/openc3/topics/telemetry_topic.rb +3 -2
- data/lib/openc3/topics/topic.rb +162 -3
- data/lib/openc3/utilities/bucket.rb +5 -0
- data/lib/openc3/utilities/cli_generator.rb +67 -15
- data/lib/openc3/utilities/config_overlay.rb +35 -17
- data/lib/openc3/utilities/default_plugins.rb +33 -0
- data/lib/openc3/utilities/local_mode.rb +82 -46
- data/lib/openc3/utilities/metric.rb +16 -0
- data/lib/openc3/utilities/process_stats.rb +160 -0
- data/lib/openc3/utilities/pypi_url.rb +65 -0
- data/lib/openc3/utilities/questdb_client.rb +140 -32
- data/lib/openc3/utilities/running_script.rb +22 -18
- data/lib/openc3/utilities/secrets.rb +5 -0
- data/lib/openc3/utilities/store_autoload.rb +23 -4
- data/lib/openc3/utilities/store_queued.rb +37 -9
- data/lib/openc3/utilities/target_file.rb +57 -12
- data/lib/openc3/utilities/topic_lag_monitor.rb +193 -0
- data/lib/openc3/version.rb +5 -5
- data/templates/conversion/conversion.py +6 -2
- data/templates/conversion/conversion.rb +5 -0
- data/templates/plugin/.github/workflows/playwright.yml +62 -0
- data/templates/plugin/.github/workflows/release.yml +45 -0
- data/templates/plugin/.github/workflows/unit-tests.yml +35 -0
- data/templates/plugin/.gitignore +6 -0
- data/templates/plugin/README.md +114 -2
- data/templates/plugin/lib/PACKAGE/__init__.py +4 -0
- data/templates/plugin/plugin.gemspec +10 -1
- data/templates/plugin/pyproject.toml +119 -0
- data/templates/tool_angular/package.json +2 -2
- data/templates/tool_angular/pnpm-workspace.yaml +16 -0
- data/templates/tool_react/package.json +1 -1
- data/templates/tool_react/pnpm-workspace.yaml +16 -0
- data/templates/tool_svelte/package.json +2 -1
- data/templates/tool_svelte/pnpm-workspace.yaml +20 -0
- data/templates/tool_vue/package.json +3 -3
- data/templates/tool_vue/pnpm-workspace.yaml +16 -0
- data/templates/widget/package.json +2 -2
- data/templates/widget/pnpm-workspace.yaml +16 -0
- metadata +16 -1
|
@@ -0,0 +1,331 @@
|
|
|
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 'timeout' # For Timeout::Error
|
|
15
|
+
require 'openc3/interfaces/interface'
|
|
16
|
+
require 'openc3/core_ext/exception'
|
|
17
|
+
require 'openc3/core_ext/time'
|
|
18
|
+
require 'openc3/utilities/logger'
|
|
19
|
+
|
|
20
|
+
module OpenC3
|
|
21
|
+
# Interface base class which reads an underlying socket / stream (the reader)
|
|
22
|
+
# in a dedicated thread and buffers the results on a queue. This keeps the
|
|
23
|
+
# operating system receive buffers drained as fast as possible so data isn't
|
|
24
|
+
# dropped while the packet reading thread is busy processing protocols and
|
|
25
|
+
# writing to the database.
|
|
26
|
+
#
|
|
27
|
+
# Subclasses call start_read_queue_thread with the object they read from
|
|
28
|
+
# (anything with a read method) once it is connected and stop_read_queue_thread
|
|
29
|
+
# once it is disconnected. While the thread is running the reader's read
|
|
30
|
+
# method is redirected to the queue, so the subclass (and anything else such
|
|
31
|
+
# as derived classes or protocols) keeps calling reader.read exactly as before
|
|
32
|
+
# and receives the data in the order it was read rather than competing with
|
|
33
|
+
# the read thread for it.
|
|
34
|
+
#
|
|
35
|
+
# Setting READ_QUEUE_MAX_SIZE to 0 disables the read thread so the reader is
|
|
36
|
+
# read inline, which is how interfaces read before the queue existed.
|
|
37
|
+
class ReadQueueInterface < Interface
|
|
38
|
+
# Maximum number of bytes buffered on the queue before the read thread
|
|
39
|
+
# blocks and lets the operating system do the buffering instead
|
|
40
|
+
DEFAULT_READ_QUEUE_MAX_SIZE = 20 * 1024 * 1024 # 20MB
|
|
41
|
+
|
|
42
|
+
# Approximate memory each queued read costs on top of the data itself (the
|
|
43
|
+
# String object plus the queue entry). Small reads are dominated by this so
|
|
44
|
+
# it is charged against the budget, otherwise a stream delivering a few
|
|
45
|
+
# bytes at a time would hold many times the budget in actual memory.
|
|
46
|
+
READ_QUEUE_ENTRY_OVERHEAD = 64
|
|
47
|
+
|
|
48
|
+
# How long to wait before re-checking whether the interface has been
|
|
49
|
+
# disconnected while blocked waiting for room on the queue
|
|
50
|
+
QUEUE_POLL_TIMEOUT = 0.1
|
|
51
|
+
|
|
52
|
+
# How long to wait for the read thread to exit when disconnecting
|
|
53
|
+
THREAD_JOIN_TIMEOUT = 2
|
|
54
|
+
|
|
55
|
+
# @return [Integer] Maximum number of bytes buffered on the queue
|
|
56
|
+
attr_accessor :read_queue_max_size
|
|
57
|
+
|
|
58
|
+
# @return [Time, nil] When the read thread read the data most recently
|
|
59
|
+
# returned by the reader (nil when the read queue is disabled)
|
|
60
|
+
attr_reader :read_queue_data_time
|
|
61
|
+
|
|
62
|
+
def initialize
|
|
63
|
+
super()
|
|
64
|
+
initialize_read_queue()
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Initialize (or reset) the read queue attributes. Called by initialize.
|
|
68
|
+
def initialize_read_queue(max_size = DEFAULT_READ_QUEUE_MAX_SIZE)
|
|
69
|
+
@raw_read_queue = nil
|
|
70
|
+
@raw_read_thread = nil
|
|
71
|
+
@raw_reader = nil
|
|
72
|
+
@raw_reader_read = nil
|
|
73
|
+
@raw_read_bytes = 0
|
|
74
|
+
@raw_read_budget = 0
|
|
75
|
+
@raw_read_timeout_queued = false
|
|
76
|
+
@read_queue_data_time = nil
|
|
77
|
+
# Guards @raw_read_queue / @raw_read_bytes / @raw_read_budget and wakes the
|
|
78
|
+
# read thread when the bytes it queued are consumed and there is room to
|
|
79
|
+
# queue more. Each read thread is cancelled by closing its own queue under
|
|
80
|
+
# this mutex, so a thread which outlives stop_read_queue_thread (and a new
|
|
81
|
+
# thread starting) keeps seeing its old queue closed and can never touch
|
|
82
|
+
# the byte counters or read again.
|
|
83
|
+
@raw_read_mutex = Mutex.new
|
|
84
|
+
@raw_read_condition = ConditionVariable.new
|
|
85
|
+
@read_queue_max_size = max_size
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Supported Options
|
|
89
|
+
# READ_QUEUE_MAX_SIZE - Maximum number of bytes buffered on the read queue
|
|
90
|
+
# or 0 to disable the read thread and read inline (see Interface#set_option)
|
|
91
|
+
def set_option(option_name, option_values)
|
|
92
|
+
super(option_name, option_values)
|
|
93
|
+
if option_name.upcase == 'READ_QUEUE_MAX_SIZE'
|
|
94
|
+
max_size = Integer(option_values[0])
|
|
95
|
+
raise "READ_QUEUE_MAX_SIZE must be 0 (disabled) or a positive integer but was #{max_size}" if max_size < 0
|
|
96
|
+
|
|
97
|
+
@read_queue_max_size = max_size
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# @return [Boolean] Whether reads are buffered by a read thread (false when
|
|
102
|
+
# READ_QUEUE_MAX_SIZE is 0 and reads happen inline)
|
|
103
|
+
def read_queue_enabled?
|
|
104
|
+
@read_queue_max_size > 0
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @return [Boolean] Whether a read thread has been started (and not stopped)
|
|
108
|
+
def read_queue_started?
|
|
109
|
+
!@raw_read_queue.nil?
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @return [Integer] The number of reads waiting on the read queue
|
|
113
|
+
def read_queue_size
|
|
114
|
+
queue = @raw_read_queue
|
|
115
|
+
queue ? queue.size : 0
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @return [Integer] The number of bytes waiting on the read queue
|
|
119
|
+
def read_queue_bytes
|
|
120
|
+
return 0 unless @raw_read_queue
|
|
121
|
+
|
|
122
|
+
@raw_read_mutex.synchronize { @raw_read_bytes }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Start the thread which continuously reads and queues data from the reader
|
|
126
|
+
# and redirect the reader's read method to the queue. Does nothing beyond
|
|
127
|
+
# stopping any existing thread if the read queue is disabled.
|
|
128
|
+
#
|
|
129
|
+
# @param reader [#read] Object to read from, e.g. a Stream or socket
|
|
130
|
+
# @param read_args [Array] Arguments the read thread passes to reader.read.
|
|
131
|
+
# Arguments passed to the redirected read are ignored.
|
|
132
|
+
def start_read_queue_thread(reader, *read_args)
|
|
133
|
+
stop_read_queue_thread()
|
|
134
|
+
return unless read_queue_enabled?
|
|
135
|
+
|
|
136
|
+
queue = Queue.new
|
|
137
|
+
@raw_read_mutex.synchronize do
|
|
138
|
+
@raw_read_bytes = 0
|
|
139
|
+
@raw_read_budget = 0
|
|
140
|
+
@raw_read_timeout_queued = false
|
|
141
|
+
@raw_read_queue = queue
|
|
142
|
+
end
|
|
143
|
+
begin
|
|
144
|
+
raw_read = redirect_reader(reader)
|
|
145
|
+
rescue FrozenError, TypeError
|
|
146
|
+
Logger.warn "#{@name}: Can't redirect #{reader.class}#read so disabling the read queue"
|
|
147
|
+
@raw_read_mutex.synchronize { @raw_read_queue = nil }
|
|
148
|
+
@read_queue_max_size = 0
|
|
149
|
+
return
|
|
150
|
+
end
|
|
151
|
+
@raw_read_thread = Thread.new do
|
|
152
|
+
read_queue_thread_body(queue, raw_read, read_args)
|
|
153
|
+
rescue Exception => e
|
|
154
|
+
Logger.error "#{@name}: Read queue thread unexpectedly died: #{e.formatted}"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Stop the read thread, restore the reader's read method and discard anything
|
|
159
|
+
# left on the queue. The reader should be disconnected first so the thread
|
|
160
|
+
# isn't blocked reading.
|
|
161
|
+
def stop_read_queue_thread
|
|
162
|
+
# Allow subclasses to set their reader before initialize_read_queue
|
|
163
|
+
return unless @raw_read_mutex
|
|
164
|
+
|
|
165
|
+
@raw_read_mutex.synchronize do
|
|
166
|
+
# Closing the queue under the mutex cancels the read thread (it either
|
|
167
|
+
# already charged its bytes, which are reset below, or never will) and
|
|
168
|
+
# unblocks read_queue_pop
|
|
169
|
+
@raw_read_queue.close if @raw_read_queue
|
|
170
|
+
@raw_read_queue = nil
|
|
171
|
+
@raw_read_bytes = 0
|
|
172
|
+
@raw_read_budget = 0
|
|
173
|
+
@raw_read_timeout_queued = false
|
|
174
|
+
@read_queue_data_time = nil
|
|
175
|
+
# Unblock the read thread if it is waiting for room on the queue
|
|
176
|
+
@raw_read_condition.broadcast
|
|
177
|
+
end
|
|
178
|
+
restore_reader()
|
|
179
|
+
thread = @raw_read_thread
|
|
180
|
+
@raw_read_thread = nil
|
|
181
|
+
# The read source should already be disconnected which unblocks the read
|
|
182
|
+
if thread and thread != Thread.current and !thread.join(THREAD_JOIN_TIMEOUT)
|
|
183
|
+
OpenC3.kill_thread(nil, thread)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Retrieve the next data off the read queue, blocking until it is available.
|
|
188
|
+
# This is what the reader's read method returns while it is redirected.
|
|
189
|
+
#
|
|
190
|
+
# @return [String, nil] Data read or nil if the read source is done
|
|
191
|
+
def read_queue_pop
|
|
192
|
+
queue = @raw_read_queue
|
|
193
|
+
return nil unless queue
|
|
194
|
+
|
|
195
|
+
loop do
|
|
196
|
+
data = queue.pop
|
|
197
|
+
if data.kind_of?(Array)
|
|
198
|
+
data, time = data
|
|
199
|
+
@raw_read_mutex.synchronize do
|
|
200
|
+
# Counters were reset if the queue was stopped / replaced
|
|
201
|
+
if queue.equal?(@raw_read_queue)
|
|
202
|
+
@raw_read_bytes -= data.length
|
|
203
|
+
@raw_read_budget -= data.length + READ_QUEUE_ENTRY_OVERHEAD
|
|
204
|
+
@read_queue_data_time = time
|
|
205
|
+
# Tell the read thread there is room for more data
|
|
206
|
+
@raw_read_condition.broadcast
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
return data
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
if data.kind_of?(Timeout::Error)
|
|
213
|
+
@raw_read_mutex.synchronize { @raw_read_timeout_queued = false if queue.equal?(@raw_read_queue) }
|
|
214
|
+
# Data queued behind the timeout arrived while nobody was waiting on
|
|
215
|
+
# it, so an inline read would have returned it rather than timing out
|
|
216
|
+
next unless queue.empty?
|
|
217
|
+
end
|
|
218
|
+
# Exceptions raised by the read thread are re-raised here so they are
|
|
219
|
+
# handled exactly as they were when the read happened inline
|
|
220
|
+
raise data if data.kind_of?(Exception)
|
|
221
|
+
|
|
222
|
+
return data
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# @param data [String, nil] Data returned by reader.read
|
|
227
|
+
# @return [Boolean] Whether the data indicates the reader is done (closed) so
|
|
228
|
+
# the read thread should stop. Subclasses can override this.
|
|
229
|
+
def read_queue_done?(data)
|
|
230
|
+
data.nil? or data.length <= 0
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# protected
|
|
234
|
+
|
|
235
|
+
def read_queue_thread_body(queue, raw_read, read_args)
|
|
236
|
+
until queue.closed?
|
|
237
|
+
begin
|
|
238
|
+
data = raw_read.call(*read_args)
|
|
239
|
+
data_time = Time.now.sys
|
|
240
|
+
rescue Timeout::Error => e
|
|
241
|
+
# A read timeout doesn't end the reader so pass it along and keep
|
|
242
|
+
# reading. Only one is queued at a time so a reader which times out
|
|
243
|
+
# quickly can't fill the queue with them.
|
|
244
|
+
queue_timeout = @raw_read_mutex.synchronize do
|
|
245
|
+
next false if queue.closed? or @raw_read_timeout_queued
|
|
246
|
+
|
|
247
|
+
@raw_read_timeout_queued = true
|
|
248
|
+
end
|
|
249
|
+
queue.push(e) if queue_timeout
|
|
250
|
+
next
|
|
251
|
+
rescue Exception => e
|
|
252
|
+
queue.push(e)
|
|
253
|
+
break
|
|
254
|
+
end
|
|
255
|
+
# Tell whoever reads next that the reader is done and stop reading
|
|
256
|
+
if read_queue_done?(data)
|
|
257
|
+
queue.push(data)
|
|
258
|
+
break
|
|
259
|
+
end
|
|
260
|
+
# Charge the bytes against the budget before the push so
|
|
261
|
+
# read_queue_bytes never goes negative if the data is dequeued
|
|
262
|
+
# before we get back here
|
|
263
|
+
break unless reserve_read_queue_bytes(queue, data.length)
|
|
264
|
+
|
|
265
|
+
# Queue the time of the read so packets are timestamped when the data
|
|
266
|
+
# arrived rather than when it was processed
|
|
267
|
+
queue.push([data, data_time])
|
|
268
|
+
end
|
|
269
|
+
rescue ClosedQueueError
|
|
270
|
+
# Interface disconnected while we were pushing
|
|
271
|
+
ensure
|
|
272
|
+
# Closing tells read_queue_pop there will never be more data so it
|
|
273
|
+
# returns nil rather than blocking forever
|
|
274
|
+
queue.close
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Wait until the data fits in the byte budget and charge it against the
|
|
278
|
+
# budget. A read larger than the entire budget is queued on its own rather
|
|
279
|
+
# than never fitting, so the budget can be exceeded by at most one read.
|
|
280
|
+
#
|
|
281
|
+
# @return [Boolean] Whether the bytes were reserved (false if disconnected)
|
|
282
|
+
def reserve_read_queue_bytes(queue, length)
|
|
283
|
+
cost = length + READ_QUEUE_ENTRY_OVERHEAD
|
|
284
|
+
@raw_read_mutex.synchronize do
|
|
285
|
+
while @raw_read_budget > 0 and (@raw_read_budget + cost) > @read_queue_max_size
|
|
286
|
+
return false if queue.closed?
|
|
287
|
+
|
|
288
|
+
@raw_read_condition.wait(@raw_read_mutex, QUEUE_POLL_TIMEOUT)
|
|
289
|
+
end
|
|
290
|
+
return false if queue.closed?
|
|
291
|
+
|
|
292
|
+
@raw_read_bytes += length
|
|
293
|
+
@raw_read_budget += cost
|
|
294
|
+
end
|
|
295
|
+
return true
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Redirect reader.read to the read queue so everything which reads the
|
|
299
|
+
# reader directly gets the queued data in order.
|
|
300
|
+
#
|
|
301
|
+
# @return [Method] The original read method for the read thread to call
|
|
302
|
+
def redirect_reader(reader)
|
|
303
|
+
raw_read = reader.method(:read)
|
|
304
|
+
# Remember a read already defined on the reader itself (e.g. a test stub)
|
|
305
|
+
# so it can be put back rather than removed
|
|
306
|
+
reader_read = reader.singleton_methods(false).include?(:read) ? raw_read : nil
|
|
307
|
+
interface = self
|
|
308
|
+
reader.define_singleton_method(:read) { |*_args| interface.read_queue_pop }
|
|
309
|
+
@raw_reader = reader
|
|
310
|
+
@raw_reader_read = reader_read
|
|
311
|
+
return raw_read
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Undo redirect_reader so the reader reads inline again
|
|
315
|
+
def restore_reader
|
|
316
|
+
reader = @raw_reader
|
|
317
|
+
return unless reader
|
|
318
|
+
|
|
319
|
+
if @raw_reader_read
|
|
320
|
+
reader.define_singleton_method(:read, @raw_reader_read)
|
|
321
|
+
else
|
|
322
|
+
reader.singleton_class.send(:remove_method, :read)
|
|
323
|
+
end
|
|
324
|
+
rescue NameError
|
|
325
|
+
# The redirect was already removed
|
|
326
|
+
ensure
|
|
327
|
+
@raw_reader = nil
|
|
328
|
+
@raw_reader_read = nil
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
# This file may also be used under the terms of a commercial license
|
|
16
16
|
# if purchased from OpenC3, Inc.
|
|
17
17
|
|
|
18
|
-
require 'openc3/interfaces/
|
|
18
|
+
require 'openc3/interfaces/read_queue_interface'
|
|
19
19
|
|
|
20
20
|
module OpenC3
|
|
21
21
|
# Base class for interfaces that act read and write from a stream
|
|
22
|
-
class StreamInterface <
|
|
23
|
-
|
|
22
|
+
class StreamInterface < ReadQueueInterface
|
|
23
|
+
attr_reader :stream
|
|
24
24
|
|
|
25
25
|
def initialize(protocol_type = nil, protocol_args = [])
|
|
26
26
|
super()
|
|
@@ -34,9 +34,20 @@ module OpenC3
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# Replacing the stream stops the read thread which is reading the old one
|
|
38
|
+
def stream=(stream)
|
|
39
|
+
stop_read_queue_thread()
|
|
40
|
+
@stream = stream
|
|
41
|
+
end
|
|
42
|
+
|
|
37
43
|
def connect
|
|
38
44
|
super() # Reset the protocols
|
|
39
45
|
@stream.connect if @stream
|
|
46
|
+
# The read thread continuously drains the stream so the operating system
|
|
47
|
+
# buffers don't fill up while we're busy processing the previous read.
|
|
48
|
+
# @stream.read is redirected to the queue so everything reading the
|
|
49
|
+
# stream (including subclasses and protocols) still sees all the data.
|
|
50
|
+
start_read_queue_thread(@stream) if @stream and read_allowed?
|
|
40
51
|
end
|
|
41
52
|
|
|
42
53
|
def connected?
|
|
@@ -48,11 +59,19 @@ module OpenC3
|
|
|
48
59
|
end
|
|
49
60
|
|
|
50
61
|
def disconnect
|
|
62
|
+
# Disconnect the stream first to unblock the read thread
|
|
51
63
|
@stream.disconnect if @stream
|
|
64
|
+
stop_read_queue_thread()
|
|
52
65
|
super()
|
|
53
66
|
end
|
|
54
67
|
|
|
55
68
|
def read_interface
|
|
69
|
+
# The read thread is normally started by connect but interfaces can also
|
|
70
|
+
# be handed an already connected stream so start it on demand
|
|
71
|
+
if @stream and !read_queue_started? and read_queue_enabled? and read_allowed? and connected?
|
|
72
|
+
start_read_queue_thread(@stream)
|
|
73
|
+
end
|
|
74
|
+
|
|
56
75
|
timeout = false
|
|
57
76
|
begin
|
|
58
77
|
data = @stream.read
|
|
@@ -257,6 +257,15 @@ module OpenC3
|
|
|
257
257
|
@read_queue ? @read_queue.size : 0
|
|
258
258
|
end
|
|
259
259
|
|
|
260
|
+
# @return [Integer] The number of bytes waiting on the read queues of the
|
|
261
|
+
# connected clients. The server itself has no stream so the raw reads are
|
|
262
|
+
# buffered by each client connection.
|
|
263
|
+
def read_queue_bytes
|
|
264
|
+
@connection_mutex.synchronize do
|
|
265
|
+
@read_interface_infos.sum { |rii| rii.interface.read_queue_bytes }
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
260
269
|
# @return [Integer] The number of packets waiting on the write queue
|
|
261
270
|
def write_queue_size
|
|
262
271
|
@write_queue ? @write_queue.size : 0
|
|
@@ -400,6 +409,10 @@ module OpenC3
|
|
|
400
409
|
end
|
|
401
410
|
|
|
402
411
|
interface = StreamInterface.new
|
|
412
|
+
# Only the read side of the connection needs a read thread
|
|
413
|
+
interface.read_allowed = listen_read
|
|
414
|
+
# Each connection buffers its own reads so give it the configured limit
|
|
415
|
+
interface.read_queue_max_size = @read_queue_max_size
|
|
403
416
|
interface.target_names = @target_names
|
|
404
417
|
interface.cmd_target_names = @cmd_target_names
|
|
405
418
|
interface.tlm_target_names = @tlm_target_names
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
# This file may also be used under the terms of a commercial license
|
|
16
16
|
# if purchased from OpenC3, Inc.
|
|
17
17
|
|
|
18
|
-
require 'openc3/interfaces/
|
|
18
|
+
require 'openc3/interfaces/read_queue_interface'
|
|
19
19
|
require 'openc3/io/udp_sockets'
|
|
20
20
|
require 'openc3/config/config_parser'
|
|
21
21
|
|
|
22
22
|
module OpenC3
|
|
23
23
|
# Base class for interfaces that send and receive messages over UDP
|
|
24
|
-
class UdpInterface <
|
|
24
|
+
class UdpInterface < ReadQueueInterface
|
|
25
25
|
HOST_127_0_0_1 = '127.0.0.1'
|
|
26
26
|
HOST_0_0_0_0 = '0.0.0.0'
|
|
27
27
|
|
|
@@ -140,6 +140,11 @@ module OpenC3
|
|
|
140
140
|
end
|
|
141
141
|
@thread_sleeper = nil
|
|
142
142
|
super()
|
|
143
|
+
# The read thread continuously drains the socket so the operating system
|
|
144
|
+
# buffers don't fill up (and drop datagrams) while we're busy processing
|
|
145
|
+
# the previous read. @read_socket.read is redirected to the queue so
|
|
146
|
+
# everything reading the socket (including subclasses) still sees all the data.
|
|
147
|
+
start_read_queue_thread(@read_socket, @read_timeout) if @read_socket
|
|
143
148
|
end
|
|
144
149
|
|
|
145
150
|
# @return [Boolean] Whether the active ports (read and/or write) have
|
|
@@ -163,6 +168,8 @@ module OpenC3
|
|
|
163
168
|
OpenC3.close_socket(@read_socket)
|
|
164
169
|
@write_socket = nil
|
|
165
170
|
@read_socket = nil
|
|
171
|
+
# The sockets are closed above which unblocks the read thread
|
|
172
|
+
stop_read_queue_thread()
|
|
166
173
|
@thread_sleeper.cancel if @thread_sleeper
|
|
167
174
|
@thread_sleeper = nil
|
|
168
175
|
super()
|
|
@@ -177,9 +184,16 @@ module OpenC3
|
|
|
177
184
|
return nil
|
|
178
185
|
end
|
|
179
186
|
|
|
187
|
+
# Zero length datagrams are valid so only nil means the socket is done
|
|
188
|
+
def read_queue_done?(data)
|
|
189
|
+
data.nil?
|
|
190
|
+
end
|
|
191
|
+
|
|
180
192
|
# Reads from the socket if the read_port is defined
|
|
181
193
|
def read_interface
|
|
182
194
|
data = @read_socket.read(@read_timeout)
|
|
195
|
+
return nil unless data # Disconnected
|
|
196
|
+
|
|
183
197
|
Logger.info "#{@name}: Udp read returned 0 bytes (stream closed)" if data.length <= 0
|
|
184
198
|
extra = nil
|
|
185
199
|
read_interface_base(data, extra)
|
data/lib/openc3/interfaces.rb
CHANGED
|
@@ -22,6 +22,7 @@ module OpenC3
|
|
|
22
22
|
autoload(:Interface, 'openc3/interfaces/interface.rb')
|
|
23
23
|
autoload(:MqttInterface, 'openc3/interfaces/mqtt_interface.rb')
|
|
24
24
|
autoload(:MqttStreamInterface, 'openc3/interfaces/mqtt_stream_interface.rb')
|
|
25
|
+
autoload(:ReadQueueInterface, 'openc3/interfaces/read_queue_interface.rb')
|
|
25
26
|
autoload(:SerialInterface, 'openc3/interfaces/serial_interface.rb')
|
|
26
27
|
autoload(:SimulatedTargetInterface, 'openc3/interfaces/simulated_target_interface.rb')
|
|
27
28
|
autoload(:StreamInterface, 'openc3/interfaces/stream_interface.rb')
|
|
@@ -63,7 +63,10 @@ module OpenC3
|
|
|
63
63
|
CYCLE_TIME_INTERVAL = 10
|
|
64
64
|
|
|
65
65
|
# Delay in seconds before trimming Redis streams
|
|
66
|
-
|
|
66
|
+
# This is the primary trimmer of the TELEMETRY__ and COMMAND__ streams. Writers
|
|
67
|
+
# also apply a safety MINID cap (see Topic.stream_safety_minid) which depends on this
|
|
68
|
+
# value, so it is defined in Topic.
|
|
69
|
+
CLEANUP_DELAY = Topic::LOG_CLEANUP_DELAY_SECONDS
|
|
67
70
|
|
|
68
71
|
# Mutex protecting class variables
|
|
69
72
|
@@mutex = Mutex.new
|
|
@@ -211,6 +214,15 @@ module OpenC3
|
|
|
211
214
|
end
|
|
212
215
|
end
|
|
213
216
|
|
|
217
|
+
# The db_shard of a target topic like DEFAULT__DECOM__{INST}__HEALTH_STATUS
|
|
218
|
+
# or db_shard 0 for a topic without a target
|
|
219
|
+
def self.db_shard_for_topic(redis_topic)
|
|
220
|
+
scope, _, rest = redis_topic.delete_prefix('{').partition('__')
|
|
221
|
+
# The target is a {TARGET} part between two others, e.g. DECOM__{INST}__HEALTH_STATUS
|
|
222
|
+
target = rest.split('__', -1)[1...-1]&.find { |part| part.length > 2 and part.start_with?('{') and part.end_with?('}') }
|
|
223
|
+
(!scope.empty? and target) ? Store.db_shard_for_target(target[1...-1], scope: scope) : 0
|
|
224
|
+
end
|
|
225
|
+
|
|
214
226
|
def cycle_thread_body
|
|
215
227
|
@@cycle_sleeper = Sleeper.new
|
|
216
228
|
while true
|
|
@@ -245,9 +257,7 @@ module OpenC3
|
|
|
245
257
|
# Now that the file is in S3, trim the Redis stream up until the previous file.
|
|
246
258
|
# This keeps one minute of data in Redis
|
|
247
259
|
instance.cleanup_offsets[index].each do |redis_topic, cleanup_offset|
|
|
248
|
-
|
|
249
|
-
db_shard = target_match ? Store.db_shard_for_target(target_match[1]) : 0
|
|
250
|
-
Topic.trim_topic(redis_topic, cleanup_offset, db_shard: db_shard)
|
|
260
|
+
Topic.trim_topic(redis_topic, cleanup_offset, db_shard: LogWriter.db_shard_for_topic(redis_topic))
|
|
251
261
|
end
|
|
252
262
|
indexes_to_clear << index
|
|
253
263
|
end
|
|
@@ -84,6 +84,11 @@ module OpenC3
|
|
|
84
84
|
include InterfaceDecomCommon
|
|
85
85
|
LIMITS_STATE_INDEX = { RED_LOW: 0, YELLOW_LOW: 1, YELLOW_HIGH: 2, RED_HIGH: 3, GREEN_LOW: 4, GREEN_HIGH: 5 }
|
|
86
86
|
|
|
87
|
+
# Decom never uses descriptions so don't keep them in memory
|
|
88
|
+
def load_descriptions?
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
|
|
87
92
|
def initialize(*args)
|
|
88
93
|
super(*args)
|
|
89
94
|
# Should only be one target, but there might be multiple decom microservices for a given target
|
|
@@ -94,6 +99,10 @@ module OpenC3
|
|
|
94
99
|
@limits_event_topic = "#{@scope}__openc3_limits_events"
|
|
95
100
|
@topics << @limits_event_topic
|
|
96
101
|
Topic.update_topic_offsets(@topics, db_shard: @db_shard)
|
|
102
|
+
# Last limits event read, used to detect events trimmed before they were read.
|
|
103
|
+
# Read from the stream rather than the thread's cached offsets. The run thread
|
|
104
|
+
# starts reading at or after this point, so it can only cause an extra resync.
|
|
105
|
+
@limits_event_offset = Topic.get_last_offset(@limits_event_topic, db_shard: @db_shard)
|
|
97
106
|
# Initialize before assigning limits_change_callback - sync_system below
|
|
98
107
|
# can fire the callback synchronously for any persisted-disabled items.
|
|
99
108
|
@limits_response_queue = Queue.new
|
|
@@ -116,14 +125,21 @@ module OpenC3
|
|
|
116
125
|
break if @cancel_thread
|
|
117
126
|
|
|
118
127
|
begin
|
|
128
|
+
limits_events_checked = false
|
|
119
129
|
OpenC3.in_span("read_topics") do
|
|
120
130
|
Topic.read_topics(@topics, db_shard: @db_shard) do |topic, msg_id, msg_hash, redis|
|
|
121
131
|
break if @cancel_thread
|
|
122
132
|
if topic == @microservice_topic
|
|
123
133
|
microservice_cmd(topic, msg_id, msg_hash, redis)
|
|
124
134
|
elsif topic == @limits_event_topic
|
|
135
|
+
# Any gap is before the first event in a read so only check once per read
|
|
136
|
+
unless limits_events_checked
|
|
137
|
+
resync_if_limits_events_trimmed()
|
|
138
|
+
limits_events_checked = true
|
|
139
|
+
end
|
|
140
|
+
@limits_event_offset = msg_id
|
|
125
141
|
event = JSON.parse(msg_hash['event'], allow_nan: true, create_additions: true)
|
|
126
|
-
LimitsEventTopic.process_event(event)
|
|
142
|
+
LimitsEventTopic.process_event(event, scope: @scope)
|
|
127
143
|
elsif topic =~ /__DECOMINTERFACE/
|
|
128
144
|
if msg_hash.key?('inject_tlm')
|
|
129
145
|
handle_inject_tlm_with_ack(msg_hash['inject_tlm'], msg_id)
|
|
@@ -156,11 +172,20 @@ module OpenC3
|
|
|
156
172
|
@limits_response_thread = nil
|
|
157
173
|
end
|
|
158
174
|
|
|
159
|
-
|
|
175
|
+
# Limits events are only read between batches of packets, so a decom microservice
|
|
176
|
+
# working through a backlog can fall far enough behind that a flood of LIMITS_CHANGE
|
|
177
|
+
# events trims control events (enable, settings, etc) before it reads them.
|
|
178
|
+
# Recover by reloading all limits state from the store, which is always current.
|
|
179
|
+
def resync_if_limits_events_trimmed
|
|
180
|
+
if LimitsEventTopic.events_trimmed?(@limits_event_offset, scope: @scope, db_shard: @db_shard)
|
|
181
|
+
@logger.warn("#{@name}: Limits events were trimmed before they could be read, resyncing limits")
|
|
182
|
+
LimitsEventTopic.resync_system(scope: @scope)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def decom_packet(topic, msg_id, msg_hash, _redis)
|
|
160
187
|
OpenC3.in_span("decom_packet") do
|
|
161
|
-
|
|
162
|
-
delta = Time.now.to_f - msgid_seconds_from_epoch
|
|
163
|
-
@metric.set(name: 'decom_topic_delta_seconds', value: delta, type: 'gauge', unit: 'seconds', help: 'Delta time between data written to stream and decom start')
|
|
188
|
+
update_topic_lag(topic, msg_id, metric_name: 'decom_topic_delta_seconds', help: 'Delta time between data written to stream and decom start')
|
|
164
189
|
|
|
165
190
|
#######################################
|
|
166
191
|
# Build packet object from topic data
|
|
@@ -42,6 +42,8 @@ module OpenC3
|
|
|
42
42
|
def handle_inject_tlm_with_ack(inject_tlm_json, msg_id)
|
|
43
43
|
inject_tlm_hash = JSON.parse(inject_tlm_json, allow_nan: true, create_additions: true)
|
|
44
44
|
target_name = inject_tlm_hash['target_name']
|
|
45
|
+
# Ack on the db_shard of the target where DecomInterfaceTopic.inject_tlm waits for it
|
|
46
|
+
db_shard = Store.db_shard_for_target(target_name, scope: @scope)
|
|
45
47
|
ack_topic = "{#{@scope}__ACKCMD}TARGET__#{target_name}"
|
|
46
48
|
begin
|
|
47
49
|
handle_inject_tlm(inject_tlm_json)
|
|
@@ -56,7 +58,7 @@ module OpenC3
|
|
|
56
58
|
result: error.message
|
|
57
59
|
}
|
|
58
60
|
end
|
|
59
|
-
Topic.write_topic(ack_topic, msg_hash)
|
|
61
|
+
Topic.write_topic(ack_topic, msg_hash, db_shard: db_shard)
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
def handle_build_cmd(build_cmd_json, msg_id)
|