Class: Cosmos::ActivityModel
- Defined in:
- lib/cosmos/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 __
'__cosmos_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 ⇒ 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
#as_config, #deploy, filter, find_all_by_plugin, get_all_models, get_model, handle_config, names, set, #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.
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/cosmos/models/activity_model.rb', line 114 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.
112 113 114 |
# File 'lib/cosmos/models/activity_model.rb', line 112 def data @data end |
#duration ⇒ Object (readonly)
Returns the value of attribute duration.
112 113 114 |
# File 'lib/cosmos/models/activity_model.rb', line 112 def duration @duration end |
#events ⇒ Object (readonly)
Returns the value of attribute events.
112 113 114 |
# File 'lib/cosmos/models/activity_model.rb', line 112 def events @events end |
#fulfillment ⇒ Object (readonly)
Returns the value of attribute fulfillment.
112 113 114 |
# File 'lib/cosmos/models/activity_model.rb', line 112 def fulfillment @fulfillment end |
#kind ⇒ Object (readonly)
Returns the value of attribute kind.
112 113 114 |
# File 'lib/cosmos/models/activity_model.rb', line 112 def kind @kind end |
#start ⇒ Object (readonly)
Returns the value of attribute start.
112 113 114 |
# File 'lib/cosmos/models/activity_model.rb', line 112 def start @start end |
#stop ⇒ Object (readonly)
Returns the value of attribute stop.
112 113 114 |
# File 'lib/cosmos/models/activity_model.rb', line 112 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.
41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/cosmos/models/activity_model.rb', line 41 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.
68 69 70 71 72 73 74 75 |
# File 'lib/cosmos/models/activity_model.rb', line 68 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) end return ret_array end |
.count(name:, scope:) ⇒ Integer
Returns count of the members stored under the primary key.
87 88 89 |
# File 'lib/cosmos/models/activity_model.rb', line 87 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.
93 94 95 |
# File 'lib/cosmos/models/activity_model.rb', line 93 def self.destroy(name:, scope:, score:) Store.zremrangebyscore("#{scope}#{PRIMARY_KEY}__#{name}", score, score) end |
.from_json(json, name:, scope:) ⇒ ActivityModel
Returns Model generated from the passed JSON.
104 105 106 107 108 109 110 |
# File 'lib/cosmos/models/activity_model.rb', line 104 def self.from_json(json, name:, scope:) json = JSON.parse(json) if String === json raise "json data is nil" if json.nil? json.transform_keys!(&:to_sym) self.new(**json, 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.
54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/cosmos/models/activity_model.rb', line 54 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) end return ret_array end |
.range_destroy(name:, scope:, min:, max:) ⇒ Integer
Remove members from min to max of the sorted set.
99 100 101 |
# File 'lib/cosmos/models/activity_model.rb', line 99 def self.range_destroy(name:, scope:, min:, max:) Store.zremrangebyscore("#{scope}#{PRIMARY_KEY}__#{name}", min, max) end |
.score(name:, score:, scope:) ⇒ String|nil
Returns String of the saved json or nil if score not found under primary_key.
78 79 80 81 82 83 84 |
# File 'lib/cosmos/models/activity_model.rb', line 78 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
274 275 276 277 278 279 280 |
# File 'lib/cosmos/models/activity_model.rb', line 274 def add_event(status:) event = { 'time' => Time.now.to_i, 'event' => status } @events << event end |
#as_json ⇒ Hash
Returns generated from the ActivityModel.
305 306 307 308 309 310 311 312 313 314 315 316 317 |
# File 'lib/cosmos/models/activity_model.rb', line 305 def as_json { 'name' => @name, 'updated_at' => @updated_at, 'fulfillment' => @fulfillment, 'duration' => @duration, 'start' => @start, 'stop' => @stop, 'kind' => @kind, 'events' => @events, 'data' => @data } end |
#commit(status:, message: nil, fulfillment: nil) ⇒ Object
commit will make an event and save the object to the redis database
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'lib/cosmos/models/activity_model.rb', line 256 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)) 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
212 213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/cosmos/models/activity_model.rb', line 212 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)) notify(kind: 'created') end |
#destroy ⇒ Object
destroy the activity from the redis database
283 284 285 286 |
# File 'lib/cosmos/models/activity_model.rb', line 283 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.
289 290 291 292 293 294 295 296 297 298 299 300 301 302 |
# File 'lib/cosmos/models/activity_model.rb', line 289 def notify(kind:, extra: nil) notification = { 'data' => JSON.generate(as_json()), '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…
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
# File 'lib/cosmos/models/activity_model.rb', line 194 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
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 |
# File 'lib/cosmos/models/activity_model.rb', line 228 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)) 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.
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
# File 'lib/cosmos/models/activity_model.rb', line 169 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.
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/cosmos/models/activity_model.rb', line 147 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) if ignore_score == activity['start'] next elsif activity['stop'] > @start return activity['start'] else return nil end end return nil end |