scout_agent 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|