scout_agent 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. data/AUTHORS +4 -0
  2. data/CHANGELOG +3 -0
  3. data/COPYING +340 -0
  4. data/INSTALL +17 -0
  5. data/LICENSE +6 -0
  6. data/README +3 -0
  7. data/Rakefile +123 -0
  8. data/TODO +3 -0
  9. data/bin/scout_agent +11 -0
  10. data/lib/scout_agent.rb +73 -0
  11. data/lib/scout_agent/agent.rb +42 -0
  12. data/lib/scout_agent/agent/communication_agent.rb +85 -0
  13. data/lib/scout_agent/agent/master_agent.rb +301 -0
  14. data/lib/scout_agent/api.rb +241 -0
  15. data/lib/scout_agent/assignment.rb +105 -0
  16. data/lib/scout_agent/assignment/configuration.rb +30 -0
  17. data/lib/scout_agent/assignment/identify.rb +110 -0
  18. data/lib/scout_agent/assignment/queue.rb +95 -0
  19. data/lib/scout_agent/assignment/reset.rb +91 -0
  20. data/lib/scout_agent/assignment/snapshot.rb +92 -0
  21. data/lib/scout_agent/assignment/start.rb +149 -0
  22. data/lib/scout_agent/assignment/status.rb +44 -0
  23. data/lib/scout_agent/assignment/stop.rb +60 -0
  24. data/lib/scout_agent/assignment/upload_log.rb +61 -0
  25. data/lib/scout_agent/core_extensions.rb +260 -0
  26. data/lib/scout_agent/database.rb +386 -0
  27. data/lib/scout_agent/database/mission_log.rb +282 -0
  28. data/lib/scout_agent/database/queue.rb +126 -0
  29. data/lib/scout_agent/database/snapshots.rb +187 -0
  30. data/lib/scout_agent/database/statuses.rb +65 -0
  31. data/lib/scout_agent/dispatcher.rb +157 -0
  32. data/lib/scout_agent/id_card.rb +143 -0
  33. data/lib/scout_agent/lifeline.rb +243 -0
  34. data/lib/scout_agent/mission.rb +212 -0
  35. data/lib/scout_agent/order.rb +58 -0
  36. data/lib/scout_agent/order/check_in_order.rb +32 -0
  37. data/lib/scout_agent/order/snapshot_order.rb +33 -0
  38. data/lib/scout_agent/plan.rb +306 -0
  39. data/lib/scout_agent/server.rb +123 -0
  40. data/lib/scout_agent/tracked.rb +59 -0
  41. data/lib/scout_agent/wire_tap.rb +513 -0
  42. data/setup.rb +1360 -0
  43. data/test/tc_core_extensions.rb +89 -0
  44. data/test/tc_id_card.rb +115 -0
  45. data/test/tc_plan.rb +285 -0
  46. data/test/test_helper.rb +22 -0
  47. data/test/ts_all.rb +7 -0
  48. metadata +171 -0
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ module ScoutAgent
4
+ class Database
5
+ class MissionLog < Database
6
+ #
7
+ # A default number of seconds a mission is allowed to run before it is
8
+ # halted.
9
+ #
10
+ DEFAULT_TIMEOUT = 60
11
+ # The default number of minutes before a mission is run again.
12
+ DEFAULT_INTERVAL = 3
13
+ # A size limit for the reports table to prevent data from building up.
14
+ REPORTS_LIMIT = 3000
15
+
16
+ def update_schema(version = schema_version)
17
+ case version
18
+ when 0
19
+ <<-END_INITIAL_SCHEMA.trim
20
+ CREATE TABLE plans (
21
+ last_modified REQUIRED_TEXT_TYPE COLLATE NOCASE
22
+ );
23
+
24
+ CREATE TABLE missions (
25
+ plan_id DEFAULT_INTEGER_TYPE 1,
26
+ id INTEGER NOT NULL PRIMARY KEY,
27
+ name TEXT COLLATE NOCASE,
28
+ code REQUIRED_TEXT_TYPE,
29
+ options TEXT,
30
+ memory TEXT DEFAULT '{}',
31
+ timeout POSITIVE_INTEGER_TYPE DEFAULT #{DEFAULT_TIMEOUT},
32
+ interval POSITIVE_INTEGER_TYPE DEFAULT #{DEFAULT_INTERVAL},
33
+ last_run_at DATETIME_TYPE,
34
+ next_run_at REQUIRED_DATETIME_TYPE
35
+ );
36
+ FOREIGN_KEY_CHECK_TRIGGER missions plan_id plans ROWID
37
+
38
+ CREATE TABLE reports (
39
+ mission_id INTEGER NOT NULL,
40
+ type TEXT NOT NULL COLLATE NOCASE
41
+ CHECK(type IN ('report', 'hint', 'alert', 'error')),
42
+ fields REQUIRED_TEXT_TYPE,
43
+ created_at DATETIME_TYPE
44
+ );
45
+ FOREIGN_KEY_CHECK_TRIGGER reports mission_id missions id
46
+ DEFAULT_LOCALTIME_TRIGGER reports created_at
47
+ LIMIT_TABLE_SIZE_TRIGGER reports #{REPORTS_LIMIT}
48
+ END_INITIAL_SCHEMA
49
+ end
50
+ end
51
+
52
+ def current_plan
53
+ plan = read_from_sqlite { |sqlite|
54
+ sqlite.first_row_from(<<-END_FIND_PLAN.trim)
55
+ SELECT ROWID AS id, last_modified
56
+ FROM plans
57
+ ORDER BY ROWID DESC
58
+ LIMIT 1
59
+ END_FIND_PLAN
60
+ }
61
+ plan.empty? ? nil : plan
62
+ rescue Amalgalite::SQLite3::Error => error # failed to retrieve plan
63
+ log.error("Database current plan error: #{error.message}.")
64
+ nil # not found
65
+ end
66
+
67
+ def update_plan(last_modified, missions)
68
+ write_to_sqlite do |sqlite|
69
+ begin
70
+ sqlite.execute(<<-END_INSERT_PLAN.trim, last_modified)
71
+ INSERT OR REPLACE INTO plans(ROWID, last_modified) VALUES(1, ?)
72
+ END_INSERT_PLAN
73
+
74
+ ids = missions.map { |m| m["id"] }
75
+ sqlite.execute(<<-END_DELETE_MISSIONS.trim, *ids)
76
+ DELETE FROM missions
77
+ WHERE id NOT IN (#{(['?'] * ids.size).join(', ')})
78
+ END_DELETE_MISSIONS
79
+ rescue Amalgalite::SQLite3::Error => error # failed to update plan
80
+ log.error("Database bad plan error: #{error.message}.")
81
+ sqlite.rollback # these changes are all or nothing
82
+ return # try again to update plan later
83
+ end
84
+ missions.each do |mission|
85
+ params = [ mission["name"],
86
+ mission["code"],
87
+ mission["options"].to_json,
88
+ mission["timeout"].to_s =~ /\A\d*[1-9]\z/ ?
89
+ mission["timeout"].to_i :
90
+ DEFAULT_TIMEOUT,
91
+ mission["interval"].to_s =~ /\A\d*[1-9]\z/ ?
92
+ mission["interval"].to_i :
93
+ DEFAULT_INTERVAL,
94
+ mission["id"],
95
+ Time.now.to_db_s(:trim_seconds) ]
96
+ begin
97
+ if sqlite.first_value_from(
98
+ "SELECT id FROM missions WHERE id = ? LIMIT 1",
99
+ mission["id"]
100
+ )
101
+ params.pop # remove next_run_at
102
+ sqlite.execute(<<-END_UPDATE_MISSION.trim, *params)
103
+ UPDATE missions
104
+ SET name = ?, code = ?, options = ?, timeout = ?,
105
+ interval = ?
106
+ WHERE id = ?
107
+ END_UPDATE_MISSION
108
+ else
109
+ sqlite.execute(<<-END_INSERT_MISSION.trim, *params)
110
+ INSERT INTO
111
+ missions( name, code, options, timeout,
112
+ interval, id, next_run_at )
113
+ VALUES( ?, ?, ?, ?,
114
+ ?, ?, ? )
115
+ END_INSERT_MISSION
116
+ end
117
+ rescue Amalgalite::SQLite3::Error => error # failed to set mission
118
+ # do nothing: skip bad mission and move on
119
+ log.error( "Database bad mission (#{mission['name']}) error: " +
120
+ "#{error.message}." )
121
+ end
122
+ end
123
+ end
124
+ rescue Amalgalite::SQLite3::Error => error # failed to get a write lock
125
+ # try again to update plan later
126
+ log.error("Database mission update locking error: #{error.message}.")
127
+ end
128
+
129
+ def current_mission
130
+ mission = read_from_sqlite { |sqlite|
131
+ return nil unless plan = current_plan
132
+ params = [plan[:id], Time.now.to_db_s]
133
+ sqlite.first_row_from(<<-END_FIND_MISSION.trim, *params)
134
+ SELECT id, timeout, interval, last_run_at, name,
135
+ code, options, memory
136
+ FROM missions
137
+ WHERE plan_id = ? AND next_run_at <= ?
138
+ ORDER BY ROWID
139
+ LIMIT 1
140
+ END_FIND_MISSION
141
+ }
142
+ if mission.empty?
143
+ nil # not found
144
+ else
145
+ mission[:last_run_at] = Time.from_db_s(mission[:last_run_at])
146
+ %w[options memory].each do |serialized|
147
+ begin
148
+ mission[serialized] = JSON.parse(mission[serialized].to_s)
149
+ rescue JSON::ParserError
150
+ log.warn("Mission #{serialized} could not be parsed.")
151
+ mission[serialized] = { }
152
+ end
153
+ end
154
+ mission
155
+ end
156
+ rescue Amalgalite::SQLite3::Error => error # failed to retrieve mission
157
+ log.error("Database current mission error: #{error.message}.")
158
+ nil # not found
159
+ end
160
+
161
+ def update_mission_memory(mission_id, memory)
162
+ write_to_sqlite do |sqlite|
163
+ sqlite.execute(<<-END_UPDATE_MISSION.trim, memory.to_json, mission_id)
164
+ UPDATE missions SET memory = ? WHERE id = ?
165
+ END_UPDATE_MISSION
166
+ end
167
+ rescue Amalgalite::SQLite3::Error => error # failed to update memory
168
+ # do nothing: mission will receive previous memory state
169
+ log.error("Database memory update error: #{error.message}.")
170
+ end
171
+
172
+ def complete_mission(mission)
173
+ write_to_sqlite do |sqlite|
174
+ run_time = Time.now
175
+ params = [ run_time.to_db_s,
176
+ ( run_time +
177
+ mission[:interval] * 60 ).to_db_s(:trim_seconds),
178
+ mission[:id] ]
179
+ sqlite.execute(<<-END_UPDATE_MISSION.trim, *params)
180
+ UPDATE missions SET last_run_at = ?, next_run_at = ? WHERE id = ?
181
+ END_UPDATE_MISSION
182
+ end
183
+ true # it's safe to continue
184
+ rescue Amalgalite::SQLite3::Error => error # failed to update mission
185
+ log.error("Database complete mission error: #{error.message}.")
186
+ false # warn the caller that the mission will still match
187
+ end
188
+
189
+ def reset_missions(*ids)
190
+ write_to_sqlite do |sqlite|
191
+ sqlite.execute(<<-END_RESET_MISSIONS.trim, *ids)
192
+ UPDATE missions
193
+ SET next_run_at = strftime('%Y-%m-%d %H:%M', 'now', 'localtime')
194
+ WHERE id IN (#{(['?'] * ids.size).join(', ')})
195
+ END_RESET_MISSIONS
196
+ end
197
+ rescue Amalgalite::SQLite3::Error => error # failed to reset missions
198
+ # do nothing: missions will be run at their scheduled time
199
+ log.error("Database mission reset error: #{error.message}.")
200
+ end
201
+
202
+ def write_report(mission_id, type, fields)
203
+ write_to_sqlite do |sqlite|
204
+ params = [mission_id, type, fields.to_json]
205
+ sqlite.execute(<<-END_INSERT_REPORT.trim, *params)
206
+ INSERT INTO reports(mission_id, type, fields) VALUES(?, ?, ?)
207
+ END_INSERT_REPORT
208
+ end
209
+ true # report successfully written
210
+ rescue Amalgalite::SQLite3::Error => error # failed to create report
211
+ log.error("Database write report error: #{error.message}.")
212
+ false # couldn't be written
213
+ end
214
+
215
+ def current_reports
216
+ write_to_sqlite { |sqlite|
217
+ begin
218
+ report_ids = Array.new
219
+ reports = query(<<-END_FIND_REPORTS.trim) { |row|
220
+ SELECT reports.ROWID AS id, reports.type, reports.fields,
221
+ reports.created_at, missions.id AS plugin_id
222
+ FROM reports
223
+ INNER JOIN missions ON reports.mission_id = missions.id
224
+ ORDER BY created_at
225
+ LIMIT 500
226
+ END_FIND_REPORTS
227
+ begin
228
+ row[:fields] = JSON.parse(row[:fields].to_s)
229
+ rescue JSON::ParserError
230
+ # skip the transform since we can't parse it
231
+ log.warn("Report fields malformed.")
232
+ end
233
+ if created = Time.from_db_s(row[:created_at])
234
+ row[:created_at] = created.utc.to_db_s
235
+ else
236
+ log.warn("Report timestamp missing.")
237
+ end
238
+ report_ids << row.delete_at(:id)
239
+ }
240
+ rescue Amalgalite::SQLite3::Error => error # failed to find reports
241
+ log.error("Database reports error: #{error.message}.")
242
+ return Array.new # return empty results
243
+ end
244
+ return reports if reports.empty?
245
+ begin
246
+ sqlite.execute(<<-END_DELETE_REPORTS.trim, *report_ids)
247
+ DELETE FROM reports
248
+ WHERE ROWID IN (#{(['?'] * report_ids.size).join(', ')})
249
+ END_DELETE_REPORTS
250
+ rescue Amalgalite::SQLite3::Error => error # failed to remove reports
251
+ # cancel sending this batch
252
+ log.error("Database delivered reports error: #{error.message}.")
253
+ sqlite.rollback # we can't submit unless we're sure they are gone
254
+ return Array.new # return empty results
255
+ end
256
+ reports # the reports ready for sending
257
+ }
258
+ rescue Amalgalite::SQLite3::Error => error # failed to get a write lock
259
+ # try again to read reports later
260
+ log.error("Database reports locking error: #{error.message}.")
261
+ end
262
+
263
+ def seconds_to_next_mission
264
+ default = DEFAULT_INTERVAL * 60
265
+ next_run_at = read_from_sqlite { |sqlite|
266
+ sqlite.first_value_from(<<-END_FIND_MISSION.trim)
267
+ SELECT next_run_at FROM missions ORDER BY next_run_at LIMIT 1
268
+ END_FIND_MISSION
269
+ }
270
+ if next_run = Time.from_db_s(next_run_at)
271
+ seconds = next_run - Time.now
272
+ seconds > 0 ? seconds : default
273
+ else
274
+ default
275
+ end
276
+ rescue Amalgalite::SQLite3::Error => error # failed to locate last run
277
+ log.error("Database next mission error: #{error.message}.")
278
+ default # use default
279
+ end
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ module ScoutAgent
4
+ class Database
5
+ class Queue < Database
6
+ # A size limit for the queue to prevent data from building up.
7
+ QUEUE_LIMIT = 3000
8
+
9
+ def update_schema(version = schema_version)
10
+ case version
11
+ when 0
12
+ <<-END_INITIAL_SCHEMA.trim
13
+ CREATE TABLE queue (
14
+ mission_id TEXT NOT NULL
15
+ CHECK( mission_id IN ('report', 'hint', 'alert', 'error') OR
16
+ CAST(mission_id AS 'integer') > 0 ),
17
+ fields REQUIRED_TEXT_TYPE,
18
+ created_at DATETIME_TYPE
19
+ );
20
+ DEFAULT_LOCALTIME_TRIGGER queue created_at
21
+ LIMIT_TABLE_SIZE_TRIGGER queue #{QUEUE_LIMIT}
22
+ END_INITIAL_SCHEMA
23
+ end
24
+ end
25
+
26
+ def enqueue(mission_id, fields)
27
+ write_to_sqlite do |sqlite|
28
+ sqlite.execute(<<-END_ENQUEUE.trim, mission_id, fields.to_json)
29
+ INSERT INTO queue(mission_id, fields) VALUES(?, ?)
30
+ END_ENQUEUE
31
+ end
32
+ true
33
+ rescue Amalgalite::SQLite3::Error => error # failed to enqueue message
34
+ log.error("Database queuing error: #{error.message}.")
35
+ false # reject bad message
36
+ end
37
+
38
+ def peek(mission_id)
39
+ queued = read_from_sqlite { |sqlite|
40
+ sqlite.first_row_from(<<-END_FIND_QUEUED.trim, mission_id.to_s)
41
+ SELECT ROWID AS id, fields, created_at FROM queue WHERE mission_id = ?
42
+ END_FIND_QUEUED
43
+ }
44
+ if queued.empty?
45
+ nil # not found
46
+ else
47
+ begin
48
+ queued[:fields] = JSON.parse(queued[:fields].to_s)
49
+ rescue JSON::ParserError # failed to parse
50
+ # leave for mission to decode it
51
+ log.warn("Queued fields malformed.")
52
+ end
53
+ if created = Time.from_db_s(queued[:created_at])
54
+ queued[:created_at] = created
55
+ else
56
+ log.warn("Queued timestamp missing.")
57
+ end
58
+ queued
59
+ end
60
+ rescue Amalgalite::SQLite3::Error => error # failed to retrieve message
61
+ log.error("Database peeking error: #{error.message}.")
62
+ nil # not found
63
+ end
64
+
65
+ def queued_reports
66
+ write_to_sqlite { |sqlite|
67
+ begin
68
+ report_ids = Array.new
69
+ reports = query(<<-END_FIND_REPORTS.trim) { |row|
70
+ SELECT ROWID AS id, mission_id AS type, fields, created_at
71
+ FROM queue
72
+ WHERE mission_id IN ('report', 'hint', 'alert', 'error')
73
+ ORDER BY created_at
74
+ LIMIT 500
75
+ END_FIND_REPORTS
76
+ begin
77
+ row[:fields] = JSON.parse(row[:fields].to_s)
78
+ if row[:fields].include? "plugin_id"
79
+ row[:plugin_id] = row[:fields].delete("plugin_id")
80
+ end
81
+ rescue JSON::ParserError # failed to parse
82
+ # skip the transform since we can't parse it
83
+ log.warn("Queued fields malformed.")
84
+ end
85
+ if created = Time.from_db_s(row[:created_at])
86
+ row[:created_at] = created.utc.to_db_s
87
+ else
88
+ log.warn("Queued timestamp missing.")
89
+ end
90
+ report_ids << row.delete_at(:id)
91
+ }
92
+ rescue Amalgalite::SQLite3::Error => error # failed to find reports
93
+ log.error("Database queued reports error: #{error.message}.")
94
+ return Array.new # return empty results
95
+ end
96
+ return reports if reports.empty?
97
+ unless dequeue(*report_ids)
98
+ # cancel sending this batch
99
+ sqlite.rollback # we can't submit unless we're sure they are gone
100
+ return Array.new # return empty results
101
+ end
102
+ reports # the reports ready for sending
103
+ }
104
+ rescue Amalgalite::SQLite3::Error => error # failed to get a write lock
105
+ # try again to read reports later
106
+ log.error("Database queued reports locking error: #{error.message}.")
107
+ end
108
+
109
+ def dequeue(*ids)
110
+ write_to_sqlite do |sqlite|
111
+ sqlite.execute(<<-END_DELETE_QUEUED.trim, *ids)
112
+ DELETE FROM queue WHERE ROWID IN (#{(['?'] * ids.size).join(', ')})
113
+ END_DELETE_QUEUED
114
+ end
115
+ true
116
+ rescue Amalgalite::SQLite3::Error => error # failed to remove messages
117
+ #
118
+ # do nothing: messages will be delivered again,
119
+ # mission can block duplicate
120
+ #
121
+ log.error("Database dequeuing error: #{error.message}.")
122
+ false
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ module ScoutAgent
4
+ class Database
5
+ class Snapshots < Database
6
+ # A maximum time in seconds after which a command run is halted.
7
+ DEFAULT_TIMEOUT = 60
8
+ #
9
+ # A minimum time in minutes that must pass before a command will be run
10
+ # again in the next snapshot.
11
+ #
12
+ DEFAULT_INTERVAL = 10
13
+ # A size limit for the runs table to prevent data from building up.
14
+ RUNS_LIMIT = 3000
15
+
16
+ def update_schema(version = schema_version)
17
+ case version
18
+ when 0
19
+ <<-END_INITIAL_SCHEMA.trim
20
+ CREATE TABLE commands (
21
+ code REQUIRED_TEXT_TYPE PRIMARY KEY,
22
+ timeout POSITIVE_INTEGER_TYPE DEFAULT #{DEFAULT_TIMEOUT},
23
+ interval DEFAULT_INTEGER_TYPE #{DEFAULT_INTERVAL},
24
+ last_run_at DATETIME_TYPE,
25
+ next_run_at DATETIME_TYPE
26
+ );
27
+ DEFAULT_LOCALTIME_TRIGGER commands next_run_at trim_seconds
28
+
29
+ CREATE TABLE runs (
30
+ code REQUIRED_TEXT_TYPE,
31
+ output TEXT,
32
+ exit_status INTEGER,
33
+ snapshot_at REQUIRED_DATETIME_TYPE,
34
+ run_time ZERO_OR_POSITIVE_REAL_TYPE,
35
+ PRIMARY KEY(code, snapshot_at)
36
+ );
37
+ LIMIT_TABLE_SIZE_TRIGGER runs #{RUNS_LIMIT}
38
+ END_INITIAL_SCHEMA
39
+ end
40
+ end
41
+
42
+ def update_commands(commands)
43
+ write_to_sqlite do |sqlite|
44
+ codes = commands.map { |m| m["code"] }
45
+ begin
46
+ sqlite.execute(<<-END_DELETE_COMMANDS.trim, *codes)
47
+ DELETE FROM commands
48
+ WHERE code NOT IN (#{(['?'] * codes.size).join(', ')})
49
+ END_DELETE_COMMANDS
50
+ rescue Amalgalite::SQLite3::Error => error # failed to remove
51
+ log.error("Database command updating error: #{error.message}.")
52
+ return # try again to update commands later
53
+ end
54
+ commands.each do |command|
55
+ params = [ command["timeout"].to_s =~ /\A\d*[1-9]\z/ ?
56
+ command["timeout"].to_i :
57
+ DEFAULT_TIMEOUT,
58
+ (command["interval"] || DEFAULT_INTERVAL).to_i,
59
+ command["code"] ]
60
+ begin
61
+ if sqlite.first_value_from(
62
+ "SELECT ROWID FROM commands WHERE code = ? LIMIT 1",
63
+ command["code"]
64
+ )
65
+ sqlite.execute(<<-END_UPDATE_COMMAND.trim, *params)
66
+ UPDATE commands SET timeout = ?, interval = ? WHERE code = ?
67
+ END_UPDATE_COMMAND
68
+ else
69
+ sqlite.execute(<<-END_INSERT_COMMAND.trim, *params)
70
+ INSERT INTO commands(timeout, interval, code) VALUES(?, ?, ?)
71
+ END_INSERT_COMMAND
72
+ end
73
+ rescue Amalgalite::SQLite3::Error => error # failed to set command
74
+ # do nothing: skip bad command and move on
75
+ log.error( "Database bad command (#{command['code']}) error: " +
76
+ "#{error.message}." )
77
+ end
78
+ end
79
+ end
80
+ rescue Amalgalite::SQLite3::Error => error # failed to get a write lock
81
+ # try again to update commands later
82
+ log.error("Database command update locking error: #{error.message}.")
83
+ end
84
+
85
+ def current_commands
86
+ query(<<-END_FIND_COMMANDS.trim, Time.now.to_db_s) { |row|
87
+ SELECT ROWID AS id, timeout, interval, last_run_at, code
88
+ FROM commands
89
+ WHERE next_run_at <= ?
90
+ END_FIND_COMMANDS
91
+ row[:last_run_at] = Time.from_db_s(row[:last_run_at])
92
+ }
93
+ rescue Amalgalite::SQLite3::Error => error # failed to find commands
94
+ log.error("Database commands error: #{error.message}.")
95
+ Array.new # return empty results
96
+ end
97
+
98
+ def have_commands?
99
+ read_from_sqlite { |sqlite|
100
+ !!sqlite.first_value_from("SELECT ROWID FROM commands LIMIT 1")
101
+ }
102
+ rescue Amalgalite::SQLite3::Error => error # failed to find commands
103
+ log.error("Database command check error: #{error.message}.")
104
+ nil # commands not found
105
+ end
106
+
107
+ def complete_run(command, output, exit_status, snapshot_at, run_time)
108
+ write_to_sqlite do |sqlite|
109
+ params = [ command[:code],
110
+ output,
111
+ exit_status,
112
+ snapshot_at.to_db_s,
113
+ run_time ]
114
+ begin
115
+ sqlite.execute(<<-END_INSERT_RUN.trim, *params)
116
+ INSERT INTO
117
+ runs( code, output, exit_status, snapshot_at, run_time )
118
+ VALUES( ?, ?, ?, ?, ? )
119
+ END_INSERT_RUN
120
+ rescue Amalgalite::SQLite3::Error => error # failed to add run
121
+ # do nothing: skip bad command and move on
122
+ log.error( "Database bad command run (#{command[:code]}) error: " +
123
+ "#{error.message}." )
124
+ end
125
+ run_time = Time.now
126
+ params = [ run_time.to_db_s,
127
+ ( run_time +
128
+ command[:interval] * 60 ).to_db_s(:trim_seconds),
129
+ command[:id] ]
130
+ begin
131
+ sqlite.execute(<<-END_UPDATE_COMMAND.trim, *params)
132
+ UPDATE commands SET last_run_at = ?, next_run_at = ? WHERE ROWID = ?
133
+ END_UPDATE_COMMAND
134
+ rescue Amalgalite::SQLite3::Error => error # failed to update command
135
+ # do nothing: command will be run again
136
+ log.error( "Database bad command (#{command[:code]}) update " +
137
+ "error: #{error.message}." )
138
+ end
139
+ end
140
+ rescue Amalgalite::SQLite3::Error => error # failed to get a write lock
141
+ # try again to update commands later
142
+ log.error("Database complete command locking error: #{error.message}.")
143
+ end
144
+
145
+ def current_runs
146
+ write_to_sqlite { |sqlite|
147
+ begin
148
+ run_ids = Array.new
149
+ runs = query(<<-END_FIND_RUNS.trim) { |row|
150
+ SELECT ROWID AS id, code, output, exit_status,
151
+ snapshot_at AS created_at, run_time
152
+ FROM runs
153
+ ORDER BY snapshot_at
154
+ LIMIT 500
155
+ END_FIND_RUNS
156
+ if created = Time.from_db_s(row[:created_at])
157
+ row[:created_at] = created.utc.to_db_s
158
+ else
159
+ log.warn("Run timestamp missing.")
160
+ end
161
+ run_ids << row.delete_at(:id)
162
+ }
163
+ rescue Amalgalite::SQLite3::Error => error # failed to find runs
164
+ log.error("Database runs error: #{error.message}.")
165
+ return Array.new # return empty results
166
+ end
167
+ return runs if runs.empty?
168
+ begin
169
+ sqlite.execute(<<-END_DELETE_RUNS.trim, *run_ids)
170
+ DELETE FROM runs
171
+ WHERE ROWID IN (#{(['?'] * run_ids.size).join(', ')})
172
+ END_DELETE_RUNS
173
+ rescue Amalgalite::SQLite3::Error => error # failed to remove runs
174
+ # cancel sending this batch
175
+ log.error("Database delivered runs error: #{error.message}.")
176
+ sqlite.rollback # we can't submit unless we're sure they are gone
177
+ return Array.new # return empty results
178
+ end
179
+ runs # the runs ready for sending
180
+ }
181
+ rescue Amalgalite::SQLite3::Error => error # failed to get a write lock
182
+ # try again to read runs later
183
+ log.error("Database runs locking error: #{error.message}.")
184
+ end
185
+ end
186
+ end
187
+ end