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.
- data/AUTHORS +4 -0
- data/CHANGELOG +3 -0
- data/COPYING +340 -0
- data/INSTALL +17 -0
- data/LICENSE +6 -0
- data/README +3 -0
- data/Rakefile +123 -0
- data/TODO +3 -0
- data/bin/scout_agent +11 -0
- data/lib/scout_agent.rb +73 -0
- data/lib/scout_agent/agent.rb +42 -0
- data/lib/scout_agent/agent/communication_agent.rb +85 -0
- data/lib/scout_agent/agent/master_agent.rb +301 -0
- data/lib/scout_agent/api.rb +241 -0
- data/lib/scout_agent/assignment.rb +105 -0
- data/lib/scout_agent/assignment/configuration.rb +30 -0
- data/lib/scout_agent/assignment/identify.rb +110 -0
- data/lib/scout_agent/assignment/queue.rb +95 -0
- data/lib/scout_agent/assignment/reset.rb +91 -0
- data/lib/scout_agent/assignment/snapshot.rb +92 -0
- data/lib/scout_agent/assignment/start.rb +149 -0
- data/lib/scout_agent/assignment/status.rb +44 -0
- data/lib/scout_agent/assignment/stop.rb +60 -0
- data/lib/scout_agent/assignment/upload_log.rb +61 -0
- data/lib/scout_agent/core_extensions.rb +260 -0
- data/lib/scout_agent/database.rb +386 -0
- data/lib/scout_agent/database/mission_log.rb +282 -0
- data/lib/scout_agent/database/queue.rb +126 -0
- data/lib/scout_agent/database/snapshots.rb +187 -0
- data/lib/scout_agent/database/statuses.rb +65 -0
- data/lib/scout_agent/dispatcher.rb +157 -0
- data/lib/scout_agent/id_card.rb +143 -0
- data/lib/scout_agent/lifeline.rb +243 -0
- data/lib/scout_agent/mission.rb +212 -0
- data/lib/scout_agent/order.rb +58 -0
- data/lib/scout_agent/order/check_in_order.rb +32 -0
- data/lib/scout_agent/order/snapshot_order.rb +33 -0
- data/lib/scout_agent/plan.rb +306 -0
- data/lib/scout_agent/server.rb +123 -0
- data/lib/scout_agent/tracked.rb +59 -0
- data/lib/scout_agent/wire_tap.rb +513 -0
- data/setup.rb +1360 -0
- data/test/tc_core_extensions.rb +89 -0
- data/test/tc_id_card.rb +115 -0
- data/test/tc_plan.rb +285 -0
- data/test/test_helper.rb +22 -0
- data/test/ts_all.rb +7 -0
- 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
|