Class: ScoutAgent::Database::MissionLog
- Inherits:
-
ScoutAgent::Database
- Object
- ScoutAgent::Database
- ScoutAgent::Database::MissionLog
- Defined in:
- lib/scout_agent/database/mission_log.rb
Overview
This database encapsulates the main function of the Scout agent: running missions. Details of the current plan and the missions of that plan are stored in these tables. As missions are executed, they build up reports which are also held here until they can be pushed to the Scout server.
Constant Summary collapse
- DEFAULT_TIMEOUT =
A default number of seconds a mission is allowed to run before it is halted.
60
- DEFAULT_INTERVAL =
The default number of minutes before a mission is run again.
3
- REPORTS_LIMIT =
A size limit for the reports table to prevent data from building up.
3000
Instance Attribute Summary
Attributes inherited from ScoutAgent::Database
Instance Method Summary collapse
-
#complete_mission(mission) ⇒ Object
Marks
mission
as complete in the database by recording itslast_run_at
Time and setting anext_run_at
Time. -
#current_mission ⇒ Object
Returns the current mission (
id
,timeout
,interval
,last_run_at
,name
,code
,options
, andmemory
) that should be run. -
#current_plan ⇒ Object
Returns the last known plan (
id
andlast_modified
date) ornil
if none exists. -
#current_reports ⇒ Object
This method returns an Array of all reports (
type
,fields
,created_at
, andplugin_id
) that should be submitted to the Scout server. -
#reset_missions(*ids) ⇒ Object
All passed mission
ids
are reset so they will be run again at the first available opportunity. -
#seconds_to_next_mission ⇒ Object
Returns the number of seconds until another mission will be ready for running.
-
#update_mission_memory(mission_id, memory) ⇒ Object
Given a
mission_id
and amemory
Hash, this method updates a mission’s stored memory. -
#update_plan(last_modified, missions) ⇒ Object
Given a new
last_modified
date (as a String) and an Array ofmissions
, this method attemps and all-or-nothing update of the current plan and missions. -
#update_schema(version = schema_version) ⇒ Object
Build a schema for storing plans, missions, and reports.
-
#write_report(mission_id, type, fields) ⇒ Object
Adds a report for
mission_id
oftype
withfields
to the database.
Methods inherited from ScoutAgent::Database
#initialize, load, #locked?, #maintain, #migrate, #path, path, #prepare_connection, #query, #read_from_sqlite, #read_locked?, #schema_version, #write_locked?, #write_to_sqlite
Constructor Details
This class inherits a constructor from ScoutAgent::Database
Instance Method Details
#complete_mission(mission) ⇒ Object
Marks mission
as complete in the database by recording its last_run_at
Time and setting a next_run_at
Time.
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
# File 'lib/scout_agent/database/mission_log.rb', line 216 def complete_mission(mission) write_to_sqlite do |sqlite| run_time = Time.now params = [ run_time.to_db_s, ( run_time + mission[:interval] * 60 ).to_db_s(:trim_seconds), mission[:id] ] sqlite.execute(<<-END_UPDATE_MISSION.trim, *params) UPDATE missions SET last_run_at = ?, next_run_at = ? WHERE id = ? END_UPDATE_MISSION end true # it's safe to continue rescue Amalgalite::SQLite3::Error => error # failed to update mission log.error("Database complete mission error: #{error.}.") false # warn the caller that the mission will still match end |
#current_mission ⇒ Object
Returns the current mission (id
, timeout
, interval
, last_run_at
, name
, code
, options
, and memory
) that should be run. The options
and memory
fields are JSON parsed when possible. If there are no missions scheduled to run at this time, nil
is returned.
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
# File 'lib/scout_agent/database/mission_log.rb', line 165 def current_mission mission = read_from_sqlite { |sqlite| return nil unless plan = current_plan params = [plan[:id], Time.now.to_db_s] sqlite.first_row_from(<<-END_FIND_MISSION.trim, *params) SELECT id, timeout, interval, last_run_at, name, code, options, memory FROM missions WHERE plan_id = ? AND next_run_at <= ? ORDER BY ROWID LIMIT 1 END_FIND_MISSION } if mission.empty? nil # not found else mission[:last_run_at] = Time.from_db_s(mission[:last_run_at]) %w[options memory].each do |serialized| begin mission[serialized] = JSON.parse(mission[serialized].to_s) rescue JSON::ParserError log.warn("Mission #{serialized} could not be parsed.") mission[serialized] = { } end end mission end rescue Amalgalite::SQLite3::Error => error # failed to retrieve mission log.error("Database current mission error: #{error.}.") nil # not found end |
#current_plan ⇒ Object
Returns the last known plan (id
and last_modified
date) or nil
if none exists.
71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/scout_agent/database/mission_log.rb', line 71 def current_plan plan = read_from_sqlite { |sqlite| sqlite.first_row_from(<<-END_FIND_PLAN.trim) SELECT ROWID AS id, last_modified FROM plans ORDER BY ROWID DESC LIMIT 1 END_FIND_PLAN } plan.empty? ? nil : plan rescue Amalgalite::SQLite3::Error => error # failed to retrieve plan log.error("Database current plan error: #{error.}.") nil # not found end |
#current_reports ⇒ Object
This method returns an Array of all reports (type
, fields
, created_at
, and plugin_id
) that should be submitted to the Scout server. The report fields
will be JSON parsed when possible and created_at
is converted to a proper Time object.
The act of reading these reports also triggers their removal from the database so we avoid sending duplicates to the server. This does mean that we lose data if anything goes wrong in the sending process. This is considered an acceptable risk, because even a delete-after-a-successful-send stragety is subject to duplication (the request might timeout but eventually complete on the server, for example). If anything goes wrong with the reading or deletion, the entire process is canceled and an empty Array is returned.
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 |
# File 'lib/scout_agent/database/mission_log.rb', line 311 def current_reports write_to_sqlite { |sqlite| # read the current reports begin report_ids = Array.new reports = query(<<-END_FIND_REPORTS.trim) { |row| SELECT reports.ROWID AS id, reports.type, reports.fields, reports.created_at, missions.id AS plugin_id FROM reports INNER JOIN missions ON reports.mission_id = missions.id ORDER BY created_at LIMIT 500 END_FIND_REPORTS begin row[:fields] = JSON.parse(row[:fields].to_s) rescue JSON::ParserError # skip the transform since we can't parse it log.warn("Report fields malformed.") end if created = Time.from_db_s(row[:created_at]) row[:created_at] = created.utc.to_db_s else log.warn("Report timestamp missing.") end report_ids << row.delete_at(:id) } rescue Amalgalite::SQLite3::Error => error # failed to find reports log.error("Database reports error: #{error.}.") return Array.new # return empty results end return reports if reports.empty? # delete the reports we read begin sqlite.execute(<<-END_DELETE_REPORTS.trim, *report_ids) DELETE FROM reports WHERE ROWID IN (#{(['?'] * report_ids.size).join(', ')}) END_DELETE_REPORTS rescue Amalgalite::SQLite3::Error => error # failed to remove reports # cancel sending this batch log.error("Database delivered reports error: #{error.}.") sqlite.rollback # we can't submit unless we're sure they are gone return Array.new # return empty results end reports # the reports ready for sending } rescue Amalgalite::SQLite3::Error => error # failed to get a write lock # try again to read reports later log.error("Database reports locking error: #{error.}.") end |
#reset_missions(*ids) ⇒ Object
All passed mission ids
are reset so they will be run again at the first available opportunity.
237 238 239 240 241 242 243 244 245 246 247 248 249 |
# File 'lib/scout_agent/database/mission_log.rb', line 237 def reset_missions(*ids) return if ids.empty? write_to_sqlite do |sqlite| sqlite.execute(<<-END_RESET_MISSIONS.trim, *ids) UPDATE missions SET next_run_at = strftime('%Y-%m-%d %H:%M', 'now', 'localtime') WHERE id IN (#{(['?'] * ids.size).join(', ')}) END_RESET_MISSIONS end rescue Amalgalite::SQLite3::Error => error # failed to reset missions # do nothing: missions will be run at their scheduled time log.error("Database mission reset error: #{error.}.") end |
#seconds_to_next_mission ⇒ Object
Returns the number of seconds until another mission will be ready for running. If the count would be zero or less seconds, the DEFAULT_INTERVAL
is returned (in seconds) to prevent the agent from entering a busy loop.
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 |
# File 'lib/scout_agent/database/mission_log.rb', line 257 def seconds_to_next_mission default = DEFAULT_INTERVAL * 60 next_run_at = read_from_sqlite { |sqlite| sqlite.first_value_from(<<-END_FIND_MISSION.trim) SELECT next_run_at FROM missions ORDER BY next_run_at LIMIT 1 END_FIND_MISSION } if next_run = Time.from_db_s(next_run_at) seconds = next_run - Time.now seconds > 0 ? seconds : default else default end rescue Amalgalite::SQLite3::Error => error # failed to locate last run log.error("Database next mission error: #{error.}.") default # use default end |
#update_mission_memory(mission_id, memory) ⇒ Object
Given a mission_id
and a memory
Hash, this method updates a mission’s stored memory.
201 202 203 204 205 206 207 208 209 210 |
# File 'lib/scout_agent/database/mission_log.rb', line 201 def update_mission_memory(mission_id, memory) write_to_sqlite do |sqlite| sqlite.execute(<<-END_UPDATE_MISSION.trim, memory.to_json, mission_id) UPDATE missions SET memory = ? WHERE id = ? END_UPDATE_MISSION end rescue Amalgalite::SQLite3::Error => error # failed to update memory # do nothing: mission will receive previous memory state log.error("Database memory update error: #{error.}.") end |
#update_plan(last_modified, missions) ⇒ Object
Given a new last_modified
date (as a String) and an Array of missions
, this method attemps and all-or-nothing update of the current plan and missions. The plan and any missions that were already present are simply updated. New missions are added and missions no longer in the list are removed. New missions receive a next_run_at
Time of now.
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/scout_agent/database/mission_log.rb', line 93 def update_plan(last_modified, missions) write_to_sqlite do |sqlite| begin sqlite.execute(<<-END_INSERT_PLAN.trim, last_modified) INSERT OR REPLACE INTO plans(ROWID, last_modified) VALUES(1, ?) END_INSERT_PLAN ids = missions.map { |m| m["id"] } sqlite.execute(<<-END_DELETE_MISSIONS.trim, *ids) DELETE FROM missions WHERE id NOT IN (#{(['?'] * ids.size).join(', ')}) END_DELETE_MISSIONS rescue Amalgalite::SQLite3::Error => error # failed to update plan log.error("Database bad plan error: #{error.}.") sqlite.rollback # these changes are all or nothing return # try again to update plan later end missions.each do |mission| params = [ mission["name"], mission["code"], mission["options"].to_json, mission["timeout"].to_s =~ /\A\d*[1-9]\z/ ? mission["timeout"].to_i : DEFAULT_TIMEOUT, mission["interval"].to_s =~ /\A\d*[1-9]\z/ ? mission["interval"].to_i : DEFAULT_INTERVAL, mission["id"], Time.now.to_db_s(:trim_seconds) ] begin if sqlite.first_value_from( "SELECT id FROM missions WHERE id = ? LIMIT 1", mission["id"] ) params.pop # remove next_run_at sqlite.execute(<<-END_UPDATE_MISSION.trim, *params) UPDATE missions SET name = ?, code = ?, options = ?, timeout = ?, interval = ? WHERE id = ? END_UPDATE_MISSION else sqlite.execute(<<-END_INSERT_MISSION.trim, *params) INSERT INTO missions( name, code, options, timeout, interval, id, next_run_at ) VALUES( ?, ?, ?, ?, ?, ?, ? ) END_INSERT_MISSION end rescue Amalgalite::SQLite3::Error => error # failed to set mission # do nothing: skip bad mission and move on log.error( "Database bad mission (#{mission['name']}) error: " + "#{error.}." ) end end end rescue Amalgalite::SQLite3::Error => error # failed to get a write lock # try again to update plan later log.error("Database mission update locking error: #{error.}.") end |
#update_schema(version = schema_version) ⇒ Object
Build a schema for storing plans, missions, and reports. The reports table is size controlled by trigger to prevent infinite data growth.
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/scout_agent/database/mission_log.rb', line 27 def update_schema(version = schema_version) case version when 0 <<-END_INITIAL_SCHEMA.trim CREATE TABLE plans ( last_modified REQUIRED_TEXT_TYPE COLLATE NOCASE ); CREATE TABLE missions ( plan_id DEFAULT_INTEGER_TYPE 1, id INTEGER NOT NULL PRIMARY KEY, name TEXT COLLATE NOCASE, code REQUIRED_TEXT_TYPE, options TEXT, memory TEXT DEFAULT '{}', timeout POSITIVE_INTEGER_TYPE DEFAULT #{DEFAULT_TIMEOUT}, interval POSITIVE_INTEGER_TYPE DEFAULT #{DEFAULT_INTERVAL}, last_run_at DATETIME_TYPE, next_run_at REQUIRED_DATETIME_TYPE ); FOREIGN_KEY_CHECK_TRIGGER missions plan_id plans ROWID CREATE TABLE reports ( mission_id INTEGER NOT NULL, type TEXT NOT NULL COLLATE NOCASE CHECK(type IN ('report', 'hint', 'alert', 'error')), fields REQUIRED_TEXT_TYPE, created_at DATETIME_TYPE ); FOREIGN_KEY_CHECK_TRIGGER reports mission_id missions id DEFAULT_LOCALTIME_TRIGGER reports created_at LIMIT_TABLE_SIZE_TRIGGER reports #{REPORTS_LIMIT} END_INITIAL_SCHEMA end end |
#write_report(mission_id, type, fields) ⇒ Object
Adds a report for mission_id
of type
with fields
to the database. Returns true
if the write succeeded, or false
if it did not.
283 284 285 286 287 288 289 290 291 292 293 294 |
# File 'lib/scout_agent/database/mission_log.rb', line 283 def write_report(mission_id, type, fields) write_to_sqlite do |sqlite| params = [mission_id, type, fields.to_json] sqlite.execute(<<-END_INSERT_REPORT.trim, *params) INSERT INTO reports(mission_id, type, fields) VALUES(?, ?, ?) END_INSERT_REPORT end true # report successfully written rescue Amalgalite::SQLite3::Error => error # failed to create report log.error("Database write report error: #{error.}.") false # couldn't be written end |