diff options
author | Lars Wirzenius <lars.wirzenius@codethink.co.uk> | 2014-02-24 17:24:54 +0000 |
---|---|---|
committer | Lars Wirzenius <lars.wirzenius@codethink.co.uk> | 2014-02-24 17:24:54 +0000 |
commit | ff94a614ba628906151ab6ec6e9b008b9ea8fa02 (patch) | |
tree | 0f5fb78ed1f5dc0d8ce72310ab25126a3ccd4349 | |
parent | 8e9849907e8bb64c3964373c82d39727d9ab8550 (diff) | |
download | lorry-controller-ff94a614ba628906151ab6ec6e9b008b9ea8fa02.tar.gz |
Implement stopping of jobs in WEBAPP
-rwxr-xr-x | lorry-controller-webapp | 105 | ||||
-rw-r--r-- | yarns.webapp/040-running-jobs.yarn | 54 | ||||
-rw-r--r-- | yarns.webapp/900-implementations.yarn | 13 |
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. |