summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <lars.wirzenius@codethink.co.uk>2014-02-24 17:24:54 +0000
committerLars Wirzenius <lars.wirzenius@codethink.co.uk>2014-02-24 17:24:54 +0000
commitff94a614ba628906151ab6ec6e9b008b9ea8fa02 (patch)
tree0f5fb78ed1f5dc0d8ce72310ab25126a3ccd4349
parent8e9849907e8bb64c3964373c82d39727d9ab8550 (diff)
downloadlorry-controller-ff94a614ba628906151ab6ec6e9b008b9ea8fa02.tar.gz
Implement stopping of jobs in WEBAPP
-rwxr-xr-xlorry-controller-webapp105
-rw-r--r--yarns.webapp/040-running-jobs.yarn54
-rw-r--r--yarns.webapp/900-implementations.yarn13
3 files changed, 123 insertions, 49 deletions
diff --git a/lorry-controller-webapp b/lorry-controller-webapp
index e70b132..3bc0ff3 100755
--- a/lorry-controller-webapp
+++ b/lorry-controller-webapp
@@ -116,8 +116,8 @@ class WrongNumberLorriesRunningJob(Exception):
def __init__(self, job_id, row_count):
Exception.__init__(
- self, 'STATEDB has %d Lorry specs running jobs %r, should be 1',
- row_count, job_id)
+ self, 'STATEDB has %d Lorry specs running jobs %r, should be 1' %
+ (row_count, job_id))
class StateDB(object):
@@ -142,15 +142,25 @@ class StateDB(object):
c.execute('CREATE TABLE IF NOT EXISTS running_queue (running INT)')
c.execute('INSERT INTO running_queue VALUES (0)')
+ self.lorries_fields = [
+ ('path', 'TEXT PRIMARY KEY'),
+ ('text', 'TEXT'),
+ ('generated', 'INT'),
+ ('running_job', 'INT'),
+ ('kill_job', 'INT'),
+ ('due', 'INT'),
+ ('interval', 'INT'),
+ ]
+ self.lorries_booleans = [
+ 'kill_job',
+ ]
+
+ fields_sql = ', '.join(
+ '%s %s' % (name, info) for name, info in self.lorries_fields
+ )
+
c.execute(
- 'CREATE TABLE IF NOT EXISTS lorries ('
- 'path TEXT PRIMARY KEY, '
- 'text TEXT, '
- 'generated INT, '
- 'running_job INT, '
- 'due INT, '
- 'interval INT '
- ')')
+ 'CREATE TABLE IF NOT EXISTS lorries (%s)' % fields_sql)
self._conn.commit()
@@ -192,43 +202,26 @@ class StateDB(object):
self.get_cursor().execute(
'UPDATE running_queue SET running = ?', str(new_value))
+ def make_lorry_info_from_row(self, row):
+ result = dict((t[0], row[i]) for i, t in enumerate(self.lorries_fields))
+ for field in self.lorries_booleans:
+ result[field] = bool(result[field])
+ return result
+
def get_lorry_info(self, path):
logging.debug('StateDB.get_lorry_info(path=%r) called', path)
c = self.get_cursor()
- c.execute(
- 'SELECT path, text, generated, due, interval, running_job '
- 'FROM lorries WHERE path IS ?',
- (path,))
+ c.execute('SELECT * FROM lorries WHERE path IS ?', (path,))
row = c.fetchone()
if row is None:
raise LorryNotFoundError(path)
- return {
- 'path': row[0],
- 'text': row[1],
- 'generated': row[2],
- 'due': row[3],
- 'interval': row[4],
- 'running_job': row[5],
- }
+ return self.make_lorry_info_from_row(row)
def get_all_lorries_info(self):
logging.debug('StateDB.get_all_lorries_info called')
c = self.get_cursor()
- c.execute(
- 'SELECT path, text, generated, due, interval, running_job '
- 'FROM lorries '
- 'ORDER BY due')
- return [
- {
- 'path': row[0],
- 'text': row[1],
- 'generated': row[2],
- 'due': row[3],
- 'interval': row[4],
- 'running_job': row[5],
- }
- for row in c.fetchall()
- ]
+ c.execute('SELECT * FROM lorries ORDER BY due')
+ return [self.make_lorry_info_from_row(row) for row in c.fetchall()]
def get_lorries_paths(self):
logging.debug('StateDB.get_lorries_paths called')
@@ -258,9 +251,9 @@ class StateDB(object):
c = self.get_cursor()
c.execute(
'INSERT OR REPLACE INTO lorries '
- '(path, text, generated, due, interval, running_job) '
- 'VALUES (?, ?, ?, ?, ?, ?)',
- (path, text, generated, due, interval, None))
+ '(path, text, generated, due, interval, running_job, kilL_job) '
+ 'VALUES (?, ?, ?, ?, ?, ?, ?)',
+ (path, text, generated, due, interval, None, 0))
def remove_lorry(self, path):
logging.debug('StateDB.remove_lorry(%r) called', path)
@@ -295,6 +288,18 @@ class StateDB(object):
'SELECT running_job FROM lorries WHERE running_job IS NOT NULL')
return [row[0] for row in c.fetchall()]
+ def set_kill_job(self, path, value):
+ logging.debug('StateDB.set_kill_job(%r, %r) called', path, value)
+ assert self.in_transaction
+ if value:
+ value = 1
+ else:
+ value = 0
+ c = self.get_cursor()
+ c.execute(
+ 'UPDATE lorries SET kill_job=? WHERE path=?',
+ (value, path))
+
class LorryControllerRoute(object):
@@ -598,10 +603,14 @@ class JobUpdate(LorryControllerRoute):
logging.debug('form job_id: %r', job_id)
logging.debug('form exit: %r', exit)
- if exit is not None:
+ if exit is not None and exit != 'no':
with self.statedb.transaction():
path = self.statedb.find_lorry_running_job(job_id)
self.statedb.set_running_job(path, None)
+ return self.statedb.get_lorry_info(path)
+ else:
+ path = self.statedb.find_lorry_running_job(job_id)
+ return self.statedb.get_lorry_info(path)
class ListRunningJobs(LorryControllerRoute):
@@ -618,6 +627,20 @@ class ListRunningJobs(LorryControllerRoute):
}
+class StopJob(LorryControllerRoute):
+
+ http_method = 'POST'
+ path = '/1.0/stop-job'
+
+ def run(self, **kwargs):
+ logging.debug('%s %s called', self.http_method, self.path)
+ with self.statedb.transaction():
+ job_id = bottle.request.forms.job_id
+ path = self.statedb.find_lorry_running_job(job_id)
+ self.statedb.set_kill_job(path, True)
+ return self.statedb.get_lorry_info(path)
+
+
class WEBAPP(cliapp.Application):
def add_settings(self):
diff --git a/yarns.webapp/040-running-jobs.yarn b/yarns.webapp/040-running-jobs.yarn
index f254076..2c4a076 100644
--- a/yarns.webapp/040-running-jobs.yarn
+++ b/yarns.webapp/040-running-jobs.yarn
@@ -4,6 +4,9 @@ Running jobs
This chapter contains tests that verify that WEBAPP schedules jobs,
accepts job output, and lets the admin kill running jobs.
+Run a job successfully
+----------------------
+
To start with, with an empty run-queue, nothing should be scheduled.
SCENARIO run a job
@@ -40,5 +43,56 @@ Requesting another job should now again return null.
Inform WEBAPP the job is finished.
WHEN MINION makes request POST /1.0/job-update with job_id=1&exit=0
+ THEN response has kill_job set to false
+ WHEN admin makes request GET /1.0/lorry/upstream/foo
+ THEN response has running_job set to null
+
+Cleanup.
+
+ FINALLY WEBAPP terminates
+
+
+Stop job in the middle
+----------------------
+
+We need to be able to stop jobs while they're running as well. We
+start by setting up everything so that a job is running, the same way
+we did for the successful job scenario.
+
+ SCENARIO stop a job while it's running
+ GIVEN a new git repository in CONFGIT
+ AND an empty lorry-controller.conf in CONFGIT
+ AND lorry-controller.conf in CONFGIT adds lorries *.lorry using prefix upstream
+ AND WEBAPP uses CONFGIT as its configuration directory
+ AND a running WEBAPP
+ AND Lorry file CONFGIT/foo.lorry with {"foo":{"type":"git","url":"git://foo"}}
+ WHEN admin makes request GET /1.0/read-configuration
+ AND admin makes request GET /1.0/give-me-job
+ THEN response has job_id set to 1
+ AND response has path set to "upstream/foo"
+
+Admin will now ask WEBAPP to kill the job. This changes sets a field
+in the STATEDB only.
+
+ WHEN admin makes request POST /1.0/stop-job with job_id=1
AND admin makes request GET /1.0/lorry/upstream/foo
+ THEN response has kill_job set to true
+
+Now, when MINION updates the job, WEBAPP will tell it to kill it.
+MINION will do so, and then update the job again.
+
+ WHEN MINION makes request POST /1.0/job-update with job_id=1&exit=no
+ THEN response has kill_job set to true
+ WHEN MINION makes request POST /1.0/job-update with job_id=1&exit=1
+
+Admin will now see that the job has, indeed, been killed.
+
+ WHEN admin makes request GET /1.0/lorry/upstream/foo
THEN response has running_job set to null
+
+ WHEN admin makes request GET /1.0/list-running-jobs
+ THEN response has running_jobs set to []
+
+Cleanup.
+
+ FINALLY WEBAPP terminates
diff --git a/yarns.webapp/900-implementations.yarn b/yarns.webapp/900-implementations.yarn
index 4215fd9..c629fa4 100644
--- a/yarns.webapp/900-implementations.yarn
+++ b/yarns.webapp/900-implementations.yarn
@@ -150,8 +150,8 @@ scenario doesn't need ot check that separately.
A GET request:
IMPLEMENTS WHEN admin makes request GET (\S+)
- rm -f "$DATADIR/response.headers"
- rm -f "$DATADIR/response.body"
+ > "$DATADIR/response.headers"
+ > "$DATADIR/response.body"
port=$(cat "$DATADIR/webapp.port")
# The timestamp is needed by "THEN static status page got updated"
@@ -170,8 +170,8 @@ A POST request always has a body. The body consists of `foo=bar`
pairs, separated by `&` signs.
IMPLEMENTS WHEN (\S+) makes request POST (\S+) with (.*)
- rm -f "$DATADIR/response.headers"
- rm -f "$DATADIR/response.body"
+ > "$DATADIR/response.headers"
+ > "$DATADIR/response.body"
port=$(cat "$DATADIR/webapp.port")
# The timestamp is needed by "THEN static status page got updated"
@@ -185,10 +185,7 @@ pairs, separated by `&` signs.
--data "$MATCH_3" \
"http://127.0.0.1:$port$MATCH_2"
cat "$DATADIR/response.headers"
- if [ -e "$DATADIR/response.body" ]
- then
- cat "$DATADIR/response.body"
- fi
+ cat "$DATADIR/response.body"
head -n1 "$DATADIR/response.headers" | grep '^HTTP/1\.[01] 200 '
Check the Content-Type of the response has the desired type.