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.
-
#duration ⇒ Object
readonly
Returns the value of attribute duration.
-
#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.
-
#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 ⇒ Object
destroy the activity from the redis database.
-
#initialize(name:, start:, stop:, kind:, data:, scope:, updated_at: 0, duration: 0, fulfillment: nil, events: nil) ⇒ 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) ⇒ 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 will be called on create this will pull the time up to MAX_DURATION of an activity this will make sure that the activity is the only activity on the timeline for the duration of the activity.
Methods inherited from Model
#check_disable_erb, #deploy, #destroyed?, filter, find_all_by_plugin, get_all_models, get_model, handle_config, names, set, store, #undeploy
Constructor Details
#initialize(name:, start:, stop:, kind:, data:, scope:, updated_at: 0, duration: 0, fulfillment: nil, events: nil) ⇒ ActivityModel
Returns a new instance of ActivityModel.
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/openc3/models/activity_model.rb', line 133 def initialize( name:, start:, stop:, kind:, data:, scope:, updated_at: 0, duration: 0, fulfillment: nil, events: nil ) super("#{scope}#{PRIMARY_KEY}__#{name}", name: name, scope: scope) set_input( fulfillment: fulfillment, start: start, stop: stop, kind: kind, data: data, events: events, ) @updated_at = updated_at end |
Instance Attribute Details
#data ⇒ Object (readonly)
Returns the value of attribute data.
131 132 133 |
# File 'lib/openc3/models/activity_model.rb', line 131 def data @data end |
#duration ⇒ Object (readonly)
Returns the value of attribute duration.
131 132 133 |
# File 'lib/openc3/models/activity_model.rb', line 131 def duration @duration end |
#events ⇒ Object (readonly)
Returns the value of attribute events.
131 132 133 |
# File 'lib/openc3/models/activity_model.rb', line 131 def events @events end |
#fulfillment ⇒ Object (readonly)
Returns the value of attribute fulfillment.
131 132 133 |
# File 'lib/openc3/models/activity_model.rb', line 131 def fulfillment @fulfillment end |
#kind ⇒ Object (readonly)
Returns the value of attribute kind.
131 132 133 |
# File 'lib/openc3/models/activity_model.rb', line 131 def kind @kind end |
#start ⇒ Object (readonly)
Returns the value of attribute start.
131 132 133 |
# File 'lib/openc3/models/activity_model.rb', line 131 def start @start end |
#stop ⇒ Object (readonly)
Returns the value of attribute stop.
131 132 133 |
# File 'lib/openc3/models/activity_model.rb', line 131 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.
44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/openc3/models/activity_model.rb', line 44 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.
71 72 73 74 75 76 77 78 |
# File 'lib/openc3/models/activity_model.rb', line 71 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.
90 91 92 |
# File 'lib/openc3/models/activity_model.rb', line 90 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.
96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/openc3/models/activity_model.rb', line 96 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.
125 126 127 128 129 |
# File 'lib/openc3/models/activity_model.rb', line 125 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.
57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/openc3/models/activity_model.rb', line 57 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.
111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/openc3/models/activity_model.rb', line 111 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.
81 82 83 84 85 86 87 |
# File 'lib/openc3/models/activity_model.rb', line 81 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
293 294 295 296 297 298 299 |
# File 'lib/openc3/models/activity_model.rb', line 293 def add_event(status:) event = { 'time' => Time.now.to_i, 'event' => status } @events << event end |
#as_json(*a) ⇒ Hash
Returns generated from the ActivityModel.
324 325 326 327 328 329 330 331 332 333 334 335 336 |
# File 'lib/openc3/models/activity_model.rb', line 324 def as_json(*a) { 'name' => @name, 'updated_at' => @updated_at, 'fulfillment' => @fulfillment, 'duration' => @duration, 'start' => @start, 'stop' => @stop, 'kind' => @kind, 'events' => @events, 'data' => @data.as_json(*a) } end |
#commit(status:, message: nil, fulfillment: nil) ⇒ Object
commit will make an event and save the object to the redis database
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 |
# File 'lib/openc3/models/activity_model.rb', line 275 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
231 232 233 234 235 236 237 238 239 240 241 242 |
# File 'lib/openc3/models/activity_model.rb', line 231 def create validate_input(start: @start, stop: @stop, kind: @kind, data: @data) collision = validate_time() unless collision.nil? raise ActivityOverlapError.new "no activities can overlap, collision: #{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 |
#destroy ⇒ Object
destroy the activity from the redis database
302 303 304 305 |
# File 'lib/openc3/models/activity_model.rb', line 302 def destroy Store.zremrangebyscore(@primary_key, @start, @start) notify(kind: 'deleted') end |
#notify(kind:, extra: nil) ⇒ Object
Returns [] update the redis stream / timeline topic that something has changed.
308 309 310 311 312 313 314 315 316 317 318 319 320 321 |
# File 'lib/openc3/models/activity_model.rb', line 308 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) ⇒ Object
Set the values of the instance, @start, @kind, @data, @events…
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/openc3/models/activity_model.rb', line 213 def set_input(start:, stop:, kind: nil, data: nil, events: nil, fulfillment: nil) begin DateTime.strptime(start.to_s, '%s') DateTime.strptime(stop.to_s, '%s') rescue ArgumentError raise ActivityInputError.new "invalid input must be seconds: #{start}, #{stop}" end @start = start @stop = stop @duration = @stop - @start @fulfillment = fulfillment.nil? ? false : fulfillment @kind = kind.nil? ? @kind : kind @data = data.nil? ? @data : data @events = events.nil? ? Array.new : events 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. The member via the JSON generated via calling as_json
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'lib/openc3/models/activity_model.rb', line 247 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 validate_input(start: start, stop: stop, kind: kind, data: data) 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.
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/openc3/models/activity_model.rb', line 188 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 "failed validation input must be seconds: #{start}, #{stop}" end now_i = Time.now.to_i + 10 duration = stop - start 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 will be called on create this will pull the time up to MAX_DURATION of an activity this will make sure that the activity is the only activity on the timeline for the duration of the activity. Score is the Seconds since the Unix Epoch: (%s) Number of seconds since 1970-01-01 00:00:00 UTC. We then search back from the stop of the activity and check to see if any activities are in the last x seconds (MAX_DURATION), if the zrange rev byscore finds activites from in reverse order so the first task is the closest task to the current score. In this 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.
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
# File 'lib/openc3/models/activity_model.rb', line 166 def validate_time(ignore_score = nil) max_score = @start - MAX_DURATION array = Store.zrevrangebyscore(@primary_key, @stop, max_score) 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 |