openc3 7.1.1 → 7.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 719b55747a7004cc44da1976c3f15eec6c67a3fca702b56792862f59f02cb8b5
4
- data.tar.gz: c80082e75c059dac71d15a56264cba9c968f82c494e564e33b598c75ac01a6c2
3
+ metadata.gz: 90e17f6bf28158ea786646171220e3c23aca07cce30559b3e64a29f83dabb50d
4
+ data.tar.gz: 9a13e73273ba67e7d6003a9578a34a8b1c0abadea3b8d4d369f1fd3803a4f0ec
5
5
  SHA512:
6
- metadata.gz: 5b3c1ca82717f1debd25ab4a6ac0239aa9baee2b5f736f61b7f52103397b608ab3de8fd0d50e10f0a4481b507b2a519987a4c4c68f7d0f3770cd48f3e165b8eb
7
- data.tar.gz: d2bc96742a2555be7c8149dd728155a76ba7c1a16474eb26a36388bdedaacc1fba9c447ab373beebd8fd58ac81248541531de1a163bc156be195c76e34b515f2
6
+ metadata.gz: eec5ccdb945747570717e5f60f9a89158d500d994f1fa5e6ebc81ed8bb263229950f10e1f277e0c68bf5d9fb531f3470354d45fda2164f7608c9ad22413b2022
7
+ data.tar.gz: a32f7675eacf6b41b345a4b5ae554292db011499d12a9e5e65173c514e5fac7e66da3bacea7ad5cf9a3738fcb96e3a82009997993fc70722d4a9ba4edd236261
@@ -417,12 +417,12 @@ VALIDATOR:
417
417
  return [False, "TGT PKT ITEM is 0"]
418
418
  self.cmd_acpt_cnt = tlm("INST HEALTH_STATUS CMD_ACPT_CNT")
419
419
  # Return true to indicate Success, false to indicate Failure,
420
- # and nil to indicate Unknown. The second value is the optional message.
420
+ # and None to indicate Unknown. The second value is the optional message.
421
421
  return [True, None]
422
422
 
423
423
  def post_check(self, command):
424
424
  wait_check(f"INST HEALTH_STATUS CMD_ACPT_CNT > {self.cmd_acpt_cnt}", 10)
425
425
  # Return true to indicate Success, false to indicate Failure,
426
- # and nil to indicate Unknown. The second value is the optional message.
426
+ # and None to indicate Unknown. The second value is the optional message.
427
427
  return [True, None]
428
428
  since: 5.19.0
@@ -111,20 +111,27 @@ CONVERTED_DATA:
111
111
  values: \d+
112
112
  LIMITS:
113
113
  summary: Defines a set of limits for a telemetry item
114
- description: If limits are violated a message is printed in the Command and Telemetry Server
114
+ description: |
115
+ If limits are violated a message is printed in the Command and Telemetry Server
115
116
  to indicate an item went out of limits. Other tools also use this information
116
117
  to update displays with different colored telemetry items or other useful information.
117
118
  The concept of "limits sets" is defined to allow for different limits values
118
119
  in different environments. For example, you might want tighter or looser limits
119
120
  on telemetry if your environment changes such as during thermal vacuum testing.
121
+
122
+ A DEFAULT limits set is required for every telemetry item with limits. If you
123
+ define additional named sets (e.g. TVAC), the DEFAULT set must be defined first.
124
+ Attempting to define a named set before DEFAULT will raise an error of the form
125
+ "DEFAULT limits set must be defined for TARGET PACKET ITEM before setting limits set NAME".
120
126
  example: |
121
127
  LIMITS DEFAULT 3 ENABLED -80.0 -70.0 60.0 80.0 -20.0 20.0
122
128
  LIMITS TVAC 3 ENABLED -80.0 -30.0 30.0 80.0
123
129
  parameters:
124
130
  - name: Limits Set
125
131
  required: true
126
- description: Name of the limits set. If you have no unique limits sets use
127
- the keyword DEFAULT.
132
+ description: Name of the limits set. A DEFAULT set is required and must be
133
+ defined before any other named sets for this item. If you have no unique
134
+ limits sets use the keyword DEFAULT.
128
135
  values: .+
129
136
  - name: Persistence
130
137
  required: true
@@ -457,6 +457,12 @@ module OpenC3
457
457
  alias subscribe_packet subscribe_packets
458
458
 
459
459
  # Get packets based on ID returned from subscribe_packet.
460
+ # Packets are ordered within each subscribed packet stream (target/packet pair)
461
+ # but are NOT interleaved by time across streams. If chronological order across
462
+ # streams is required, sort the returned array by the 'time' field. Sorting only
463
+ # orders the current batch - packets across separate get_packets calls may still
464
+ # arrive out of order, so subscribers needing global ordering must buffer and merge
465
+ # across calls.
460
466
  # @param id [String] ID returned from subscribe_packets or last call to get_packets
461
467
  # @param block [Integer] Unused - Blocking must be implemented at the client
462
468
  # @param count [Integer] Maximum number of packets to return from EACH packet stream
@@ -155,11 +155,26 @@ module OpenC3
155
155
 
156
156
  prefix = "#{@scope}/microservices/#{@name}/"
157
157
  file_count = 0
158
- client.list_objects(bucket: bucket, prefix: prefix).each do |object|
159
- response_target = OpenC3.sanitize_path(File.join(@temp_dir, object.key.split(prefix)[-1]))
160
- FileUtils.mkdir_p(File.dirname(response_target))
161
- client.get_object(bucket: bucket, key: object.key, path: response_target)
162
- file_count += 1
158
+ # Tolerate transient object store failures during startup. On a busy or
159
+ # underpowered cluster the bucket store (e.g. MinIO) can be briefly
160
+ # unreachable while many microservices start at once. Rather than crash
161
+ # and CrashLoopBackOff (which can outlast deploy timeouts), retry for a
162
+ # bounded time before giving up.
163
+ startup_timeout = (ENV['OPENC3_MICROSERVICE_STARTUP_BUCKET_TIMEOUT'] || 60).to_f
164
+ startup_deadline = Time.now + startup_timeout
165
+ begin
166
+ file_count = 0
167
+ client.list_objects(bucket: bucket, prefix: prefix).each do |object|
168
+ response_target = OpenC3.sanitize_path(File.join(@temp_dir, object.key.split(prefix)[-1]))
169
+ FileUtils.mkdir_p(File.dirname(response_target))
170
+ client.get_object(bucket: bucket, key: object.key, path: response_target)
171
+ file_count += 1
172
+ end
173
+ rescue => error
174
+ raise if Time.now >= startup_deadline
175
+ @logger.warn("Microservice #{@name} startup: bucket access failed (#{error.class}: #{error.message}); retrying for up to #{startup_timeout.to_i}s")
176
+ sleep(5)
177
+ retry
163
178
  end
164
179
 
165
180
  # Adjust @work_dir to microservice files downloaded if files and a relative path
@@ -238,6 +238,11 @@ module OpenC3
238
238
 
239
239
  OperatorProcess.setup()
240
240
  @cycle_time = (ENV['OPERATOR_CYCLE_TIME'] and ENV['OPERATOR_CYCLE_TIME'].to_f) || CYCLE_TIME # time in seconds
241
+ # Maximum number of new microservices to start per cycle. This spreads a
242
+ # large startup burst (e.g. installing a plugin with many targets) across
243
+ # multiple cycles so the shared services (object store, redis) aren't
244
+ # stampeded by every microservice connecting at once. 0 = no limit.
245
+ @max_start_per_cycle = (ENV['OPENC3_OPERATOR_MAX_START_PER_CYCLE'] || 5).to_i
241
246
 
242
247
  @ruby_process_name = ENV['OPENC3_RUBY']
243
248
  if RUBY_ENGINE != 'ruby'
@@ -262,10 +267,17 @@ module OpenC3
262
267
  def start_new
263
268
  @mutex.synchronize do
264
269
  if @new_processes.length > 0
265
- # Start all the processes
266
- Logger.info("#{self.class} starting each new process...")
267
- @new_processes.each { |_name, p| p.start }
268
- @new_processes = {}
270
+ # Start at most @max_start_per_cycle processes this cycle; any
271
+ # remaining stay queued in @new_processes and start on later cycles.
272
+ # This avoids a startup stampede when many microservices appear at
273
+ # once (e.g. a plugin install) overwhelming the object store / redis.
274
+ start_names = @new_processes.keys
275
+ start_names = start_names[0...@max_start_per_cycle] if @max_start_per_cycle > 0
276
+ Logger.info("#{self.class} starting #{start_names.length} of #{@new_processes.length} new process(es)...")
277
+ start_names.each do |name|
278
+ @new_processes[name].start
279
+ @new_processes.delete(name)
280
+ end
269
281
  end
270
282
  end
271
283
  end
@@ -273,12 +285,22 @@ module OpenC3
273
285
  def respawn_changed
274
286
  @mutex.synchronize do
275
287
  if @changed_processes.length > 0
276
- Logger.info("Cycling #{@changed_processes.length} changed microservices...")
277
- shutdown_processes(@changed_processes)
288
+ # Cycle at most @max_start_per_cycle changed microservices this cycle;
289
+ # any remaining stay queued in @changed_processes and are cycled on
290
+ # later cycles. This avoids a restart stampede when many microservices
291
+ # change at once (e.g. a configmap change) overwhelming shared
292
+ # services. Processes not yet cycled keep running until their turn.
293
+ cycle_names = @changed_processes.keys
294
+ cycle_names = cycle_names[0...@max_start_per_cycle] if @max_start_per_cycle > 0
295
+ cycle = @changed_processes.slice(*cycle_names)
296
+ Logger.info("Cycling #{cycle.length} of #{@changed_processes.length} changed microservices...")
297
+ shutdown_processes(cycle)
278
298
  break if @shutdown
279
299
 
280
- @changed_processes.each { |_name, p| p.start }
281
- @changed_processes = {}
300
+ cycle_names.each do |name|
301
+ @changed_processes[name].start
302
+ @changed_processes.delete(name)
303
+ end
282
304
  end
283
305
  end
284
306
  end
@@ -295,8 +317,11 @@ module OpenC3
295
317
 
296
318
  def respawn_dead
297
319
  @mutex.synchronize do
298
- @processes.each do |_name, p|
320
+ @processes.each do |name, p|
299
321
  break if @shutdown
322
+ # Skip processes still queued by the per-cycle start limit; they
323
+ # haven't been started yet so they aren't "dead" to be respawned.
324
+ next if @new_processes[name]
300
325
  p.output_increment
301
326
  unless p.alive?
302
327
  # Respawn process
@@ -437,12 +437,25 @@ module OpenC3
437
437
  if packet.id_items.length > 0
438
438
  key = []
439
439
  id_signature = ""
440
- # Accessor class is part of the signature so packets in the same target
441
- # with different accessors trigger unique_id_mode (different accessors
442
- # decode the buffer differently, so the hash-lookup path is unsafe).
440
+ # Accessor class AND args are part of the signature so packets in the
441
+ # same target with different accessors -- or the same accessor class
442
+ # configured with different args -- trigger unique_id_mode. Different
443
+ # accessors (or same accessor, different args) decode the buffer
444
+ # differently, so the shared hash-lookup path is unsafe.
443
445
  packet.id_items.each do |item|
444
446
  key << item.id_value
445
- id_signature << "__#{item.key}__#{item.bit_offset}__#{item.bit_size}__#{item.data_type}__#{packet.accessor.class.to_s}"
447
+ id_signature << "__#{item.key}__#{item.bit_offset}__#{item.bit_size}__#{item.data_type}__#{packet.accessor.class.to_s}__#{packet.accessor.args.inspect}"
448
+ # STRUCTURE-derived id_items are decoded by the parent's structure
449
+ # accessor (see Accessor#read_item), so include that accessor's class
450
+ # and args in the signature too.
451
+ if item.parent_item
452
+ parent = packet.get_item(item.parent_item)
453
+ structure = parent.structure
454
+ if structure
455
+ structure_accessor = structure.accessor
456
+ id_signature << "__#{structure_accessor.class.to_s}__#{structure_accessor.args.inspect}"
457
+ end
458
+ end
446
459
  end
447
460
  target_id_value_hash[key] = packet
448
461
  target_id_signature = id_signature_hash[packet.target_name]
@@ -287,7 +287,7 @@ module OpenC3
287
287
  # Find all the script methods
288
288
  methods = []
289
289
  self.instance_methods.each do |method_name|
290
- if /^test|^script|op_/.match?(method_name.to_s)
290
+ if /^test_|^script_|^op_/.match?(method_name.to_s)
291
291
  methods << method_name.to_s
292
292
  end
293
293
  end
@@ -91,6 +91,10 @@ module OpenC3
91
91
  # Will subscribe to the channel based on @identifier
92
92
  def subscribe
93
93
  unless @subscribed
94
+ # Token is part of the identifier so it surfaces as params[:token] in
95
+ # ApplicationCable::Channel#authenticate_subscription! — ActionCable
96
+ # ignores `data` on `subscribe` commands.
97
+ @identifier['token'] = @authentication.token(include_bearer: false)
94
98
  json_hash = {}
95
99
  json_hash['command'] = 'subscribe'
96
100
  json_hash['identifier'] = JSON.generate(@identifier, allow_nan: true)
@@ -128,7 +132,7 @@ module OpenC3
128
132
  # Connect to the websocket with authorization in query params
129
133
  def connect
130
134
  disconnect()
131
- final_url = @url + "?scope=#{@scope}&authorization=#{@authentication.get_otp(scope: @scope)}"
135
+ final_url = @url + "?scope=#{@scope}"
132
136
  @stream = WebSocketClientStream.new(final_url, @write_timeout, @read_timeout, @connect_timeout)
133
137
  @stream.headers = {
134
138
  'Sec-WebSocket-Protocol' => 'actioncable-v1-json, actioncable-unsupported',