Handle migrations and server startup better

The server part of the guix-data-service doesn't work great as a guix service,
since it often fails to start if the migrations take any time at all.

To address this, start the server before running the migrations, and serve the
pages that work without the database, plus a general 503 response. Once the
migrations have completed, switch to the normal behaviour.
This commit is contained in:
Christopher Baines 2022-06-17 12:55:05 +01:00
parent d19eb07138
commit 8e23d38660
4 changed files with 249 additions and 194 deletions

View File

@ -71,6 +71,7 @@
#:use-module (guix-data-service web repository controller) #:use-module (guix-data-service web repository controller)
#:use-module (guix-data-service web package controller) #:use-module (guix-data-service web package controller)
#:export (%show-error-details #:export (%show-error-details
handle-static-assets
controller)) controller))
(define cache-control-default-max-age (define cache-control-default-max-age
@ -513,16 +514,26 @@
(define* (controller request method-and-path-components (define* (controller request method-and-path-components
mime-types body mime-types body
secret-key-base) secret-key-base
(define (controller-thunk) startup-completed?)
(define (running-controller-thunk)
(actual-controller request (actual-controller request
method-and-path-components method-and-path-components
mime-types mime-types
body body
secret-key-base)) secret-key-base))
(define (startup-controller-thunk)
(or
(base-controller request method-and-path-components)
(render-html
#:sxml (server-starting-up-page)
#:code 503)))
(call-with-error-handling (call-with-error-handling
controller-thunk (if startup-completed?
running-controller-thunk
startup-controller-thunk)
#:on-error 'backtrace #:on-error 'backtrace
#:post-error (lambda args #:post-error (lambda args
(render-html #:sxml (error-page (render-html #:sxml (error-page
@ -531,51 +542,8 @@
#f)) #f))
#:code 500)))) #:code 500))))
(define (actual-controller request (define (base-controller request method-and-path-components)
method-and-path-components
mime-types
body
secret-key-base)
(define path
(uri-path (request-uri request)))
(define (delegate-to f)
(or (f request
method-and-path-components
mime-types
body)
(render-html
#:sxml (general-not-found
"Page not found"
"")
#:code 404)))
(define (delegate-to-with-secret-key-base f)
(or (f request
method-and-path-components
mime-types
body
secret-key-base)
(render-html
#:sxml (general-not-found
"Page not found"
"")
#:code 404)))
(match method-and-path-components (match method-and-path-components
(('GET)
(render-html
#:sxml (index
(parallel-via-thread-pool-channel
(with-thread-postgresql-connection
(lambda (conn)
(map
(lambda (git-repository-details)
(cons
git-repository-details
(all-branches-with-most-recent-commit
conn (first git-repository-details))))
(all-git-repositories conn))))))))
(('GET "assets" rest ...) (('GET "assets" rest ...)
(or (handle-static-assets (string-join rest "/") (or (handle-static-assets (string-join rest "/")
(request-headers request)) (request-headers request))
@ -610,75 +578,124 @@
"README not found" "README not found"
"The README.html file does not exist") "The README.html file does not exist")
#:code 404)))) #:code 404))))
(('GET "builds") ((method path ...) #f)))
(delegate-to build-controller))
(('GET "statistics")
(letpar& ((guix-revisions-count
(with-thread-postgresql-connection count-guix-revisions))
(count-derivations
(with-thread-postgresql-connection count-derivations)))
(render-html (define (actual-controller request
#:sxml (view-statistics guix-revisions-count method-and-path-components
count-derivations)))) mime-types
(('GET "metrics") body
(render-metrics)) secret-key-base)
(('GET "revision" args ...) (define path
(delegate-to revision-controller)) (uri-path (request-uri request)))
(('GET "repositories")
(delegate-to repository-controller)) (define (delegate-to f)
(('GET "repository" _ ...) (or (f request
(delegate-to repository-controller)) method-and-path-components
(('GET "package" _ ...) mime-types
(delegate-to package-controller)) body)
(('GET "gnu" "store" filename) (render-html
;; These routes are a little special, as the extensions aren't used for #:sxml (general-not-found
;; content negotiation, so just use the path from the request "Page not found"
(let ((path (uri-path (request-uri request)))) "")
(if (string-suffix? ".drv" path) #:code 404)))
(render-derivation (uri-decode path))
(render-store-item (uri-decode path))))) (define (delegate-to-with-secret-key-base f)
(('GET "gnu" "store" filename "formatted") (or (f request
(if (string-suffix? ".drv" filename) method-and-path-components
(render-formatted-derivation (string-append "/gnu/store/" filename)) mime-types
(render-html body
#:sxml (general-not-found secret-key-base)
"Not a derivation" (render-html
"The formatted display is only for derivations, where the filename ends in .drv") #:sxml (general-not-found
#:code 404))) "Page not found"
(('GET "gnu" "store" filename "plain") "")
(if (string-suffix? ".drv" filename) #:code 404)))
(let ((raw-drv
(parallel-via-thread-pool-channel (or
(with-thread-postgresql-connection (base-controller request method-and-path-components)
(lambda (conn) (match method-and-path-components
(select-serialized-derivation-by-file-name (('GET)
conn (render-html
(string-append "/gnu/store/" filename))))))) #:sxml (index
(if raw-drv (parallel-via-thread-pool-channel
(render-text raw-drv) (with-thread-postgresql-connection
(not-found (request-uri request)))) (lambda (conn)
(not-found (request-uri request)))) (map
(('GET "gnu" "store" filename "narinfos") (lambda (git-repository-details)
(render-narinfos filename)) (cons
(('GET "gnu" "store" filename "json") git-repository-details
(if (string-suffix? ".drv" filename) (all-branches-with-most-recent-commit
(render-json-derivation (string-append "/gnu/store/" filename)) conn (first git-repository-details))))
(render-json-store-item (string-append "/gnu/store/" filename)))) (all-git-repositories conn))))))))
(('GET "build-servers") (('GET "builds")
(delegate-to-with-secret-key-base build-server-controller)) (delegate-to build-controller))
(('GET "dumps" _ ...) (('GET "statistics")
(delegate-to dumps-controller)) (letpar& ((guix-revisions-count
(((or 'GET 'POST) "build-server" _ ...) (with-thread-postgresql-connection count-guix-revisions))
(delegate-to-with-secret-key-base build-server-controller)) (count-derivations
(('GET "compare" _ ...) (delegate-to compare-controller)) (with-thread-postgresql-connection count-derivations)))
(('GET "compare-by-datetime" _ ...) (delegate-to compare-controller))
(('GET "jobs" _ ...) (delegate-to jobs-controller)) (render-html
(('GET "job" job-id) (delegate-to jobs-controller)) #:sxml (view-statistics guix-revisions-count
(('GET _ ...) (delegate-to nar-controller)) count-derivations))))
((method path ...) (('GET "metrics")
(render-html (render-metrics))
#:sxml (general-not-found (('GET "revision" args ...)
"Page not found" (delegate-to revision-controller))
"") (('GET "repositories")
#:code 404)))) (delegate-to repository-controller))
(('GET "repository" _ ...)
(delegate-to repository-controller))
(('GET "package" _ ...)
(delegate-to package-controller))
(('GET "gnu" "store" filename)
;; These routes are a little special, as the extensions aren't used for
;; content negotiation, so just use the path from the request
(let ((path (uri-path (request-uri request))))
(if (string-suffix? ".drv" path)
(render-derivation (uri-decode path))
(render-store-item (uri-decode path)))))
(('GET "gnu" "store" filename "formatted")
(if (string-suffix? ".drv" filename)
(render-formatted-derivation (string-append "/gnu/store/" filename))
(render-html
#:sxml (general-not-found
"Not a derivation"
"The formatted display is only for derivations, where the filename ends in .drv")
#:code 404)))
(('GET "gnu" "store" filename "plain")
(if (string-suffix? ".drv" filename)
(let ((raw-drv
(parallel-via-thread-pool-channel
(with-thread-postgresql-connection
(lambda (conn)
(select-serialized-derivation-by-file-name
conn
(string-append "/gnu/store/" filename)))))))
(if raw-drv
(render-text raw-drv)
(not-found (request-uri request))))
(not-found (request-uri request))))
(('GET "gnu" "store" filename "narinfos")
(render-narinfos filename))
(('GET "gnu" "store" filename "json")
(if (string-suffix? ".drv" filename)
(render-json-derivation (string-append "/gnu/store/" filename))
(render-json-store-item (string-append "/gnu/store/" filename))))
(('GET "build-servers")
(delegate-to-with-secret-key-base build-server-controller))
(('GET "dumps" _ ...)
(delegate-to dumps-controller))
(((or 'GET 'POST) "build-server" _ ...)
(delegate-to-with-secret-key-base build-server-controller))
(('GET "compare" _ ...) (delegate-to compare-controller))
(('GET "compare-by-datetime" _ ...) (delegate-to compare-controller))
(('GET "jobs" _ ...) (delegate-to jobs-controller))
(('GET "job" job-id) (delegate-to jobs-controller))
(('GET _ ...) (delegate-to nar-controller))
((method path ...)
(render-html
#:sxml (general-not-found
"Page not found"
"")
#:code 404)))))

View File

@ -24,12 +24,22 @@
#:use-module (web request) #:use-module (web request)
#:use-module (web uri) #:use-module (web uri)
#:use-module (system repl error-handling) #:use-module (system repl error-handling)
#:use-module (ice-9 atomic)
#:use-module (fibers web server) #:use-module (fibers web server)
#:use-module (guix-data-service web controller) #:use-module (guix-data-service web controller)
#:use-module (guix-data-service web util) #:use-module (guix-data-service web util)
#:export (start-guix-data-service-web-server)) #:export (start-guix-data-service-web-server))
(define (handler request body controller secret-key-base) (define (check-startup-completed startup-completed)
(if (atomic-box-ref startup-completed)
(begin
;; Just in case this atomic-box-ref is expensive, only do it when
;; necessary
(set! check-startup-completed (const #t))
#t)
#f))
(define (handler request body controller secret-key-base startup-completed)
(display (display
(format #f "~a ~a\n" (format #f "~a ~a\n"
(request-method request) (request-method request)
@ -42,14 +52,17 @@
request-components) request-components)
mime-types mime-types
body body
secret-key-base)))) secret-key-base
(check-startup-completed startup-completed)))))
(define* (start-guix-data-service-web-server port host secret-key-base) (define* (start-guix-data-service-web-server port host secret-key-base
startup-completed)
(call-with-error-handling (call-with-error-handling
(lambda () (lambda ()
(run-server (lambda (request body) (run-server (lambda (request body)
(handler request body controller (handler request body controller
secret-key-base)) secret-key-base
startup-completed))
#:host host #:host host
#:port port)) #:port port))
#:on-error 'backtrace #:on-error 'backtrace

View File

@ -50,7 +50,8 @@
view-narinfos view-narinfos
view-store-item view-store-item
view-derivation-source-file view-derivation-source-file
error-page)) error-page
server-starting-up-page))
(define* (header) (define* (header)
`(nav `(nav
@ -1004,3 +1005,11 @@
`((b ,key) `((b ,key)
(pre ,args)))) (pre ,args))))
'()))))) '())))))
(define* (server-starting-up-page)
(layout
#:body
`(,(header)
(div (@ (class "container"))
(h1 "Server is starting up")
(p "Database migrations are running, this can take some time.")))))

View File

@ -26,6 +26,8 @@
(use-modules (srfi srfi-1) (use-modules (srfi srfi-1)
(srfi srfi-37) (srfi srfi-37)
(ice-9 match) (ice-9 match)
(ice-9 atomic)
(ice-9 threads)
(ice-9 textual-ports) (ice-9 textual-ports)
(system repl server) (system repl server)
(system repl repl) (system repl repl)
@ -137,63 +139,6 @@
(when repl-port (when repl-port
(spawn-server (make-tcp-server-socket #:port repl-port)))) (spawn-server (make-tcp-server-socket #:port repl-port))))
(when (assoc-ref opts 'update-database)
(let ((command
(list (%config 'sqitch)
"deploy"
"--db-client" (%config 'sqitch-psql)
;; For some reason, sqitch behaves differently when the
;; guix-data-service is packaged, and when it's not, so try
;; and hack around this here.
"--chdir" (let ((base (dirname (%config 'sqitch-plan))))
(if (string-prefix? "/gnu" (%config 'sqitch-plan))
base
(dirname base)))
"--plan-file" (%config 'sqitch-plan)
"--mode" "change" ; this helps when migrations don't
; have the revert bit implemented
(let* ((database-config (get-database-config))
(params (string-join
(map
(match-lambda
((key . val)
(string-append key "=" val)))
(filter
(match-lambda
((key . _)
(not (member key '("user"
"host"
"dbname")))))
database-config))
"&")))
(string-append "db:pg://"
(assoc-ref database-config "user")
"@"
(if (string=? (assoc-ref database-config "host")
"localhost")
"" ; This means the unix socket
; connection will be used
(assoc-ref database-config "host"))
"/"
(assoc-ref database-config "dbname")
(if (string-null? params)
""
"?")
params)))))
(simple-format #t "running command: ~A\n"
(string-join command))
(unless (zero? (apply system* command))
(simple-format
(current-error-port)
"error: sqitch command failed\n")
(exit 1))))
(let ((pid-file (assq-ref opts 'pid-file)))
(when pid-file
(call-with-output-file pid-file
(lambda (port)
(simple-format port "~A\n" (getpid))))))
(parameterize ((%narinfo-signing-public-key (parameterize ((%narinfo-signing-public-key
(catch (catch
'system-error 'system-error
@ -228,20 +173,91 @@
(%show-error-details (%show-error-details
(assoc-ref opts 'show-error-details))) (assoc-ref opts 'show-error-details)))
(start-substitute-query-thread) (let* ((startup-completed
(make-atomic-box
(if (assoc-ref opts 'update-database)
#f
#t)))
(server-thread
(call-with-new-thread
(lambda ()
(with-postgresql-connection-per-thread
"web"
(lambda ()
;; Provide some visual space between the startup output and the server
;; starting
(simple-format #t "\n\nStarting the server on http://~A:~A/\n\n"
(assq-ref opts 'host)
(assq-ref opts 'port))
;; Provide some visual space between the startup output and the server (start-guix-data-service-web-server
;; starting (assq-ref opts 'port)
(simple-format #t "\n\nStarting the server on http://~A:~A/\n\n"
(assq-ref opts 'host) (assq-ref opts 'host)
(assq-ref opts 'port)) (assq-ref opts 'secret-key-base)
startup-completed))
#:statement-timeout
(assq-ref opts 'postgresql-statement-timeout)))))
(with-postgresql-connection-per-thread (pid-file (assq-ref opts 'pid-file)))
"web"
(lambda () (when pid-file
(start-guix-data-service-web-server (call-with-output-file pid-file
(assq-ref opts 'port) (lambda (port)
(assq-ref opts 'host) (simple-format port "~A\n" (getpid)))))
(assq-ref opts 'secret-key-base)))
#:statement-timeout (when (assoc-ref opts 'update-database)
(assq-ref opts 'postgresql-statement-timeout)))) (let ((command
(list (%config 'sqitch)
"deploy"
"--db-client" (%config 'sqitch-psql)
;; For some reason, sqitch behaves differently when the
;; guix-data-service is packaged, and when it's not, so try
;; and hack around this here.
"--chdir" (let ((base (dirname (%config 'sqitch-plan))))
(if (string-prefix? "/gnu" (%config 'sqitch-plan))
base
(dirname base)))
"--plan-file" (%config 'sqitch-plan)
"--mode" "change" ; this helps when migrations don't
; have the revert bit implemented
(let* ((database-config (get-database-config))
(params (string-join
(map
(match-lambda
((key . val)
(string-append key "=" val)))
(filter
(match-lambda
((key . _)
(not (member key '("user"
"host"
"dbname")))))
database-config))
"&")))
(string-append "db:pg://"
(assoc-ref database-config "user")
"@"
(if (string=? (assoc-ref database-config "host")
"localhost")
"" ; This means the unix socket
; connection will be used
(assoc-ref database-config "host"))
"/"
(assoc-ref database-config "dbname")
(if (string-null? params)
""
"?")
params)))))
(simple-format #t "running command: ~A\n"
(string-join command))
(unless (zero? (apply system* command))
(simple-format
(current-error-port)
"error: sqitch command failed\n")
(exit 1))
(atomic-box-set! startup-completed #t)))
(start-substitute-query-thread)
(join-thread server-thread))))