cosmos 5.0.4 → 5.0.5

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.
@@ -32,9 +32,41 @@ module Cosmos
32
32
  # @return [true/false] Whether logging is enabled
33
33
  attr_reader :logging_enabled
34
34
 
35
+ # @return cycle_time [Integer] The amount of time in seconds before creating
36
+ # a new log file. This can be combined with cycle_size but is better used
37
+ # independently.
38
+ attr_reader :cycle_time
39
+
40
+ # @return cycle_hour [Integer] The time at which to cycle the log. Combined with
41
+ # cycle_minute to cycle the log daily at the specified time. If nil, the log
42
+ # will be cycled hourly at the specified cycle_minute.
43
+ attr_reader :cycle_hour
44
+
45
+ # @return cycle_minute [Integer] The time at which to cycle the log. See cycle_hour
46
+ # for more information.
47
+ attr_reader :cycle_minute
48
+
49
+ # @return [Time] Time that the current log file started
50
+ attr_reader :start_time
51
+
52
+ # @return [Mutex] Instance mutex protecting file
53
+ attr_reader :mutex
54
+
35
55
  # The cycle time interval. Cycle times are only checked at this level of
36
56
  # granularity.
37
- CYCLE_TIME_INTERVAL = 2
57
+ CYCLE_TIME_INTERVAL = 10
58
+
59
+ # Mutex protecting class variables
60
+ @@mutex = Mutex.new
61
+
62
+ # Array of instances used to keep track of cycling logs
63
+ @@instances = []
64
+
65
+ # Thread used to cycle logs across all log writers
66
+ @@cycle_thread = nil
67
+
68
+ # Sleeper used to delay cycle thread
69
+ @@cycle_sleeper = nil
38
70
 
39
71
  # @param remote_log_directory [String] The s3 path to store the log files
40
72
  # @param logging_enabled [Boolean] Whether to start with logging enabled
@@ -89,11 +121,15 @@ module Cosmos
89
121
  # each time we create an entry which we do a LOT!
90
122
  @entry = String.new
91
123
 
92
- @cycle_thread = nil
93
124
  if @cycle_time or @cycle_hour or @cycle_minute
94
- @cycle_sleeper = Sleeper.new
95
- @cycle_thread = Cosmos.safe_thread("Log cycle") do
96
- cycle_thread_body()
125
+ @@mutex.synchronize do
126
+ @@instances << self
127
+
128
+ unless @@cycle_thread
129
+ @@cycle_thread = Cosmos.safe_thread("Log cycle") do
130
+ cycle_thread_body()
131
+ end
132
+ end
97
133
  end
98
134
  end
99
135
  end
@@ -113,10 +149,13 @@ module Cosmos
113
149
  # Stop all logging, close the current log file, and kill the logging threads.
114
150
  def shutdown
115
151
  stop()
116
- if @cycle_thread
117
- @cycle_sleeper.cancel
118
- Cosmos.kill_thread(self, @cycle_thread)
119
- @cycle_thread = nil
152
+ @@mutex.synchronize do
153
+ @@instances.delete(self)
154
+ if @@instances.length <= 0
155
+ @@cycle_sleeper.cancel if @@cycle_sleeper
156
+ Cosmos.kill_thread(self, @@cycle_thread) if @@cycle_thread
157
+ @@cycle_thread = nil
158
+ end
120
159
  end
121
160
  end
122
161
 
@@ -143,28 +182,38 @@ module Cosmos
143
182
  end
144
183
 
145
184
  def cycle_thread_body
185
+ @@cycle_sleeper = Sleeper.new
146
186
  while true
147
- # The check against start_time needs to be mutex protected to prevent a packet coming in between the check
148
- # and closing the file
149
- @mutex.synchronize do
150
- utc_now = Time.now.utc
151
- # Logger.debug("start:#{@start_time.to_f} now:#{utc_now.to_f} cycle:#{@cycle_time} new:#{(utc_now - @start_time) > @cycle_time}")
152
- if @logging_enabled and
153
- (
154
- # Cycle based on total time logging
155
- (@cycle_time and (utc_now - @start_time) > @cycle_time) or
156
-
157
- # Cycle daily at a specific time
158
- (@cycle_hour and @cycle_minute and utc_now.hour == @cycle_hour and utc_now.min == @cycle_minute and @start_time.yday != utc_now.yday) or
159
-
160
- # Cycle hourly at a specific time
161
- (@cycle_minute and not @cycle_hour and utc_now.min == @cycle_minute and @start_time.hour != utc_now.hour)
162
- )
163
- close_file(false)
187
+ start_time = Time.now
188
+ @@mutex.synchronize do
189
+ @@instances.each do |instance|
190
+ # The check against start_time needs to be mutex protected to prevent a packet coming in between the check
191
+ # and closing the file
192
+ instance.mutex.synchronize do
193
+ utc_now = Time.now.utc
194
+ # Logger.debug("start:#{@start_time.to_f} now:#{utc_now.to_f} cycle:#{@cycle_time} new:#{(utc_now - @start_time) > @cycle_time}")
195
+ if instance.logging_enabled and
196
+ (
197
+ # Cycle based on total time logging
198
+ (instance.cycle_time and (utc_now - instance.start_time) > instance.cycle_time) or
199
+
200
+ # Cycle daily at a specific time
201
+ (instance.cycle_hour and instance.cycle_minute and utc_now.hour == instance.cycle_hour and utc_now.min == instance.cycle_minute and instance.start_time.yday != utc_now.yday) or
202
+
203
+ # Cycle hourly at a specific time
204
+ (instance.cycle_minute and not instance.cycle_hour and utc_now.min == instance.cycle_minute and instance.start_time.hour != utc_now.hour)
205
+ )
206
+ instance.close_file(false)
207
+ end
208
+ end
164
209
  end
165
210
  end
211
+
166
212
  # Only check whether to cycle at a set interval
167
- break if @cycle_sleeper.sleep(CYCLE_TIME_INTERVAL)
213
+ run_time = Time.now - start_time
214
+ sleep_time = CYCLE_TIME_INTERVAL - run_time
215
+ sleep_time = 0 if sleep_time < 0
216
+ break if @@cycle_sleeper.sleep(sleep_time)
168
217
  end
169
218
  end
170
219
 
@@ -83,11 +83,6 @@ module Cosmos
83
83
  @interface.write_raw(msg_hash['raw'])
84
84
  next 'SUCCESS'
85
85
  end
86
- if msg_hash['inject_tlm']
87
- Logger.info "#{@interface.name}: Inject telemetry" if msg_hash['log']
88
- @tlm.inject_tlm(msg_hash)
89
- next 'SUCCESS'
90
- end
91
86
  if msg_hash.key?('log_raw')
92
87
  if msg_hash['log_raw'] == 'true'
93
88
  Logger.info "#{@interface.name}: Enable raw logging"
@@ -412,16 +407,6 @@ module Cosmos
412
407
  TelemetryTopic.write_packet(packet, scope: @scope)
413
408
  end
414
409
 
415
- def inject_tlm(hash)
416
- packet = System.telemetry.packet(hash['target_name'], hash['packet_name']).clone
417
- if hash['item_hash']
418
- JSON.parse(hash['item_hash']).each do |item, value|
419
- packet.write(item.to_s, value, hash['type'].to_sym)
420
- end
421
- end
422
- handle_packet(packet)
423
- end
424
-
425
410
  def handle_connection_failed(connect_error)
426
411
  @error = connect_error
427
412
  Logger.error "#{@interface.name}: Connection Failed: #{connect_error.formatted(false, false)}"
@@ -81,7 +81,13 @@ module Cosmos
81
81
  else
82
82
  gem_file_path = get(temp_dir, name_or_path)
83
83
  end
84
- rubygems_url = get_setting('rubygems_url', scope: scope)
84
+ begin
85
+ rubygems_url = get_setting('rubygems_url', scope: scope)
86
+ rescue
87
+ # If Redis isn't running try the ENV, then simply rubygems.org
88
+ rubygems_url = ENV['RUBYGEMS_URL']
89
+ rubygems_url ||= 'https://rubygems.org'
90
+ end
85
91
  Gem.sources = [rubygems_url] if rubygems_url
86
92
  Gem.done_installing_hooks.clear
87
93
  Gem.install(gem_file_path, "> 0.pre", :build_args => ['--no-document'], :prerelease => true)
@@ -237,7 +237,7 @@ module Cosmos
237
237
  end
238
238
 
239
239
  # Creates a MicroserviceModel to deploy the Interface/Router
240
- def deploy(gem_path, variables)
240
+ def deploy(gem_path, variables, validate_only: false)
241
241
  type = self.class._get_type
242
242
  microservice_name = "#{@scope}__#{type}__#{@name}"
243
243
  microservice = MicroserviceModel.new(
@@ -249,9 +249,12 @@ module Cosmos
249
249
  needs_dependencies: @needs_dependencies,
250
250
  scope: @scope
251
251
  )
252
- microservice.create
253
- microservice.deploy(gem_path, variables)
254
- Logger.info "Configured #{type.downcase} microservice #{microservice_name}"
252
+ unless validate_only
253
+ microservice.create
254
+ microservice.deploy(gem_path, variables)
255
+ ConfigTopic.write({ kind: 'created', type: type.downcase, name: @name, plugin: @plugin }, scope: @scope)
256
+ Logger.info "Configured #{type.downcase} microservice #{microservice_name}"
257
+ end
255
258
  microservice
256
259
  end
257
260
 
@@ -259,9 +262,15 @@ module Cosmos
259
262
  # should should trigger the operator to kill the microservice that in turn
260
263
  # will destroy the InterfaceStatusModel when a stop is called.
261
264
  def undeploy
262
- model = MicroserviceModel.get_model(name: "#{@scope}__#{self.class._get_type}__#{@name}", scope: @scope)
263
- model.destroy if model
264
- if self.class._get_type == 'INTERFACE'
265
+ type = self.class._get_type
266
+ name = "#{@scope}__#{type}__#{@name}"
267
+ model = MicroserviceModel.get_model(name: name, scope: @scope)
268
+ if model
269
+ model.destroy
270
+ ConfigTopic.write({ kind: 'deleted', type: type.downcase, name: @name, plugin: @plugin }, scope: @scope)
271
+ end
272
+
273
+ if type == 'INTERFACE'
265
274
  status_model = InterfaceStatusModel.get_model(name: @name, scope: @scope)
266
275
  else
267
276
  status_model = RouterStatusModel.get_model(name: @name, scope: @scope)
@@ -30,17 +30,19 @@ module Cosmos
30
30
  "#{scope}#{PRIMARY_KEY}"
31
31
  end
32
32
 
33
- attr_reader :color, :metadata, :type
33
+ attr_reader :color, :metadata, :constraints, :type
34
34
 
35
- # @param [Integer] start - time metadata is active in seconds from Epoch
35
+ # @param [Integer] start - Time metadata is active in seconds from Epoch
36
36
  # @param [String] color - The event color
37
- # @param [String] metadata - Key value pair object to link to name
37
+ # @param [Hash] metadata - Hash of metadata values
38
+ # @param [Hash] constraints - Constraints to apply to the metadata
38
39
  # @param [String] scope - Cosmos scope to track event to
39
40
  def initialize(
40
41
  scope:,
41
42
  start:,
42
43
  color: nil,
43
44
  metadata:,
45
+ constraints: nil,
44
46
  type: METADATA_TYPE,
45
47
  updated_at: 0
46
48
  )
@@ -48,6 +50,7 @@ module Cosmos
48
50
  @start = start
49
51
  @color = color
50
52
  @metadata = metadata
53
+ @constraints = constraints if constraints
51
54
  @type = type # For the as_json, from_json round trip
52
55
  end
53
56
 
@@ -56,6 +59,7 @@ module Cosmos
56
59
  validate_start(update: update)
57
60
  validate_color()
58
61
  validate_metadata()
62
+ validate_constraints() if @constraints
59
63
  end
60
64
 
61
65
  def validate_color()
@@ -72,6 +76,26 @@ module Cosmos
72
76
  unless @metadata.is_a?(Hash)
73
77
  raise SortedInputError.new "Metadata must be a hash/object: #{@metadata}"
74
78
  end
79
+ # Convert keys to strings. This isn't quite as efficient as symbols
80
+ # but we store as JSON which is all strings and it makes comparisons easier.
81
+ @metadata = @metadata.transform_keys(&:to_s)
82
+ end
83
+
84
+ def validate_constraints()
85
+ unless @constraints.is_a?(Hash)
86
+ raise SortedInputError.new "Constraints must be a hash/object: #{@constraints}"
87
+ end
88
+ # Convert keys to strings. This isn't quite as efficient as symbols
89
+ # but we store as JSON which is all strings and it makes comparisons easier.
90
+ @constraints = @constraints.transform_keys(&:to_s)
91
+ unless (@constraints.keys - @metadata.keys).empty?
92
+ raise SortedInputError.new "Constraints keys must be subset of metadata: #{@constraints.keys} subset #{@metadata.keys}"
93
+ end
94
+ @constraints.each do |key, constraint|
95
+ unless constraint.include?(@metadata[key])
96
+ raise SortedInputError.new "Constraint violation! key:#{key} value:#{@metadata[key]} constraint:#{constraint}"
97
+ end
98
+ end
75
99
  end
76
100
 
77
101
  # Update the Redis hash at primary_key based on the initial passed start
@@ -79,6 +103,7 @@ module Cosmos
79
103
  def create(update: false)
80
104
  validate(update: update)
81
105
  @updated_at = Time.now.to_nsec_from_epoch
106
+ MetadataModel.destroy(scope: @scope, start: update) if update
82
107
  Store.zadd(@primary_key, @start, JSON.generate(as_json()))
83
108
  if update
84
109
  notify(kind: 'updated')
@@ -87,29 +112,28 @@ module Cosmos
87
112
  end
88
113
  end
89
114
 
90
- # Update the Redis hash at primary_key
91
- def update(start:, color:, metadata:)
92
- @start = start
93
- @color = color
94
- @metadata = metadata
95
- create(update: true)
115
+ # Update the model. All arguments are optional, only those set will be updated.
116
+ def update(start: nil, color: nil, metadata: nil, constraints: nil)
117
+ orig_start = @start
118
+ @start = start if start
119
+ @color = color if color
120
+ @metadata = metadata if metadata
121
+ @constraints = constraints if constraints
122
+ create(update: orig_start)
96
123
  end
97
124
 
98
125
  # @return [Hash] generated from the MetadataModel
99
126
  def as_json
100
- return {
127
+ {
101
128
  'scope' => @scope,
102
129
  'start' => @start,
103
130
  'color' => @color,
104
131
  'metadata' => @metadata,
132
+ 'constraints' => @constraints,
105
133
  'type' => METADATA_TYPE,
106
134
  'updated_at' => @updated_at,
107
135
  }
108
136
  end
109
-
110
- # @return [String] string view of metadata
111
- def to_s
112
- return "<MetadataModel s: #{@start}, c: #{@color}, m: #{@metadata}>"
113
- end
137
+ alias to_s as_json
114
138
  end
115
139
  end
@@ -171,11 +171,10 @@ module Cosmos
171
171
  return nil
172
172
  end
173
173
 
174
- def deploy(gem_path, variables)
174
+ def deploy(gem_path, variables, validate_only: false)
175
175
  return unless @folder_name
176
176
 
177
177
  variables["microservice_name"] = @name
178
- rubys3_client = Aws::S3::Client.new
179
178
  start_path = "/microservices/#{@folder_name}/"
180
179
  Dir.glob(gem_path + start_path + "**/*") do |filename|
181
180
  next if filename == '.' or filename == '..' or File.directory?(filename)
@@ -188,7 +187,10 @@ module Cosmos
188
187
  Cosmos.set_working_dir(File.dirname(filename)) do
189
188
  data = ERB.new(data, trim_mode: "-").result(binding.set_variables(variables)) if data.is_printable?
190
189
  end
191
- rubys3_client.put_object(bucket: 'config', key: key, body: data)
190
+ unless validate_only
191
+ Aws::S3::Client.new.put_object(bucket: 'config', key: key, body: data)
192
+ ConfigTopic.write({ kind: 'created', type: 'microservice', name: @name, plugin: @plugin }, scope: @scope)
193
+ end
192
194
  end
193
195
  end
194
196
 
@@ -198,6 +200,7 @@ module Cosmos
198
200
  rubys3_client.list_objects(bucket: 'config', prefix: prefix).contents.each do |object|
199
201
  rubys3_client.delete_object(bucket: 'config', key: object.key)
200
202
  end
203
+ ConfigTopic.write({ kind: 'deleted', type: 'microservice', name: @name, plugin: @plugin }, scope: @scope)
201
204
  end
202
205
  end
203
206
  end
@@ -128,6 +128,7 @@ module Cosmos
128
128
  @updated_at = kw_args[:updated_at]
129
129
  @plugin = kw_args[:plugin]
130
130
  @scope = kw_args[:scope]
131
+ @destroyed = false
131
132
  end
132
133
 
133
134
  # Update the Redis hash at primary_key and set the field "name"
@@ -163,10 +164,16 @@ module Cosmos
163
164
 
164
165
  # Delete the model from the Store
165
166
  def destroy
167
+ @destroyed = true
166
168
  undeploy()
167
169
  self.class.store.hdel(@primary_key, @name)
168
170
  end
169
171
 
172
+ # Indicate if destroy has been called
173
+ def destroyed?
174
+ @destroyed
175
+ end
176
+
170
177
  # @return [Hash] JSON encoding of this model
171
178
  def as_json
172
179
  { 'name' => @name,
@@ -86,6 +86,7 @@ module Cosmos
86
86
  def create(update: false)
87
87
  validate(update: update)
88
88
  @updated_at = Time.now.to_nsec_from_epoch
89
+ NoteModel.destroy(scope: @scope, start: update) if update
89
90
  Store.zadd(@primary_key, @start, JSON.generate(as_json()))
90
91
  if update
91
92
  notify(kind: 'updated')
@@ -96,11 +97,12 @@ module Cosmos
96
97
 
97
98
  # Update the Redis hash at primary_key
98
99
  def update(start:, stop:, color:, description:)
100
+ orig_start = @start
99
101
  @start = start
100
102
  @stop = stop
101
103
  @color = color
102
104
  @description = description
103
- create(update: true)
105
+ create(update: orig_start)
104
106
  end
105
107
 
106
108
  # @return [Hash] generated from the NoteModel
@@ -115,10 +117,6 @@ module Cosmos
115
117
  'updated_at' => @updated_at,
116
118
  }
117
119
  end
118
-
119
- # @return [String] string view of NoteModel
120
- def to_s
121
- return "<NoteModel s: #{@start}, x: #{@stop}, c: #{@color}, d: #{@description}>"
122
- end
120
+ alias to_s as_json
123
121
  end
124
122
  end