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
@@ -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
- Topic.read_topics(RouterTopic.topics(router, scope: scope)) do |topic, msg_id, msg_hash, redis|
40
- result = yield topic, msg_id, msg_hash, redis
41
- if result and /CMD}ROUTER/.match?(topic)
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 }, msg_id, 100)
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
- Topic.write_topic("{#{scope}__CMD}ROUTER__#{router_name}", { 'disconnect' => 'true' }, '*', 100)
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
- Topic.write_topic("{#{scope}__CMD}ROUTER__#{router_name}", { 'log_stream' => 'true' }, '*', 100)
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
- Topic.write_topic("{#{scope}__CMD}ROUTER__#{router_name}", { 'log_stream' => 'false' }, '*', 100)
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
- Topic.write_topic("{#{scope}__CMD}ROUTER__#{router.name}", { 'shutdown' => 'true' }, '*', 100)
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
- Topic.write_topic(PRIMARY_KEY, {event: JSON.generate(event, allow_nan: true)}, '*', 1000)
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
- Topic.write_topic("#{scope}__DECOM__{#{packet.target_name}}__#{packet.packet_name}", msg_hash, id)
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("#{scope}__TELEMETRY__{#{packet.target_name}}__#{packet.packet_name}", msg_hash)
37
+ EphemeralStoreQueued.instance(db_shard: db_shard).write_topic(topic, msg_hash)
36
38
  else
37
- Topic.write_topic("#{scope}__TELEMETRY__{#{packet.target_name}}__#{packet.packet_name}", msg_hash)
39
+ Topic.write_topic(topic, msg_hash, db_shard: db_shard)
38
40
  end
39
41
  end
40
42
  end
@@ -19,18 +19,90 @@ require 'openc3/utilities/store'
19
19
 
20
20
  module OpenC3
21
21
  class Topic
22
- # Delegate all unknown class methods to delegate to the EphemeralStore
22
+ # Delegate all unknown class methods to EphemeralStore db_shard 0 (system-level topics)
23
23
  def self.method_missing(message, *args, **kwargs, &block)
24
24
  EphemeralStore.public_send(message, *args, **kwargs, &block)
25
25
  end
26
26
 
27
- def self.clear_topics(topics, maxlen = 0)
28
- topics.each { |topic| EphemeralStore.xtrim(topic, maxlen) }
27
+ def self.clear_topics(topics, maxlen = 0, db_shard: 0)
28
+ store = EphemeralStore.instance(db_shard: db_shard)
29
+ topics.each { |topic| store.xtrim(topic, maxlen) }
29
30
  end
30
31
 
31
- def self.get_cnt(topic)
32
- _, packet = EphemeralStore.get_newest_message(topic)
32
+ def self.get_cnt(topic, db_shard: 0)
33
+ _, packet = EphemeralStore.instance(db_shard: db_shard).get_newest_message(topic)
33
34
  packet ? packet["received_count"].to_i : 0
34
35
  end
36
+
37
+ # DB_Shard-aware topic methods for target-specific streams.
38
+ # These explicitly route to the correct EphemeralStore db_shard.
39
+
40
+ def self.write_topic(topic, msg_hash, id = '*', maxlen = nil, approximate = 'true', db_shard: 0)
41
+ EphemeralStore.instance(db_shard: db_shard).write_topic(topic, msg_hash, id, maxlen, approximate)
42
+ end
43
+
44
+ def self.read_topics(topics, offsets = nil, timeout_ms = 1000, count = nil, db_shard: 0, &block)
45
+ EphemeralStore.instance(db_shard: db_shard).read_topics(topics, offsets, timeout_ms, count, &block)
46
+ end
47
+
48
+ def self.get_newest_message(topic, db_shard: 0)
49
+ EphemeralStore.instance(db_shard: db_shard).get_newest_message(topic)
50
+ end
51
+
52
+ def self.get_oldest_message(topic, db_shard: 0)
53
+ EphemeralStore.instance(db_shard: db_shard).get_oldest_message(topic)
54
+ end
55
+
56
+ def self.get_last_offset(topic, db_shard: 0)
57
+ EphemeralStore.instance(db_shard: db_shard).get_last_offset(topic)
58
+ end
59
+
60
+ def self.update_topic_offsets(topics, db_shard: 0)
61
+ EphemeralStore.instance(db_shard: db_shard).update_topic_offsets(topics)
62
+ end
63
+
64
+ def self.trim_topic(topic, minid, approximate = true, limit: 0, db_shard: 0)
65
+ EphemeralStore.instance(db_shard: db_shard).trim_topic(topic, minid, approximate, limit: limit)
66
+ end
67
+
68
+ def self.del(topic, db_shard: 0)
69
+ EphemeralStore.instance(db_shard: db_shard).del(topic)
70
+ end
71
+
72
+ # Group topics by db_shard. Each topic's target name is extracted and looked up.
73
+ # Topics matching target_pattern are db_sharded; others go to db_shard 0.
74
+ # @param topics [Array<String>] List of topic strings
75
+ # @param target_pattern [String] Substring to identify target-specific topics (e.g. 'CMD}TARGET__', '__TELEMETRY__')
76
+ # @param scope [String] Scope name for db_shard lookup
77
+ # @return [Hash] { db_shard => [topic, ...] }
78
+ def self.group_topics_by_db_shard(topics, target_pattern:, scope:)
79
+ groups = {}
80
+ topics.each do |topic|
81
+ if topic.include?(target_pattern)
82
+ target_name = topic.match(/__\{?([^}_]+)\}?__/)[1] rescue nil
83
+ # Handle CMD}TARGET__ pattern where target is after TARGET__
84
+ target_name = topic.split('TARGET__')[1] if target_pattern.include?('TARGET__') && target_name.nil?
85
+ db_shard = (Store.db_shard_for_target(target_name, scope: scope) || 0).to_i
86
+ else
87
+ db_shard = 0
88
+ end
89
+ groups[db_shard] ||= []
90
+ groups[db_shard] << topic
91
+ end
92
+ groups
93
+ end
94
+
95
+ # Check if all db_shard groups resolve to a single db_shard (fast path).
96
+ def self.all_same_db_shard?(db_shard_groups)
97
+ db_shard_groups.length <= 1
98
+ end
99
+
100
+ # Build the ACK topic from a command/router topic and write the ack.
101
+ def self.write_ack(topic, result, msg_id, db_shard: 0)
102
+ ack_topic = topic.split("__")
103
+ ack_topic[1] = 'ACK' + ack_topic[1]
104
+ ack_topic = ack_topic.join("__")
105
+ Topic.write_topic(ack_topic, { 'result' => result, 'id' => msg_id }, '*', 100, db_shard: db_shard)
106
+ end
35
107
  end
36
108
  end
@@ -177,6 +177,7 @@ module OpenC3
177
177
  @client.put_bucket_policy(options)
178
178
  rescue Aws::S3::Errors::NotImplemented, Aws::S3::Errors::ServiceError, Aws::S3::Errors::InternalError => e
179
179
  Logger.warn("put_bucket_policy for #{config_bucket} not supported by S3 backend: #{e.message}")
180
+ Logger.warn("Policy applied:\n#{config_policy}")
180
181
  end
181
182
 
182
183
  begin
@@ -186,6 +187,7 @@ module OpenC3
186
187
  @client.put_bucket_policy(options)
187
188
  rescue Aws::S3::Errors::NotImplemented, Aws::S3::Errors::ServiceError, Aws::S3::Errors::InternalError => e
188
189
  Logger.warn("put_bucket_policy for #{logs_bucket} not supported by S3 backend: #{e.message}")
190
+ Logger.warn("Policy applied:\n#{logs_policy}")
189
191
  end
190
192
  end
191
193
 
@@ -141,8 +141,9 @@ module OpenC3
141
141
  abort("Usage: cli generate #{args[0]} <NAME> (--ruby or --python)")
142
142
  end
143
143
 
144
- # Create the local variables
145
- plugin = args[1].downcase.gsub(/_+|-+/, '-')
144
+ # Create the local variables that are used in process_template below (see openc3/templates/plugin/plugin.gemspec as an example)
145
+ plugin_orig = args[1]
146
+ plugin = plugin_orig.downcase.gsub(/_+|-+/, '-')
146
147
  plugin_name = "openc3-cosmos-#{plugin}"
147
148
  if File.exist?(plugin_name)
148
149
  abort("Plugin #{plugin_name} already exists!")
@@ -37,15 +37,28 @@ module OpenC3
37
37
 
38
38
  attr_reader :microservice
39
39
  attr_reader :scope
40
+ attr_reader :db_shard
40
41
  attr_reader :data
41
42
  attr_reader :mutex
42
43
 
43
- def initialize(microservice:, scope:)
44
+ def initialize(microservice:, scope:, db_shard: nil)
44
45
  @scope = scope
45
46
  @microservice = microservice
46
47
  @data = {}
47
48
  @mutex = Mutex.new
48
49
 
50
+ if db_shard
51
+ @db_shard = db_shard
52
+ else
53
+ # Look up db_shard from MicroserviceModel
54
+ begin
55
+ json = Store.hget('openc3_microservices', microservice)
56
+ @db_shard = json ? JSON.parse(json)['db_shard'].to_i : 0
57
+ rescue
58
+ @db_shard = 0
59
+ end
60
+ end
61
+
49
62
  # Always make sure there is a update thread
50
63
  @@mutex.synchronize do
51
64
  @@instances << self
@@ -90,6 +103,7 @@ module OpenC3
90
103
  instance.mutex.synchronize do
91
104
  json = {}
92
105
  json['name'] = instance.microservice
106
+ json['db_shard'] = instance.db_shard
93
107
  values = instance.data
94
108
  json['values'] = values
95
109
  MetricModel.set(json, scope: instance.scope) if values.length > 0