Class: OpenC3::ActivityModel
- Defined in:
- lib/openc3/models/activity_model.rb
Constant Summary collapse
- MAX_DURATION =
Time::SEC_PER_DAY
- PRIMARY_KEY =
MUST be equal to ‘TimelineModel::PRIMARY_KEY` minus the leading __
'__openc3_timelines'.freeze
Instance Attribute Summary collapse
-
#data ⇒ Object
readonly
Returns the value of attribute data.
-
#events ⇒ Object
readonly
Returns the value of attribute events.
-
#fulfillment ⇒ Object
readonly
Returns the value of attribute fulfillment.
-
#kind ⇒ Object
readonly
Returns the value of attribute kind.
-
#recurring ⇒ Object
readonly
Returns the value of attribute recurring.
-
#start ⇒ Object
readonly
Returns the value of attribute start.
-
#stop ⇒ Object
readonly
Returns the value of attribute stop.
Attributes inherited from Model
#name, #plugin, #scope, #updated_at
Class Method Summary collapse
-
.activities(name:, scope:) ⇒ Array|nil
Called via the microservice this gets the previous 00:00:15 to 01:01:00.
-
.all(name:, scope:, limit: 100) ⇒ Array<Hash>
Array up to the limit of the models (as Hash objects) stored under the primary key.
-
.count(name:, scope:) ⇒ Integer
Count of the members stored under the primary key.
-
.destroy(name:, scope:, score:) ⇒ Integer
Remove one member from a sorted set.
-
.from_json(json, name:, scope:) ⇒ ActivityModel
Model generated from the passed JSON.
-
.get(name:, start:, stop:, scope:, limit: 100) ⇒ Array|nil
Array up to 100 of this model or empty array if name not found under primary_key.
-
.range_destroy(name:, scope:, min:, max:) ⇒ Integer
Remove members from min to max of the sorted set.
-
.score(name:, score:, scope:) ⇒ String|nil
String of the saved json or nil if score not found under primary_key.
Instance Method Summary collapse
-
#add_event(status:) ⇒ Object
add_event will make an event.
-
#as_json(*a) ⇒ Hash
Generated from the ActivityModel.
-
#commit(status:, message: nil, fulfillment: nil) ⇒ Object
commit will make an event and save the object to the redis database.
-
#create ⇒ Object
Update the Redis hash at primary_key and set the score equal to the start Epoch time the member is set to the JSON generated via calling as_json.
-
#destroy(recurring: false) ⇒ Object
destroy the activity from the redis database.
-
#initialize(name:, start:, stop:, kind:, data:, scope:, updated_at: 0, fulfillment: nil, events: nil, recurring: {}) ⇒ ActivityModel
constructor
A new instance of ActivityModel.
-
#notify(kind:, extra: nil) ⇒ Object
update the redis stream / timeline topic that something has changed.
-
#set_input(start:, stop:, kind: nil, data: nil, events: nil, fulfillment: nil, recurring: nil) ⇒ Object
Set the values of the instance, @start, @kind, @data, @events…
-
#update(start:, stop:, kind:, data:) ⇒ Object
Update the Redis hash at primary_key and remove the current activity at the current score and update the score to the new score equal to the start Epoch time this uses a multi to execute both the remove and create.
-
#validate_input(start:, stop:, kind:, data:) ⇒ Object
validate the input to the rules we have created for timelines.
-
#validate_time(ignore_score = nil) ⇒ Object
validate_time searches from the current activity @stop - 1 (because we allow overlap of stop with start) back through @start - MAX_DURATION.
Methods inherited from Model
#check_disable_erb, #deploy, #destroyed?, filter, find_all_by_plugin, get_all_models, get_model, handle_config, names, set, store, store_queued, #undeploy
Constructor Details
#initialize(name:, start:, stop:, kind:, data:, scope:, updated_at: 0, fulfillment: nil, events: nil, recurring: {}) ⇒ ActivityModel
Returns a new instance of ActivityModel.
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/openc3/models/activity_model.rb', line 134 def initialize( name:, start:, stop:, kind:, data:, scope:, updated_at: 0, fulfillment: nil, events: nil, recurring: {} ) super("#{scope}#{PRIMARY_KEY}__#{name}", name: name, scope: scope) set_input( fulfillment: fulfillment, start: start, stop: stop, kind: kind, data: data, events: events, recurring: recurring, ) @updated_at = updated_at end |
Instance Attribute Details
#data ⇒ Object (readonly)
Returns the value of attribute data.
132 133 134 |
# File 'lib/openc3/models/activity_model.rb', line 132 def data @data end |
#events ⇒ Object (readonly)
Returns the value of attribute events.
132 133 134 |
# File 'lib/openc3/models/activity_model.rb', line 132 def events @events end |
#fulfillment ⇒ Object (readonly)
Returns the value of attribute fulfillment.
132 133 134 |
# File 'lib/openc3/models/activity_model.rb', line 132 def fulfillment @fulfillment end |
#kind ⇒ Object (readonly)
Returns the value of attribute kind.
132 133 134 |
# File 'lib/openc3/models/activity_model.rb', line 132 def kind @kind end |
#recurring ⇒ Object (readonly)
Returns the value of attribute recurring.
132 133 134 |
# File 'lib/openc3/models/activity_model.rb', line 132 def recurring @recurring end |
#start ⇒ Object (readonly)
Returns the value of attribute start.
132 133 134 |
# File 'lib/openc3/models/activity_model.rb', line 132 def start @start end |
#stop ⇒ Object (readonly)
Returns the value of attribute stop.
132 133 134 |
# File 'lib/openc3/models/activity_model.rb', line 132 def stop @stop end |
Class Method Details
.activities(name:, scope:) ⇒ Array|nil
Called via the microservice this gets the previous 00:00:15 to 01:01:00. This should allow for a small buffer around the timeline to make sure the schedule doesn’t get stale. 00:00:15 was selected as the schedule queue used in the microservice has round robin array with 15 slots to make sure we don’t miss a planned task.
45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/openc3/models/activity_model.rb', line 45 def self.activities(name:, scope:) now = Time.now.to_i start_score = now - 15 stop_score = (now + 3660) array = Store.zrangebyscore("#{scope}#{PRIMARY_KEY}__#{name}", start_score, stop_score) ret_array = Array.new array.each do |value| ret_array << ActivityModel.from_json(value, name: name, scope: scope) end return ret_array end |
.all(name:, scope:, limit: 100) ⇒ Array<Hash>
Returns Array up to the limit of the models (as Hash objects) stored under the primary key.
72 73 74 75 76 77 78 79 |
# File 'lib/openc3/models/activity_model.rb', line 72 def self.all(name:, scope:, limit: 100) array = Store.zrange("#{scope}#{PRIMARY_KEY}__#{name}", 0, -1, :limit => [0, limit]) ret_array = Array.new array.each do |value| ret_array << JSON.parse(value, :allow_nan => true, :create_additions => true) end return ret_array end |
.count(name:, scope:) ⇒ Integer
Returns count of the members stored under the primary key.
91 92 93 |
# File 'lib/openc3/models/activity_model.rb', line 91 def self.count(name:, scope:) return Store.zcard("#{scope}#{PRIMARY_KEY}__#{name}") end |
.destroy(name:, scope:, score:) ⇒ Integer
Remove one member from a sorted set.
97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/openc3/models/activity_model.rb', line 97 def self.destroy(name:, scope:, score:) result = Store.zremrangebyscore("#{scope}#{PRIMARY_KEY}__#{name}", score, score) notification = { # start / stop to match SortedModel 'data' => JSON.generate({'start' => score}), 'kind' => 'deleted', 'type' => 'activity', 'timeline' => name } TimelineTopic.write_activity(notification, scope: scope) return result end |
.from_json(json, name:, scope:) ⇒ ActivityModel
Returns Model generated from the passed JSON.
126 127 128 129 130 |
# File 'lib/openc3/models/activity_model.rb', line 126 def self.from_json(json, name:, scope:) json = JSON.parse(json, :allow_nan => true, :create_additions => true) if String === json raise "json data is nil" if json.nil? self.new(**json.transform_keys(&:to_sym), name: name, scope: scope) end |
.get(name:, start:, stop:, scope:, limit: 100) ⇒ Array|nil
Returns Array up to 100 of this model or empty array if name not found under primary_key.
58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/openc3/models/activity_model.rb', line 58 def self.get(name:, start:, stop:, scope:, limit: 100) if start > stop raise ActivityInputError.new "start: #{start} must be before stop: #{stop}" end array = Store.zrangebyscore("#{scope}#{PRIMARY_KEY}__#{name}", start, stop, :limit => [0, limit]) ret_array = Array.new array.each do |value| ret_array << JSON.parse(value, :allow_nan => true, :create_additions => true) end return ret_array end |
.range_destroy(name:, scope:, min:, max:) ⇒ Integer
Remove members from min to max of the sorted set.
112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/openc3/models/activity_model.rb', line 112 def self.range_destroy(name:, scope:, min:, max:) result = Store.zremrangebyscore("#{scope}#{PRIMARY_KEY}__#{name}", min, max) notification = { # start / stop to match SortedModel 'data' => JSON.generate({'start' => min, 'stop' => max}), 'kind' => 'deleted', 'type' => 'activity', 'timeline' => name } TimelineTopic.write_activity(notification, scope: scope) return result end |
.score(name:, score:, scope:) ⇒ String|nil
Returns String of the saved json or nil if score not found under primary_key.
82 83 84 85 86 87 88 |
# File 'lib/openc3/models/activity_model.rb', line 82 def self.score(name:, score:, scope:) array = Store.zrangebyscore("#{scope}#{PRIMARY_KEY}__#{name}", score, score, :limit => [0, 1]) array.each do |value| return ActivityModel.from_json(value, name: name, scope: scope) end return nil end |
Instance Method Details
#add_event(status:) ⇒ Object
add_event will make an event. This will NOT save the object to the redis database
345 346 347 348 349 350 351 |
# File 'lib/openc3/models/activity_model.rb', line 345 def add_event(status:) event = { 'time' => Time.now.to_i, 'event' => status } @events << event end |
#as_json(*a) ⇒ Hash
Returns generated from the ActivityModel.
388 389 390 391 392 393 394 395 396 397 398 399 400 |
# File 'lib/openc3/models/activity_model.rb', line 388 def as_json(*a) { 'name' => @name, 'updated_at' => @updated_at, 'fulfillment' => @fulfillment, 'start' => @start, 'stop' => @stop, 'kind' => @kind, 'events' => @events, 'data' => @data.as_json(*a), 'recurring' => @recurring.as_json(*a) } end |
#commit(status:, message: nil, fulfillment: nil) ⇒ Object
commit will make an event and save the object to the redis database
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 |
# File 'lib/openc3/models/activity_model.rb', line 327 def commit(status:, message: nil, fulfillment: nil) event = { 'time' => Time.now.to_i, 'event' => status, 'commit' => true } event['message'] = unless .nil? @fulfillment = fulfillment.nil? ? @fulfillment : fulfillment @events << event Store.multi do |multi| multi.zremrangebyscore(@primary_key, @start, @start) multi.zadd(@primary_key, @start, JSON.generate(self.as_json(:allow_nan => true))) end notify(kind: 'event') end |
#create ⇒ Object
Update the Redis hash at primary_key and set the score equal to the start Epoch time the member is set to the JSON generated via calling as_json
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 |
# File 'lib/openc3/models/activity_model.rb', line 233 def create if @recurring['end'] and @recurring['frequency'] and @recurring['span'] # First validate the initial recurring activity ... all others are just offsets validate_input(start: @start, stop: @stop, kind: @kind, data: @data) # Create a uuid for deleting related recurring in the future @recurring['uuid'] = SecureRandom.uuid @recurring['start'] = @start duration = @stop - @start recurrance = 0 case @recurring['span'] when 'minutes' recurrance = @recurring['frequency'].to_i * 60 when 'hours' recurrance = @recurring['frequency'].to_i * 3600 when 'days' recurrance = @recurring['frequency'].to_i * 86400 end # Get all the existing events in the recurring time range as well as those before # the start of the recurring time range to ensure we don't start inside an existing event existing = Store.zrevrangebyscore(@primary_key, @recurring['end'] - 1, @recurring['start'] - MAX_DURATION) existing.map! {|value| JSON.parse(value, :allow_nan => true, :create_additions => true) } last_stop = nil # Update @updated_at and add an event assuming it all completes ok @updated_at = Time.now.to_nsec_from_epoch add_event(status: 'created') Store.multi do |multi| (@start..@recurring['end']).step(recurrance).each do |start_time| @start = start_time @stop = start_time + duration if last_stop and @start < last_stop @events.pop # Remove previously created event raise ActivityOverlapError.new "Recurring activity overlap. Increase recurrance delta or decrease activity duration." end existing.each do |value| if (@start >= value['start'] and @start < value['stop']) || (@stop > value['start'] and @stop <= value['stop']) @events.pop # Remove previously created event raise ActivityOverlapError.new "activity overlaps existing at #{value['start']}" end end multi.zadd(@primary_key, @start, JSON.generate(self.as_json(:allow_nan => true))) last_stop = @stop end end notify(kind: 'created') else validate_input(start: @start, stop: @stop, kind: @kind, data: @data) collision = validate_time() unless collision.nil? raise ActivityOverlapError.new "activity overlaps existing at #{collision}" end @updated_at = Time.now.to_nsec_from_epoch add_event(status: 'created') Store.zadd(@primary_key, @start, JSON.generate(self.as_json(:allow_nan => true))) notify(kind: 'created') end end |
#destroy(recurring: false) ⇒ Object
destroy the activity from the redis database
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 |
# File 'lib/openc3/models/activity_model.rb', line 354 def destroy(recurring: false) # Delete all recurring activities if recurring and @recurring['end'] and @recurring['uuid'] uuid = @recurring['uuid'] array = Store.zrangebyscore("#{scope}#{PRIMARY_KEY}__#{@name}", @recurring['start'], @recurring['end']) array.each do |value| model = ActivityModel.from_json(value, name: @name, scope: @scope) if model.recurring['uuid'] == uuid Store.zremrangebyscore(@primary_key, model.start, model.start) end end else Store.zremrangebyscore(@primary_key, @start, @start) end notify(kind: 'deleted') end |
#notify(kind:, extra: nil) ⇒ Object
update the redis stream / timeline topic that something has changed
372 373 374 375 376 377 378 379 380 381 382 383 384 385 |
# File 'lib/openc3/models/activity_model.rb', line 372 def notify(kind:, extra: nil) notification = { 'data' => JSON.generate(as_json(:allow_nan => true)), 'kind' => kind, 'type' => 'activity', 'timeline' => @name } notification['extra'] = extra unless extra.nil? begin TimelineTopic.write_activity(notification, scope: @scope) rescue StandardError => e raise ActivityError.new "Failed to write to stream: #{notification}, #{e}" end end |
#set_input(start:, stop:, kind: nil, data: nil, events: nil, fulfillment: nil, recurring: nil) ⇒ Object
Set the values of the instance, @start, @kind, @data, @events…
220 221 222 223 224 225 226 227 228 229 |
# File 'lib/openc3/models/activity_model.rb', line 220 def set_input(start:, stop:, kind: nil, data: nil, events: nil, fulfillment: nil, recurring: nil) validate_input(start: start, stop: stop, kind: kind, data: data) @start = start @stop = stop @fulfillment = fulfillment.nil? ? false : fulfillment @kind = kind.nil? ? @kind : kind @data = data.nil? ? @data : data @events = events.nil? ? Array.new : events @recurring = recurring.nil? ? @recurring : recurring end |
#update(start:, stop:, kind:, data:) ⇒ Object
Update the Redis hash at primary_key and remove the current activity at the current score and update the score to the new score equal to the start Epoch time this uses a multi to execute both the remove and create.
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 |
# File 'lib/openc3/models/activity_model.rb', line 300 def update(start:, stop:, kind:, data:) array = Store.zrangebyscore(@primary_key, @start, @start) if array.length == 0 raise ActivityError.new "failed to find activity at: #{@start}" end old_start = @start set_input(start: start, stop: stop, kind: kind, data: data, events: @events) @updated_at = Time.now.to_nsec_from_epoch # copy of create collision = validate_time(old_start) unless collision.nil? raise ActivityOverlapError.new "failed to update #{old_start}, no activities can overlap, collision: #{collision}" end add_event(status: 'updated') Store.multi do |multi| multi.zremrangebyscore(@primary_key, old_start, old_start) multi.zadd(@primary_key, @start, JSON.generate(self.as_json(:allow_nan => true))) end notify(kind: 'updated', extra: old_start) return @start end |
#validate_input(start:, stop:, kind:, data:) ⇒ Object
validate the input to the rules we have created for timelines.
-
A task’s start MUST NOT be in the past.
-
A task’s start MUST be before the stop.
-
A task CAN NOT be longer than MAX_DURATION (86400) in seconds.
-
A task MUST have a kind.
-
A task MUST have a data object/hash.
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/openc3/models/activity_model.rb', line 191 def validate_input(start:, stop:, kind:, data:) begin DateTime.strptime(start.to_s, '%s') DateTime.strptime(stop.to_s, '%s') rescue Date::Error raise ActivityInputError.new "start and stop must be seconds: #{start}, #{stop}" end now_i = Time.now.to_i begin duration = stop - start rescue NoMethodError raise ActivityInputError.new "start and stop must be seconds: #{start}, #{stop}" end if now_i >= start raise ActivityInputError.new "activity must be in the future, current_time: #{now_i} vs #{start}" elsif duration >= MAX_DURATION raise ActivityInputError.new "activity can not be longer than #{MAX_DURATION} seconds" elsif duration <= 0 raise ActivityInputError.new "start: #{start} must be before stop: #{stop}" elsif kind.nil? raise ActivityInputError.new "kind must not be nil: #{kind}" elsif data.nil? raise ActivityInputError.new "data must not be nil: #{data}" elsif data.is_a?(Hash) == false raise ActivityInputError.new "data must be a json object/hash: #{data}" end end |
#validate_time(ignore_score = nil) ⇒ Object
validate_time searches from the current activity @stop - 1 (because we allow overlap of stop with start) back through @start - MAX_DURATION. The method is trying to validate that this new activity does not overlap with anything else. The reason we search back past @start through MAX_DURATION is because we need to return all the activities that may start before us and verify that we don’t overlap them. Activities are only inserted by @start time so we need to go back to verify we don’t overlap existing @stop. Note: Score is the Seconds since the Unix Epoch: (%s) Number of seconds since 1970-01-01 00:00:00 UTC. zrange rev byscore finds activites from in reverse order so the first task is the closest task to the current score. In this case a parameter ignore_score allows the request to ignore that time and skip to the next time but if nothing is found in the time range we can return nil.
170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
# File 'lib/openc3/models/activity_model.rb', line 170 def validate_time(ignore_score = nil) array = Store.zrevrangebyscore(@primary_key, @stop - 1, @start - MAX_DURATION) array.each do |value| activity = JSON.parse(value, :allow_nan => true, :create_additions => true) if ignore_score == activity['start'] next elsif activity['stop'] > @start return activity['start'] else return nil end end return nil end |