cosmos 5.0.4 → 5.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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