openc3 7.4.0 → 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/openc3cli +4 -3
- 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 +21 -21
- 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/protocols/protocol.rb +8 -0
- 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/io/json_api_object.rb +24 -3
- data/lib/openc3/io/json_drb_object.rb +3 -2
- 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/reaction_model.rb +6 -3
- data/lib/openc3/models/setting_model.rb +5 -3
- 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 +48 -2
- data/lib/openc3/packets/packet_item.rb +27 -14
- data/lib/openc3/packets/parsers/state_parser.rb +1 -1
- data/lib/openc3/packets/parsers/xtce_parser.rb +8 -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/script/tables.rb +5 -1
- 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 +43 -46
- 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 +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 +36 -1
|
@@ -70,6 +70,9 @@ module OpenC3
|
|
|
70
70
|
# this interface
|
|
71
71
|
attr_accessor :disable_disconnect
|
|
72
72
|
|
|
73
|
+
# @return [Boolean] Flag indicating if the interface is allowed to read
|
|
74
|
+
attr_writer :read_allowed
|
|
75
|
+
|
|
73
76
|
# @return [StreamLogPair] StreamLogPair instance or nil
|
|
74
77
|
attr_accessor :stream_log_pair
|
|
75
78
|
|
|
@@ -326,6 +329,12 @@ module OpenC3
|
|
|
326
329
|
# Return packet
|
|
327
330
|
@read_count += 1
|
|
328
331
|
Logger.warn("#{@name}: Interface unexpectedly requested disconnect") unless packet
|
|
332
|
+
# Timestamp buffered reads with when the data arrived rather than when
|
|
333
|
+
# it was processed (otherwise received_time is set when it is handled)
|
|
334
|
+
if packet and !packet.received_time
|
|
335
|
+
data_time = read_queue_data_time
|
|
336
|
+
packet.received_time = data_time if data_time
|
|
337
|
+
end
|
|
329
338
|
return packet
|
|
330
339
|
end
|
|
331
340
|
rescue Exception => e
|
|
@@ -349,7 +358,8 @@ module OpenC3
|
|
|
349
358
|
if packet == :DISCONNECT
|
|
350
359
|
Logger.info("#{@name}: Protocol #{protocol.class} write_packet requested disconnect")
|
|
351
360
|
disconnect()
|
|
352
|
-
|
|
361
|
+
# Nothing was written so reject rather than report success
|
|
362
|
+
raise WriteRejectError.new("Protocol #{protocol.class} write_packet requested disconnect")
|
|
353
363
|
end
|
|
354
364
|
return if packet == :STOP
|
|
355
365
|
end
|
|
@@ -369,7 +379,8 @@ module OpenC3
|
|
|
369
379
|
if data == :DISCONNECT
|
|
370
380
|
Logger.info("#{@name}: Protocol #{protocol.class} write_data requested disconnect")
|
|
371
381
|
disconnect()
|
|
372
|
-
|
|
382
|
+
# Nothing was written so reject rather than report success
|
|
383
|
+
raise WriteRejectError.new("Protocol #{protocol.class} write_data requested disconnect")
|
|
373
384
|
end
|
|
374
385
|
return if data == :STOP
|
|
375
386
|
end
|
|
@@ -443,6 +454,19 @@ module OpenC3
|
|
|
443
454
|
config
|
|
444
455
|
end
|
|
445
456
|
|
|
457
|
+
# @return [Integer] The number of bytes waiting on the read queue.
|
|
458
|
+
# Interfaces which buffer raw reads override this (see ReadQueueInterface).
|
|
459
|
+
def read_queue_bytes
|
|
460
|
+
0
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# @return [Time, nil] When the data most recently returned by
|
|
464
|
+
# read_interface was read from the underlying source, or nil if unknown.
|
|
465
|
+
# Interfaces which buffer raw reads override this (see ReadQueueInterface).
|
|
466
|
+
def read_queue_data_time
|
|
467
|
+
nil
|
|
468
|
+
end
|
|
469
|
+
|
|
446
470
|
# @return [Boolean] Whether reading is allowed
|
|
447
471
|
def read_allowed?
|
|
448
472
|
@read_allowed
|
|
@@ -564,7 +588,7 @@ module OpenC3
|
|
|
564
588
|
# @return [String] Raw packet data
|
|
565
589
|
def read_interface_base(data, _extra = nil)
|
|
566
590
|
if @save_raw_data
|
|
567
|
-
@read_raw_data_time = Time.now
|
|
591
|
+
@read_raw_data_time = read_queue_data_time || Time.now
|
|
568
592
|
@read_raw_data = data.clone
|
|
569
593
|
end
|
|
570
594
|
@bytes_read += data.length
|
|
@@ -36,6 +36,14 @@ module OpenC3
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def reset
|
|
39
|
+
@read_data_input_time = nil
|
|
40
|
+
@read_data_input = ''
|
|
41
|
+
@read_data_output_time = nil
|
|
42
|
+
@read_data_output = ''
|
|
43
|
+
@write_data_input_time = nil
|
|
44
|
+
@write_data_input = ''
|
|
45
|
+
@write_data_output_time = nil
|
|
46
|
+
@write_data_output = ''
|
|
39
47
|
@extra = nil
|
|
40
48
|
end
|
|
41
49
|
|
|
@@ -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')
|
|
@@ -53,10 +53,19 @@ module OpenC3
|
|
|
53
53
|
|
|
54
54
|
USER_AGENT = 'OpenC3 / v7 (ruby/openc3/lib/io/json_api_object)'.freeze
|
|
55
55
|
|
|
56
|
+
# Time limit for the first response byte from the server. This must be an
|
|
57
|
+
# explicit number rather than nil, see connect() for why. Requests can
|
|
58
|
+
# legitimately block for a long time, e.g. cmd() with a large timeout waiting
|
|
59
|
+
# on an interface ack, so this is effectively "wait forever" while still
|
|
60
|
+
# bounded so a wedged connection eventually releases the thread.
|
|
61
|
+
DEFAULT_READ_TIMEOUT_S = 86400
|
|
62
|
+
|
|
56
63
|
# @param url [String] The url of openc3-cosmos-cmd-tlm-api http://openc3-cosmos-cmd-tlm-api:2901
|
|
57
64
|
# @param timeout [Float] The time to wait before disconnecting 1.0
|
|
58
65
|
# @param authentication [OpenC3Authentication] The authentication object if nill initialize will generate
|
|
59
|
-
|
|
66
|
+
# @param read_timeout [Float] The time to wait for the first response byte from the server.
|
|
67
|
+
# Defaults to the OPENC3_API_READ_TIMEOUT environment variable or DEFAULT_READ_TIMEOUT_S.
|
|
68
|
+
def initialize(url:, timeout: 1.0, authentication: nil, read_timeout: nil)
|
|
60
69
|
@http = nil
|
|
61
70
|
@mutex = Mutex.new
|
|
62
71
|
@request_data = ""
|
|
@@ -65,6 +74,7 @@ module OpenC3
|
|
|
65
74
|
@log = [nil, nil, nil]
|
|
66
75
|
@authentication = authentication.nil? ? generate_auth() : authentication
|
|
67
76
|
@timeout = timeout
|
|
77
|
+
@read_timeout = (read_timeout || ENV.fetch('OPENC3_API_READ_TIMEOUT', DEFAULT_READ_TIMEOUT_S)).to_f
|
|
68
78
|
@shutdown = false
|
|
69
79
|
# JsonDRb.debug = true # Enable for debugging
|
|
70
80
|
end
|
|
@@ -120,7 +130,11 @@ module OpenC3
|
|
|
120
130
|
# :open_timeout - time limit for just the connection phase (e.g. handshake) (Integer in seconds)
|
|
121
131
|
# :read_timeout - time limit for the first response byte received from the server (Integer in seconds)
|
|
122
132
|
# :write_timeout - time limit for the client to send the request to the server (Integer in seconds)
|
|
123
|
-
|
|
133
|
+
# NOTE: read_timeout must be an explicit number. Faraday only assigns it to Net::HTTP
|
|
134
|
+
# when the value is truthy (see Faraday::Adapter::NetHttp#configure_request), so passing
|
|
135
|
+
# nil does NOT mean "wait forever", it silently leaves Net::HTTP's 60s default in place.
|
|
136
|
+
# That 60s then caps every request regardless of any longer application level timeout.
|
|
137
|
+
@http = Faraday.new(request: { open_timeout: @timeout.to_i, read_timeout: @read_timeout }) do |f|
|
|
124
138
|
f.adapter :net_http # adds the adapter to the connection, defaults to `Faraday.default_adapter`
|
|
125
139
|
end
|
|
126
140
|
rescue => e
|
|
@@ -212,7 +226,9 @@ module OpenC3
|
|
|
212
226
|
@response_data = resp.body
|
|
213
227
|
return resp
|
|
214
228
|
rescue Faraday::ConnectionFailed, Errno::ECONNRESET, Errno::EPIPE, IOError => e
|
|
215
|
-
#
|
|
229
|
+
# Retryable. Nothing was successfully sent, so replaying is safe. This includes
|
|
230
|
+
# Net::OpenTimeout, which Faraday maps to ConnectionFailed and which covers a
|
|
231
|
+
# service that is still starting up.
|
|
216
232
|
retry_count += 1
|
|
217
233
|
@log[2] = "#{method} Exception: #{e.class}, #{e.message}, #{e.backtrace}"
|
|
218
234
|
if retry_count <= RETRY_COUNT
|
|
@@ -225,6 +241,11 @@ module OpenC3
|
|
|
225
241
|
raise error
|
|
226
242
|
end
|
|
227
243
|
rescue StandardError => e
|
|
244
|
+
# Everything else, including Faraday::TimeoutError from a read timeout, is NOT
|
|
245
|
+
# retryable. A read timeout means the request was fully sent and the server may
|
|
246
|
+
# have already acted on it. Many endpoints are not idempotent (create_activity,
|
|
247
|
+
# script_run, cmd), so a retry risks duplicating the operation. It would also blow
|
|
248
|
+
# past the caller's deadline by RETRY_COUNT times.
|
|
228
249
|
@log[2] = "#{method} Exception: #{e.class}, #{e.message}, #{e.backtrace}"
|
|
229
250
|
disconnect()
|
|
230
251
|
error = "Api Exception: #{@log[0]} ::: #{@log[1]} ::: #{@log[2]}"
|
|
@@ -38,8 +38,9 @@ module OpenC3
|
|
|
38
38
|
# @param url [String] The url of openc3-cosmos-cmd-tlm-api http://openc3-cosmos-cmd-tlm-api:2901
|
|
39
39
|
# @param timeout [Float] The time to wait before disconnecting 1.0
|
|
40
40
|
# @param authentication [OpenC3Authentication] The authentication object if nill initialize will generate
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
# @param read_timeout [Float] The time to wait for the first response byte from the server
|
|
42
|
+
def initialize(url:, timeout: 1.0, authentication: nil, read_timeout: nil)
|
|
43
|
+
super(url: url, timeout: timeout, authentication: authentication, read_timeout: read_timeout)
|
|
43
44
|
@uri = URI("#{url}/openc3-api/api")
|
|
44
45
|
end
|
|
45
46
|
|
|
@@ -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
|