database: Serialize all database accesses in a thread.

Fixes <https://bugs.gnu.org/32234>.

* bin/cuirass.in (main): Keep only one WITH-DATABASE call around all fibers.
Remove all DB arguments.
* src/cuirass/base.scm (evaluate, update-build-statuses!, spawn-builds,
handle-build-event, build-packages): Remove all DB arguments.
(clear-build-queue, cancel-old-builds): Wrap in WITH-DB-CRITICAL-SECTION,
remove all DB arguments.
(restart-builds): Remove the NON-BLOCKING call, remove all DB arguments.
(process-specs): Remove all DB arguments, remove the WITH-DATABASE call.
* src/cuirass/database.scm (%db-channel): New parameter.
(with-db-critical-section): New macro.
(db-add-input, db-add-specification, db-get-inputs, db-get-specifications,
db-add-evaluation, db-add-build, db-update-build-status!, db-get-outputs,
db-get-builds, db-get-build, db-get-pending-derivations, db-get-stamp,
db-add-stamp, db-get-evaluations, db-get-evaluations-build-summary,
db-get-evaluations-id-min, db-get-evaluations-id-max, db-get-builds-min,
db-get-builds-max): Wrap in WITH-DB-CRITICAL-SECTION, remove all DB arguments.
(with-database): Wrap BODY in PARAMETERIZE form that sets %DB-CHANNEL to the
channel returned by MAKE-CRITICAL-SECTION.
* src/cuirass/http.scm (handle-build-request, handle-builds-request): Remove
all DB arguments.
(url-handler): Remove all DB arguments, remove the DB-CHANNEL state, remove
the WITH-CRITICAL-SECTION calls.
(run-cuirass-server): Remove the DB arguments, remove the
MAKE-CRITICAL-SECTION call.
* src/cuirass/utils.scm (make-critical-section): Replace SPAWN-FIBER with
CALL-WITH-NEW-THREAD.  Wrap body in PARAMETERIZE form that clears
CURRENT-FIBER.
* tests/database.scm (with-temporary-database, "db-add-specification",
"db-add-build", "db-update-build-status!", "db-get-builds",
"db-get-pending-derivations"): Remove the DB arguments.
("db-init"): Set the %DB-CHANNEL parameter to the channel returned by
MAKE-CRITICAL-SECTION, and return #t.
("database"): Set %DB-CHANNEL to #f during cleanup.
* tests/http.scm ("db-init"): Set the %DB-CHANNEL parameter to the channel
returned by MAKE-CRITICAL-SECTION, and return #t.
("cuirass-run", "fill-db"): Remove the DB arguments.
("http"): Set %DB-CHANNEL to #f during cleanup.
This commit is contained in:
Clément Lassieur 2018-08-05 13:14:44 +02:00
parent e66e545b69
commit 4612a3a70f
No known key found for this signature in database
GPG Key ID: 89F96D4808F359C7
8 changed files with 475 additions and 441 deletions

View File

@ -12,7 +12,8 @@
(eval put 'call-with-time 'scheme-indent-function 1)
(eval put 'test-error 'scheme-indent-function 1)
(eval put 'make-parameter 'scheme-indent-function 1)
(eval put 'with-database 'scheme-indent-function 1)
(eval put 'with-database 'scheme-indent-function 0)
(eval put 'with-db-critical-section 'scheme-indent-function 1)
(eval . (put 'with-critical-section 'scheme-indent-function 2)))
(texinfo-mode
(indent-tabs-mode)

View File

@ -115,19 +115,19 @@ exec ${GUILE:-@GUILE@} --no-auto-compile -e main -s "$0" "$@"
(log-message "running Fibers on ~a kernel threads" threads)
(run-fibers
(lambda ()
(with-database db
(with-database
(and specfile
(let ((new-specs (save-module-excursion
(lambda ()
(set-current-module (make-user-module '()))
(primitive-load specfile)))))
(for-each (lambda (spec) (db-add-specification db spec))
(for-each (lambda (spec) (db-add-specification spec))
new-specs)))
(if one-shot?
(process-specs db (db-get-specifications db))
(process-specs (db-get-specifications))
(let ((exit-channel (make-channel)))
(clear-build-queue db)
(clear-build-queue)
;; First off, restart builds that had not completed or
;; were not even started on a previous run.
@ -135,25 +135,22 @@ exec ${GUILE:-@GUILE@} --no-auto-compile -e main -s "$0" "$@"
(essential-task
'restart-builds exit-channel
(lambda ()
(with-database db
(restart-builds db)))))
(restart-builds))))
(spawn-fiber
(essential-task
'build exit-channel
(lambda ()
(with-database db
(while #t
(process-specs db (db-get-specifications db))
(process-specs (db-get-specifications))
(log-message "next evaluation in ~a seconds" interval)
(sleep interval))))))
(sleep interval)))))
(spawn-fiber
(essential-task
'web-server exit-channel
(lambda ()
(with-database db
(run-cuirass-server db #:host host #:port port))))
(run-cuirass-server #:host host #:port port)))
#:parallel? #t)
(spawn-fiber

View File

@ -248,7 +248,7 @@ fibers."
(logior (@ (fibers epoll) EPOLLERR)
(@ (fibers epoll) EPOLLHUP)))))
(define (evaluate store db spec checkouts commits)
(define (evaluate store spec checkouts commits)
"Evaluate and build package derivations defined in SPEC, using CHECKOUTS.
Return a list of jobs."
(define (augment-job job eval-id)
@ -277,7 +277,7 @@ Return a list of jobs."
(('evaluation jobs)
(let* ((spec-name (assq-ref spec #:name))
(eval-id (db-add-evaluation
db `((#:specification . ,spec-name)
`((#:specification . ,spec-name)
(#:commits . ,commits)))))
(log-message "created evaluation ~a for '~a'" eval-id spec-name)
(map (lambda (job)
@ -368,7 +368,7 @@ Essentially this procedure inverts the inversion-of-control that
;; Our shuffling algorithm is simple: we sort by .drv file name. :-)
(sort drv string<?))
(define (update-build-statuses! store db lst)
(define (update-build-statuses! store lst)
"Update the build status of the derivations listed in LST, which have just
been passed to 'build-derivations' (meaning that we can assume that, if their
outputs are invalid, that they failed to build.)"
@ -376,8 +376,8 @@ outputs are invalid, that they failed to build.)"
(match (derivation-path->output-paths drv)
(((_ . outputs) ...)
(if (any (cut valid-path? store <>) outputs)
(db-update-build-status! db drv (build-status succeeded))
(db-update-build-status! db drv (build-status failed))))))
(db-update-build-status! drv (build-status succeeded))
(db-update-build-status! drv (build-status failed))))))
(for-each update! lst))
@ -393,10 +393,11 @@ and returns the values RESULTS."
(print-exception (current-error-port) frame key args)
(apply values results)))))
(define* (spawn-builds store db drv
(define* (spawn-builds store drv
#:key (max-batch-size 200))
"Build the derivations listed in DRV, updating DB as builds complete.
Derivations are submitted in batches of at most MAX-BATCH-SIZE items."
"Build the derivations listed in DRV, updating the database as builds
complete. Derivations are submitted in batches of at most MAX-BATCH-SIZE
items."
;; XXX: We want to pass 'build-derivations' as many derivations at once so
;; we benefit from as much parallelism as possible (we must be using
;; #:keep-going? #t).
@ -444,7 +445,7 @@ Derivations are submitted in batches of at most MAX-BATCH-SIZE items."
;; from PORT and eventually close it.
(catch #t
(lambda ()
(handle-build-event db event))
(handle-build-event event))
(exception-reporter state)))
#t)
(close-port port)
@ -455,14 +456,14 @@ Derivations are submitted in batches of at most MAX-BATCH-SIZE items."
;; derivations were built "behind our back", in which case
;; 'build-derivations' doesn't actually do anything and
;; 'handle-build-event' doesn't see any event. Because of that,
;; adjust DB here.
(update-build-statuses! store db batch)
;; adjust the database here.
(update-build-statuses! store batch)
(loop rest (max (- count max-batch-size) 0))))))
(define* (handle-build-event db event)
(define* (handle-build-event event)
"Handle EVENT, a build event sexp as produced by 'build-event-output-port',
updating DB accordingly."
updating the database accordingly."
(define (valid? file)
;; FIXME: Sometimes we might get bogus events due to the interleaving of
;; build messages. This procedure prevents us from propagating the bogus
@ -475,7 +476,7 @@ updating DB accordingly."
(if (valid? drv)
(begin
(log-message "build started: '~a'" drv)
(db-update-build-status! db drv (build-status started)))
(db-update-build-status! drv (build-status started)))
(log-message "bogus build-started event for '~a'" drv)))
(('build-remote drv host _ ...)
(log-message "'~a' offloaded to '~a'" drv host))
@ -483,13 +484,13 @@ updating DB accordingly."
(if (valid? drv)
(begin
(log-message "build succeeded: '~a'" drv)
(db-update-build-status! db drv (build-status succeeded)))
(db-update-build-status! drv (build-status succeeded)))
(log-message "bogus build-succeeded event for '~a'" drv)))
(('build-failed drv _ ...)
(if (valid? drv)
(begin
(log-message "build failed: '~a'" drv)
(db-update-build-status! db drv (build-status failed)))
(db-update-build-status! drv (build-status failed)))
(log-message "bogus build-failed event for '~a'" drv)))
(('substituter-started item _ ...)
(log-message "substituter started: '~a'" item))
@ -503,42 +504,42 @@ updating DB accordingly."
(string=? (assq-ref build1 #:derivation)
(assq-ref build2 #:derivation)))
(define (clear-build-queue db)
"Reset the status of builds in DB that are marked as \"started\". This
procedure is meant to be called at startup."
(define (clear-build-queue)
"Reset the status of builds in the database that are marked as \"started\".
This procedure is meant to be called at startup."
(log-message "marking stale builds as \"scheduled\"...")
(sqlite-exec db "UPDATE Builds SET status = -2 WHERE status = -1;"))
(with-db-critical-section db
(sqlite-exec db "UPDATE Builds SET status = -2 WHERE status = -1;")))
(define (cancel-old-builds db age)
(define (cancel-old-builds age)
"Cancel builds older than AGE seconds."
(log-message "canceling builds older than ~a seconds..." age)
(sqlite-exec db
"UPDATE Builds SET status = 4 WHERE status = -2 AND timestamp < "
(- (time-second (current-time time-utc)) age)
";"))
(with-db-critical-section db
(sqlite-exec
db "UPDATE Builds SET status = 4 WHERE status = -2 AND timestamp < "
(- (time-second (current-time time-utc)) age) ";")))
(define (restart-builds db)
"Restart builds whose status in DB is \"pending\" (scheduled or started)."
(define (restart-builds)
"Restart builds whose status in the database is \"pending\" (scheduled or
started)."
(with-store store
;; Note: On a big database, 'db-get-pending-derivations' can take a couple
;; of minutes, hence 'non-blocking'.
(log-message "retrieving list of pending builds...")
(let*-values (((valid stale)
(partition (cut valid-path? store <>)
(non-blocking (db-get-pending-derivations db)))))
(db-get-pending-derivations))))
;; We cannot restart builds listed in STALE, so mark them as canceled.
(log-message "canceling ~a stale builds" (length stale))
(for-each (lambda (drv)
(db-update-build-status! db drv (build-status canceled)))
(db-update-build-status! drv (build-status canceled)))
stale)
;; Those in VALID can be restarted. If some of them were built in the
;; meantime behind our back, that's fine: 'spawn-builds' will DTRT.
(log-message "restarting ~a pending builds" (length valid))
(spawn-builds store db valid)
(spawn-builds store valid)
(log-message "done with restarted builds"))))
(define (build-packages store db jobs)
(define (build-packages store jobs)
"Build JOBS and return a list of Build results."
(define (register job)
(let* ((name (assq-ref job #:job-name))
@ -570,14 +571,14 @@ procedure is meant to be called at startup."
(#:timestamp . ,cur-time)
(#:starttime . 0)
(#:stoptime . 0))))
(db-add-build db build))))
(db-add-build build))))
(define derivations
(filter-map register jobs))
(spawn-builds store db derivations)
(spawn-builds store derivations)
(let* ((results (filter-map (cut db-get-build db <>) derivations))
(let* ((results (filter-map (cut db-get-build <>) derivations))
(status (map (cut assq-ref <> #:status) results))
(success (count (lambda (status)
(= status (build-status succeeded)))
@ -651,11 +652,11 @@ procedure is meant to be called at startup."
checkout)
results)))
(define (process-specs db jobspecs)
"Evaluate and build JOBSPECS and store results in DB."
(define (process-specs jobspecs)
"Evaluate and build JOBSPECS and store results in the database."
(define (process spec)
(with-store store
(let* ((stamp (db-get-stamp db spec))
(let* ((stamp (db-get-stamp spec))
(name (assoc-ref spec #:name))
(checkouts (fetch-inputs spec))
(commits (map (cut assq-ref <> #:commit) checkouts))
@ -663,7 +664,7 @@ procedure is meant to be called at startup."
(unless (equal? commits-str stamp)
;; Immediately mark SPEC's INPUTS as being processed so we don't
;; spawn a concurrent evaluation of that same commit.
(db-add-stamp db spec commits-str)
(db-add-stamp spec commits-str)
(compile-checkouts spec (filter compile? checkouts))
(spawn-fiber
(lambda ()
@ -674,11 +675,10 @@ procedure is meant to be called at startup."
(log-message "evaluating spec '~a': stamp ~s different from ~s"
name commits-str stamp)
(with-store store
(with-database db
(let ((jobs (evaluate store db spec checkouts commits)))
(let ((jobs (evaluate store spec checkouts commits)))
(log-message "building ~a jobs for '~a'"
(length jobs) name)
(build-packages store db jobs)))))))
(build-packages store jobs))))))
;; 'spawn-fiber' returns zero values but we need one.
*unspecified*))))

View File

@ -59,7 +59,9 @@
;; Parameters.
%package-database
%package-schema-file
%db-channel
;; Macros.
with-db-critical-section
with-database))
(define (%sqlite-exec db sql . args)
@ -139,6 +141,16 @@ question marks matches the number of arguments to bind."
(string-append %datadir "/" %package))
"/sql")))
(define %db-channel
(make-parameter #f))
(define-syntax-rule (with-db-critical-section db exp ...)
"Evaluate EXP... in the critical section corresponding to %DB-CHANNEL.
DB is bound to the argument of that critical section: the database
connection."
(call-with-critical-section (%db-channel)
(lambda (db) exp ...)))
(define (read-sql-file file-name)
"Return a list of string containing SQL instructions from FILE-NAME."
(call-with-input-file file-name
@ -238,7 +250,8 @@ database object."
(vector-ref (car (sqlite-exec db "SELECT last_insert_rowid();"))
0))
(define (db-add-input db spec-name input)
(define (db-add-input spec-name input)
(with-db-critical-section db
(sqlite-exec db "\
INSERT OR IGNORE INTO Inputs (specification, name, url, load_path, branch, \
tag, revision, no_compile_p) VALUES ("
@ -250,29 +263,33 @@ tag, revision, no_compile_p) VALUES ("
(assq-ref input #:tag) ", "
(assq-ref input #:commit) ", "
(if (assq-ref input #:no-compile?) 1 0) ");")
(last-insert-rowid db))
(last-insert-rowid db)))
(define (db-add-specification db spec)
"Store SPEC in database DB. SPEC inputs are stored in the INPUTS table."
(define (db-add-specification spec)
"Store SPEC in database the database. SPEC inputs are stored in the INPUTS
table."
(with-db-critical-section db
(sqlite-exec db "\
INSERT OR IGNORE INTO Specifications (name, load_path_inputs, \
package_path_inputs, proc_input, proc_file, proc, proc_args) \
VALUES ("
(assq-ref spec #:name) ", "
(assq-ref spec #:load-path-inputs) ", "
(assq-ref spec #:package-path-inputs)", "
(assq-ref spec #:package-path-inputs) ", "
(assq-ref spec #:proc-input) ", "
(assq-ref spec #:proc-file) ", "
(symbol->string (assq-ref spec #:proc)) ", "
(assq-ref spec #:proc-args) ");")
(let ((spec-id (last-insert-rowid db)))
(for-each (lambda (input)
(db-add-input db (assq-ref spec #:name) input))
(db-add-input (assq-ref spec #:name) input))
(assq-ref spec #:inputs))
spec-id))
spec-id)))
(define (db-get-inputs db spec-name)
(let loop ((rows (sqlite-exec db "SELECT * FROM Inputs WHERE specification="
(define (db-get-inputs spec-name)
(with-db-critical-section db
(let loop ((rows (sqlite-exec
db "SELECT * FROM Inputs WHERE specification="
spec-name ";"))
(inputs '()))
(match rows
@ -287,9 +304,10 @@ package_path_inputs, proc_input, proc_file, proc, proc_args) \
(#:tag . ,tag)
(#:commit . ,revision)
(#:no-compile? . ,(positive? no-compile-p)))
inputs))))))
inputs)))))))
(define (db-get-specifications db)
(define (db-get-specifications)
(with-db-critical-section db
(let loop ((rows (sqlite-exec db "SELECT * FROM Specifications;"))
(specs '()))
(match rows
@ -307,23 +325,36 @@ package_path_inputs, proc_input, proc_file, proc, proc_args) \
(#:proc-file . ,proc-file)
(#:proc . ,(with-input-from-string proc read))
(#:proc-args . ,(with-input-from-string proc-args read))
(#:inputs . ,(db-get-inputs db name)))
specs))))))
(#:inputs . ,(db-get-inputs name)))
specs)))))))
(define (db-add-evaluation db eval)
(define (db-add-evaluation eval)
(with-db-critical-section db
(sqlite-exec db "\
INSERT INTO Evaluations (specification, commits) VALUES ("
(assq-ref eval #:specification) ", "
(string-join (assq-ref eval #:commits)) ");")
(last-insert-rowid db))
(last-insert-rowid db)))
(define-syntax-rule (with-database db body ...)
"Run BODY with a connection to the database which is bound to DB in BODY."
(define-syntax-rule (with-database body ...)
"Run BODY with %DB-CHANNEL being dynamically bound to a channel implementing
a critical section that allows database operations to be serialized."
;; XXX: We don't install an unwind handler to play well with delimited
;; continuations and fibers. But as a consequence, we leak DB when BODY
;; raises an exception.
(let ((db (db-open)))
(unwind-protect body ... (db-close db))))
(unwind-protect
;; Process database queries sequentially in a thread. We need this
;; because otherwise we would need to use the SQLite multithreading
;; feature for which it is required to wait until the database is
;; available, and the waiting would happen in non-cooperative and
;; non-resumable code that blocks the fibers scheduler. Now the database
;; access blocks on PUT-MESSAGE, which allows the scheduler to schedule
;; another fiber. Also, creating one new handle for each request would
;; be costly and may defeat statement caching.
(parameterize ((%db-channel (make-critical-section db)))
body ...)
(db-close db))))
(define* (read-quoted-string #:optional (port (current-input-port)))
"Read all of the characters out of PORT and return them as a SQL quoted
@ -353,9 +384,10 @@ string."
(failed-other 3)
(canceled 4))
(define (db-add-build db build)
"Store BUILD in database DB. BUILD eventual outputs are stored
in the OUTPUTS table."
(define (db-add-build build)
"Store BUILD in database the database. BUILD eventual outputs are stored in
the OUTPUTS table."
(with-db-critical-section db
(catch 'sqlite-error
(lambda ()
(sqlite-exec db "
@ -388,33 +420,37 @@ INSERT INTO Outputs (derivation, name, path) VALUES ("
;; produce the same derivation, and we can ignore it.
(if (= code SQLITE_CONSTRAINT_PRIMARYKEY)
#f
(apply throw key who code rest)))))
(apply throw key who code rest))))))
(define* (db-update-build-status! db drv status #:key log-file)
"Update DB so that DRV's status is STATUS. This also updates the
(define* (db-update-build-status! drv status #:key log-file)
"Update the database so that DRV's status is STATUS. This also updates the
'starttime' or 'stoptime' fields. If LOG-FILE is true, record it as the build
log file for DRV."
(define now
(time-second (current-time time-utc)))
(with-db-critical-section db
(if (= status (build-status started))
(sqlite-exec db "UPDATE Builds SET starttime=" now ", status="
status "WHERE derivation=" drv ";")
;; Update only if we're switching to a different status; otherwise leave
;; things unchanged. This ensures that 'stoptime' remains valid and
;; doesn't change every time we mark DRV as 'succeeded' several times in
;; a row, for instance.
;; Update only if we're switching to a different status; otherwise
;; leave things unchanged. This ensures that 'stoptime' remains valid
;; and doesn't change every time we mark DRV as 'succeeded' several
;; times in a row, for instance.
(if log-file
(sqlite-exec db "UPDATE Builds SET stoptime=" now
", status=" status ", log=" log-file
"WHERE derivation=" drv "AND status != " status ";")
(sqlite-exec db "UPDATE Builds SET stoptime=" now
", status=" status
"WHERE derivation=" drv " AND status != " status ";"))))
"WHERE derivation=" drv " AND status != " status
";")))))
(define (db-get-outputs db derivation)
"Retrieve the OUTPUTS of the build identified by DERIVATION in DB database."
(define (db-get-outputs derivation)
"Retrieve the OUTPUTS of the build identified by DERIVATION in the
database."
(with-db-critical-section db
(let loop ((rows
(sqlite-exec db "SELECT name, path FROM Outputs
WHERE derivation =" derivation ";"))
@ -425,7 +461,7 @@ WHERE derivation =" derivation ";"))
. rest)
(loop rest
(cons `(,name . ((#:path . ,path)))
outputs))))))
outputs)))))))
(define (filters->order filters)
(match (assq 'order filters)
@ -440,10 +476,11 @@ WHERE derivation =" derivation ";"))
(('order . 'status+submission-time) "status DESC, timestamp DESC")
(_ "rowid DESC")))
(define (db-get-builds db filters)
"Retrieve all builds in database DB which are matched by given FILTERS.
(define (db-get-builds filters)
"Retrieve all builds in the database which are matched by given FILTERS.
FILTERS is an assoc list whose possible keys are 'derivation | 'id | 'jobset |
'job | 'system | 'nr | 'order | 'status | 'evaluation."
(with-db-critical-section db
(let* ((order (filters->order filters))
(stmt-text (format #f "SELECT * FROM (
SELECT Builds.derivation, Builds.rowid, Builds.timestamp, Builds.starttime,
@ -511,42 +548,47 @@ ORDER BY ~a, rowid ASC;" order))
(#:system . ,system)
(#:nix-name . ,nix-name)
(#:specification . ,specification)
(#:outputs . ,(db-get-outputs db derivation)))
builds)))))))
(#:outputs . ,(db-get-outputs derivation)))
builds))))))))
(define (db-get-build db derivation-or-id)
"Retrieve a build in database DB which corresponds to DERIVATION-OR-ID."
(define (db-get-build derivation-or-id)
"Retrieve a build in the database which corresponds to DERIVATION-OR-ID."
(with-db-critical-section db
(let ((key (if (number? derivation-or-id) 'id 'derivation)))
(match (db-get-builds db `((,key . ,derivation-or-id)))
(match (db-get-builds `((,key . ,derivation-or-id)))
((build)
build)
(() #f))))
(() #f)))))
(define (db-get-pending-derivations db)
(define (db-get-pending-derivations)
"Return the list of derivation file names corresponding to pending builds in
DB. The returned list is guaranteed to not have any duplicates."
the database. The returned list is guaranteed to not have any duplicates."
(with-db-critical-section db
(map (match-lambda (#(drv) drv))
(sqlite-exec db "
SELECT derivation FROM Builds WHERE Builds.status < 0;")))
SELECT derivation FROM Builds WHERE Builds.status < 0;"))))
(define (db-get-stamp db spec)
"Return a stamp corresponding to specification SPEC in database DB."
(define (db-get-stamp spec)
"Return a stamp corresponding to specification SPEC in the database."
(with-db-critical-section db
(let ((res (sqlite-exec db "SELECT * FROM Stamps WHERE specification="
(assq-ref spec #:name) ";")))
(match res
(() #f)
((#(spec stamp)) stamp))))
((#(spec stamp)) stamp)))))
(define (db-add-stamp db spec stamp)
"Associate STAMP to specification SPEC in database DB."
(if (db-get-stamp db spec)
(define (db-add-stamp spec stamp)
"Associate STAMP to specification SPEC in the database."
(with-db-critical-section db
(if (db-get-stamp spec)
(sqlite-exec db "UPDATE Stamps SET stamp=" stamp
"WHERE specification=" (assq-ref spec #:name) ";")
(sqlite-exec db "\
INSERT INTO Stamps (specification, stamp) VALUES ("
(assq-ref spec #:name) ", " stamp ");")))
(assq-ref spec #:name) ", " stamp ");"))))
(define (db-get-evaluations db limit)
(define (db-get-evaluations limit)
(with-db-critical-section db
(let loop ((rows (sqlite-exec db "SELECT id, specification, commits
FROM Evaluations ORDER BY id DESC LIMIT " limit ";"))
(evaluations '()))
@ -558,9 +600,10 @@ FROM Evaluations ORDER BY id DESC LIMIT " limit ";"))
(cons `((#:id . ,id)
(#:specification . ,specification)
(#:commits . ,(string-tokenize commits)))
evaluations))))))
evaluations)))))))
(define (db-get-evaluations-build-summary db spec limit border-low border-high)
(define (db-get-evaluations-build-summary spec limit border-low border-high)
(with-db-critical-section db
(let loop ((rows (sqlite-exec db "
SELECT E.id, E.commits, B.succeeded, B.failed, B.scheduled
FROM
@ -588,40 +631,44 @@ ORDER BY E.id ASC;"))
(#:succeeded . ,(or succeeded 0))
(#:failed . ,(or failed 0))
(#:scheduled . ,(or scheduled 0)))
evaluations))))))
evaluations)))))))
(define (db-get-evaluations-id-min db spec)
(define (db-get-evaluations-id-min spec)
"Return the min id of evaluations for the given specification SPEC."
(with-db-critical-section db
(let ((rows (sqlite-exec db "
SELECT MIN(id) FROM Evaluations
WHERE specification=" spec)))
(vector-ref (car rows) 0)))
(vector-ref (car rows) 0))))
(define (db-get-evaluations-id-max db spec)
(define (db-get-evaluations-id-max spec)
"Return the max id of evaluations for the given specification SPEC."
(with-db-critical-section db
(let ((rows (sqlite-exec db "
SELECT MAX(id) FROM Evaluations
WHERE specification=" spec)))
(vector-ref (car rows) 0)))
(vector-ref (car rows) 0))))
(define (db-get-builds-min db eval)
(define (db-get-builds-min eval)
"Return the min build (stoptime, id) pair for
the given evaluation EVAL."
(with-db-critical-section db
(let ((rows (sqlite-exec db "
SELECT stoptime, MIN(rowid) FROM
(SELECT rowid, stoptime FROM Builds
WHERE evaluation=" eval " AND
stoptime = (SELECT MIN(stoptime)
FROM Builds WHERE evaluation=" eval "))")))
(vector->list (car rows))))
(vector->list (car rows)))))
(define (db-get-builds-max db eval)
(define (db-get-builds-max eval)
"Return the max build (stoptime, id) pair for
the given evaluation EVAL."
(with-db-critical-section db
(let ((rows (sqlite-exec db "
SELECT stoptime, MAX(rowid) FROM
(SELECT rowid, stoptime FROM Builds
WHERE evaluation=" eval " AND
stoptime = (SELECT MAX(stoptime)
FROM Builds WHERE evaluation=" eval "))")))
(vector->list (car rows))))
(vector->list (car rows)))))

View File

@ -103,17 +103,17 @@
(#:releasename . #nil)
(#:buildinputs_builds . #nil)))
(define (handle-build-request db build-id)
"Retrieve build identified by BUILD-ID over DB and convert it
to hydra format. Return #f is not build was found."
(let ((build (db-get-build db build-id)))
(define (handle-build-request build-id)
"Retrieve build identified by BUILD-ID over the database and convert it to
hydra format. Return #f is not build was found."
(let ((build (db-get-build build-id)))
(and=> build build->hydra-build)))
(define (handle-builds-request db filters)
"Retrieve all builds matched by FILTERS in DB and convert them
to Hydra format."
(define (handle-builds-request filters)
"Retrieve all builds matched by FILTERS in the database and convert them to
Hydra format."
(let ((builds (with-time-logging "builds request"
(db-get-builds db filters))))
(db-get-builds filters))))
(map build->hydra-build builds)))
(define (request-parameters request)
@ -146,10 +146,10 @@
(define (request-path-components request)
(split-and-decode-uri-path (uri-path (request-uri request))))
(define (url-handler request body db-channel)
(define (url-handler request body)
(define* (respond response #:key body (db-channel db-channel))
(values response body db-channel))
(define* (respond response #:key body)
(values response body #f))
(define-syntax-rule (respond-json body ...)
(respond '((content-type . (application/json)))
@ -213,19 +213,14 @@
(request-path-components request)
'method-not-allowed)
(((or "jobsets" "specifications") . rest)
(respond-json (object->json-string
(with-critical-section db-channel (db)
(db-get-specifications db)))))
(respond-json (object->json-string (db-get-specifications))))
(("build" build-id)
(let ((hydra-build
(with-critical-section db-channel (db)
(handle-build-request db (string->number build-id)))))
(let ((hydra-build (handle-build-request (string->number build-id))))
(if hydra-build
(respond-json (object->json-string hydra-build))
(respond-build-not-found build-id))))
(("build" build-id "log" "raw")
(let ((build (with-critical-section db-channel (db)
(db-get-build db (string->number build-id)))))
(let ((build (db-get-build (string->number build-id))))
(if build
(match (assq-ref build #:outputs)
(((_ (#:path . (? string? output))) _ ...)
@ -250,9 +245,7 @@
;; 'nr parameter is mandatory to limit query size.
(nr (assq-ref params 'nr)))
(if nr
(respond-json (object->json-string
(with-critical-section db-channel (db)
(db-get-evaluations db nr))))
(respond-json (object->json-string (db-get-evaluations nr)))
(respond-json-with-error 500 "Parameter not defined!"))))
(("api" "latestbuilds")
(let* ((params (request-parameters request))
@ -262,10 +255,9 @@
;; Limit results to builds that are "done".
(respond-json
(object->json-string
(with-critical-section db-channel (db)
(handle-builds-request db `((status . done)
(handle-builds-request `((status . done)
,@params
(order . finish-time))))))
(order . finish-time)))))
(respond-json-with-error 500 "Parameter not defined!"))))
(("api" "queue")
(let* ((params (request-parameters request))
@ -276,41 +268,35 @@
(object->json-string
;; Use the 'status+submission-time' order so that builds in
;; 'running' state appear before builds in 'scheduled' state.
(with-critical-section db-channel (db)
(handle-builds-request db `((status . pending)
(handle-builds-request `((status . pending)
,@params
(order . status+submission-time))))))
(order . status+submission-time)))))
(respond-json-with-error 500 "Parameter not defined!"))))
('()
(respond-html (html-page
"Cuirass"
(specifications-table
(with-critical-section db-channel (db)
(db-get-specifications db))))))
(specifications-table (db-get-specifications)))))
(("jobset" name)
(respond-html
(with-critical-section db-channel (db)
(let* ((evaluation-id-max (db-get-evaluations-id-max db name))
(evaluation-id-min (db-get-evaluations-id-min db name))
(let* ((evaluation-id-max (db-get-evaluations-id-max name))
(evaluation-id-min (db-get-evaluations-id-min name))
(params (request-parameters request))
(border-high (assq-ref params 'border-high))
(border-low (assq-ref params 'border-low))
(evaluations (db-get-evaluations-build-summary db
name
(evaluations (db-get-evaluations-build-summary name
%page-size
border-low
border-high)))
(html-page name (evaluation-info-table name
evaluations
evaluation-id-min
evaluation-id-max))))))
evaluation-id-max)))))
(("eval" id)
(respond-html
(with-critical-section db-channel (db)
(let* ((builds-id-max (db-get-builds-max db id))
(builds-id-min (db-get-builds-min db id))
(let* ((builds-id-max (db-get-builds-max id))
(builds-id-min (db-get-builds-min id))
(params (request-parameters request))
(border-high-time (assq-ref params 'border-high-time))
(border-low-time (assq-ref params 'border-low-time))
@ -319,7 +305,7 @@
(html-page
"Evaluation"
(build-eval-table
(handle-builds-request db `((evaluation . ,id)
(handle-builds-request `((evaluation . ,id)
(nr . ,%page-size)
(order . finish-time+build-id)
(border-high-time . ,border-high-time)
@ -327,26 +313,20 @@
(border-high-id . ,border-high-id)
(border-low-id . ,border-low-id)))
builds-id-min
builds-id-max))))))
builds-id-max)))))
(("static" path ...)
(respond-static-file path))
('method-not-allowed
;; 405 "Method Not Allowed"
(values (build-response #:code 405) #f db-channel))
(values (build-response #:code 405) #f #f))
(_
(respond-not-found (uri->string (request-uri request))))))
(define* (run-cuirass-server db #:key (host "localhost") (port 8080))
(define* (run-cuirass-server #:key (host "localhost") (port 8080))
(let* ((host-info (gethostbyname host))
(address (inet-ntop (hostent:addrtype host-info)
(car (hostent:addr-list host-info))))
;; Spawn a fiber to process database queries sequentially. We need
;; this because guile-sqlite3 handles are not thread-safe (caching in
;; particular), and creating one new handle for each request would be
;; costly and may defeat statement caching.
(db-channel (make-critical-section db)))
(car (hostent:addr-list host-info)))))
(log-message "listening on ~A:~A" address port)
;; Here we use our own web backend, call 'fiberized'. We cannot use the
@ -371,7 +351,7 @@
(spawn-fiber
(lambda ()
(let-values (((response body state)
(handle-request (cut url-handler <> <> db-channel)
(handle-request (cut url-handler <> <>)
request body '())))
(write-client impl server client response body)))))
(loop)))))

View File

@ -103,9 +103,10 @@ then be passed to 'join-critical-section', which will ensure sequential
ordering. ARGS are the arguments of the critical section.
Critical sections are implemented by passing the procedure to execute to a
dedicated fiber."
dedicated thread."
(parameterize (((@@ (fibers internal) current-fiber) #f))
(let ((channel (make-channel)))
(spawn-fiber
(call-with-new-thread
(lambda ()
(parameterize ((%critical-section-args args))
(let loop ()
@ -113,7 +114,7 @@ dedicated fiber."
(((? channel? reply) . (? procedure? proc))
(put-message reply (apply proc args))))
(loop)))))
channel))
channel)))
(define (call-with-critical-section channel proc)
"Send PROC to the critical section through CHANNEL. Return the result of

View File

@ -21,6 +21,7 @@
(use-modules (cuirass database)
((guix utils) #:select (call-with-temporary-output-file))
(cuirass utils)
(srfi srfi-64))
(define example-spec
@ -61,12 +62,12 @@
(#:log . "log")
(#:outputs . (("foo" . "/foo")))))
(define-syntax-rule (with-temporary-database db body ...)
(define-syntax-rule (with-temporary-database body ...)
(call-with-temporary-output-file
(lambda (file port)
(parameterize ((%package-database file))
(db-init file)
(with-database db
(with-database
body ...)))))
(define %db
@ -79,7 +80,10 @@
(test-group-with-cleanup "database"
(test-assert "db-init"
(%db (db-init database-name)))
(begin
(%db (db-init database-name))
(%db-channel (make-critical-section (%db)))
#t))
(test-assert "sqlite-exec"
(begin
@ -94,41 +98,40 @@ INSERT INTO Evaluations (specification, commits) VALUES (3, 3);")
(test-equal "db-add-specification"
example-spec
(begin
(db-add-specification (%db) example-spec)
(car (db-get-specifications (%db)))))
(db-add-specification example-spec)
(car (db-get-specifications))))
(test-equal "db-add-build"
#f
(let ((build (make-dummy-build "/foo.drv")))
(db-add-build (%db) build)
(db-add-build build)
;; Should return #f when adding a build whose derivation is already
;; there, see <https://bugs.gnu.org/28094>.
(db-add-build (%db) build)))
(db-add-build build)))
(test-equal "db-update-build-status!"
(list (build-status scheduled)
(build-status started)
(build-status succeeded)
"/foo.drv.log")
(with-temporary-database db
(with-temporary-database
(let* ((derivation (db-add-build
db
(make-dummy-build "/foo.drv" 1
#:outputs '(("out" . "/foo")))))
(get-status (lambda* (#:optional (key #:status))
(assq-ref (db-get-build db derivation) key))))
(db-add-evaluation db (make-dummy-eval))
(db-add-specification db example-spec)
(assq-ref (db-get-build derivation) key))))
(db-add-evaluation (make-dummy-eval))
(db-add-specification example-spec)
(let ((status0 (get-status)))
(db-update-build-status! db "/foo.drv" (build-status started))
(db-update-build-status! "/foo.drv" (build-status started))
(let ((status1 (get-status)))
(db-update-build-status! db "/foo.drv" (build-status succeeded)
(db-update-build-status! "/foo.drv" (build-status succeeded)
#:log-file "/foo.drv.log")
;; Second call shouldn't make any difference.
(db-update-build-status! db "/foo.drv" (build-status succeeded)
(db-update-build-status! "/foo.drv" (build-status succeeded)
#:log-file "/foo.drv.log")
(let ((status2 (get-status))
@ -144,61 +147,61 @@ INSERT INTO Evaluations (specification, commits) VALUES (3, 3);")
((3 "/baz.drv") (2 "/bar.drv") (1 "/foo.drv")) ;ditto
((3 "/baz.drv")) ;nr = 1
((2 "/bar.drv") (1 "/foo.drv") (3 "/baz.drv"))) ;status+submission-time
(with-temporary-database db
(with-temporary-database
;; Populate the 'Builds'', 'Evaluations', and
;; 'Specifications' tables in a consistent way, as expected by the
;; 'db-get-builds' query.
(db-add-build db (make-dummy-build "/foo.drv" 1
(db-add-build (make-dummy-build "/foo.drv" 1
#:outputs `(("out" . "/foo"))))
(db-add-build db (make-dummy-build "/bar.drv" 2
(db-add-build (make-dummy-build "/bar.drv" 2
#:outputs `(("out" . "/bar"))))
(db-add-build db (make-dummy-build "/baz.drv" 3
(db-add-build (make-dummy-build "/baz.drv" 3
#:outputs `(("out" . "/baz"))))
(db-add-evaluation db (make-dummy-eval))
(db-add-evaluation db (make-dummy-eval))
(db-add-evaluation db (make-dummy-eval))
(db-add-specification db example-spec)
(db-add-evaluation (make-dummy-eval))
(db-add-evaluation (make-dummy-eval))
(db-add-evaluation (make-dummy-eval))
(db-add-specification example-spec)
(db-update-build-status! db "/bar.drv" (build-status started)
(db-update-build-status! "/bar.drv" (build-status started)
#:log-file "/bar.drv.log")
(let ((summarize (lambda (alist)
(list (assq-ref alist #:id)
(assq-ref alist #:derivation)))))
(vector (map summarize (db-get-builds db '((nr . 3)
(order . build-id))))
(map summarize (db-get-builds db '()))
(map summarize (db-get-builds db '((jobset . "guix"))))
(map summarize (db-get-builds db '((nr . 1))))
(vector (map summarize (db-get-builds '((nr . 3) (order . build-id))))
(map summarize (db-get-builds '()))
(map summarize (db-get-builds '((jobset . "guix"))))
(map summarize (db-get-builds '((nr . 1))))
(map summarize
(db-get-builds
db '((order . status+submission-time))))))))
(db-get-builds '((order . status+submission-time))))))))
(test-equal "db-get-pending-derivations"
'("/bar.drv" "/foo.drv")
(with-temporary-database db
(with-temporary-database
;; Populate the 'Builds', 'Evaluations', and
;; 'Specifications' tables. Here, two builds map to the same derivation
;; but the result of 'db-get-pending-derivations' must not contain any
;; duplicate.
(db-add-build db (make-dummy-build "/foo.drv" 1
(db-add-build (make-dummy-build "/foo.drv" 1
#:outputs `(("out" . "/foo"))))
(db-add-build db (make-dummy-build "/bar.drv" 2
(db-add-build (make-dummy-build "/bar.drv" 2
#:outputs `(("out" . "/bar"))))
(db-add-build db (make-dummy-build "/foo.drv" 3
(db-add-build (make-dummy-build "/foo.drv" 3
#:outputs `(("out" . "/foo"))))
(db-add-evaluation db (make-dummy-eval))
(db-add-evaluation db (make-dummy-eval))
(db-add-evaluation db (make-dummy-eval))
(db-add-specification db example-spec)
(db-add-evaluation (make-dummy-eval))
(db-add-evaluation (make-dummy-eval))
(db-add-evaluation (make-dummy-eval))
(db-add-specification example-spec)
(sort (db-get-pending-derivations db) string<?)))
(sort (db-get-pending-derivations) string<?)))
(test-assert "db-close"
(db-close (%db)))
(delete-file database-name))
(begin
(%db-channel #f)
(delete-file database-name)))
;;; Local Variables:
;;; eval: (put 'with-temporary-database 'scheme-indent-function 1)
;;; eval: (put 'with-temporary-database 'scheme-indent-function 0)
;;; End:

View File

@ -125,14 +125,17 @@
json->scm)))
(test-assert "db-init"
(%db (db-init database-name)))
(begin
(%db (db-init database-name))
(%db-channel (make-critical-section (%db)))
#t))
(test-assert "cuirass-run"
(call-with-new-thread
(lambda ()
(run-fibers
(lambda ()
(run-cuirass-server (%db) #:port 6688))
(run-cuirass-server #:port 6688))
#:drain? #t))))
(test-assert "wait-server"
@ -184,11 +187,11 @@
(evaluation2
'((#:specification . "guix")
(#:commits . ("fakesha2" "fakesha3")))))
(db-add-build (%db) build1)
(db-add-build (%db) build2)
(db-add-specification (%db) specification)
(db-add-evaluation (%db) evaluation1)
(db-add-evaluation (%db) evaluation2)))
(db-add-build build1)
(db-add-build build2)
(db-add-specification specification)
(db-add-evaluation evaluation1)
(db-add-evaluation evaluation2)))
(test-assert "/build/1"
(hash-table=?
@ -275,4 +278,6 @@
(test-assert "db-close"
(db-close (%db)))
(delete-file database-name))
(begin
(%db-channel #f)
(delete-file database-name)))