openc3 7.4.1 → 7.5.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/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 +1 -1
- 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 +6 -10
- 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 +13 -4
- data/lib/openc3/microservices/decom_microservice.rb +7 -4
- 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/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 +83 -9
- 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 +27 -14
- 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 +58 -84
- data/lib/openc3/script/extract.rb +215 -19
- data/lib/openc3/script/plugins.rb +4 -11
- 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/router_topic.rb +2 -1
- 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 +154 -2
- 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 +14 -0
- data/lib/openc3/utilities/running_script.rb +22 -18
- data/lib/openc3/utilities/store_autoload.rb +20 -3
- 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 +6 -6
- 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
|
|
@@ -77,6 +80,14 @@ module OpenC3
|
|
|
77
80
|
# Sleeper used to delay cycle thread
|
|
78
81
|
@@cycle_sleeper = nil
|
|
79
82
|
|
|
83
|
+
# Returns the Redis db_shard holding a target stream such as
|
|
84
|
+
# SCOPE__TELEMETRY__{TARGET}__PACKET, or 0 if no target can be determined
|
|
85
|
+
def self.db_shard_for_topic(redis_topic)
|
|
86
|
+
target_match = redis_topic.match(/\{([^{}]+)\}/)
|
|
87
|
+
return 0 unless target_match
|
|
88
|
+
Store.db_shard_for_target(target_match[1], scope: redis_topic.split('__')[0])
|
|
89
|
+
end
|
|
90
|
+
|
|
80
91
|
# @param remote_log_directory [String] The path to store the log files
|
|
81
92
|
# @param logging_enabled [Boolean] Whether to start with logging enabled
|
|
82
93
|
# @param cycle_time [Integer] The amount of time in seconds before creating
|
|
@@ -245,9 +256,7 @@ module OpenC3
|
|
|
245
256
|
# Now that the file is in S3, trim the Redis stream up until the previous file.
|
|
246
257
|
# This keeps one minute of data in Redis
|
|
247
258
|
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)
|
|
259
|
+
Topic.trim_topic(redis_topic, cleanup_offset, db_shard: LogWriter.db_shard_for_topic(redis_topic))
|
|
251
260
|
end
|
|
252
261
|
indexes_to_clear << index
|
|
253
262
|
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
|
|
@@ -156,11 +161,9 @@ module OpenC3
|
|
|
156
161
|
@limits_response_thread = nil
|
|
157
162
|
end
|
|
158
163
|
|
|
159
|
-
def decom_packet(
|
|
164
|
+
def decom_packet(topic, msg_id, msg_hash, _redis)
|
|
160
165
|
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')
|
|
166
|
+
update_topic_lag(topic, msg_id, metric_name: 'decom_topic_delta_seconds', help: 'Delta time between data written to stream and decom start')
|
|
164
167
|
|
|
165
168
|
#######################################
|
|
166
169
|
# Build packet object from topic data
|