openc3 7.0.0.pre.rc3 → 7.0.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/data/config/interface_modifiers.yaml +1 -1
- data/data/config/item_modifiers.yaml +18 -6
- data/data/config/telemetry.yaml +1 -1
- data/lib/openc3/accessors/json_accessor.rb +1 -1
- data/lib/openc3/api/tlm_api.rb +3 -3
- data/lib/openc3/config/config_parser.rb +4 -4
- data/lib/openc3/conversions/conversion.rb +3 -3
- data/lib/openc3/core_ext/faraday.rb +4 -0
- data/lib/openc3/logs/log_writer.rb +24 -6
- data/lib/openc3/logs/packet_log_writer.rb +1 -4
- data/lib/openc3/logs/stream_log_pair.rb +11 -4
- data/lib/openc3/logs/text_log_writer.rb +1 -4
- data/lib/openc3/microservices/interface_microservice.rb +8 -2
- data/lib/openc3/microservices/log_microservice.rb +7 -2
- data/lib/openc3/microservices/microservice.rb +10 -4
- data/lib/openc3/microservices/queue_microservice.rb +3 -0
- data/lib/openc3/microservices/scope_cleanup_microservice.rb +116 -1
- data/lib/openc3/microservices/text_log_microservice.rb +4 -1
- data/lib/openc3/migrations/20260204000000_remove_decom_reducer.rb +2 -0
- data/lib/openc3/models/activity_model.rb +15 -3
- data/lib/openc3/models/cvt_model.rb +2 -247
- data/lib/openc3/models/plugin_store_model.rb +1 -1
- data/lib/openc3/models/script_engine_model.rb +1 -1
- data/lib/openc3/models/target_model.rb +32 -34
- data/lib/openc3/models/tool_model.rb +18 -5
- data/lib/openc3/models/trigger_model.rb +1 -1
- data/lib/openc3/models/widget_model.rb +1 -2
- data/lib/openc3/operators/operator.rb +9 -7
- data/lib/openc3/packets/json_packet.rb +2 -0
- data/lib/openc3/packets/packet.rb +1 -0
- data/lib/openc3/packets/packet_config.rb +28 -12
- data/lib/openc3/script/calendar.rb +8 -0
- data/lib/openc3/script/script.rb +19 -0
- data/lib/openc3/script/storage.rb +6 -6
- data/lib/openc3/system/system.rb +6 -6
- data/lib/openc3/tools/cmd_tlm_server/interface_thread.rb +0 -2
- data/lib/openc3/top_level.rb +15 -63
- data/lib/openc3/topics/limits_event_topic.rb +1 -1
- data/lib/openc3/utilities/bucket_utilities.rb +3 -1
- data/lib/openc3/utilities/cli_generator.rb +7 -0
- data/lib/openc3/utilities/cmd_log.rb +1 -1
- data/lib/openc3/utilities/local_mode.rb +3 -0
- data/lib/openc3/utilities/process_manager.rb +1 -1
- data/lib/openc3/utilities/python_proxy.rb +11 -4
- data/lib/openc3/utilities/questdb_client.rb +735 -19
- data/lib/openc3/utilities/running_script.rb +25 -7
- data/lib/openc3/utilities/script.rb +452 -0
- data/lib/openc3/utilities/secrets.rb +1 -1
- data/lib/openc3/version.rb +5 -5
- data/templates/conversion/conversion.py +0 -8
- data/templates/conversion/conversion.rb +0 -11
- data/templates/tool_angular/package.json +2 -2
- data/templates/tool_react/package.json +1 -1
- data/templates/tool_svelte/package.json +1 -1
- data/templates/tool_vue/package.json +3 -3
- data/templates/widget/package.json +2 -2
- metadata +16 -2
- data/lib/openc3/migrations/20251022000000_remove_unique_id.rb +0 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0a06901f01bc9d8e7b6f0b123296da67475c08ea9cf50ef788a86b6da1308fcb
|
|
4
|
+
data.tar.gz: 34732b912cea1b4677f3e94ac0ba9f437e99217ccf54f9250816176ef5b63db1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5cda606ccb010606ad4b432685f53428cbf10b216f1cd06f2f55baac28fb723514dc3f8dfeadafc0958734237d3a7faf3f42439695481145914434e2a4bb1a58
|
|
7
|
+
data.tar.gz: 98532ea424c421554d45a6d155fadfc27e67ddf969a96be7653daebb30d5530dbb7601e13b5d3a3afa7dfd56718c161c060e81d67a5ee967a9248225554e1a03
|
|
@@ -53,7 +53,7 @@ RECONNECT_DELAY:
|
|
|
53
53
|
parameters:
|
|
54
54
|
- name: Delay
|
|
55
55
|
required: true
|
|
56
|
-
description: Delay in seconds between reconnect attempts. The default is
|
|
56
|
+
description: Delay in seconds between reconnect attempts. The default is 5 seconds.
|
|
57
57
|
values: ([0-9]*[.])?[0-9]+
|
|
58
58
|
DISABLE_DISCONNECT:
|
|
59
59
|
summary: Disable the Disconnect button on the Interfaces tab in the Server
|
|
@@ -72,7 +72,8 @@ GENERIC_READ_CONVERSION_START:
|
|
|
72
72
|
class (Note, referencing the packet as 'myself' is still supported for backwards
|
|
73
73
|
compatibility). The last line of code should return the converted
|
|
74
74
|
value. The GENERIC_READ_CONVERSION_END keyword specifies that all lines of
|
|
75
|
-
code for the conversion have been given.
|
|
75
|
+
code for the conversion have been given. To specify the bit size, type, and array size of the converted data,
|
|
76
|
+
use the CONVERTED_DATA keyword.
|
|
76
77
|
warning: Generic conversions are not a good long term solution. Consider creating
|
|
77
78
|
a conversion class and using READ_CONVERSION instead. READ_CONVERSION is easier
|
|
78
79
|
to debug and has higher performance.
|
|
@@ -81,22 +82,33 @@ GENERIC_READ_CONVERSION_START:
|
|
|
81
82
|
GENERIC_READ_CONVERSION_START
|
|
82
83
|
(value * 1.5).to_i # Convert the value by a scale factor
|
|
83
84
|
GENERIC_READ_CONVERSION_END
|
|
85
|
+
CONVERTED_DATA 32 UINT
|
|
84
86
|
python_example: |
|
|
85
87
|
APPEND_ITEM ITEM1 32 UINT
|
|
86
88
|
GENERIC_READ_CONVERSION_START
|
|
87
89
|
int(value * 1.5) # Convert the value by a scale factor
|
|
88
90
|
GENERIC_READ_CONVERSION_END
|
|
91
|
+
CONVERTED_DATA 32 UINT
|
|
92
|
+
GENERIC_READ_CONVERSION_END:
|
|
93
|
+
summary: Complete a generic read conversion
|
|
94
|
+
CONVERTED_DATA:
|
|
95
|
+
summary: Defines the bit size, type, and array size of the converted data for a read conversion
|
|
96
|
+
description: This keyword is used in conjunction with DERIVED items to specify the bit size, type, and array size of the converted data.
|
|
97
|
+
If this keyword is not used, DERIVED items are stored as strings in the decommutated data.
|
|
98
|
+
since: 7.0.0
|
|
89
99
|
parameters:
|
|
100
|
+
- name: Converted Bit Size
|
|
101
|
+
required: true
|
|
102
|
+
description: Bit size of converted value
|
|
103
|
+
values: \d+
|
|
90
104
|
- name: Converted Type
|
|
91
|
-
required:
|
|
105
|
+
required: true
|
|
92
106
|
description: Type of the converted value
|
|
93
107
|
values: <%= %w(INT UINT FLOAT STRING BLOCK) %>
|
|
94
|
-
- name: Converted
|
|
108
|
+
- name: Converted Array Size
|
|
95
109
|
required: false
|
|
96
|
-
description: Bit size of converted value
|
|
110
|
+
description: Bit size of the total array if the converted value is an array. Only specified if the converted type is an array.
|
|
97
111
|
values: \d+
|
|
98
|
-
GENERIC_READ_CONVERSION_END:
|
|
99
|
-
summary: Complete a generic read conversion
|
|
100
112
|
LIMITS:
|
|
101
113
|
summary: Defines a set of limits for a telemetry item
|
|
102
114
|
description: If limits are violated a message is printed in the Command and Telemetry Server
|
data/data/config/telemetry.yaml
CHANGED
|
@@ -20,7 +20,7 @@ require 'openc3/accessors/accessor'
|
|
|
20
20
|
OpenC3.disable_warnings do
|
|
21
21
|
class JsonPath
|
|
22
22
|
def self.process_object(obj_or_str, opts = {})
|
|
23
|
-
obj_or_str.is_a?(String) ?
|
|
23
|
+
obj_or_str.is_a?(String) ? JSON.parse(obj_or_str, max_nesting: opts[:max_nesting], create_additions: true, allow_nan: true) : obj_or_str
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
end
|
data/lib/openc3/api/tlm_api.rb
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
require 'openc3/models/target_model'
|
|
25
25
|
require 'openc3/models/cvt_model'
|
|
26
|
-
require 'openc3/packets/packet'
|
|
26
|
+
# require 'openc3/packets/packet' # Circular require
|
|
27
27
|
require 'openc3/topics/telemetry_topic'
|
|
28
28
|
require 'openc3/topics/interface_topic'
|
|
29
29
|
require 'openc3/topics/decom_interface_topic'
|
|
@@ -281,7 +281,7 @@ module OpenC3
|
|
|
281
281
|
|
|
282
282
|
case value_type
|
|
283
283
|
when 'FORMATTED', 'WITH_UNITS'
|
|
284
|
-
if item['format_string']
|
|
284
|
+
if item['format_string'] or item['units']
|
|
285
285
|
results << [target_name, orig_packet_name, item_name, 'FORMATTED'].join('__')
|
|
286
286
|
# This logic must match the logic in Packet#decom
|
|
287
287
|
elsif item['states'] or (item['read_conversion'] and item['data_type'] != 'DERIVED')
|
|
@@ -304,7 +304,7 @@ module OpenC3
|
|
|
304
304
|
if item['limits']['DEFAULT']
|
|
305
305
|
results[-1] += '__LIMITS'
|
|
306
306
|
end
|
|
307
|
-
rescue RuntimeError
|
|
307
|
+
rescue RuntimeError
|
|
308
308
|
results << nil
|
|
309
309
|
end
|
|
310
310
|
end
|
|
@@ -15,10 +15,11 @@
|
|
|
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/top_level'
|
|
18
|
+
# require 'openc3/top_level' # Circular require
|
|
19
19
|
require 'openc3/ext/config_parser' if RUBY_ENGINE == 'ruby' and !ENV['OPENC3_NO_EXT']
|
|
20
20
|
require 'erb'
|
|
21
21
|
require 'fileutils'
|
|
22
|
+
require 'tempfile'
|
|
22
23
|
|
|
23
24
|
module OpenC3
|
|
24
25
|
# Reads OpenC3 style configuration data which consists of keywords followed
|
|
@@ -218,6 +219,7 @@ module OpenC3
|
|
|
218
219
|
&)
|
|
219
220
|
ensure
|
|
220
221
|
file.close unless file.closed?
|
|
222
|
+
file.unlink
|
|
221
223
|
end
|
|
222
224
|
end
|
|
223
225
|
|
|
@@ -417,9 +419,7 @@ module OpenC3
|
|
|
417
419
|
elsif copy.include?(':') # Check for Windows drive letter
|
|
418
420
|
copy = copy.split(':')[1]
|
|
419
421
|
end
|
|
420
|
-
|
|
421
|
-
FileUtils.mkdir_p(File.dirname(parsed_filename)) # Create the path
|
|
422
|
-
file = File.open(parsed_filename, 'w+')
|
|
422
|
+
file = Tempfile.new(copy)
|
|
423
423
|
file.puts output
|
|
424
424
|
file.rewind # Rewind so the file is ready to read
|
|
425
425
|
file
|
|
@@ -20,11 +20,11 @@ module OpenC3
|
|
|
20
20
|
class Conversion
|
|
21
21
|
# @return [Symbol] The converted data type. Must be one of
|
|
22
22
|
# {OpenC3::StructureItem#data_type}
|
|
23
|
-
|
|
23
|
+
attr_accessor :converted_type
|
|
24
24
|
# @return [Integer] The size in bits of the converted value
|
|
25
|
-
|
|
25
|
+
attr_accessor :converted_bit_size
|
|
26
26
|
# @return [Integer] The size in bits of the converted array value
|
|
27
|
-
|
|
27
|
+
attr_accessor :converted_array_size
|
|
28
28
|
# @return [Array] The arguments passed to the conversion
|
|
29
29
|
attr_reader :params
|
|
30
30
|
|
|
@@ -126,6 +126,7 @@ module OpenC3
|
|
|
126
126
|
@cleanup_times = []
|
|
127
127
|
@previous_time_nsec_since_epoch = nil
|
|
128
128
|
@tmp_dir = Dir.mktmpdir
|
|
129
|
+
@wait_threads = []
|
|
129
130
|
|
|
130
131
|
# This is an optimization to avoid creating a new entry object
|
|
131
132
|
# each time we create an entry which we do a LOT!
|
|
@@ -154,9 +155,8 @@ module OpenC3
|
|
|
154
155
|
|
|
155
156
|
# Stops all logging and closes the current log file.
|
|
156
157
|
def stop
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return threads
|
|
158
|
+
@mutex.synchronize { close_file(false); @logging_enabled = false; }
|
|
159
|
+
return @wait_threads
|
|
160
160
|
end
|
|
161
161
|
|
|
162
162
|
# Stop all logging, close the current log file, and kill the logging threads.
|
|
@@ -173,6 +173,13 @@ module OpenC3
|
|
|
173
173
|
return threads
|
|
174
174
|
end
|
|
175
175
|
|
|
176
|
+
def cleanup
|
|
177
|
+
if @tmp_dir
|
|
178
|
+
FileUtils.remove_entry_secure(@tmp_dir, true)
|
|
179
|
+
@tmp_dir = nil
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
176
183
|
def graceful_kill
|
|
177
184
|
@cancel_threads = true
|
|
178
185
|
end
|
|
@@ -307,8 +314,19 @@ module OpenC3
|
|
|
307
314
|
# to keep a full file's worth of data in the stream. This is what prevents continuous stream growth.
|
|
308
315
|
# Returns thread that moves log to bucket
|
|
309
316
|
def close_file(take_mutex = true)
|
|
310
|
-
threads = []
|
|
311
317
|
@mutex.lock if take_mutex
|
|
318
|
+
|
|
319
|
+
# Remove old wait_threads
|
|
320
|
+
to_remove = []
|
|
321
|
+
@wait_threads.each do |thread|
|
|
322
|
+
unless thread.alive?
|
|
323
|
+
to_remove << thread
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
to_remove.each do |thread|
|
|
327
|
+
@wait_threads.delete(thread)
|
|
328
|
+
end
|
|
329
|
+
|
|
312
330
|
begin
|
|
313
331
|
if @file
|
|
314
332
|
begin
|
|
@@ -322,7 +340,7 @@ module OpenC3
|
|
|
322
340
|
# Cleanup timestamps here so they are unset for the next file
|
|
323
341
|
@first_time = nil
|
|
324
342
|
@last_time = nil
|
|
325
|
-
|
|
343
|
+
@wait_threads << BucketUtilities.move_log_file_to_bucket(@filename, bucket_key)
|
|
326
344
|
# Now that the file is in storage, trim the Redis stream after a delay
|
|
327
345
|
@cleanup_offsets << {}
|
|
328
346
|
@last_offsets.each do |redis_topic, last_offset|
|
|
@@ -342,7 +360,7 @@ module OpenC3
|
|
|
342
360
|
ensure
|
|
343
361
|
@mutex.unlock if take_mutex
|
|
344
362
|
end
|
|
345
|
-
return
|
|
363
|
+
return @wait_threads
|
|
346
364
|
end
|
|
347
365
|
|
|
348
366
|
def bucket_filename
|
|
@@ -137,7 +137,6 @@ module OpenC3
|
|
|
137
137
|
# Closing a log file isn't critical so we just log an error
|
|
138
138
|
# Returns threads that moves log to bucket
|
|
139
139
|
def close_file(take_mutex = true)
|
|
140
|
-
threads = []
|
|
141
140
|
@mutex.lock if take_mutex
|
|
142
141
|
begin
|
|
143
142
|
# Need to write the OFFSET_MARKER for each packet
|
|
@@ -145,12 +144,10 @@ module OpenC3
|
|
|
145
144
|
write_entry(:OFFSET_MARKER, nil, nil, nil, nil, nil, last_offset + ',' + redis_topic, nil) if @file
|
|
146
145
|
end
|
|
147
146
|
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
return super(false)
|
|
150
148
|
ensure
|
|
151
149
|
@mutex.unlock if take_mutex
|
|
152
150
|
end
|
|
153
|
-
return threads
|
|
154
151
|
end
|
|
155
152
|
|
|
156
153
|
def get_packet_index(cmd_or_tlm, target_name, packet_name, entry_type, data)
|
|
@@ -43,13 +43,20 @@ module OpenC3
|
|
|
43
43
|
|
|
44
44
|
# Close any open stream log files
|
|
45
45
|
def stop
|
|
46
|
-
@read_log.stop
|
|
47
|
-
@write_log.stop
|
|
46
|
+
threads = @read_log.stop
|
|
47
|
+
threads.concat(@write_log.stop)
|
|
48
|
+
return threads
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
def shutdown
|
|
51
|
-
@read_log.shutdown
|
|
52
|
-
@write_log.shutdown
|
|
52
|
+
threads = @read_log.shutdown
|
|
53
|
+
threads.concat(@write_log.shutdown)
|
|
54
|
+
return threads
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def cleanup
|
|
58
|
+
@read_log.cleanup
|
|
59
|
+
@write_log.cleanup
|
|
53
60
|
end
|
|
54
61
|
|
|
55
62
|
# Clone the stream log pair
|
|
@@ -68,7 +68,6 @@ module OpenC3
|
|
|
68
68
|
# Closing a log file isn't critical so we just log an error
|
|
69
69
|
# Returns threads that moves log to bucket
|
|
70
70
|
def close_file(take_mutex = true)
|
|
71
|
-
threads = []
|
|
72
71
|
@mutex.lock if take_mutex
|
|
73
72
|
begin
|
|
74
73
|
# Need to write the OFFSET_MARKER for each packet
|
|
@@ -79,12 +78,10 @@ module OpenC3
|
|
|
79
78
|
write_entry(time.to_nsec_from_epoch, data.as_json(allow_nan: true).to_json(allow_nan: true)) if @file
|
|
80
79
|
end
|
|
81
80
|
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
return super(false)
|
|
84
82
|
ensure
|
|
85
83
|
@mutex.unlock if take_mutex
|
|
86
84
|
end
|
|
87
|
-
return threads
|
|
88
85
|
end
|
|
89
86
|
|
|
90
87
|
def extension
|
|
@@ -779,7 +779,6 @@ module OpenC3
|
|
|
779
779
|
else
|
|
780
780
|
@logger.error "#{@interface.name}: #{connect_error.formatted}"
|
|
781
781
|
unless @connection_failed_messages.include?(connect_error.message)
|
|
782
|
-
OpenC3.write_exception_file(connect_error)
|
|
783
782
|
@connection_failed_messages << connect_error.message
|
|
784
783
|
end
|
|
785
784
|
end
|
|
@@ -800,7 +799,6 @@ module OpenC3
|
|
|
800
799
|
else
|
|
801
800
|
@logger.error "#{@interface.name}: #{err.formatted}"
|
|
802
801
|
unless @connection_lost_messages.include?(err.message)
|
|
803
|
-
OpenC3.write_exception_file(err)
|
|
804
802
|
@connection_lost_messages << err.message
|
|
805
803
|
end
|
|
806
804
|
end
|
|
@@ -888,6 +886,14 @@ module OpenC3
|
|
|
888
886
|
def shutdown(_sig = nil)
|
|
889
887
|
@logger.info "#{@interface ? @interface.name : @name}: shutdown requested"
|
|
890
888
|
stop()
|
|
889
|
+
if @interface and @interface.stream_log_pair
|
|
890
|
+
threads = @interface.stream_log_pair.shutdown
|
|
891
|
+
# Wait for all the logging threads to move files to buckets
|
|
892
|
+
threads.flatten.compact.each do |thread|
|
|
893
|
+
thread.join
|
|
894
|
+
end
|
|
895
|
+
@interface.stream_log_pair.cleanup
|
|
896
|
+
end
|
|
891
897
|
super()
|
|
892
898
|
end
|
|
893
899
|
|
|
@@ -125,8 +125,8 @@ module OpenC3
|
|
|
125
125
|
def shutdown
|
|
126
126
|
# Make sure all the existing logs are properly closed down
|
|
127
127
|
threads = []
|
|
128
|
-
@plws.each do |
|
|
129
|
-
plw_hash.each do |
|
|
128
|
+
@plws.each do |_target_name, plw_hash|
|
|
129
|
+
plw_hash.each do |_type, plw|
|
|
130
130
|
threads.concat(plw.shutdown)
|
|
131
131
|
end
|
|
132
132
|
end
|
|
@@ -134,6 +134,11 @@ module OpenC3
|
|
|
134
134
|
threads.flatten.compact.each do |thread|
|
|
135
135
|
thread.join
|
|
136
136
|
end
|
|
137
|
+
@plws.each do |_target_name, plw_hash|
|
|
138
|
+
plw_hash.each do |_type, plw|
|
|
139
|
+
plw.cleanup
|
|
140
|
+
end
|
|
141
|
+
end
|
|
137
142
|
super()
|
|
138
143
|
end
|
|
139
144
|
end
|
|
@@ -88,6 +88,7 @@ module OpenC3
|
|
|
88
88
|
@name = name
|
|
89
89
|
split_name = name.split("__")
|
|
90
90
|
raise "Name #{name} doesn't match convention of SCOPE__TYPE__NAME" if split_name.length != 3
|
|
91
|
+
microservice_type = split_name[1].to_s.upcase
|
|
91
92
|
|
|
92
93
|
@scope = split_name[0]
|
|
93
94
|
$openc3_scope = @scope
|
|
@@ -102,8 +103,14 @@ module OpenC3
|
|
|
102
103
|
|
|
103
104
|
OpenC3.setup_open_telemetry(@name, false)
|
|
104
105
|
|
|
106
|
+
@temp_dir = OpenC3.sanitize_path(File.join(Dir.tmpdir, @name))
|
|
107
|
+
|
|
105
108
|
# Create temp folder for this microservice
|
|
106
|
-
|
|
109
|
+
# This will already have been setup by plugin_microservice.rb if USER
|
|
110
|
+
if is_plugin or microservice_type != 'USER'
|
|
111
|
+
FileUtils.remove_entry_secure(@temp_dir, true)
|
|
112
|
+
Dir.mkdir(@temp_dir)
|
|
113
|
+
end
|
|
107
114
|
|
|
108
115
|
# Get microservice configuration from Redis
|
|
109
116
|
@config = MicroserviceModel.get(name: @name, scope: @scope)
|
|
@@ -142,14 +149,13 @@ module OpenC3
|
|
|
142
149
|
cmd_array = @config["cmd"]
|
|
143
150
|
|
|
144
151
|
# Get Microservice files from bucket storage
|
|
145
|
-
temp_dir = Dir.mktmpdir
|
|
146
152
|
bucket = ENV['OPENC3_CONFIG_BUCKET']
|
|
147
153
|
client = Bucket.getClient()
|
|
148
154
|
|
|
149
155
|
prefix = "#{@scope}/microservices/#{@name}/"
|
|
150
156
|
file_count = 0
|
|
151
157
|
client.list_objects(bucket: bucket, prefix: prefix).each do |object|
|
|
152
|
-
response_target = File.join(temp_dir, object.key.split(prefix)[-1])
|
|
158
|
+
response_target = OpenC3.sanitize_path(File.join(@temp_dir, object.key.split(prefix)[-1]))
|
|
153
159
|
FileUtils.mkdir_p(File.dirname(response_target))
|
|
154
160
|
client.get_object(bucket: bucket, key: object.key, path: response_target)
|
|
155
161
|
file_count += 1
|
|
@@ -157,7 +163,7 @@ module OpenC3
|
|
|
157
163
|
|
|
158
164
|
# Adjust @work_dir to microservice files downloaded if files and a relative path
|
|
159
165
|
if file_count > 0 and @work_dir[0] != '/'
|
|
160
|
-
@work_dir = File.join(temp_dir, @work_dir)
|
|
166
|
+
@work_dir = OpenC3.sanitize_path(File.join(@temp_dir, @work_dir))
|
|
161
167
|
end
|
|
162
168
|
|
|
163
169
|
# Check Syntax on any ruby files
|
|
@@ -18,6 +18,8 @@ require 'openc3/utilities/authentication'
|
|
|
18
18
|
require 'openc3/api/api'
|
|
19
19
|
|
|
20
20
|
module OpenC3
|
|
21
|
+
saved_verbose = $VERBOSE
|
|
22
|
+
$VERBOSE = false
|
|
21
23
|
module Script
|
|
22
24
|
private
|
|
23
25
|
# Override the prompt_for_hazardous method to always return true since there is no user to prompt
|
|
@@ -25,6 +27,7 @@ module OpenC3
|
|
|
25
27
|
return true
|
|
26
28
|
end
|
|
27
29
|
end
|
|
30
|
+
$VERBOSE = saved_verbose
|
|
28
31
|
|
|
29
32
|
# The queue processor runs in a single thread and processes commands via cmd_api.
|
|
30
33
|
class QueueProcessor
|
|
@@ -16,6 +16,120 @@ require 'openc3/microservices/cleanup_microservice'
|
|
|
16
16
|
|
|
17
17
|
module OpenC3
|
|
18
18
|
class ScopeCleanupMicroservice < CleanupMicroservice
|
|
19
|
+
TSDB_HEALTH_QUERY =
|
|
20
|
+
"SELECT
|
|
21
|
+
table_name,
|
|
22
|
+
table_row_count,
|
|
23
|
+
wal_pending_row_count,
|
|
24
|
+
CASE
|
|
25
|
+
WHEN table_suspended THEN 'SUSPENDED'
|
|
26
|
+
WHEN table_memory_pressure_level = 2 THEN 'BACKOFF'
|
|
27
|
+
WHEN table_memory_pressure_level = 1 THEN 'PRESSURE'
|
|
28
|
+
ELSE 'OK'
|
|
29
|
+
END AS status,
|
|
30
|
+
wal_txn - table_txn AS lag_txns,
|
|
31
|
+
table_write_amp_p50 AS write_amp,
|
|
32
|
+
table_merge_rate_p99 AS slowest_merge
|
|
33
|
+
FROM tables()
|
|
34
|
+
WHERE walEnabled
|
|
35
|
+
ORDER BY
|
|
36
|
+
table_suspended DESC,
|
|
37
|
+
table_memory_pressure_level DESC,
|
|
38
|
+
wal_pending_row_count DESC;"
|
|
39
|
+
|
|
40
|
+
GROWTH_NUM_SAMPLE_PERIODS = 4
|
|
41
|
+
|
|
42
|
+
def initialize(*args)
|
|
43
|
+
super(*args)
|
|
44
|
+
@run_time = nil
|
|
45
|
+
@cleanup_poll_time = nil
|
|
46
|
+
@delta_time = 0.0
|
|
47
|
+
@wal_pending_row_count = {}
|
|
48
|
+
@lag_txns = {}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def cleanup(areas, bucket)
|
|
52
|
+
current_time = Time.now
|
|
53
|
+
if @run_time
|
|
54
|
+
delta = current_time - @run_time
|
|
55
|
+
if delta > 0.0
|
|
56
|
+
@delta_time += delta
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
@run_time = current_time
|
|
60
|
+
if @delta_time > @cleanup_poll_time
|
|
61
|
+
@delta_time = 0.0
|
|
62
|
+
super(areas, bucket)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Always check TSDB health
|
|
66
|
+
if @scope == 'DEFAULT'
|
|
67
|
+
begin
|
|
68
|
+
conn = OpenC3::QuestDBClient.connection
|
|
69
|
+
result = conn.exec(TSDB_HEALTH_QUERY)
|
|
70
|
+
columns = result.fields
|
|
71
|
+
rows = result.values
|
|
72
|
+
|
|
73
|
+
table_name_column = columns.index("table_name")
|
|
74
|
+
wal_pending_row_count_column = columns.index("wal_pending_row_count")
|
|
75
|
+
status_column = columns.index("status")
|
|
76
|
+
lag_txns_column = columns.index("lag_txns")
|
|
77
|
+
|
|
78
|
+
rows.each do |values|
|
|
79
|
+
table_name = values[table_name_column]
|
|
80
|
+
wal_pending_row_count = values[wal_pending_row_count_column].to_i
|
|
81
|
+
status = values[status_column]
|
|
82
|
+
lag_txns = values[lag_txns_column].to_i
|
|
83
|
+
|
|
84
|
+
if status != 'OK'
|
|
85
|
+
@logger.error("QuestDB: #{table_name} in bad state: #{status}")
|
|
86
|
+
|
|
87
|
+
if status == 'SUSPENDED'
|
|
88
|
+
# Try to automatically unsuspend
|
|
89
|
+
@logger.info("QuestDB: Attempting to unsuspend: #{table_name}")
|
|
90
|
+
conn.exec("ALTER TABLE #{table_name} RESUME WAL;")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
@wal_pending_row_count[table_name] ||= []
|
|
95
|
+
@wal_pending_row_count[table_name] << wal_pending_row_count
|
|
96
|
+
@lag_txns[table_name] ||= []
|
|
97
|
+
@lag_txns[table_name] << lag_txns
|
|
98
|
+
|
|
99
|
+
if @wal_pending_row_count[table_name].length > GROWTH_NUM_SAMPLE_PERIODS
|
|
100
|
+
if detect_growth(@wal_pending_row_count[table_name], GROWTH_NUM_SAMPLE_PERIODS)
|
|
101
|
+
# Crossed threshold of sample periods of growth
|
|
102
|
+
@logger.error("QuestDB: #{table_name} has growing wal_pending_row_count: #{wal_pending_row_count}")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Leave the last GROWTH_NUM_SAMPLE_PERIODS samples
|
|
106
|
+
@wal_pending_row_count[table_name] = @wal_pending_row_count[table_name][-GROWTH_NUM_SAMPLE_PERIODS..-1]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
if @lag_txns[table_name].length > GROWTH_NUM_SAMPLE_PERIODS
|
|
110
|
+
if detect_growth(@lag_txns[table_name], GROWTH_NUM_SAMPLE_PERIODS)
|
|
111
|
+
# Crossed threshold of sample periods of growth
|
|
112
|
+
@logger.error("QuestDB: #{table_name} has growing lag_txns: #{lag_txns}")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Leave the last GROWTH_NUM_SAMPLE_PERIODS samples
|
|
116
|
+
@lag_txns[table_name] = @lag_txns[table_name][-GROWTH_NUM_SAMPLE_PERIODS..-1]
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
rescue => e
|
|
120
|
+
OpenC3::QuestDBClient.disconnect
|
|
121
|
+
@logger.error("QuestDB Error: #{e.formatted}")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def detect_growth(array, num_samples)
|
|
127
|
+
num_samples.times do |index|
|
|
128
|
+
return false if array[index + 1] <= array[index]
|
|
129
|
+
end
|
|
130
|
+
return true
|
|
131
|
+
end
|
|
132
|
+
|
|
19
133
|
def get_areas_and_poll_time
|
|
20
134
|
scope = ScopeModel.get_model(name: @scope)
|
|
21
135
|
areas = [
|
|
@@ -28,7 +142,8 @@ module OpenC3
|
|
|
28
142
|
areas << ["NOSCOPE/tool_logs/sr", scope.tool_log_retain_time]
|
|
29
143
|
end
|
|
30
144
|
|
|
31
|
-
|
|
145
|
+
@cleanup_poll_time = scope.cleanup_poll_time
|
|
146
|
+
return areas, 60 # Run every 1 minute for TSDB checks
|
|
32
147
|
end
|
|
33
148
|
end
|
|
34
149
|
end
|
|
@@ -98,13 +98,16 @@ module OpenC3
|
|
|
98
98
|
def shutdown
|
|
99
99
|
# Make sure all the existing logs are properly closed down
|
|
100
100
|
threads = []
|
|
101
|
-
@tlws.each do |
|
|
101
|
+
@tlws.each do |_topic, tlw|
|
|
102
102
|
threads.concat(tlw.shutdown)
|
|
103
103
|
end
|
|
104
104
|
# Wait for all the logging threads to move files to buckets
|
|
105
105
|
threads.flatten.compact.each do |thread|
|
|
106
106
|
thread.join
|
|
107
107
|
end
|
|
108
|
+
@tlws.each do |_topic, tlw|
|
|
109
|
+
tlw.cleanup
|
|
110
|
+
end
|
|
108
111
|
super()
|
|
109
112
|
end
|
|
110
113
|
end
|
|
@@ -23,6 +23,8 @@ module OpenC3
|
|
|
23
23
|
target_models = TargetModel.all(scope: scope)
|
|
24
24
|
target_models.each do |name, target_model|
|
|
25
25
|
# Remove deprecated decom log settings from target model
|
|
26
|
+
target_model.delete("cmd_unique_id_mode")
|
|
27
|
+
target_model.delete("tlm_unique_id_mode")
|
|
26
28
|
target_model.delete("cmd_decom_log_cycle_time")
|
|
27
29
|
target_model.delete("cmd_decom_log_cycle_size")
|
|
28
30
|
target_model.delete("cmd_decom_log_retain_time")
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
# https://www.rubydoc.info/gems/redis/Redis/Commands/SortedSets
|
|
19
19
|
|
|
20
20
|
require 'openc3/models/model'
|
|
21
|
+
require 'openc3/models/timeline_model'
|
|
21
22
|
require 'openc3/topics/timeline_topic'
|
|
22
23
|
require 'securerandom'
|
|
23
24
|
|
|
@@ -28,6 +29,11 @@ module OpenC3
|
|
|
28
29
|
|
|
29
30
|
class ActivityModel < Model
|
|
30
31
|
MAX_DURATION = Time::SEC_PER_DAY
|
|
32
|
+
# Grace window (in seconds) to allow creating activities slightly in the past.
|
|
33
|
+
# This handles race conditions where real-time activity notifications arrive
|
|
34
|
+
# after the start time has already passed (e.g. from external systems).
|
|
35
|
+
# This is consistent with the -15 second window in the timeline microservice.
|
|
36
|
+
START_GRACE_SECONDS = 15
|
|
31
37
|
PRIMARY_KEY = '__openc3_timelines'.freeze # MUST be equal to `TimelineModel::PRIMARY_KEY` minus the leading __
|
|
32
38
|
# See run_activity(activity) in openc3/lib/openc3/microservices/timeline_microservice.rb
|
|
33
39
|
VALID_KINDS = %w(command script reserve expire)
|
|
@@ -212,7 +218,7 @@ module OpenC3
|
|
|
212
218
|
end
|
|
213
219
|
|
|
214
220
|
# validate the input to the rules we have created for timelines.
|
|
215
|
-
# - A task's start MUST NOT be in the past.
|
|
221
|
+
# - A task's start MUST NOT be more than START_GRACE_SECONDS in the past.
|
|
216
222
|
# - A task's start MUST be before the stop.
|
|
217
223
|
# - A task CAN NOT be longer than MAX_DURATION (86400) in seconds.
|
|
218
224
|
# - A task MUST have a kind.
|
|
@@ -230,8 +236,8 @@ module OpenC3
|
|
|
230
236
|
rescue NoMethodError
|
|
231
237
|
raise ActivityInputError.new "start and stop must be seconds: #{start}, #{stop}"
|
|
232
238
|
end
|
|
233
|
-
if now_f >= start and kind != 'expire'
|
|
234
|
-
raise ActivityInputError.new "activity must be in the
|
|
239
|
+
if now_f >= start + START_GRACE_SECONDS and kind != 'expire'
|
|
240
|
+
raise ActivityInputError.new "activity must not be more than #{START_GRACE_SECONDS} seconds in the past, current_time: #{now_f} vs #{start}"
|
|
235
241
|
elsif duration > MAX_DURATION and kind != 'expire'
|
|
236
242
|
raise ActivityInputError.new "activity can not be longer than #{MAX_DURATION} seconds"
|
|
237
243
|
elsif duration <= 0
|
|
@@ -262,6 +268,12 @@ module OpenC3
|
|
|
262
268
|
# Update the Redis hash at primary_key and set the score equal to the start Epoch time
|
|
263
269
|
# the member is set to the JSON generated via calling as_json
|
|
264
270
|
def create(overlap: true, username: nil)
|
|
271
|
+
# Validate that the timeline exists in this scope before creating activities.
|
|
272
|
+
# Activities must be attached to an existing timeline within the same scope.
|
|
273
|
+
unless TimelineModel.get(name: @name, scope: @scope)
|
|
274
|
+
raise ActivityError.new "timeline '#{@name}' does not exist in scope '#{@scope}'"
|
|
275
|
+
end
|
|
276
|
+
|
|
265
277
|
if @recurring['end'] and @recurring['frequency'] and @recurring['span']
|
|
266
278
|
# First validate the initial recurring activity ... all others are just offsets
|
|
267
279
|
validate_input(start: @start, stop: @stop, kind: @kind, data: @data)
|