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
@@ -77,7 +77,8 @@ module OpenC3
77
77
  end
78
78
  end
79
79
 
80
- def self.setup_targets(target_names, base_dir, scope:)
80
+ # target_version can also be the actual hash used in the target_archives folder
81
+ def self.setup_targets(target_names, base_dir, target_version: 'current', scope:)
81
82
  # Nothing to do if there are no targets
82
83
  return if target_names.nil? or target_names.length == 0
83
84
  if @@instance.nil?
@@ -85,10 +86,15 @@ module OpenC3
85
86
  FileUtils.mkdir_p(targets_path)
86
87
  bucket = Bucket.getClient()
87
88
  target_names.each do |target_name|
89
+ # Remove any prior extraction so re-running setup_targets (e.g. after
90
+ # reset_instance! during reingest) starts from a clean slate. Without
91
+ # this, Zip::File#extract fails on files left behind by a previous run.
92
+ FileUtils.rm_rf("#{targets_path}/#{target_name}")
93
+
88
94
  # Retrieve bucket/targets/target_name/<TARGET>_current.zip
89
- zip_path = "#{targets_path}/#{target_name}_current.zip"
95
+ zip_path = "#{targets_path}/#{target_name}_#{target_version}.zip"
90
96
  FileUtils.mkdir_p(File.dirname(zip_path))
91
- bucket_key = "#{scope}/target_archives/#{target_name}/#{target_name}_current.zip"
97
+ bucket_key = "#{scope}/target_archives/#{target_name}/#{target_name}_#{target_version}.zip"
92
98
  Logger.info("Retrieving #{bucket_key} from targets bucket")
93
99
  bucket.get_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: bucket_key, path: zip_path)
94
100
  Zip::File.open(zip_path) do |zip_file|
@@ -116,6 +122,17 @@ module OpenC3
116
122
  end
117
123
  end
118
124
 
125
+ # Clears the System singleton so the next call to setup_targets or
126
+ # instance rebuilds it. Intended for admin flows (e.g. reingest) that
127
+ # need to load a specific target_version distinct from whatever is
128
+ # currently loaded. Callers must hold an external lock if they need to
129
+ # protect other threads from observing a nil @@instance briefly.
130
+ def self.reset_instance!
131
+ @@instance_mutex.synchronize do
132
+ @@instance = nil
133
+ end
134
+ end
135
+
119
136
  # Get the singleton instance of System
120
137
  #
121
138
  # @param target_names [Array of target_names]
@@ -42,11 +42,13 @@ module OpenC3
42
42
  end
43
43
  msg_hash['json_data'] = JSON.generate(json_hash.as_json, allow_nan: true)
44
44
  msg_hash['extra'] = JSON.generate(packet.extra.as_json, allow_nan: true) if packet.extra
45
- EphemeralStoreQueued.write_topic(topic, msg_hash)
45
+ db_shard = Store.db_shard_for_target(packet.target_name, scope: scope)
46
+ EphemeralStoreQueued.instance(db_shard: db_shard).write_topic(topic, msg_hash)
46
47
  end
47
48
 
48
49
  def self.get_cmd_item(target_name, packet_name, param_name, type: :FORMATTED, scope: $openc3_scope)
49
- msg_id, msg_hash = Topic.get_newest_message("#{scope}__DECOMCMD__{#{target_name}}__#{packet_name}")
50
+ db_shard = Store.db_shard_for_target(target_name, scope: scope)
51
+ msg_id, msg_hash = Topic.get_newest_message("#{scope}__DECOMCMD__{#{target_name}}__#{packet_name}", db_shard: db_shard)
50
52
  if msg_id
51
53
  if param_name == 'RECEIVED_COUNT'
52
54
  msg_hash['received_count'].to_i
@@ -33,7 +33,8 @@ module OpenC3
33
33
  received_count: packet.received_count,
34
34
  stored: packet.stored.to_s,
35
35
  buffer: packet.buffer(false) }
36
- EphemeralStoreQueued.write_topic(topic, msg_hash)
36
+ db_shard = Store.db_shard_for_target(packet.target_name, scope: scope)
37
+ EphemeralStoreQueued.instance(db_shard: db_shard).write_topic(topic, msg_hash)
37
38
  end
38
39
 
39
40
  # @param command [Hash] Command hash structure read to be written to a topic
@@ -45,20 +46,22 @@ module OpenC3
45
46
  command['cmd_params'] = JSON.generate(command['cmd_params'].as_json, allow_nan: true)
46
47
  OpenC3.inject_context(command)
47
48
 
49
+ db_shard = Store.db_shard_for_target(command['target_name'], scope: scope)
50
+
48
51
  # Fire-and-forget mode: skip ACK waiting when timeout <= 0
49
52
  if timeout <= 0
50
- Topic.write_topic("{#{scope}__CMD}TARGET__#{command['target_name']}", command, '*', 100)
53
+ Topic.write_topic("{#{scope}__CMD}TARGET__#{command['target_name']}", command, '*', 100, db_shard: db_shard)
51
54
  command["cmd_params"] = cmd_params # Restore the original cmd_params Hash
52
55
  return command
53
56
  end
54
57
 
55
58
  ack_topic = "{#{scope}__ACKCMD}TARGET__#{command['target_name']}"
56
- Topic.update_topic_offsets([ack_topic])
57
- cmd_id = Topic.write_topic("{#{scope}__CMD}TARGET__#{command['target_name']}", command, '*', 100)
59
+ Topic.update_topic_offsets([ack_topic], db_shard: db_shard)
60
+ cmd_id = Topic.write_topic("{#{scope}__CMD}TARGET__#{command['target_name']}", command, '*', 100, db_shard: db_shard)
58
61
  command["cmd_params"] = cmd_params # Restore the original cmd_params Hash
59
62
  time = Time.now
60
63
  while (Time.now - time) < timeout
61
- Topic.read_topics([ack_topic]) do |_topic, _msg_id, msg_hash, _redis|
64
+ Topic.read_topics([ack_topic], db_shard: db_shard) do |_topic, _msg_id, msg_hash, _redis|
62
65
  if msg_hash["id"] == cmd_id
63
66
  if msg_hash["result"] == "SUCCESS"
64
67
  return command
@@ -26,13 +26,14 @@ module OpenC3
26
26
  # DecomMicroservice is listening to the DECOMINTERFACE topic and is responsible
27
27
  # for actually building the command. This was deliberate to allow this to work
28
28
  # with or without an interface.
29
+ db_shard = Store.db_shard_for_target(target_name, scope: scope)
29
30
  ack_topic = "{#{scope}__ACKCMD}TARGET__#{target_name}"
30
- Topic.update_topic_offsets([ack_topic])
31
+ Topic.update_topic_offsets([ack_topic], db_shard: db_shard)
31
32
  decom_id = Topic.write_topic("#{scope}__DECOMINTERFACE__{#{target_name}}",
32
- { 'build_cmd' => JSON.generate(data, allow_nan: true) }, '*', 100)
33
+ { 'build_cmd' => JSON.generate(data, allow_nan: true) }, '*', 100, db_shard: db_shard)
33
34
  time = Time.now
34
35
  while (Time.now - time) < timeout
35
- Topic.read_topics([ack_topic]) do |_topic, _msg_id, msg_hash, _redis|
36
+ Topic.read_topics([ack_topic], db_shard: db_shard) do |_topic, _msg_id, msg_hash, _redis|
36
37
  if msg_hash["id"] == decom_id
37
38
  if msg_hash["result"] == "SUCCESS"
38
39
  return msg_hash
@@ -45,19 +46,22 @@ module OpenC3
45
46
  raise "Timeout of #{timeout}s waiting for cmd ack. Does target '#{target_name}' exist?"
46
47
  end
47
48
 
48
- def self.inject_tlm(target_name, packet_name, item_hash = nil, type: :CONVERTED, timeout: 5, scope:)
49
+ def self.inject_tlm(target_name, packet_name, item_hash = nil, type: :CONVERTED, stored: false, timeout: 5, scope:)
49
50
  data = {}
50
51
  data['target_name'] = target_name.to_s.upcase
51
52
  data['packet_name'] = packet_name.to_s.upcase
52
53
  data['item_hash'] = item_hash
53
54
  data['type'] = type
55
+
56
+ db_shard = Store.db_shard_for_target(target_name, scope: scope)
57
+ data['stored'] = stored
54
58
  ack_topic = "{#{scope}__ACKCMD}TARGET__#{target_name}"
55
- Topic.update_topic_offsets([ack_topic])
59
+ Topic.update_topic_offsets([ack_topic], db_shard: db_shard)
56
60
  decom_id = Topic.write_topic("#{scope}__DECOMINTERFACE__{#{target_name}}",
57
- { 'inject_tlm' => JSON.generate(data, allow_nan: true) }, '*', 100)
61
+ { 'inject_tlm' => JSON.generate(data, allow_nan: true) }, '*', 100, db_shard: db_shard)
58
62
  time = Time.now
59
63
  while (Time.now - time) < timeout
60
- Topic.read_topics([ack_topic]) do |_topic, _msg_id, msg_hash, _redis|
64
+ Topic.read_topics([ack_topic], db_shard: db_shard) do |_topic, _msg_id, msg_hash, _redis|
61
65
  if msg_hash["id"] == decom_id
62
66
  if msg_hash["result"] == "SUCCESS"
63
67
  return
@@ -76,13 +80,14 @@ module OpenC3
76
80
  data['packet_name'] = packet_name.to_s.upcase
77
81
  # DecomMicroservice is listening to the DECOMINTERFACE topic and has
78
82
  # the most recent decommed packets including subpackets
83
+ db_shard = Store.db_shard_for_target(target_name, scope: scope)
79
84
  ack_topic = "{#{scope}__ACKCMD}TARGET__#{target_name}"
80
- Topic.update_topic_offsets([ack_topic])
85
+ Topic.update_topic_offsets([ack_topic], db_shard: db_shard)
81
86
  decom_id = Topic.write_topic("#{scope}__DECOMINTERFACE__{#{target_name}}",
82
- { 'get_tlm_buffer' => JSON.generate(data, allow_nan: true) }, '*', 100)
87
+ { 'get_tlm_buffer' => JSON.generate(data, allow_nan: true) }, '*', 100, db_shard: db_shard)
83
88
  time = Time.now
84
89
  while (Time.now - time) < timeout
85
- Topic.read_topics([ack_topic]) do |_topic, _msg_id, msg_hash, _redis|
90
+ Topic.read_topics([ack_topic], db_shard: db_shard) do |_topic, _msg_id, msg_hash, _redis|
86
91
  if msg_hash["id"] == decom_id
87
92
  if msg_hash["result"] == "SUCCESS"
88
93
  msg_hash["stored"] = ConfigParser.handle_true_false(msg_hash["stored"])
@@ -21,6 +21,12 @@ module OpenC3
21
21
  class InterfaceTopic < Topic
22
22
  COMMAND_ACK_TIMEOUT_S = 30
23
23
 
24
+ # Look up db_shard from Interface
25
+ def self._db_shard_for_interface(interface_name, scope:)
26
+ json = Store.hget("#{scope}__openc3_interfaces", interface_name)
27
+ json ? (JSON.parse(json, allow_nan: true, create_additions: true)['db_shard'] || 0).to_i : 0
28
+ end
29
+
24
30
  # Generate a list of topics for this interface. This includes the interface itself
25
31
  # and all the targets which are assigned to this interface.
26
32
  def self.topics(interface, scope:)
@@ -33,16 +39,39 @@ module OpenC3
33
39
  topics
34
40
  end
35
41
 
36
- def self.receive_commands(interface, scope:)
42
+ def self.receive_commands(interface, scope:, db_shard: 0)
43
+ db_shard = db_shard.to_i
44
+ interface_cmd_topic = "{#{scope}__CMD}INTERFACE__#{interface.name}"
45
+ system_events_topic = "OPENC3__SYSTEM__EVENTS"
46
+
47
+ target_topics = []
48
+ interface.cmd_target_names.each do |target_name|
49
+ target_topics << "{#{scope}__CMD}TARGET__#{target_name}"
50
+ end
51
+
52
+ # Group target command topics by db_shard; include interface cmd and system events on db_shard
53
+ db_shard_groups = Topic.group_topics_by_db_shard(target_topics, target_pattern: 'CMD}TARGET__', scope: scope)
54
+ db_shard_groups[db_shard] ||= []
55
+ db_shard_groups[db_shard] << interface_cmd_topic
56
+ db_shard_groups[db_shard] << system_events_topic
57
+
58
+ all_same_db_shard = Topic.all_same_db_shard?(db_shard_groups)
59
+
37
60
  while true
38
- Topic.read_topics(InterfaceTopic.topics(interface, scope: scope)) do |topic, msg_id, msg_hash, redis|
39
- result = yield topic, msg_id, msg_hash, redis
40
- if result
41
- # Only ack if we intend to - Disabled targets will not ack
42
- ack_topic = topic.split("__")
43
- ack_topic[1] = 'ACK' + ack_topic[1]
44
- ack_topic = ack_topic.join("__")
45
- Topic.write_topic(ack_topic, { 'result' => result, 'id' => msg_id }, '*', 100)
61
+ if all_same_db_shard
62
+ # Fast path: everything on one db_shard, single read
63
+ db_shard = db_shard_groups.keys.first || 0
64
+ Topic.read_topics(db_shard_groups[db_shard], db_shard: db_shard) do |topic, msg_id, msg_hash, redis|
65
+ result = yield topic, msg_id, msg_hash, redis
66
+ Topic.write_ack(topic, result, msg_id, db_shard: db_shard) if result
67
+ end
68
+ else
69
+ timeout_per_db_shard = [1000 / [db_shard_groups.length, 1].max, 100].max
70
+ db_shard_groups.each do |db_shard, topics|
71
+ Topic.read_topics(topics, nil, timeout_per_db_shard, db_shard: db_shard) do |topic, msg_id, msg_hash, redis|
72
+ result = yield topic, msg_id, msg_hash, redis
73
+ Topic.write_ack(topic, result, msg_id, db_shard: db_shard) if result
74
+ end
46
75
  end
47
76
  end
48
77
  end
@@ -50,15 +79,16 @@ module OpenC3
50
79
 
51
80
  def self.write_raw(interface_name, data, timeout: nil, scope:)
52
81
  interface_name = interface_name.upcase
82
+ db_shard = _db_shard_for_interface(interface_name, scope: scope)
53
83
 
54
84
  timeout = COMMAND_ACK_TIMEOUT_S unless timeout
55
85
  ack_topic = "{#{scope}__ACKCMD}INTERFACE__#{interface_name}"
56
- Topic.update_topic_offsets([ack_topic])
86
+ Topic.update_topic_offsets([ack_topic], db_shard: db_shard)
57
87
 
58
- cmd_id = Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'raw' => data }, '*', 100)
88
+ cmd_id = Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'raw' => data }, '*', 100, db_shard: db_shard)
59
89
  time = Time.now
60
90
  while (Time.now - time) < timeout
61
- Topic.read_topics([ack_topic]) do |_topic, _msg_id, msg_hash, _redis|
91
+ Topic.read_topics([ack_topic], db_shard: db_shard) do |_topic, _msg_id, msg_hash, _redis|
62
92
  if msg_hash["id"] == cmd_id
63
93
  if msg_hash["result"] == "SUCCESS"
64
94
  return
@@ -72,61 +102,70 @@ module OpenC3
72
102
  end
73
103
 
74
104
  def self.connect_interface(interface_name, *interface_params, scope:)
105
+ db_shard = _db_shard_for_interface(interface_name, scope: scope)
75
106
  if interface_params && !interface_params.empty?
76
- Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'connect' => 'true', 'params' => JSON.generate(interface_params, allow_nan: true) }, '*', 100)
107
+ Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'connect' => 'true', 'params' => JSON.generate(interface_params, allow_nan: true) }, '*', 100, db_shard: db_shard)
77
108
  else
78
- Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'connect' => 'true' }, '*', 100)
109
+ Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'connect' => 'true' }, '*', 100, db_shard: db_shard)
79
110
  end
80
111
  end
81
112
 
82
113
  def self.disconnect_interface(interface_name, scope:)
83
- Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'disconnect' => 'true' }, '*', 100)
114
+ db_shard = _db_shard_for_interface(interface_name, scope: scope)
115
+ Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'disconnect' => 'true' }, '*', 100, db_shard: db_shard)
84
116
  end
85
117
 
86
118
  def self.start_raw_logging(interface_name, scope:)
87
- Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'log_stream' => 'true' }, '*', 100)
119
+ db_shard = _db_shard_for_interface(interface_name, scope: scope)
120
+ Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'log_stream' => 'true' }, '*', 100, db_shard: db_shard)
88
121
  end
89
122
 
90
123
  def self.stop_raw_logging(interface_name, scope:)
91
- Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'log_stream' => 'false' }, '*', 100)
124
+ db_shard = _db_shard_for_interface(interface_name, scope: scope)
125
+ Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'log_stream' => 'false' }, '*', 100, db_shard: db_shard)
92
126
  end
93
127
 
94
128
  def self.shutdown(interface, scope:)
95
- Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface.name}", { 'shutdown' => 'true' }, '*', 100)
129
+ db_shard = _db_shard_for_interface(interface.name, scope: scope)
130
+ Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface.name}", { 'shutdown' => 'true' }, '*', 100, db_shard: db_shard)
96
131
  end
97
132
 
98
133
  def self.interface_cmd(interface_name, cmd_name, *cmd_params, scope:)
134
+ db_shard = _db_shard_for_interface(interface_name, scope: scope)
99
135
  data = {}
100
136
  data['cmd_name'] = cmd_name
101
137
  data['cmd_params'] = cmd_params
102
- Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'interface_cmd' => JSON.generate(data, allow_nan: true) }, '*', 100)
138
+ Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'interface_cmd' => JSON.generate(data, allow_nan: true) }, '*', 100, db_shard: db_shard)
103
139
  end
104
140
 
105
141
  def self.protocol_cmd(interface_name, cmd_name, *cmd_params, read_write: :READ_WRITE, index: -1, scope:)
142
+ db_shard = _db_shard_for_interface(interface_name, scope: scope)
106
143
  data = {}
107
144
  data['cmd_name'] = cmd_name
108
145
  data['cmd_params'] = cmd_params
109
146
  data['read_write'] = read_write.to_s.upcase
110
147
  data['index'] = index
111
- Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'protocol_cmd' => JSON.generate(data, allow_nan: true) }, '*', 100)
148
+ Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'protocol_cmd' => JSON.generate(data, allow_nan: true) }, '*', 100, db_shard: db_shard)
112
149
  end
113
150
 
114
- def self.inject_tlm(interface_name, target_name, packet_name, item_hash = nil, type: :CONVERTED, timeout: nil, scope:)
151
+ def self.inject_tlm(interface_name, target_name, packet_name, item_hash = nil, type: :CONVERTED, stored: false, timeout: nil, scope:)
115
152
  interface_name = interface_name.upcase
153
+ db_shard = _db_shard_for_interface(interface_name, scope: scope)
116
154
 
117
155
  timeout = COMMAND_ACK_TIMEOUT_S unless timeout
118
156
  ack_topic = "{#{scope}__ACKCMD}INTERFACE__#{interface_name}"
119
- Topic.update_topic_offsets([ack_topic])
157
+ Topic.update_topic_offsets([ack_topic], db_shard: db_shard)
120
158
 
121
159
  data = {}
122
160
  data['target_name'] = target_name.to_s.upcase
123
161
  data['packet_name'] = packet_name.to_s.upcase
124
162
  data['item_hash'] = item_hash
125
163
  data['type'] = type
126
- cmd_id = Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'inject_tlm' => JSON.generate(data, allow_nan: true) }, '*', 100)
164
+ data['stored'] = stored
165
+ cmd_id = Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'inject_tlm' => JSON.generate(data, allow_nan: true) }, '*', 100, db_shard: db_shard)
127
166
  time = Time.now
128
167
  while (Time.now - time) < timeout
129
- Topic.read_topics([ack_topic]) do |_topic, _msg_id, msg_hash, _redis|
168
+ Topic.read_topics([ack_topic], db_shard: db_shard) do |_topic, _msg_id, msg_hash, _redis|
130
169
  if msg_hash["id"] == cmd_id
131
170
  if msg_hash["result"] == "SUCCESS"
132
171
  return
@@ -140,34 +179,37 @@ module OpenC3
140
179
  end
141
180
 
142
181
  def self.interface_target_enable(interface_name, target_name, cmd_only: false, tlm_only: false, scope:)
182
+ db_shard = _db_shard_for_interface(interface_name, scope: scope)
143
183
  data = {}
144
184
  data['target_name'] = target_name.to_s.upcase
145
185
  data['cmd_only'] = cmd_only
146
186
  data['tlm_only'] = tlm_only
147
187
  data['action'] = 'enable'
148
- Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'target_control' => JSON.generate(data, allow_nan: true) }, '*', 100)
188
+ Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'target_control' => JSON.generate(data, allow_nan: true) }, '*', 100, db_shard: db_shard)
149
189
  end
150
190
 
151
191
  def self.interface_target_disable(interface_name, target_name, cmd_only: false, tlm_only: false, scope:)
192
+ db_shard = _db_shard_for_interface(interface_name, scope: scope)
152
193
  data = {}
153
194
  data['target_name'] = target_name.to_s.upcase
154
195
  data['cmd_only'] = cmd_only
155
196
  data['tlm_only'] = tlm_only
156
197
  data['action'] = 'disable'
157
- Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'target_control' => JSON.generate(data, allow_nan: true) }, '*', 100)
198
+ Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'target_control' => JSON.generate(data, allow_nan: true) }, '*', 100, db_shard: db_shard)
158
199
  end
159
200
 
160
201
  def self.interface_details(interface_name, timeout: nil, scope:)
161
202
  interface_name = interface_name.upcase
203
+ db_shard = _db_shard_for_interface(interface_name, scope: scope)
162
204
 
163
205
  timeout = COMMAND_ACK_TIMEOUT_S unless timeout
164
206
  ack_topic = "{#{scope}__ACKCMD}INTERFACE__#{interface_name}"
165
- Topic.update_topic_offsets([ack_topic])
207
+ Topic.update_topic_offsets([ack_topic], db_shard: db_shard)
166
208
 
167
- cmd_id = Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'interface_details' => 'true' }, '*', 100)
209
+ cmd_id = Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'interface_details' => 'true' }, '*', 100, db_shard: db_shard)
168
210
  time = Time.now
169
211
  while (Time.now - time) < timeout
170
- Topic.read_topics([ack_topic]) do |_topic, _msg_id, msg_hash, _redis|
212
+ Topic.read_topics([ack_topic], db_shard: db_shard) do |_topic, _msg_id, msg_hash, _redis|
171
213
  if msg_hash["id"] == cmd_id
172
214
  return JSON.parse(msg_hash["result"], :allow_nan => true, :create_additions => true)
173
215
  end
@@ -25,6 +25,16 @@ module OpenC3
25
25
  # While this isn't a clean separation of topics (streams) and models (key-value)
26
26
  # it helps maintain consistency as the topic and model are linked.
27
27
  class LimitsEventTopic < Topic
28
+ # Collect all unique target db_shards from TargetModel
29
+ def self._active_db_shards(scope:)
30
+ db_shards = Set.new([0])
31
+ Store.hgetall("#{scope}__openc3_targets").each do |_name, json|
32
+ parsed = JSON.parse(json, allow_nan: true, create_additions: true)
33
+ db_shards << (parsed['db_shard'] || 0).to_i
34
+ end
35
+ db_shards
36
+ end
37
+
28
38
  def self.write(event, scope:)
29
39
  case event[:type]
30
40
  when :LIMITS_CHANGE
@@ -81,7 +91,10 @@ module OpenC3
81
91
  raise "Invalid limits event type '#{event[:type]}'"
82
92
  end
83
93
 
84
- Topic.write_topic("#{scope}__openc3_limits_events", {event: JSON.generate(event, allow_nan: true)}, '*', 1000)
94
+ # Write to all active db_shards so each decom microservice can read limits events inline
95
+ _active_db_shards(scope: scope).each do |db_shard|
96
+ Topic.write_topic("#{scope}__openc3_limits_events", {event: JSON.generate(event, allow_nan: true)}, '*', 1000, db_shard: db_shard)
97
+ end
85
98
  end
86
99
 
87
100
  # Remove the JSON encoding to return hashes directly
@@ -189,51 +202,59 @@ module OpenC3
189
202
  end
190
203
  end
191
204
 
192
- # Update the local system based on limits events
193
- def self.sync_system_thread_body(scope:, block_ms: nil)
194
- telemetry = System.telemetry.all
195
- topics = ["#{scope}__openc3_limits_events"]
196
- Topic.read_topics(topics, nil, block_ms) do |_topic, _msg_id, event, _redis|
197
- event = JSON.parse(event['event'], allow_nan: true, create_additions: true)
198
- case event['type']
199
- when 'LIMITS_CHANGE'
200
- # Ignore
201
- when 'LIMITS_SETTINGS'
202
- target_name = event['target_name']
203
- packet_name = event['packet_name']
204
- item_name = event['item_name']
205
- target = telemetry[target_name]
206
- if target
207
- packet = target[packet_name]
208
- if packet
209
- enabled = ConfigParser.handle_true_false_nil(event['enabled'])
210
- persistence = event['persistence']
211
- System.limits.set(target_name, packet_name, item_name,
212
- event['red_low'], event['yellow_low'], event['yellow_high'], event['red_high'],
213
- event['green_low'], event['green_high'], event['limits_set'], persistence, enabled)
214
- end
205
+ # Process a single limits event hash and update the local System accordingly.
206
+ # Called inline by DecomMicroservice when reading from the limits events topic.
207
+ def self.process_event(event, telemetry: nil)
208
+ telemetry ||= System.telemetry.all
209
+ case event['type']
210
+ when 'LIMITS_CHANGE'
211
+ # Ignore
212
+ when 'LIMITS_SETTINGS'
213
+ target_name = event['target_name']
214
+ packet_name = event['packet_name']
215
+ item_name = event['item_name']
216
+ target = telemetry[target_name]
217
+ if target
218
+ packet = target[packet_name]
219
+ if packet
220
+ enabled = ConfigParser.handle_true_false_nil(event['enabled'])
221
+ persistence = event['persistence']
222
+ System.limits.set(target_name, packet_name, item_name,
223
+ event['red_low'], event['yellow_low'], event['yellow_high'], event['red_high'],
224
+ event['green_low'], event['green_high'], event['limits_set'], persistence, enabled)
215
225
  end
226
+ end
216
227
 
217
- when 'LIMITS_ENABLE_STATE'
218
- target_name = event['target_name']
219
- packet_name = event['packet_name']
220
- item_name = event['item_name']
221
- target = telemetry[target_name]
222
- if target
223
- packet = target[packet_name]
224
- if packet
225
- enabled = ConfigParser.handle_true_false_nil(event['enabled'])
226
- if enabled
227
- System.limits.enable(target_name, packet_name, item_name)
228
- else
229
- System.limits.disable(target_name, packet_name, item_name)
230
- end
228
+ when 'LIMITS_ENABLE_STATE'
229
+ target_name = event['target_name']
230
+ packet_name = event['packet_name']
231
+ item_name = event['item_name']
232
+ target = telemetry[target_name]
233
+ if target
234
+ packet = target[packet_name]
235
+ if packet
236
+ enabled = ConfigParser.handle_true_false_nil(event['enabled'])
237
+ if enabled
238
+ System.limits.enable(target_name, packet_name, item_name)
239
+ else
240
+ System.limits.disable(target_name, packet_name, item_name)
231
241
  end
232
242
  end
233
-
234
- when 'LIMITS_SET'
235
- System.limits_set = event['set']
236
243
  end
244
+
245
+ when 'LIMITS_SET'
246
+ System.limits_set = event['set']
247
+ end
248
+ end
249
+
250
+ # Update the local system based on limits events (standalone read loop).
251
+ # Still available for non-decom consumers that need to sync limits.
252
+ def self.sync_system_thread_body(scope:, block_ms: nil)
253
+ telemetry = System.telemetry.all
254
+ topics = ["#{scope}__openc3_limits_events"]
255
+ Topic.read_topics(topics, nil, block_ms) do |_topic, _msg_id, event, _redis|
256
+ event = JSON.parse(event['event'], allow_nan: true, create_additions: true)
257
+ process_event(event, telemetry: telemetry)
237
258
  end
238
259
  end
239
260
  end