openc3 7.0.1 → 7.1.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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +47 -3
  3. data/data/config/item_modifiers.yaml +1 -1
  4. data/data/config/microservice.yaml +12 -1
  5. data/data/config/parameter_modifiers.yaml +49 -7
  6. data/data/config/target.yaml +11 -0
  7. data/data/config/target_config.yaml +6 -2
  8. data/lib/openc3/api/cmd_api.rb +2 -1
  9. data/lib/openc3/api/metrics_api.rb +11 -1
  10. data/lib/openc3/api/tlm_api.rb +21 -6
  11. data/lib/openc3/core_ext/faraday.rb +1 -1
  12. data/lib/openc3/io/json_api.rb +1 -1
  13. data/lib/openc3/logs/log_writer.rb +3 -1
  14. data/lib/openc3/microservices/decom_common.rb +128 -0
  15. data/lib/openc3/microservices/decom_microservice.rb +26 -95
  16. data/lib/openc3/microservices/interface_decom_common.rb +6 -2
  17. data/lib/openc3/microservices/interface_microservice.rb +10 -8
  18. data/lib/openc3/microservices/log_microservice.rb +1 -1
  19. data/lib/openc3/microservices/microservice.rb +3 -2
  20. data/lib/openc3/microservices/queue_microservice.rb +1 -1
  21. data/lib/openc3/microservices/scope_cleanup_microservice.rb +60 -46
  22. data/lib/openc3/microservices/text_log_microservice.rb +1 -2
  23. data/lib/openc3/models/cvt_model.rb +24 -13
  24. data/lib/openc3/models/db_sharded_model.rb +110 -0
  25. data/lib/openc3/models/interface_model.rb +9 -0
  26. data/lib/openc3/models/interface_status_model.rb +33 -3
  27. data/lib/openc3/models/metric_model.rb +96 -37
  28. data/lib/openc3/models/microservice_model.rb +7 -0
  29. data/lib/openc3/models/microservice_status_model.rb +30 -3
  30. data/lib/openc3/models/reingest_job_model.rb +153 -0
  31. data/lib/openc3/models/scope_model.rb +3 -2
  32. data/lib/openc3/models/script_status_model.rb +4 -20
  33. data/lib/openc3/models/target_model.rb +113 -100
  34. data/lib/openc3/packets/packet_config.rb +4 -1
  35. data/lib/openc3/script/script.rb +2 -2
  36. data/lib/openc3/script/script_runner.rb +4 -4
  37. data/lib/openc3/script/telemetry.rb +3 -3
  38. data/lib/openc3/script/web_socket_api.rb +29 -22
  39. data/lib/openc3/system/system.rb +20 -3
  40. data/lib/openc3/topics/command_decom_topic.rb +4 -2
  41. data/lib/openc3/topics/command_topic.rb +8 -5
  42. data/lib/openc3/topics/decom_interface_topic.rb +15 -10
  43. data/lib/openc3/topics/interface_topic.rb +71 -29
  44. data/lib/openc3/topics/limits_event_topic.rb +62 -41
  45. data/lib/openc3/topics/router_topic.rb +61 -21
  46. data/lib/openc3/topics/system_events_topic.rb +18 -1
  47. data/lib/openc3/topics/telemetry_decom_topic.rb +2 -1
  48. data/lib/openc3/topics/telemetry_topic.rb +4 -2
  49. data/lib/openc3/topics/topic.rb +77 -5
  50. data/lib/openc3/utilities/aws_bucket.rb +2 -0
  51. data/lib/openc3/utilities/cli_generator.rb +3 -2
  52. data/lib/openc3/utilities/metric.rb +15 -1
  53. data/lib/openc3/utilities/questdb_client.rb +173 -37
  54. data/lib/openc3/utilities/reingest_job.rb +377 -0
  55. data/lib/openc3/utilities/ruby_lex_utils.rb +2 -0
  56. data/lib/openc3/utilities/store_autoload.rb +78 -52
  57. data/lib/openc3/utilities/store_queued.rb +20 -12
  58. data/lib/openc3/version.rb +6 -6
  59. data/templates/plugin/plugin.gemspec +13 -1
  60. data/templates/tool_angular/package.json +2 -2
  61. data/templates/tool_react/package.json +1 -1
  62. data/templates/tool_svelte/package.json +1 -1
  63. data/templates/tool_vue/package.json +3 -3
  64. data/templates/tool_vue/src/router.js +2 -2
  65. data/templates/widget/package.json +2 -2
  66. metadata +7 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36f12f777f56b7d64a19cfd89b8886352b6f0fb06bbda59d95d5643b8d4c71f6
4
- data.tar.gz: 1ebffbb1399279e391a47dde42e6f8b9492be10b389c8e63d148aab402ee614c
3
+ metadata.gz: fa824943ce72cce586cdfe03c1a5a8395120801bd845174c25433a15bf986306
4
+ data.tar.gz: 620cc66576e14e7e696419e5be0b5704190452d29efc35a315a489099b20f82e
5
5
  SHA512:
6
- metadata.gz: ee971acf5bfd378f5a238263b467385286997900bea7fe549b8458c707568fb3a2265b261ee8794601bd42db7d0c2e8a983cf3d81a52c0c5069b9be0b41c4966
7
- data.tar.gz: 5ba0ea7fb684a6e3e029864289612975db9555d1754f30f65baf4cf3193208516ea74029bb889b4575014b7953c45990c7e515dcf8f43d292bf8fc844de15a6b
6
+ metadata.gz: 1f392d51b2e5870128a995aa9e5717f9a4ed3bf0b8f55952b9141ec34898406734a2a834cf18d09b78d6e82f5675c2c0f3269e2ef62964ec58f2b96b8933ce80
7
+ data.tar.gz: a92a073ff672da55a44b85b700cb5b6a908208e9a10ddcad77828f02de0f877c3db2252d7d93857ab5b65d55b39594dfc109d72028a82cbfeaa6e468878eef94
data/bin/openc3cli CHANGED
@@ -45,7 +45,8 @@ require 'irb/completion'
45
45
  require 'digest'
46
46
  require 'argon2'
47
47
 
48
- $redis_url = "redis://#{ENV['OPENC3_REDIS_HOSTNAME']}:#{ENV['OPENC3_REDIS_PORT']}"
48
+ $redis_shardnum = ENV['OPENC3_SHARDNUM'] || "0"
49
+ $redis_url = "redis://#{ENV['OPENC3_REDIS_HOSTNAME'].to_s.gsub("SHARDNUM", $redis_shardnum)}:#{ENV['OPENC3_REDIS_PORT']}"
49
50
 
50
51
  ERROR_CODE = 1
51
52
 
@@ -1143,6 +1144,48 @@ if not ARGV[0].nil? # argument(s) given
1143
1144
  end
1144
1145
  cli_pkg_uninstall(ARGV[1], scope: ARGV[2])
1145
1146
 
1147
+ when 'reingest'
1148
+ # Internal command spawned by StorageController via ProcessManager so the
1149
+ # reingest runs in its own process and System singleton resets cannot
1150
+ # collide with the cmd-tlm-api Rails server.
1151
+ if ARGV[1].nil? || ARGV[2].nil? || ARGV[1] == '--help' || ARGV[1] == '-h'
1152
+ puts "Usage: cli reingest JOB_ID SCOPE"
1153
+ exit(ARGV[1].nil? ? 1 : 0)
1154
+ end
1155
+ require 'openc3/utilities/reingest_job'
1156
+ require 'openc3/models/reingest_job_model'
1157
+ job_id = ARGV[1]
1158
+ scope = ARGV[2]
1159
+ job = OpenC3::ReingestJobModel.get_model(name: job_id, scope: scope)
1160
+ if job.nil?
1161
+ OpenC3::Logger.error("Reingest job #{job_id} not found in scope #{scope}")
1162
+ exit(1)
1163
+ end
1164
+ begin
1165
+ OpenC3::ReingestJob.new(
1166
+ job_id: job_id,
1167
+ files: job.files,
1168
+ path: job.path,
1169
+ bucket: job.bucket,
1170
+ scope: scope,
1171
+ target_version: job.target_version,
1172
+ ).run
1173
+ rescue Exception => e
1174
+ # ReingestJob#run already marks Crashed for errors raised during the run.
1175
+ # This catches failures from the constructor itself (or anything before
1176
+ # run gets its rescue installed) so the model doesn't sit in Queued forever.
1177
+ OpenC3::Logger.error("Reingest job #{job_id} crashed before run: #{e.formatted}")
1178
+ begin
1179
+ job.state = 'Crashed'
1180
+ job.error = e.message
1181
+ job.finished_at = Time.now.utc.iso8601
1182
+ job.update
1183
+ rescue => e2
1184
+ OpenC3::Logger.error("Reingest job #{job_id} failed to mark Crashed: #{e2.message}")
1185
+ end
1186
+ exit(1)
1187
+ end
1188
+
1146
1189
  when 'generate'
1147
1190
  # To test against a local copy call this file from the root cosmos directory like this:
1148
1191
  # ruby -Iopenc3/lib openc3/bin/openc3cli generate ...
@@ -1390,8 +1433,9 @@ if not ARGV[0].nil? # argument(s) given
1390
1433
  end
1391
1434
  end
1392
1435
  end
1393
- # Unless explicitly disabled, ensure the tools bucket is public
1394
- unless ENV.fetch("OPENC3_NO_BUCKET_POLICY", false)
1436
+ # Unless explicitly disabled, ensure the tools bucket is public.
1437
+ # OPENC3_TOOLS_BUCKET_PRIVATE keeps the tools bucket private; the cmd-tlm-api proxies reads via ToolsController.
1438
+ unless ENV.fetch("OPENC3_NO_BUCKET_POLICY", false) || ENV.fetch("OPENC3_TOOLS_BUCKET_PRIVATE", false)
1395
1439
  client.ensure_public(ENV['OPENC3_TOOLS_BUCKET'])
1396
1440
  end
1397
1441
  # Always ensure the scriptrunner policy is in place since it is required for script execution
@@ -193,4 +193,4 @@ HIDDEN:
193
193
  description: This item will not appear in PacketViewer or Item Choosers.
194
194
  It also hides this item from appearing in the Script Runner popup helper
195
195
  when writing scripts. The item will also not be included in decom data.
196
- since: 6.10.1
196
+ since: 6.10.0
@@ -168,11 +168,22 @@ MICROSERVICE:
168
168
  since: 6.0.0
169
169
  parameters:
170
170
  - name: Shard
171
- required: false
171
+ required: true
172
172
  description: Shard number starting from 0
173
173
  values: \d+
174
174
  example: |
175
175
  SHARD 0
176
+ DB_SHARD:
177
+ summary: Shard for target database database if sharding Redis/TSDB
178
+ description: DB Shard. Only used if running multiple database shards typically in Kubernetes
179
+ since: 7.1.0
180
+ parameters:
181
+ - name: DB Shard
182
+ required: true
183
+ description: DB Shard number starting from 0
184
+ values: \d+
185
+ example: |
186
+ DB_SHARD 0
176
187
  STOPPED:
177
188
  summary: Initially creates the microservice in a stopped state (not enabled)
178
189
  since: 6.2.0
@@ -82,14 +82,15 @@ WRITE_CONVERSION:
82
82
  [INST inst_cmds.txt](https://github.com/OpenC3/cosmos/blob/main/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/cmd_tlm/inst_cmds.txt)
83
83
  or [INST2 inst_cmds.txt](https://github.com/OpenC3/cosmos/blob/main/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/cmd_tlm/inst_cmds.txt).
84
84
 
85
- :::info Multiple write conversions on command parameters
85
+ :::info[Multiple write conversions on command parameters]
86
86
  When a command is built, each item gets written (and write conversions are run)
87
87
  to set the default value. Then items are written (again write conversions are run)
88
88
  with user provided values. Thus write conversions can be run twice. Also there are
89
89
  no guarantees which parameters have already been written. The packet itself has a
90
- given_values() method which can be used to retrieve a hash of the user provided
90
+ `given_values` attribute which can be used to retrieve a hash of the user provided
91
91
  values to the command. That can be used to check parameter values passed in.
92
92
  :::
93
+
93
94
  parameters:
94
95
  - name: Class Filename
95
96
  required: true
@@ -105,9 +106,48 @@ WRITE_CONVERSION:
105
106
  to the class constructor.
106
107
  values: .*
107
108
  ruby_example: |
108
- WRITE_CONVERSION ip_write_conversion.rb
109
+ # Example command with a WRITE_CONVERSION that sets a command parameter
110
+ # based on the given values of other parameters
111
+ COMMAND INST BLOCK BIG_ENDIAN "Send variable block of data"
112
+ APPEND_PARAMETER BYTE 8 UINT MIN MAX 0x55 "Byte to duplicate"
113
+ FORMAT_STRING "0x%0X"
114
+ APPEND_PARAMETER LENGTH 32 UINT MIN MAX 0 "Length of data"
115
+ APPEND_PARAMETER DATA 0 BLOCK "" "Variable block of data"
116
+ WRITE_CONVERSION block_conversion.rb
117
+ HIDDEN # Because we're filling it in with a conversion
118
+
119
+ # Implemented in INST/lib/block_conversion.rb:
120
+ require 'openc3/conversions/conversion'
121
+ module OpenC3
122
+ class BlockConversion < Conversion
123
+ def call(value, packet, buffer)
124
+ # Use the packet.given_values hash to access user provided values to the command
125
+ byte = packet.given_values['BYTE'] || 0x55
126
+ length = packet.given_values['LENGTH'] || 0
127
+ [byte].pack('C') * length
128
+ end
129
+ end
130
+ end
109
131
  python_example: |
110
- WRITE_CONVERSION openc3/conversions/ip_write_conversion.py
132
+ # Example command with a WRITE_CONVERSION that sets a command parameter
133
+ # based on the given values of other parameters
134
+ COMMAND INST BLOCK BIG_ENDIAN "Send variable block of data"
135
+ APPEND_PARAMETER BYTE 8 UINT MIN MAX 0x55 "Byte to duplicate"
136
+ FORMAT_STRING "0x%0X"
137
+ APPEND_PARAMETER LENGTH 32 UINT MIN MAX 0 "Length of data"
138
+ APPEND_PARAMETER DATA 0 BLOCK "" "Variable block of data"
139
+ WRITE_CONVERSION block_conversion.py
140
+ HIDDEN # Because we're filling it in with a conversion
141
+
142
+ # Implemented in INST/lib/block_conversion.py:
143
+ from openc3.conversions.conversion import Conversion
144
+ class BlockConversion(Conversion):
145
+ def call(self, value, packet, buffer):
146
+ # Use the packet.given_values hash to access user provided values to the command
147
+ byte = packet.given_values.get('BYTE', 0x55)
148
+ length = packet.given_values.get('LENGTH', 0)
149
+ return bytes([byte]) * length
150
+
111
151
  POLY_WRITE_CONVERSION:
112
152
  summary: Adds a polynomial conversion factor to the current command parameter
113
153
  description: See [Polynomial Conversion](/docs/configuration/conversions#polynomial_conversion) for more information.
@@ -127,14 +167,15 @@ GENERIC_WRITE_CONVERSION_START:
127
167
  value. The GENERIC_WRITE_CONVERSION_END keyword specifies that all lines of
128
168
  code for the conversion have been given.
129
169
 
130
- :::info Multiple write conversions on command parameters
170
+ :::info[Multiple write conversions on command parameters]
131
171
  When a command is built, each item gets written (and write conversions are run)
132
172
  to set the default value. Then items are written (again write conversions are run)
133
173
  with user provided values. Thus write conversions can be run twice. Also there are
134
174
  no guarantees which parameters have already been written. The packet itself has a
135
- given_values() method which can be used to retrieve a hash of the user provided
175
+ `given_values` attribute which can be used to retrieve a hash of the user provided
136
176
  values to the command. That can be used to check parameter values passed in.
137
177
  :::
178
+
138
179
  warning: Generic conversions are not a good long term solution. Consider creating
139
180
  a conversion class and using WRITE_CONVERSION instead. WRITE_CONVERSION is easier
140
181
  to debug and higher performance.
@@ -171,4 +212,5 @@ HIDDEN:
171
212
  summary: Hides this parameter from all the OpenC3 tools
172
213
  description: This item will not appear in CmdSender.
173
214
  It also hides this item from appearing in the Script Runner popup helper
174
- when writing scripts. The parameter should not be provided to commands.
215
+ when writing scripts. The parameter should not be provided to commands.
216
+ since: 6.10.0
@@ -159,3 +159,14 @@ TARGET:
159
159
  values: \d+
160
160
  example: |
161
161
  SHARD 0
162
+ DB_SHARD:
163
+ summary: Shard for target database database if sharding Redis/TSDB
164
+ description: DB Shard. Only used if running multiple database shards typically in Kubernetes
165
+ since: 7.1.0
166
+ parameters:
167
+ - name: DB Shard
168
+ required: true
169
+ description: DB Shard number starting from 0
170
+ values: \d+
171
+ example: |
172
+ DB_SHARD 0
@@ -2,8 +2,12 @@
2
2
  LANGUAGE:
3
3
  summary: Programming language of the target interfaces and microservices
4
4
  description: The target language must be either Ruby or Python. The language
5
- determines how the target's interfaces and microservices are run. Note that
6
- both Ruby and Python still use ERB to perform templating.
5
+ determines how the target's interfaces and microservices are run. A target
6
+ must pick one language for its interfaces and microservices &mdash; you cannot
7
+ mix Ruby and Python interfaces/microservices within the same target. Scripts
8
+ executed in Script Runner are independent of this setting and may be written
9
+ in either Ruby or Python regardless of the target's LANGUAGE. Note that both
10
+ Ruby and Python still use ERB to perform templating.
7
11
  example: LANGUAGE python
8
12
  parameters:
9
13
  - language: Programming language
@@ -183,7 +183,8 @@ module OpenC3
183
183
  authorize(permission: 'cmd_info', target_name: target_name, packet_name: command_name, manual: manual, scope: scope, token: token)
184
184
  TargetModel.packet(target_name, command_name, type: :CMD, scope: scope)
185
185
  topic = "#{scope}__COMMAND__{#{target_name}}__#{command_name}"
186
- msg_id, msg_hash = Topic.get_newest_message(topic)
186
+ db_shard = Store.db_shard_for_target(target_name, scope: scope)
187
+ msg_id, msg_hash = Topic.get_newest_message(topic, db_shard: db_shard)
187
188
  if msg_id
188
189
  msg_hash['buffer'] = msg_hash['buffer'].b
189
190
  return msg_hash
@@ -28,9 +28,11 @@ module OpenC3
28
28
  DELAY_METRICS['log_topic_delta_seconds'] = 0.0
29
29
  DELAY_METRICS['router_topic_delta_seconds'] = 0.0
30
30
  DELAY_METRICS['text_log_topic_delta_seconds'] = 0.0
31
+ DELAY_METRICS['tsdb_ingest_topic_delta_seconds'] = 0.0
31
32
 
32
33
  DURATION_METRICS = {}
33
34
  DURATION_METRICS['decom_duration_seconds'] = 0.0
35
+ DURATION_METRICS['tsdb_ingest_duration_seconds'] = 0.0
34
36
 
35
37
  SUM_METRICS = {}
36
38
  SUM_METRICS['cleanup_total'] = 0
@@ -48,6 +50,8 @@ module OpenC3
48
50
  SUM_METRICS['router_directive_total'] = 0
49
51
  SUM_METRICS['text_log_total'] = 0
50
52
  SUM_METRICS['text_log_error_total'] = 0
53
+ SUM_METRICS['tsdb_ingest_total'] = 0
54
+ SUM_METRICS['tsdb_ingest_error_total'] = 0
51
55
 
52
56
  def get_metrics(manual: false, scope: $openc3_scope, token: $openc3_token)
53
57
  authorize(permission: 'system', manual: manual, scope: scope, token: token)
@@ -79,7 +83,13 @@ module OpenC3
79
83
  result.merge!(duration_metrics)
80
84
  result.merge!(sum_metrics)
81
85
 
82
- result.merge!(MetricModel.redis_metrics)
86
+ redis_metrics = MetricModel.redis_metrics
87
+ redis_metrics.each do |_db_shard, values|
88
+ values.each do |key, value|
89
+ existing = result[key]
90
+ result[key] = value if existing.nil? or value > existing
91
+ end
92
+ end
83
93
 
84
94
  return result
85
95
  end
@@ -117,7 +117,7 @@ module OpenC3
117
117
  # @param packet_name [String] Packet name of the packet
118
118
  # @param item_hash [Hash] Hash of item_name and value for each item you want to change from the current value table
119
119
  # @param type [Symbol] Telemetry type, :RAW, :CONVERTED (default), :FORMATTED
120
- def inject_tlm(target_name, packet_name, item_hash = nil, type: :CONVERTED, manual: false, scope: $openc3_scope, token: $openc3_token)
120
+ def inject_tlm(target_name, packet_name, item_hash = nil, type: :CONVERTED, stored: false, manual: false, scope: $openc3_scope, token: $openc3_token)
121
121
  authorize(permission: 'tlm_set', target_name: target_name, packet_name: packet_name, manual: manual, scope: scope, token: token)
122
122
  type = type.to_s.intern
123
123
  target_name = target_name.upcase
@@ -155,9 +155,9 @@ module OpenC3
155
155
 
156
156
  # Use an interface microservice if it exists, other use the decom microservice
157
157
  if interface_name
158
- InterfaceTopic.inject_tlm(interface_name, target_name, packet_name, item_hash, type: type, scope: scope)
158
+ InterfaceTopic.inject_tlm(interface_name, target_name, packet_name, item_hash, type: type, stored: stored, scope: scope)
159
159
  else
160
- DecomInterfaceTopic.inject_tlm(target_name, packet_name, item_hash, type: type, scope: scope)
160
+ DecomInterfaceTopic.inject_tlm(target_name, packet_name, item_hash, type: type, stored: stored, scope: scope)
161
161
  end
162
162
  end
163
163
 
@@ -221,7 +221,8 @@ module OpenC3
221
221
  return msg_hash
222
222
  else
223
223
  topic = "#{scope}__TELEMETRY__{#{target_name}}__#{packet_name}"
224
- msg_id, msg_hash = Topic.get_newest_message(topic)
224
+ db_shard = Store.db_shard_for_target(target_name, scope: scope)
225
+ msg_id, msg_hash = Topic.get_newest_message(topic, db_shard: db_shard)
225
226
  if msg_id
226
227
  msg_hash['buffer'] = msg_hash['buffer'].b
227
228
  return msg_hash
@@ -446,7 +447,8 @@ module OpenC3
446
447
  packet_name = packet_name.upcase
447
448
  authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, manual: manual, scope: scope, token: token)
448
449
  topic = "#{scope}__DECOM__{#{target_name}}__#{packet_name}"
449
- id, _ = Topic.get_newest_message(topic)
450
+ db_shard = Store.db_shard_for_target(target_name, scope: scope)
451
+ id, = Topic.get_newest_message(topic, db_shard: db_shard)
450
452
  results[topic] = id ? id : '0-0'
451
453
  end
452
454
  results.to_a.join(SUBSCRIPTION_DELIMITER)
@@ -463,7 +465,20 @@ module OpenC3
463
465
  authorize(permission: 'tlm', manual: manual, scope: scope, token: token)
464
466
  # Split the list of topic, ID values and turn it into a hash for easy updates
465
467
  lookup = Hash[*id.split(SUBSCRIPTION_DELIMITER)]
466
- xread = Topic.read_topics(lookup.keys, lookup.values, nil, count) # Always don't block
468
+ # Group topics by db_shard for multi-shard support
469
+ db_shard_groups = {}
470
+ lookup.each do |topic, offset|
471
+ target_name = topic.match(/__\{?([^}_]+)\}?__/)[1] rescue nil
472
+ db_shard = Store.db_shard_for_target(target_name, scope: scope)
473
+ db_shard_groups[db_shard] ||= { topics: [], offsets: [] }
474
+ db_shard_groups[db_shard][:topics] << topic
475
+ db_shard_groups[db_shard][:offsets] << offset
476
+ end
477
+ xread = {}
478
+ db_shard_groups.each do |db_shard, group|
479
+ result = Topic.read_topics(group[:topics], group[:offsets], nil, count, db_shard: db_shard) # Always don't block
480
+ xread.merge!(result) if result
481
+ end
467
482
  # Return the original ID and and empty array if we didn't get anything
468
483
  packets = []
469
484
  return [id, packets] if xread.empty?
@@ -1,6 +1,6 @@
1
1
  # Remove warnings in CGI
2
2
  saved_verbose = $VERBOSE
3
- $VERBOSE = false
3
+ $VERBOSE = nil
4
4
  require 'faraday'
5
5
  $VERBOSE = saved_verbose
6
6
 
@@ -65,7 +65,7 @@ module OpenC3
65
65
 
66
66
  def _request(*method_params, **kw_params)
67
67
  kw_params[:scope] = $openc3_scope unless kw_params[:scope]
68
- kw_params[:json] = true unless kw_params[:json]
68
+ kw_params[:json] = true # This is JsonApi so should always be speaking json
69
69
  @json_api.request(*method_params, **kw_params)
70
70
  end
71
71
  end
@@ -237,7 +237,9 @@ module OpenC3
237
237
  # Now that the file is in S3, trim the Redis stream up until the previous file.
238
238
  # This keeps one minute of data in Redis
239
239
  instance.cleanup_offsets[index].each do |redis_topic, cleanup_offset|
240
- Topic.trim_topic(redis_topic, cleanup_offset)
240
+ target_match = redis_topic.match(/__\{?([^}_]+)\}?__/)
241
+ db_shard = target_match ? Store.db_shard_for_target(target_match[1]) : 0
242
+ Topic.trim_topic(redis_topic, cleanup_offset, db_shard: db_shard)
241
243
  end
242
244
  indexes_to_clear << index
243
245
  end
@@ -0,0 +1,128 @@
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 'openc3/system/system'
15
+ require 'openc3/microservices/interface_microservice'
16
+ require 'openc3/topics/telemetry_decom_topic'
17
+ require 'openc3/models/target_model'
18
+
19
+ module OpenC3
20
+ # Shared decom pipeline used by DecomMicroservice (live telemetry) and
21
+ # ReingestJob (historical raw log replay). The reingest path passes
22
+ # check_limits: false so historical data does not re-fire limits events.
23
+ module DecomCommon
24
+ extend self
25
+
26
+ # Decommutate a Packet and publish it on the TelemetryDecomTopic. This is the
27
+ # step that lands data in the CVT and in the Python TsdbMicroservice → QuestDB.
28
+ #
29
+ # @param packet [Packet] A fully buffered Packet. Caller sets received_time,
30
+ # received_count, stored, extra, buffer.
31
+ # @param scope [String] Scope name.
32
+ # @param target_names [Array<String>] Used when a subpacket must be re-identified.
33
+ # @param logger [Logger] Destination for warnings.
34
+ # @param name [String] Identifier used in subpacket warning messages
35
+ # (microservice name, or "REINGEST:<job_id>").
36
+ # @param check_limits [Boolean] When false, skips the Packet#check_limits call.
37
+ # Reingest passes false so historical data does not re-fire limits events.
38
+ # @param metric [Metric, nil] Optional; when set, records decom_duration_seconds.
39
+ # @param error_callback [Proc, nil] Called as error_callback.call(exception) when
40
+ # Packet#process or Packet#check_limits raises. The microservice uses this to
41
+ # bump its decom_error_total metric.
42
+ # @return [Integer] Number of (sub)packets published.
43
+ def decom_and_publish(packet, scope:, target_names:, logger:, name:,
44
+ check_limits: true, metric: nil, error_callback: nil)
45
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC) if metric
46
+ published = 0
47
+
48
+ packet_and_subpackets = packet.subpacketize
49
+ packet_and_subpackets.each do |packet_or_subpacket|
50
+ if packet_or_subpacket.subpacket
51
+ packet_or_subpacket = handle_subpacket(packet, packet_or_subpacket,
52
+ target_names: target_names,
53
+ scope: scope,
54
+ logger: logger,
55
+ name: name)
56
+ end
57
+
58
+ begin
59
+ packet_or_subpacket.process
60
+ rescue Exception => e
61
+ error_callback&.call(e)
62
+ logger.error e.message
63
+ end
64
+
65
+ packet_or_subpacket.check_limits(System.limits_set) if check_limits
66
+
67
+ TelemetryDecomTopic.write_packet(packet_or_subpacket, scope: scope)
68
+ published += 1
69
+ end
70
+
71
+ if metric
72
+ diff = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
73
+ metric.set(name: 'decom_duration_seconds', value: diff, type: 'gauge', unit: 'seconds')
74
+ end
75
+
76
+ published
77
+ end
78
+
79
+ # Identify a subpacket and (except for stored telemetry) update the CVT.
80
+ # Extracted from DecomMicroservice so reingest can handle subpackets too.
81
+ def handle_subpacket(packet, subpacket, target_names:, scope:, logger:, name:)
82
+ subpacket.received_time = packet.received_time
83
+ subpacket.stored = packet.stored
84
+ subpacket.extra = packet.extra
85
+
86
+ if subpacket.stored
87
+ identified_subpacket = System.telemetry.identify_and_define_packet(subpacket, target_names, subpackets: true)
88
+ else
89
+ if subpacket.identified?
90
+ begin
91
+ identified_subpacket = System.telemetry.update!(subpacket.target_name,
92
+ subpacket.packet_name,
93
+ subpacket.buffer)
94
+ rescue RuntimeError
95
+ logger.warn "#{name}: Received unknown identified subpacket: #{subpacket.target_name} #{subpacket.packet_name}"
96
+ subpacket.target_name = nil
97
+ subpacket.packet_name = nil
98
+ identified_subpacket = System.telemetry.identify!(subpacket.buffer,
99
+ target_names, subpackets: true)
100
+ end
101
+ else
102
+ identified_subpacket = System.telemetry.identify!(subpacket.buffer,
103
+ target_names, subpackets: true)
104
+ end
105
+ end
106
+
107
+ if identified_subpacket
108
+ identified_subpacket.received_time = subpacket.received_time
109
+ identified_subpacket.stored = subpacket.stored
110
+ identified_subpacket.extra = subpacket.extra
111
+ subpacket = identified_subpacket
112
+ else
113
+ unknown_subpacket = System.telemetry.update!('UNKNOWN', 'UNKNOWN', subpacket.buffer)
114
+ unknown_subpacket.received_time = subpacket.received_time
115
+ unknown_subpacket.stored = subpacket.stored
116
+ unknown_subpacket.extra = subpacket.extra
117
+ subpacket = unknown_subpacket
118
+ num_bytes_to_print = [InterfaceMicroservice::UNKNOWN_BYTES_TO_PRINT, subpacket.length].min
119
+ data = subpacket.buffer(false)[0..(num_bytes_to_print - 1)]
120
+ prefix = data.each_byte.map { |byte| sprintf("%02X", byte) }.join()
121
+ logger.warn "#{name} #{subpacket.target_name} packet length: #{subpacket.length} starting with: #{prefix}"
122
+ end
123
+
124
+ TargetModel.sync_tlm_packet_counts(subpacket, target_names, scope: scope)
125
+ subpacket
126
+ end
127
+ end
128
+ end