openc3 7.0.0 → 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.
- checksums.yaml +4 -4
- data/bin/openc3cli +105 -13
- data/bin/pipinstall +38 -6
- data/data/config/command_modifiers.yaml +1 -0
- data/data/config/item_modifiers.yaml +2 -1
- data/data/config/microservice.yaml +12 -1
- data/data/config/parameter_modifiers.yaml +49 -7
- data/data/config/table_parameter_modifiers.yaml +3 -1
- data/data/config/target.yaml +11 -0
- data/data/config/target_config.yaml +6 -2
- data/lib/openc3/accessors/template_accessor.rb +9 -0
- data/lib/openc3/api/cmd_api.rb +2 -1
- data/lib/openc3/api/metrics_api.rb +11 -1
- data/lib/openc3/api/tlm_api.rb +21 -6
- data/lib/openc3/core_ext/faraday.rb +1 -1
- data/lib/openc3/interfaces/interface.rb +1 -6
- data/lib/openc3/io/json_api.rb +1 -1
- data/lib/openc3/logs/log_writer.rb +3 -1
- data/lib/openc3/microservices/decom_common.rb +128 -0
- data/lib/openc3/microservices/decom_microservice.rb +27 -96
- data/lib/openc3/microservices/interface_decom_common.rb +28 -10
- data/lib/openc3/microservices/interface_microservice.rb +16 -9
- data/lib/openc3/microservices/log_microservice.rb +1 -1
- data/lib/openc3/microservices/microservice.rb +3 -2
- data/lib/openc3/microservices/queue_microservice.rb +1 -1
- data/lib/openc3/microservices/scope_cleanup_microservice.rb +60 -46
- data/lib/openc3/microservices/text_log_microservice.rb +1 -2
- data/lib/openc3/models/cvt_model.rb +24 -13
- data/lib/openc3/models/db_sharded_model.rb +110 -0
- data/lib/openc3/models/interface_model.rb +9 -0
- data/lib/openc3/models/interface_status_model.rb +33 -3
- data/lib/openc3/models/metric_model.rb +96 -37
- data/lib/openc3/models/microservice_model.rb +7 -0
- data/lib/openc3/models/microservice_status_model.rb +30 -3
- data/lib/openc3/models/plugin_model.rb +9 -1
- data/lib/openc3/models/python_package_model.rb +1 -1
- data/lib/openc3/models/reaction_model.rb +27 -9
- data/lib/openc3/models/reingest_job_model.rb +153 -0
- data/lib/openc3/models/scope_model.rb +3 -2
- data/lib/openc3/models/script_status_model.rb +4 -20
- data/lib/openc3/models/target_model.rb +113 -100
- data/lib/openc3/models/trigger_model.rb +24 -7
- data/lib/openc3/packets/packet_config.rb +4 -1
- data/lib/openc3/script/api_shared.rb +39 -2
- data/lib/openc3/script/calendar.rb +32 -10
- data/lib/openc3/script/extract.rb +46 -13
- data/lib/openc3/script/script.rb +2 -2
- data/lib/openc3/script/script_runner.rb +4 -4
- data/lib/openc3/script/telemetry.rb +3 -3
- data/lib/openc3/script/web_socket_api.rb +29 -22
- data/lib/openc3/system/system.rb +20 -3
- data/lib/openc3/topics/command_decom_topic.rb +4 -2
- data/lib/openc3/topics/command_topic.rb +8 -5
- data/lib/openc3/topics/decom_interface_topic.rb +31 -11
- data/lib/openc3/topics/interface_topic.rb +88 -27
- data/lib/openc3/topics/limits_event_topic.rb +62 -41
- data/lib/openc3/topics/router_topic.rb +61 -21
- data/lib/openc3/topics/system_events_topic.rb +18 -1
- data/lib/openc3/topics/telemetry_decom_topic.rb +2 -1
- data/lib/openc3/topics/telemetry_topic.rb +4 -2
- data/lib/openc3/topics/topic.rb +77 -5
- data/lib/openc3/utilities/aws_bucket.rb +2 -0
- data/lib/openc3/utilities/cli_generator.rb +3 -2
- data/lib/openc3/utilities/ctrf.rb +231 -0
- data/lib/openc3/utilities/metric.rb +15 -1
- data/lib/openc3/utilities/questdb_client.rb +177 -40
- data/lib/openc3/utilities/reingest_job.rb +377 -0
- data/lib/openc3/utilities/ruby_lex_utils.rb +2 -0
- data/lib/openc3/utilities/store_autoload.rb +78 -52
- data/lib/openc3/utilities/store_queued.rb +20 -12
- data/lib/openc3/version.rb +5 -5
- data/templates/plugin/plugin.gemspec +13 -1
- 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 -4
- data/templates/tool_vue/src/router.js +2 -2
- data/templates/widget/package.json +2 -2
- metadata +8 -3
|
@@ -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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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,83 +102,114 @@ 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, scope:)
|
|
151
|
+
def self.inject_tlm(interface_name, target_name, packet_name, item_hash = nil, type: :CONVERTED, stored: false, timeout: nil, scope:)
|
|
152
|
+
interface_name = interface_name.upcase
|
|
153
|
+
db_shard = _db_shard_for_interface(interface_name, scope: scope)
|
|
154
|
+
|
|
155
|
+
timeout = COMMAND_ACK_TIMEOUT_S unless timeout
|
|
156
|
+
ack_topic = "{#{scope}__ACKCMD}INTERFACE__#{interface_name}"
|
|
157
|
+
Topic.update_topic_offsets([ack_topic], db_shard: db_shard)
|
|
158
|
+
|
|
115
159
|
data = {}
|
|
116
160
|
data['target_name'] = target_name.to_s.upcase
|
|
117
161
|
data['packet_name'] = packet_name.to_s.upcase
|
|
118
162
|
data['item_hash'] = item_hash
|
|
119
163
|
data['type'] = type
|
|
120
|
-
|
|
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)
|
|
166
|
+
time = Time.now
|
|
167
|
+
while (Time.now - time) < timeout
|
|
168
|
+
Topic.read_topics([ack_topic], db_shard: db_shard) do |_topic, _msg_id, msg_hash, _redis|
|
|
169
|
+
if msg_hash["id"] == cmd_id
|
|
170
|
+
if msg_hash["result"] == "SUCCESS"
|
|
171
|
+
return
|
|
172
|
+
else
|
|
173
|
+
raise msg_hash["result"]
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
raise "Timeout of #{timeout}s waiting for cmd ack"
|
|
121
179
|
end
|
|
122
180
|
|
|
123
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)
|
|
124
183
|
data = {}
|
|
125
184
|
data['target_name'] = target_name.to_s.upcase
|
|
126
185
|
data['cmd_only'] = cmd_only
|
|
127
186
|
data['tlm_only'] = tlm_only
|
|
128
187
|
data['action'] = 'enable'
|
|
129
|
-
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)
|
|
130
189
|
end
|
|
131
190
|
|
|
132
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)
|
|
133
193
|
data = {}
|
|
134
194
|
data['target_name'] = target_name.to_s.upcase
|
|
135
195
|
data['cmd_only'] = cmd_only
|
|
136
196
|
data['tlm_only'] = tlm_only
|
|
137
197
|
data['action'] = 'disable'
|
|
138
|
-
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)
|
|
139
199
|
end
|
|
140
200
|
|
|
141
201
|
def self.interface_details(interface_name, timeout: nil, scope:)
|
|
142
202
|
interface_name = interface_name.upcase
|
|
203
|
+
db_shard = _db_shard_for_interface(interface_name, scope: scope)
|
|
143
204
|
|
|
144
205
|
timeout = COMMAND_ACK_TIMEOUT_S unless timeout
|
|
145
206
|
ack_topic = "{#{scope}__ACKCMD}INTERFACE__#{interface_name}"
|
|
146
|
-
Topic.update_topic_offsets([ack_topic])
|
|
207
|
+
Topic.update_topic_offsets([ack_topic], db_shard: db_shard)
|
|
147
208
|
|
|
148
|
-
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)
|
|
149
210
|
time = Time.now
|
|
150
211
|
while (Time.now - time) < timeout
|
|
151
|
-
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|
|
|
152
213
|
if msg_hash["id"] == cmd_id
|
|
153
214
|
return JSON.parse(msg_hash["result"], :allow_nan => true, :create_additions => true)
|
|
154
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
|
-
|
|
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
|
-
#
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|
@@ -21,6 +21,12 @@ module OpenC3
|
|
|
21
21
|
class RouterTopic < Topic
|
|
22
22
|
COMMAND_ACK_TIMEOUT_S = 30
|
|
23
23
|
|
|
24
|
+
# Look up db_shard from RouterModel
|
|
25
|
+
def self._db_shard_for_router(router_name, scope:)
|
|
26
|
+
json = Store.hget("#{scope}__openc3_routers", router_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 router. This includes the router itself
|
|
25
31
|
# and all the targets which are assigned to this router.
|
|
26
32
|
def self.topics(router, scope:)
|
|
@@ -34,15 +40,39 @@ module OpenC3
|
|
|
34
40
|
topics
|
|
35
41
|
end
|
|
36
42
|
|
|
37
|
-
def self.receive_telemetry(router, scope:)
|
|
43
|
+
def self.receive_telemetry(router, scope:, db_shard: 0)
|
|
44
|
+
db_shard = db_shard.to_i
|
|
45
|
+
router_cmd_topic = "{#{scope}__CMD}ROUTER__#{router.name}"
|
|
46
|
+
|
|
47
|
+
target_topics = []
|
|
48
|
+
router.tlm_target_names.each do |target_name|
|
|
49
|
+
System.telemetry.packets(target_name).each do |_packet_name, packet|
|
|
50
|
+
target_topics << "#{scope}__TELEMETRY__{#{packet.target_name}}__#{packet.packet_name}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Group telemetry topics by db_shard; include router cmd topic on db_shard
|
|
55
|
+
db_shard_groups = Topic.group_topics_by_db_shard(target_topics, target_pattern: '__TELEMETRY__', scope: scope)
|
|
56
|
+
db_shard_groups[db_shard] ||= []
|
|
57
|
+
db_shard_groups[db_shard] << router_cmd_topic
|
|
58
|
+
|
|
59
|
+
all_same_db_shard = Topic.all_same_db_shard?(db_shard_groups)
|
|
60
|
+
|
|
38
61
|
while true
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
62
|
+
if all_same_db_shard
|
|
63
|
+
# Fast path: everything on one db_shard, single read
|
|
64
|
+
db_shard = db_shard_groups.keys.first || 0
|
|
65
|
+
Topic.read_topics(db_shard_groups[db_shard], db_shard: db_shard) do |topic, msg_id, msg_hash, redis|
|
|
66
|
+
result = yield topic, msg_id, msg_hash, redis
|
|
67
|
+
Topic.write_ack(topic, result, msg_id, db_shard: db_shard) if result and /CMD}ROUTER/.match?(topic)
|
|
68
|
+
end
|
|
69
|
+
else
|
|
70
|
+
timeout_per_db_shard = [1000 / [db_shard_groups.length, 1].max, 100].max
|
|
71
|
+
db_shard_groups.each do |db_shard, topics|
|
|
72
|
+
Topic.read_topics(topics, nil, timeout_per_db_shard, db_shard: db_shard) do |topic, msg_id, msg_hash, redis|
|
|
73
|
+
result = yield topic, msg_id, msg_hash, redis
|
|
74
|
+
Topic.write_ack(topic, result, msg_id, db_shard: db_shard) if result and /CMD}ROUTER/.match?(topic)
|
|
75
|
+
end
|
|
46
76
|
end
|
|
47
77
|
end
|
|
48
78
|
end
|
|
@@ -61,74 +91,84 @@ module OpenC3
|
|
|
61
91
|
end
|
|
62
92
|
|
|
63
93
|
def self.connect_router(router_name, *router_params, scope:)
|
|
94
|
+
db_shard = _db_shard_for_router(router_name, scope: scope)
|
|
64
95
|
if router_params && !router_params.empty?
|
|
65
|
-
Topic.write_topic("{#{scope}__CMD}ROUTER__#{router_name}", { 'connect' => 'true', 'params' => JSON.generate(router_params, allow_nan: true) }, '*', 100)
|
|
96
|
+
Topic.write_topic("{#{scope}__CMD}ROUTER__#{router_name}", { 'connect' => 'true', 'params' => JSON.generate(router_params, allow_nan: true) }, '*', 100, db_shard: db_shard)
|
|
66
97
|
else
|
|
67
|
-
Topic.write_topic("{#{scope}__CMD}ROUTER__#{router_name}", { 'connect' => 'true' }, '*', 100)
|
|
98
|
+
Topic.write_topic("{#{scope}__CMD}ROUTER__#{router_name}", { 'connect' => 'true' }, '*', 100, db_shard: db_shard)
|
|
68
99
|
end
|
|
69
100
|
end
|
|
70
101
|
|
|
71
102
|
def self.disconnect_router(router_name, scope:)
|
|
72
|
-
|
|
103
|
+
db_shard = _db_shard_for_router(router_name, scope: scope)
|
|
104
|
+
Topic.write_topic("{#{scope}__CMD}ROUTER__#{router_name}", { 'disconnect' => 'true' }, '*', 100, db_shard: db_shard)
|
|
73
105
|
end
|
|
74
106
|
|
|
75
107
|
def self.start_raw_logging(router_name, scope:)
|
|
76
|
-
|
|
108
|
+
db_shard = _db_shard_for_router(router_name, scope: scope)
|
|
109
|
+
Topic.write_topic("{#{scope}__CMD}ROUTER__#{router_name}", { 'log_stream' => 'true' }, '*', 100, db_shard: db_shard)
|
|
77
110
|
end
|
|
78
111
|
|
|
79
112
|
def self.stop_raw_logging(router_name, scope:)
|
|
80
|
-
|
|
113
|
+
db_shard = _db_shard_for_router(router_name, scope: scope)
|
|
114
|
+
Topic.write_topic("{#{scope}__CMD}ROUTER__#{router_name}", { 'log_stream' => 'false' }, '*', 100, db_shard: db_shard)
|
|
81
115
|
end
|
|
82
116
|
|
|
83
117
|
def self.shutdown(router, scope:)
|
|
84
|
-
|
|
118
|
+
db_shard = _db_shard_for_router(router.name, scope: scope)
|
|
119
|
+
Topic.write_topic("{#{scope}__CMD}ROUTER__#{router.name}", { 'shutdown' => 'true' }, '*', 100, db_shard: db_shard)
|
|
85
120
|
end
|
|
86
121
|
|
|
87
122
|
def self.router_cmd(router_name, cmd_name, *cmd_params, scope:)
|
|
123
|
+
db_shard = _db_shard_for_router(router_name, scope: scope)
|
|
88
124
|
data = {}
|
|
89
125
|
data['cmd_name'] = cmd_name
|
|
90
126
|
data['cmd_params'] = cmd_params
|
|
91
|
-
Topic.write_topic("{#{scope}__CMD}ROUTER__#{router_name}", { 'router_cmd' => JSON.generate(data, allow_nan: true) }, '*', 100)
|
|
127
|
+
Topic.write_topic("{#{scope}__CMD}ROUTER__#{router_name}", { 'router_cmd' => JSON.generate(data, allow_nan: true) }, '*', 100, db_shard: db_shard)
|
|
92
128
|
end
|
|
93
129
|
|
|
94
130
|
def self.protocol_cmd(router_name, cmd_name, *cmd_params, read_write: :READ_WRITE, index: -1, scope:)
|
|
131
|
+
db_shard = _db_shard_for_router(router_name, scope: scope)
|
|
95
132
|
data = {}
|
|
96
133
|
data['cmd_name'] = cmd_name
|
|
97
134
|
data['cmd_params'] = cmd_params
|
|
98
135
|
data['read_write'] = read_write.to_s.upcase
|
|
99
136
|
data['index'] = index
|
|
100
|
-
Topic.write_topic("{#{scope}__CMD}ROUTER__#{router_name}", { 'protocol_cmd' => JSON.generate(data, allow_nan: true) }, '*', 100)
|
|
137
|
+
Topic.write_topic("{#{scope}__CMD}ROUTER__#{router_name}", { 'protocol_cmd' => JSON.generate(data, allow_nan: true) }, '*', 100, db_shard: db_shard)
|
|
101
138
|
end
|
|
102
139
|
|
|
103
140
|
def self.router_target_enable(router_name, target_name, cmd_only: false, tlm_only: false, scope:)
|
|
141
|
+
db_shard = _db_shard_for_router(router_name, scope: scope)
|
|
104
142
|
data = {}
|
|
105
143
|
data['target_name'] = target_name.to_s.upcase
|
|
106
144
|
data['cmd_only'] = cmd_only
|
|
107
145
|
data['tlm_only'] = tlm_only
|
|
108
146
|
data['action'] = 'enable'
|
|
109
|
-
Topic.write_topic("{#{scope}__CMD}ROUTER__#{router_name}", { 'target_control' => JSON.generate(data, allow_nan: true) }, '*', 100)
|
|
147
|
+
Topic.write_topic("{#{scope}__CMD}ROUTER__#{router_name}", { 'target_control' => JSON.generate(data, allow_nan: true) }, '*', 100, db_shard: db_shard)
|
|
110
148
|
end
|
|
111
149
|
|
|
112
150
|
def self.router_target_disable(router_name, target_name, cmd_only: false, tlm_only: false, scope:)
|
|
151
|
+
db_shard = _db_shard_for_router(router_name, scope: scope)
|
|
113
152
|
data = {}
|
|
114
153
|
data['target_name'] = target_name.to_s.upcase
|
|
115
154
|
data['cmd_only'] = cmd_only
|
|
116
155
|
data['tlm_only'] = tlm_only
|
|
117
156
|
data['action'] = 'disable'
|
|
118
|
-
Topic.write_topic("{#{scope}__CMD}ROUTER__#{router_name}", { 'target_control' => JSON.generate(data, allow_nan: true) }, '*', 100)
|
|
157
|
+
Topic.write_topic("{#{scope}__CMD}ROUTER__#{router_name}", { 'target_control' => JSON.generate(data, allow_nan: true) }, '*', 100, db_shard: db_shard)
|
|
119
158
|
end
|
|
120
159
|
|
|
121
160
|
def self.router_details(router_name, timeout: nil, scope:)
|
|
122
161
|
router_name = router_name.upcase
|
|
162
|
+
db_shard = _db_shard_for_router(router_name, scope: scope)
|
|
123
163
|
|
|
124
164
|
timeout = COMMAND_ACK_TIMEOUT_S unless timeout
|
|
125
165
|
ack_topic = "{#{scope}__ACKCMD}ROUTER__#{router_name}"
|
|
126
|
-
Topic.update_topic_offsets([ack_topic])
|
|
166
|
+
Topic.update_topic_offsets([ack_topic], db_shard: db_shard)
|
|
127
167
|
|
|
128
|
-
cmd_id = Topic.write_topic("{#{scope}__CMD}ROUTER__#{router_name}", { 'router_details' => 'true' }, '*', 100)
|
|
168
|
+
cmd_id = Topic.write_topic("{#{scope}__CMD}ROUTER__#{router_name}", { 'router_details' => 'true' }, '*', 100, db_shard: db_shard)
|
|
129
169
|
time = Time.now
|
|
130
170
|
while (Time.now - time) < timeout
|
|
131
|
-
Topic.read_topics([ack_topic]) do |_topic, _msg_id, msg_hash, _redis|
|
|
171
|
+
Topic.read_topics([ack_topic], db_shard: db_shard) do |_topic, _msg_id, msg_hash, _redis|
|
|
132
172
|
if msg_hash["id"] == cmd_id
|
|
133
173
|
return JSON.parse(msg_hash["result"], :allow_nan => true, :create_additions => true)
|
|
134
174
|
end
|
|
@@ -17,13 +17,30 @@ module OpenC3
|
|
|
17
17
|
class SystemEventsTopic < Topic
|
|
18
18
|
PRIMARY_KEY = "OPENC3__SYSTEM__EVENTS".freeze
|
|
19
19
|
|
|
20
|
+
# Collect all unique target db_shards from TargetModel
|
|
21
|
+
def self._active_db_shards
|
|
22
|
+
db_shards = Set.new([0])
|
|
23
|
+
# Iterate all scopes to find all target db_shards
|
|
24
|
+
Store.scan_each(match: '*__openc3_targets', type: 'hash') do |key|
|
|
25
|
+
Store.hgetall(key).each do |_name, json|
|
|
26
|
+
parsed = JSON.parse(json, allow_nan: true, create_additions: true)
|
|
27
|
+
db_shards << (parsed['db_shard'] || 0).to_i
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
db_shards
|
|
31
|
+
end
|
|
32
|
+
|
|
20
33
|
def self.update_topic_offsets()
|
|
21
34
|
Topic.update_topic_offsets([PRIMARY_KEY])
|
|
22
35
|
end
|
|
23
36
|
|
|
24
37
|
def self.write(type, event)
|
|
25
38
|
event['type'] = type
|
|
26
|
-
|
|
39
|
+
msg = {event: JSON.generate(event, allow_nan: true)}
|
|
40
|
+
# Write to all active db_shards so every interface microservice can read system events inline
|
|
41
|
+
_active_db_shards.each do |db_shard|
|
|
42
|
+
Topic.write_topic(PRIMARY_KEY, msg, '*', 1000, db_shard: db_shard)
|
|
43
|
+
end
|
|
27
44
|
end
|
|
28
45
|
|
|
29
46
|
def self.read()
|
|
@@ -43,7 +43,8 @@ module OpenC3
|
|
|
43
43
|
:received_count => packet.received_count,
|
|
44
44
|
:json_data => json_data,
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
db_shard = Store.db_shard_for_target(packet.target_name, scope: scope)
|
|
47
|
+
Topic.write_topic("#{scope}__DECOM__{#{packet.target_name}}__#{packet.packet_name}", msg_hash, id, db_shard: db_shard)
|
|
47
48
|
|
|
48
49
|
unless packet.stored
|
|
49
50
|
# Also update the current value table with the latest decommutated data
|
|
@@ -31,10 +31,12 @@ module OpenC3
|
|
|
31
31
|
:buffer => packet.buffer(false)
|
|
32
32
|
}
|
|
33
33
|
msg_hash[:extra] = JSON.generate(packet.extra.as_json, allow_nan: true) if packet.extra
|
|
34
|
+
topic = "#{scope}__TELEMETRY__{#{packet.target_name}}__#{packet.packet_name}"
|
|
35
|
+
db_shard = Store.db_shard_for_target(packet.target_name, scope: scope)
|
|
34
36
|
if queued
|
|
35
|
-
EphemeralStoreQueued.write_topic(
|
|
37
|
+
EphemeralStoreQueued.instance(db_shard: db_shard).write_topic(topic, msg_hash)
|
|
36
38
|
else
|
|
37
|
-
Topic.write_topic(
|
|
39
|
+
Topic.write_topic(topic, msg_hash, db_shard: db_shard)
|
|
38
40
|
end
|
|
39
41
|
end
|
|
40
42
|
end
|