From debbugs-submit-bounces@debbugs.gnu.org Wed Dec 02 06:05:03 2020 Received: (at submit) by debbugs.gnu.org; 2 Dec 2020 11:05:03 +0000 Received: from localhost ([127.0.0.1]:34503 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1kkPwV-0001pW-Om for submit@debbugs.gnu.org; Wed, 02 Dec 2020 06:05:03 -0500 Received: from lists.gnu.org ([209.51.188.17]:53274) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1kkPwU-0001pE-CR for submit@debbugs.gnu.org; Wed, 02 Dec 2020 06:05:02 -0500 Received: from eggs.gnu.org ([2001:470:142:3::10]:37864) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1kkPwR-0003yn-Rx for guix-patches@gnu.org; Wed, 02 Dec 2020 06:05:02 -0500 Received: from fencepost.gnu.org ([2001:470:142:3::e]:47393) by eggs.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1kkPwR-0003NP-JO for guix-patches@gnu.org; Wed, 02 Dec 2020 06:04:59 -0500 Received: from [2a01:e0a:19b:d9a0:35e9:f6a0:9d50:eebf] (port=50492 helo=cervin) by fencepost.gnu.org with esmtpsa (TLS1.2:RSA_AES_256_CBC_SHA1:256) (Exim 4.82) (envelope-from ) id 1kkPwQ-00052l-7h for guix-patches@gnu.org; Wed, 02 Dec 2020 06:04:59 -0500 From: Mathieu Othacehe To: guix-patches@gnu.org Subject: cuirass: Add remote build support. Date: Wed, 02 Dec 2020 12:04:56 +0100 Message-ID: <87czzso4dj.fsf@gnu.org> User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/27.1 (gnu/linux) MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" X-Debbugs-Envelope-To: submit X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" --=-=-= Content-Type: text/plain Hello, Here's a patch adding remote build support to Cuirass, as presented during Guix Days[1]. The concept is the following: * Cuirass talks to a "remote server" instead of the "guix-daemon" to build derivations when the "--build-remote" option is passed. * The "remote server" is advertised using Avahi. It queues the received build requests. It also starts a "publish" server. * The "remote workers" discover the "remote server" using Avahi, connect to it and request some builds. The "remote server" publish server is added to the workers "guix-daemon" substitute urls list. * On build completion, the "remote server" downloads the build outputs as nar and narinfo files from the worker "publish" server and store them in a cache directory. It can also add them to the store if the "--add-to-store" option is passed. * Cuirass is notified by the "remote server" when a build starts, fails or completes and can update its database accordingly. * The communication between Cuirass, the "remote server" and the "remote workers" is done by sending SEXP over ZMQ. This is still a bit rough on the edges, but I have tested it on berlin spawning ~30 workers and building ~10K derivations, it seems to work fine. The corresponding patch and an architecture overview diagram are attached. Thanks, Mathieu [1]: https://xana.lepiller.eu/guix-days-2020/guix-days-2020-mathieu-otacehe-fixing-the-ci.mp4 --=-=-= Content-Type: text/x-diff; charset=utf-8 Content-Disposition: inline; filename=0001-Add-remote-build-support.patch Content-Transfer-Encoding: quoted-printable >From 94898f67e1dca6152c434ff50e860691ce813018 Mon Sep 17 00:00:00 2001 From: Mathieu Othacehe Date: Wed, 2 Dec 2020 11:13:33 +0100 Subject: [PATCH] Add remote build support. * src/cuirass/remote.scm: New file. * src/cuirass/remote-server.scm: New file. * src/cuirass/remote-worker.scm: New file. * bin/remote-server.in: New file. * bin/remote-worker.in: New file. * Makefile.am (bin_SCRIPTS): Add new binaries, (dist_pkgmodule_DATA): add new files, (EXTRA_DIST): add new binaries, (bin/remote-server, bin/remote-worker): new targets. * .gitignore: Add new binaries. * bin/cuirass.in (%options): Add "--build-remote" option, (show-help): document it, (main): honor it. * src/cuirass/base.scm (with-build-offload-thread): New macro, (%build-remote?, %build-offload-channel): new parameters, (make-build-offload-thread): new procedure, (build-derivations/offload): new procedure, (restart-builds): use it to offload builds when %build-remote? is set, (build-packages): ditto. --- .gitignore | 2 + Makefile.am | 16 +- bin/cuirass.in | 162 ++++++----- bin/remote-server.in | 29 ++ bin/remote-worker.in | 29 ++ src/cuirass/base.scm | 65 ++++- src/cuirass/remote-server.scm | 518 ++++++++++++++++++++++++++++++++++ src/cuirass/remote-worker.scm | 286 +++++++++++++++++++ src/cuirass/remote.scm | 292 +++++++++++++++++++ 9 files changed, 1318 insertions(+), 81 deletions(-) create mode 100644 bin/remote-server.in create mode 100644 bin/remote-worker.in create mode 100644 src/cuirass/remote-server.scm create mode 100644 src/cuirass/remote-worker.scm create mode 100644 src/cuirass/remote.scm diff --git a/.gitignore b/.gitignore index beabf29..7cd0e1f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ /bin/cuirass /bin/cuirass-send-events /bin/evaluate +/bin/remote-server +/bin/remote-worker /build-aux/config.guess /build-aux/config.sub /build-aux/install-sh diff --git a/Makefile.am b/Makefile.am index 17a73f0..270c0ed 100644 --- a/Makefile.am +++ b/Makefile.am @@ -22,7 +22,13 @@ # You should have received a copy of the GNU General Public License # along with Cuirass. If not, see . =20 -bin_SCRIPTS =3D bin/cuirass bin/cuirass-send-events bin/evaluate +bin_SCRIPTS =3D \ + bin/cuirass \ + bin/cuirass-send-events \ + bin/evaluate \ + bin/remote-server \ + bin/remote-worker + noinst_SCRIPTS =3D pre-inst-env =20 guilesitedir =3D $(datarootdir)/guile/site/@GUILE_EFFECTIVE_VERSION@ @@ -48,6 +54,9 @@ dist_pkgmodule_DATA =3D \ src/cuirass/http.scm \ src/cuirass/logging.scm \ src/cuirass/metrics.scm \ + src/cuirass/remote.scm \ + src/cuirass/remote-server.scm \ + src/cuirass/remote-worker.scm \ src/cuirass/send-events.scm \ src/cuirass/ui.scm \ src/cuirass/utils.scm \ @@ -166,6 +175,8 @@ EXTRA_DIST =3D \ bin/cuirass.in \ bin/cuirass-send-events.in \ bin/evaluate.in \ + bin/remote-server.in \ + bin/remote-worker.in \ bootstrap \ build-aux/guix.scm \ src/cuirass/config.scm.in \ @@ -226,6 +237,9 @@ generate_file =3D \ bin/cuirass: $(srcdir)/bin/cuirass.in bin/cuirass-send-events: $(srcdir)/bin/cuirass-send-events.in bin/evaluate: $(srcdir)/bin/evaluate.in +bin/remote-server: $(srcdir)/bin/remote-server.in +bin/remote-worker: $(srcdir)/bin/remote-worker.in + $(bin_SCRIPTS): Makefile $(generate_file); chmod +x $@ src/cuirass/config.scm: $(srcdir)/src/cuirass/config.scm.in Makefile diff --git a/bin/cuirass.in b/bin/cuirass.in index aef4a65..ac9811c 100644 --- a/bin/cuirass.in +++ b/bin/cuirass.in @@ -57,6 +57,7 @@ exec ${GUILE:-@GUILE@} --no-auto-compile -e main -s "$0" = "$@" --listen=3DHOST Listen on the network interface for HOST -I, --interval=3DN Wait N seconds between each poll --log-queries=3DFILE Log SQL queries in FILE. + --build-remote Use the remote build mechanism --use-substitutes Allow usage of pre-built substitutes --record-events Record events for distribution --threads=3DN Use up to N kernel threads @@ -74,6 +75,7 @@ exec ${GUILE:-@GUILE@} --no-auto-compile -e main -s "$0" = "$@" (port (single-char #\p) (value #t)) (listen (value #t)) (interval (single-char #\I) (value #t)) + (build-remote (value #f)) (use-substitutes (value #f)) (threads (value #t)) (fallback (value #f)) @@ -100,6 +102,7 @@ exec ${GUILE:-@GUILE@} --no-auto-compile -e main -s "$0= " "$@" (%package-database (option-ref opts 'database (%package-database)= )) (%package-cachedir (option-ref opts 'cache-directory (%package-cachedir))) + (%build-remote? (option-ref opts 'build-remote #f)) (%use-substitutes? (option-ref opts 'use-substitutes #f)) (%fallback? (option-ref opts 'fallback #f)) (%record-events? (option-ref opts 'record-events #f)) @@ -141,84 +144,87 @@ exec ${GUILE:-@GUILE@} --no-auto-compile -e main -s "= $0" "$@" (lambda () (with-database (with-queue-writer-worker - (and specfile - (let ((new-specs (save-module-excursion - (lambda () - (set-current-module (make-user-mo= dule '())) - (primitive-load specfile))))) - (for-each db-add-specification new-specs))) - - (when queries-file - (log-message "Enable SQL query logging.") - (db-log-queries queries-file)) - - (if one-shot? - (process-specs (db-get-specifications)) - (let ((exit-channel (make-channel))) - (start-watchdog) - (if (option-ref opts 'web #f) - (begin - (spawn-fiber - (essential-task - 'web exit-channel - (lambda () - (run-cuirass-server #:host host #:port por= t))) - #:parallel? #t) - - (spawn-fiber - (essential-task - 'monitor exit-channel - (lambda () - (while #t - (log-monitoring-stats) - (sleep 600)))))) - - (begin - (clear-build-queue) - - ;; If Cuirass was stopped during an evaluation, - ;; abort it. Builds that were not registered - ;; during this evaluation will be registered - ;; during the next evaluation. - (db-abort-pending-evaluations) - - ;; First off, restart builds that had not - ;; completed or were not even started on a - ;; previous run. - (spawn-fiber - (essential-task - 'restart-builds exit-channel - (lambda () - (restart-builds)))) - - (spawn-fiber - (essential-task - 'build exit-channel - (lambda () - (while #t - (process-specs (db-get-specifications)) - (log-message - "next evaluation in ~a seconds" interva= l) - (sleep interval))))) - - (spawn-fiber - (essential-task - 'metrics exit-channel - (lambda () - (while #t - (with-time-logging - "Metrics update" - (db-update-metrics)) - (sleep 3600))))) - - (spawn-fiber - (essential-task - 'monitor exit-channel - (lambda () - (while #t - (log-monitoring-stats) - (sleep 600))))))) - (primitive-exit (get-message exit-channel))))))) + (with-build-offload-thread + (and specfile + (let ((new-specs (save-module-excursion + (lambda () + (set-current-module + (make-user-module '())) + (primitive-load specfile))))) + (for-each db-add-specification new-specs))) + + (when queries-file + (log-message "Enable SQL query logging.") + (db-log-queries queries-file)) + + (if one-shot? + (process-specs (db-get-specifications)) + (let ((exit-channel (make-channel))) + (start-watchdog) + (if (option-ref opts 'web #f) + (begin + (spawn-fiber + (essential-task + 'web exit-channel + (lambda () + (run-cuirass-server #:host host + #:port port))) + #:parallel? #t) + + (spawn-fiber + (essential-task + 'monitor exit-channel + (lambda () + (while #t + (log-monitoring-stats) + (sleep 600)))))) + + (begin + (clear-build-queue) + + ;; If Cuirass was stopped during an evaluati= on, + ;; abort it. Builds that were not registered + ;; during this evaluation will be registered + ;; during the next evaluation. + (db-abort-pending-evaluations) + + ;; First off, restart builds that had not + ;; completed or were not even started on a + ;; previous run. + (spawn-fiber + (essential-task + 'restart-builds exit-channel + (lambda () + (restart-builds)))) + + (spawn-fiber + (essential-task + 'build exit-channel + (lambda () + (while #t + (process-specs (db-get-specifications)) + (log-message + "next evaluation in ~a seconds" inter= val) + (sleep interval))))) + + (spawn-fiber + (essential-task + 'metrics exit-channel + (lambda () + (while #t + (with-time-logging + "Metrics update" + (db-update-metrics)) + (sleep 3600))))) + + (spawn-fiber + (essential-task + 'monitor exit-channel + (lambda () + (while #t + (log-monitoring-stats) + (sleep 600))))))) + (primitive-exit (get-message exit-channel)))))))) =20 ;; Most of our code is I/O so preemption doesn't matter much (it ;; could help while we're doing SQL requests, for instance, but= it diff --git a/bin/remote-server.in b/bin/remote-server.in new file mode 100644 index 0000000..6425d51 --- /dev/null +++ b/bin/remote-server.in @@ -0,0 +1,29 @@ +#!/bin/sh +# -*- scheme -*- +# @configure_input@ +#GUILE_LOAD_PATH=3D"@PACKAGE_LOAD_PATH@${GUILE_LOAD_PATH:+:}$GUILE_LOAD_PA= TH" +#GUILE_LOAD_COMPILED_PATH=3D"@PACKAGE_LOAD_COMPILED_PATH@${GUILE_LOAD_COMP= ILED_PATH:+:}$GUILE_LOAD_COMPILED_PATH" +exec ${GUILE:-@GUILE@} --no-auto-compile -e main -s "$0" "$@" +!# +;;; remote-server.in -- Remote build server. +;;; Copyright =C2=A9 2020 Mathieu Othacehe +;;; +;;; This file is part of Cuirass. +;;; +;;; Cuirass is free software: you can redistribute it and/or modify +;;; it under the terms of the GNU General Public License as published by +;;; the Free Software Foundation, either version 3 of the License, or +;;; (at your option) any later version. +;;; +;;; Cuirass is distributed in the hope that it will be useful, +;;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;; GNU General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with Cuirass. If not, see . + +(use-modules (cuirass remote-server)) + +(define* (main #:optional (args (command-line))) + (remote-server (cdr args))) diff --git a/bin/remote-worker.in b/bin/remote-worker.in new file mode 100644 index 0000000..8a3830c --- /dev/null +++ b/bin/remote-worker.in @@ -0,0 +1,29 @@ +#!/bin/sh +# -*- scheme -*- +# @configure_input@ +#GUILE_LOAD_PATH=3D"@PACKAGE_LOAD_PATH@${GUILE_LOAD_PATH:+:}$GUILE_LOAD_PA= TH" +#GUILE_LOAD_COMPILED_PATH=3D"@PACKAGE_LOAD_COMPILED_PATH@${GUILE_LOAD_COMP= ILED_PATH:+:}$GUILE_LOAD_COMPILED_PATH" +exec ${GUILE:-@GUILE@} --no-auto-compile -e main -s "$0" "$@" +!# +;;; remote-worker.in -- Remote build worker. +;;; Copyright =C2=A9 2020 Mathieu Othacehe +;;; +;;; This file is part of Cuirass. +;;; +;;; Cuirass is free software: you can redistribute it and/or modify +;;; it under the terms of the GNU General Public License as published by +;;; the Free Software Foundation, either version 3 of the License, or +;;; (at your option) any later version. +;;; +;;; Cuirass is distributed in the hope that it will be useful, +;;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;; GNU General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with Cuirass. If not, see . + +(use-modules (cuirass remote-worker)) + +(define* (main #:optional (args (command-line))) + (remote-worker (cdr args))) diff --git a/src/cuirass/base.scm b/src/cuirass/base.scm index c3ce900..c526f5c 100644 --- a/src/cuirass/base.scm +++ b/src/cuirass/base.scm @@ -22,8 +22,10 @@ =20 (define-module (cuirass base) #:use-module (fibers) + #:use-module (fibers channels) #:use-module (cuirass logging) #:use-module (cuirass database) + #:use-module (cuirass remote) #:use-module (cuirass utils) #:use-module ((cuirass config) #:select (%localstatedir)) #:use-module (gnu packages) @@ -36,9 +38,13 @@ #:use-module ((guix config) #:select (%state-directory)) #:use-module (git) #:use-module (ice-9 binary-ports) + #:use-module ((ice-9 suspendable-ports) + #:select (current-read-waiter + current-write-waiter)) #:use-module (ice-9 format) #:use-module (ice-9 match) #:use-module (ice-9 popen) + #:use-module (ice-9 ports internal) #:use-module (ice-9 rdelim) #:use-module (ice-9 receive) #:use-module (ice-9 regex) @@ -65,11 +71,13 @@ prepare-git process-specs evaluation-log-file + with-build-offload-thread =20 ;; Parameters. %package-cachedir %gc-root-directory %gc-root-ttl + %build-remote? %use-substitutes? %fallback?)) =20 @@ -102,6 +110,10 @@ (define time-monotonic time-tai)) (else #t)) =20 +(define %build-remote? + ;; Define whether to use the remote build mechanism. + (make-parameter #f)) + (define %use-substitutes? ;; Define whether to use substitutes (make-parameter #f)) @@ -110,6 +122,10 @@ ;; Define whether to fall back to building when the substituter fails. (make-parameter #f)) =20 +(define %build-offload-channel + ;; Channel to communicate with the remote build server. + (make-parameter #f)) + (define %package-cachedir ;; Define to location of cache directory of this package. (make-parameter (or (getenv "CUIRASS_CACHEDIR") @@ -436,6 +452,39 @@ Essentially this procedure inverts the inversion-of-co= ntrol that (raise c)) (x x))))))) =20 +(define (make-build-offload-thread) + "Return a channel used to offload builds by communicating with the remote +build server in a separate thread. The spawned thread also polls for build +events sent by the remote server and calls HANDLE-BUILD-EVENT to register = them +in the database." + (let ((channel (make-channel))) + (call-with-new-thread + (lambda () + (parameterize (((@@ (fibers internal) current-fiber) #f) + (current-read-waiter (lambda (port) + (port-poll port "r"))) + (current-write-waiter (lambda (port) + (port-poll port "w")))) + (let ((socket (remote-build-socket))) + (let loop () + (remote-build-poll socket handle-build-event) + (match (get-message-with-timeout channel + #:seconds 1 + #:retry? #f) + ((drvs . systems) + (remote-build socket drvs systems)) + ('timeout #f)) + (loop)))))) + channel)) + +(define-syntax-rule (with-build-offload-thread body ...) + (parameterize ((%build-offload-channel + (make-build-offload-thread))) + body ...)) + +(define (build-derivations/offload drvs systems) + (put-message (%build-offload-channel) (cons drvs systems))) + ;;; ;;; Building packages. @@ -641,7 +690,14 @@ started)." ;; Those in VALID can be restarted. If some of them were built in t= he ;; meantime behind our back, that's fine: 'spawn-builds' will DTRT. (log-message "restarting ~a pending builds" (length valid)) - (spawn-builds store valid) + (if (%build-remote?) + (let* ((builds (map db-get-build valid)) + (systems (map (cut assq-ref <> #:system) builds))) + ;; The system could by read from the store by the remote build + ;; server using the derivation name, but it is far less expens= ive + ;; to read it from the database. + (build-derivations/offload valid systems)) + (spawn-builds store valid)) (log-message "done with restarted builds")))) =20 (define (create-build-outputs build product-specs) @@ -690,7 +746,12 @@ by PRODUCT-SPECS." (db-set-evaluation-status eval-id (evaluation-status succeeded)) =20 - (spawn-builds store derivations) + (if (%build-remote?) + (let* ((builds (map db-get-build derivations)) + (systems (map (cut assq-ref <> #:system) builds))) + ;; See the comment above regarding system read. + (build-derivations/offload derivations systems)) + (spawn-builds store derivations)) =20 (let* ((results (filter-map (cut db-get-build <>) derivations)) (status (map (cut assq-ref <> #:status) results)) diff --git a/src/cuirass/remote-server.scm b/src/cuirass/remote-server.scm new file mode 100644 index 0000000..6217918 --- /dev/null +++ b/src/cuirass/remote-server.scm @@ -0,0 +1,518 @@ +;;; remote-server.scm -- Remote build server. +;;; Copyright =C2=A9 2020 Mathieu Othacehe +;;; +;;; This file is part of Cuirass. +;;; +;;; GNU Guix is free software; you can redistribute it and/or modify it +;;; under the terms of the GNU General Public License as published by +;;; the Free Software Foundation; either version 3 of the License, or (at +;;; your option) any later version. +;;; +;;; GNU Guix is distributed in the hope that it will be useful, but +;;; WITHOUT ANY WARRANTY; without even the implied warranty of +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;; GNU General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with GNU Guix. If not, see . + +(define-module (cuirass remote-server) + #:use-module (cuirass base) + #:use-module (cuirass remote) + #:use-module (gcrypt pk-crypto) + #:use-module (guix avahi) + #:use-module (guix base32) + #:use-module (guix base64) + #:use-module (guix config) + #:use-module (guix derivations) + #:use-module (guix records) + #:use-module (guix packages) + #:use-module (guix pki) + #:use-module (guix scripts) + #:use-module (guix store) + #:use-module (guix ui) + #:use-module (guix workers) + #:use-module (guix build download) + #:use-module (guix build syscalls) + #:use-module (gcrypt hash) + #:use-module (gcrypt pk-crypto) + #:use-module (simple-zmq) + #:use-module (rnrs bytevectors) + #:use-module (srfi srfi-1) + #:use-module (srfi srfi-26) + #:use-module (srfi srfi-37) + #:use-module (ice-9 atomic) + #:use-module (ice-9 match) + #:use-module (ice-9 q) + #:use-module (ice-9 rdelim) + #:use-module (ice-9 regex) + #:use-module (ice-9 threads) + + #:export (remote-server)) + +;; Indicate if the process has to be stopped. +(define %stop-process? + (make-atomic-box #f)) + +;; Whether to add build items to the store. +(define %add-to-store? + (make-parameter #f)) + +(define %cache-directory + (make-parameter #f)) + +(define %private-key + (make-parameter #f)) + +(define %public-key + (make-parameter #f)) + +(define service-name + "Cuirass remote server") + +(define (show-help) + (format #t (G_ "Usage: remote-server [OPTION]... +Start a remote build server.\n")) + (display (G_ " + -a, --add-to-store register built items to the store")) + (display (G_ " + -b, --backend-port=3DPORT listen worker connections on PORT")) + (display (G_ " + -p, --publish-port=3DPORT publish substitutes on PORT")) + (display (G_ " + -c, --cache=3DDIRECTORY cache built items to DIRECTORY")) + (display (G_ " + --public-key=3DFILE use FILE as the public key for signatures")) + (display (G_ " + --private-key=3DFILE use FILE as the private key for signatures")) + (newline) + (display (G_ " + -h, --help display this help and exit")) + (display (G_ " + -V, --version display version information and exit")) + (newline) + (show-bug-report-information)) + +(define %options + (list (option '(#\h "help") #f #f + (lambda _ + (show-help) + (exit 0))) + (option '(#\V "version") #f #f + (lambda _ + (show-version-and-exit "guix publish"))) + (option '(#\a "add-to-store") #t #f + (lambda (opt name arg result) + (alist-cons 'add-to-store? arg result))) + (option '(#\b "backend-port") #t #f + (lambda (opt name arg result) + (alist-cons 'backend-port (string->number* arg) result))) + (option '(#\p "publish-port") #t #f + (lambda (opt name arg result) + (alist-cons 'publish-port (string->number* arg) result))) + (option '(#\c "cache") #t #f + (lambda (opt name arg result) + (alist-cons 'cache arg result))) + (option '("public-key") #t #f + (lambda (opt name arg result) + (alist-cons 'public-key-file arg result))) + (option '("private-key") #t #f + (lambda (opt name arg result) + (alist-cons 'private-key-file arg result))))) + +(define %default-options + `((backend-port . 5555) + (publish-port . 5556) + (public-key-file . ,%public-key-file) + (private-key-file . ,%private-key-file))) + + +;;; +;;; Build workers. +;;; + +(define %workers + ;; Set of connected workers. + (make-hash-table)) + +(define %build-queues + ;; Builds request queue. + (map (lambda (system) + (cons system (make-q))) + %supported-systems)) + +(define (find-system-queues systems) + "Return the list of build queues for SYSTEMS that are not empty." + (filter-map (match-lambda + ((system . queue) + (and (member system systems) + (not (q-empty? queue)) + queue))) + %build-queues)) + +(define (build-available? name) + "Return #t if there is some available work for the worker with the given +NAME and #f otherwise." + (let* ((worker (hash-ref %workers name)) + (systems (worker-systems worker)) + (queues (find-system-queues systems))) + (not (null? queues)))) + +(define (pop-random-build name) + "Pop randomly and return a build from all the build queues with available +work for the worker with the given NAME." + (define (random-queue queues) + (list-ref queues (random (length queues)))) + + (let* ((worker (hash-ref %workers name)) + (systems (worker-systems worker)) + (queues (find-system-queues systems))) + (q-pop! (random-queue queues)))) + +(define* (read-client-exp client exp) + "Read the given EXP sent by CLIENT." + (catch 'system-error + (lambda () + (match (zmq-read-message exp) + (('build ('drv drv) ('system system)) + (let ((system (or system + (derivation-system + (read-derivation-from-file drv))))) + ;; Push the derivation to the matching queue according to the + ;; targeted system. Also save the client ID in the queue to be = able + ;; to send it build events later on. + (q-push! (assoc-ref %build-queues system) + (list client drv)))))) + (const #f))) + +(define* (read-worker-exp exp #:key reply-worker) + "Read the given EXP sent by a worker. REPLY-WORKER is a procedure that = can +be used to reply to the worker." + (match (zmq-read-message exp) + (('worker-ready worker) + (let* ((worker* (sexp->worker worker)) + (name (worker-name worker*))) + (info (G_ "Worker `~a' is ready.~%") name) + (hash-set! %workers name worker*))) + (('worker-request-work name) + (if (build-available? name) + (match (pop-random-build name) + ((client drv) + (reply-worker client (zmq-build-request-message drv)))) + (reply-worker + (zmq-empty-delimiter) + (zmq-no-build-message)))))) + + +;;; +;;; Fetch workers. +;;; + +(define (zmq-fetch-workers-endpoint) + "inproc://fetch-workers") + +(define (zmq-fetch-worker-socket) + "Return a socket used to communicate with the fetch workers." + (let ((socket (zmq-create-socket %zmq-context ZMQ_DEALER)) + (endpoint (zmq-fetch-workers-endpoint))) + (zmq-connect socket endpoint) + socket)) + +(define (strip-store-prefix file) + ; Given a file name like "/gnu/store/=E2=80=A6-foo-1.2/bin/foo", return + ;; "/bin/foo". + (let* ((len (string-length %store-directory)) + (base (string-drop file (+ 1 len)))) + (match (string-index base #\/) + (#f base) + (index (string-drop base index))))) + +(define (publish-nar-url publish-url store-hash) + "Return the URL of STORE-HASH nar substitute on PUBLISH-URL." + (format #f "~a/nar/gzip/~a" publish-url store-hash)) + +(define (publish-narinfo-url publish-url store-hash) + "Return the URL of STORE-HASH narinfo file on PUBLISH-URL." + (let ((hash (and=3D> (string-index store-hash #\-) + (cut string-take store-hash <>)))) + (format #f "~a/~a.narinfo" publish-url hash))) + +(define (nar-path cache-directory output) + "Return the path of the NAR file for OUTPUT in CACHE-DIRECTORY." + (string-append cache-directory "/" (basename output) ".nar")) + +(define (narinfo-path cache-directory output) + "Return the path of the NARINFO file for OUTPUT in CACHE-DIRECTORY." + (string-append cache-directory "/" (basename output) ".narinfo")) + +(define* (sign-narinfo! narinfo) + "Edit the given NARINFO file to replace the worker signature by the remo= te +build server signature." + (define (signed-string s) + (let* ((hash (bytevector->hash-data (sha256 (string->utf8 s)) + #:key-type (key-type (%public-key)= )))) + (signature-sexp hash (%private-key) (%public-key)))) + + (define base64-encode-string + (compose base64-encode string->utf8)) + + (define lines + (call-with-input-file narinfo + (lambda (port) + (let loop ((line (read-line port)) + (lines '())) + (if (eof-object? line) + (reverse lines) + (loop (read-line port) + (cons line lines))))))) + (let* ((lines + (filter (lambda (line) + (not (string-match "^Signature:" line))) + lines)) + (info (format #f "~a~%" (string-join lines "\n"))) + (signature (base64-encode-string + (canonical-sexp->string (signed-string info))))) + (call-with-output-file narinfo + (lambda (port) + (format port "~aSignature: 1;~a;~a~%" + info (gethostname) signature))))) + +(define (download-nar cache-directory outputs url) + "Download in CACHE-DIRECTORY the OUTPUTS from the substitute server at U= RL." + (for-each + (lambda (output) + (let* ((path (derivation-output-path output)) + (store-hash (strip-store-prefix path)) + (nar-file (nar-path cache-directory store-hash)) + (narinfo-file (narinfo-path cache-directory store-hash)) + (nar-url (publish-nar-url url store-hash)) + (narinfo-url (publish-narinfo-url url store-hash))) + (unless (file-exists? nar-file) + (url-fetch nar-url nar-file)) + + (unless (file-exists? narinfo-file) + (url-fetch narinfo-url narinfo-file) + (sign-narinfo! narinfo-file)))) + outputs)) + +(define (add-to-store outputs url) + "Add the OUTPUTS that are available from the substitute server at URL to= the +store." + (with-store store + (for-each (lambda (output) + (add-substitute-url store url) + (ensure-path store output)) + (map derivation-output-path outputs)))) + +(define (need-fetching? message) + "Return #t if the received MESSAGE implies that some output fetching is +required and #f otherwise." + (match (zmq-read-message message) + (('build-succeeded ('drv drv) ('url url)) + #t) + (else #f))) + +(define* (run-fetch message #:key reply) + "Read MESSAGE and download the corresponding build outputs. If +%CACHE-DIRECTORY is set, download the matching NAR and NARINFO files in th= is +directory. If %ADD-TO-STORE? is set, add the build outputs to the store. + +REPLY is procedure used to forward MESSAGE to the client once the build +outputs are downloaded." + (define (build-outputs drv) + (catch 'system-error + (lambda () + (map (match-lambda + ((output-name . output) + output)) + (derivation-outputs + (read-derivation-from-file drv)))) + (const '()))) + + (match (zmq-read-message message) + (('build-succeeded ('drv drv) ('url url)) + (let ((outputs (build-outputs drv))) + (when (%add-to-store?) + (add-to-store outputs url)) + (when (%cache-directory) + (download-nar (%cache-directory) outputs url)) + (reply message))))) + +(define (start-fetch-worker name) + "Start a fetch worker thread with the given NAME. This worker takes car= e of +downloading build outputs. It communicates with the remote server using a= ZMQ +socket." + (define (reply socket client) + (lambda (message) + (zmq-send-msg-parts-bytevector + socket + (list client (zmq-empty-delimiter) (string->bv message))))) + + (call-with-new-thread + (lambda () + (set-thread-name name) + (let ((socket (zmq-fetch-worker-socket))) + (let loop () + (match (zmq-get-msg-parts-bytevector socket '()) + ((client empty rest) + (let ((message (bv->string rest))) + (run-fetch (bv->string rest) + #:reply (reply socket client))))) + (loop)))))) + + +;;; +;;; ZMQ connection. +;;; + +(define %zmq-context + (zmq-create-context)) + +(define (zmq-backend-endpoint backend-port) + "Return a ZMQ endpoint string allowing TCP connections on BACKEND-PORT f= rom +all network interfaces." + (string-append "tcp://*:" (number->string backend-port))) + +(define (zmq-start-proxy backend-port) + "This procedure starts a proxy between client connections from the IPC +frontend to the workers connected through the TCP backend." + (define (socket-ready? items socket) + (find (lambda (item) + (eq? (poll-item-socket item) socket)) + items)) + + (let* ((client-socket + (zmq-create-socket %zmq-context ZMQ_ROUTER)) + (build-socket + (zmq-create-socket %zmq-context ZMQ_ROUTER)) + (fetch-socket + (zmq-create-socket %zmq-context ZMQ_DEALER)) + (poll-items (list + (poll-item client-socket ZMQ_POLLIN) + (poll-item build-socket ZMQ_POLLIN) + (poll-item fetch-socket ZMQ_POLLIN)))) + + (zmq-bind-socket client-socket (zmq-frontend-endpoint)) + (zmq-bind-socket build-socket (zmq-backend-endpoint backend-port)) + (zmq-bind-socket fetch-socket (zmq-fetch-workers-endpoint)) + + ;; Change frontend socket permissions. + (chmod (zmq-frontend-socket-name) #o666) + + ;; Do not use the built-in zmq-proxy as we want to edit the envelope of + ;; frontend messages before forwarding them to the backend. + (let loop () + (let ((items (zmq-poll* poll-items))) + ;; CLIENT -> REMOTE-SERVER. + (when (zmq-socket-ready? items client-socket) + (match (zmq-get-msg-parts-bytevector client-socket) + ((client empty rest) + (read-client-exp client (bv->string rest))))) + ;; BUILD-WORKER -> REMOTE-SERVER. + (when (zmq-socket-ready? items build-socket) + (match (zmq-get-msg-parts-bytevector build-socket) + ((worker empty rest) + (let ((reply-worker + (lambda (client message) + (zmq-send-msg-parts-bytevector + build-socket + (list worker + (zmq-empty-delimiter) + client + (zmq-empty-delimiter) + (string->bv message)))))) + (read-worker-exp (bv->string rest) + #:reply-worker reply-worker))) + ((worker empty client empty rest) + (let ((message (list client (zmq-empty-delimiter) rest))) + (if (need-fetching? (bv->string rest)) + (zmq-send-msg-parts-bytevector fetch-socket message) + (zmq-send-msg-parts-bytevector client-socket message)))= ))) + ;; FETCH-WORKER -> REMOTE-SERVER. + (when (zmq-socket-ready? items fetch-socket) + (let ((msg (zmq-get-msg-parts-bytevector fetch-socket))) + (zmq-send-msg-parts-bytevector client-socket msg))) + + (loop))))) + + +;;; +;;; Entry point. +;;; + +;; The PID of the publish process. +(define %publish-pid + (make-atomic-box #f)) + +;; The thread running the Avahi publish service. +(define %avahi-thread + (make-atomic-box #f)) + +(define (signal-handler) + "Catch SIGINT to stop the Avahi event loop and the publish process before +exiting." + (sigaction SIGINT + (lambda (signum) + (let ((publish-pid (atomic-box-ref %publish-pid)) + (avahi-thread (atomic-box-ref %avahi-thread))) + (atomic-box-set! %stop-process? #t) + + (and publish-pid + (begin + (kill publish-pid SIGHUP) + (waitpid publish-pid))) + + (and avahi-thread + (join-thread avahi-thread)) + + (exit 1))))) + +(define (remote-server args) + (signal-handler) + + (with-error-handling + (let* ((opts (args-fold* args %options + (lambda (opt name arg result) + (leave (G_ "~A: unrecognized option~%") nam= e)) + (lambda (arg result) + (leave (G_ "~A: extraneous argument~%") arg= )) + %default-options)) + (add-to-store? (assoc-ref opts 'add-to-store?)) + (backend-port (assoc-ref opts 'backend-port)) + (publish-port (assoc-ref opts 'publish-port)) + (cache (assoc-ref opts 'cache)) + (public-key + (read-file-sexp + (assoc-ref opts 'public-key-file))) + (private-key + (read-file-sexp + (assoc-ref opts 'private-key-file)))) + + (parameterize ((%add-to-store? add-to-store?) + (%cache-directory cache) + (%public-key public-key) + (%private-key private-key)) + + (atomic-box-set! + %publish-pid + (publish-server publish-port + #:public-key public-key + #:private-key private-key)) + + (atomic-box-set! + %avahi-thread + (avahi-publish-service-thread + service-name + #:type remote-server-service-type + #:port backend-port + #:stop-loop? (lambda () + (atomic-box-ref %stop-process?)) + #:txt (list (string-append "publish=3D" + (number->string publish-port))))) + + (for-each (lambda (number) + (start-fetch-worker + (string-append "fetch-worker-" (number->string number= )))) + (iota 4)) + + (zmq-start-proxy backend-port))))) diff --git a/src/cuirass/remote-worker.scm b/src/cuirass/remote-worker.scm new file mode 100644 index 0000000..c253ee5 --- /dev/null +++ b/src/cuirass/remote-worker.scm @@ -0,0 +1,286 @@ +;;; remote-worker.scm -- Remote build worker. +;;; Copyright =C2=A9 2020 Mathieu Othacehe +;;; +;;; This file is part of Cuirass. +;;; +;;; GNU Guix is free software; you can redistribute it and/or modify it +;;; under the terms of the GNU General Public License as published by +;;; the Free Software Foundation; either version 3 of the License, or (at +;;; your option) any later version. +;;; +;;; GNU Guix is distributed in the hope that it will be useful, but +;;; WITHOUT ANY WARRANTY; without even the implied warranty of +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;; GNU General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with GNU Guix. If not, see . + +(define-module (cuirass remote-worker) + #:use-module (cuirass remote) + #:use-module (gcrypt pk-crypto) + #:use-module (guix) + #:use-module (guix avahi) + #:use-module (guix config) + #:use-module (guix diagnostics) + #:use-module (guix pki) + #:use-module (guix records) + #:use-module (guix scripts) + #:use-module (guix ui) + #:use-module (guix build syscalls) + #:use-module (guix scripts publish) + #:use-module (simple-zmq) + #:use-module (rnrs bytevectors) + #:use-module (srfi srfi-1) + #:use-module (srfi srfi-26) + #:use-module (srfi srfi-34) + #:use-module (srfi srfi-37) + #:use-module (ice-9 atomic) + #:use-module (ice-9 match) + #:use-module (ice-9 threads) + + #:export (remote-worker)) + +;; Indicate if the process has to be stopped. +(define %stop-process? + (make-atomic-box #f)) + +(define (show-help) + (format #t (G_ "Usage: remote-worker [OPTION]... +Start a remote build worker.\n")) + (display (G_ " + -w, --workers=3DCOUNT start COUNT parallel workers")) + (display (G_ " + -p, --publish-port=3DPORT publish substitutes on PORT")) + (display (G_ " + --public-key=3DFILE use FILE as the public key for signatures")) + (display (G_ " + --private-key=3DFILE use FILE as the private key for signatures")) + (newline) + (display (G_ " + -h, --help display this help and exit")) + (display (G_ " + -V, --version display version information and exit")) + (newline) + (show-bug-report-information)) + +(define %options + (list (option '(#\h "help") #f #f + (lambda _ + (show-help) + (exit 0))) + (option '(#\V "version") #f #f + (lambda _ + (show-version-and-exit "guix publish"))) + (option '(#\w "workers") #t #f + (lambda (opt name arg result) + (alist-cons 'workers (string->number* arg) result))) + (option '(#\p "publish-port") #t #f + (lambda (opt name arg result) + (alist-cons 'publish-port (string->number* arg) result))) + (option '("public-key") #t #f + (lambda (opt name arg result) + (alist-cons 'public-key-file arg result))) + (option '("private-key") #t #f + (lambda (opt name arg result) + (alist-cons 'private-key-file arg result))))) + +(define %default-options + `((workers . 1) + (publish-port . 5558) + (public-key-file . ,%public-key-file) + (private-key-file . ,%private-key-file))) + + +;;; +;;; ZMQ connection. +;;; + +(define %zmq-context + (zmq-create-context)) + +(define (zmq-backend-endpoint address port) + "Return a ZMQ endpoint identifying the build server available by TCP at +ADDRESS and PORT." + (string-append "tcp://" address ":" (number->string port))) + +(define (zmq-dealer-socket) + "The ZMQ socket to communicate with the worker threads." + (zmq-create-socket %zmq-context ZMQ_DEALER)) + + +;;; +;;; Worker. +;;; + +;; The port of the local publish server. +(define %local-publish-port + (make-atomic-box #f)) + +(define (server-publish-url address port) + "Return the server publish url at ADDRESS and PORT." + (string-append "http://" address ":" (number->string port))) + +(define (service-txt->publish-port txt) + "Parse the service TXT record and return the server publish port." + (define (parse-txt) + (fold (lambda (param params) + (match (string-split param #\=3D) + ((key value) + (cons (cons (string->symbol key) value) + params)))) + '() + txt)) + + (let ((params (parse-txt))) + (string->number (assq-ref params 'publish)))) + +(define (service->publish-url service) + "Return the URL of the publish server corresponding to the service with = the +given NAME." + (let* ((address (avahi-service-address service)) + (txt (avahi-service-txt service)) + (publish-port + (service-txt->publish-port txt))) + (server-publish-url address publish-port))) + +(define (service->local-publish-url service) + "Return the URL of the local publish server." + (let* ((local-address (avahi-service-local-address service)) + (port (atomic-box-ref %local-publish-port))) + (server-publish-url local-address port))) + +(define* (run-build drv service #:key reply) + "Build DRV and send messages upon build start, failure or completion to = the +build server identified by SERVICE-NAME using the REPLY procedure. + +The publish server of the build server is added to the list of the store +substitutes-urls. This way derivations that are not present on the worker= can +still be substituted." + (with-store store + (let ((publish-url (service->publish-url service)) + (local-publish-url (service->local-publish-url service))) + (add-substitute-url store publish-url) + (reply (zmq-build-started-message drv)) + (guard (c ((store-protocol-error? c) + (info (G_ "Derivation `~a' build failed: ~a~%") + drv (store-protocol-error-message c)) + (reply (zmq-build-failed-message drv)))) + (if (build-derivations store (list drv)) + (reply (zmq-build-succeeded-message drv local-publish-url)) + (reply (zmq-build-failed-message drv))))))) + +(define* (run-command command service #:key reply) + "Run COMMAND. SERVICE-NAME is the name of the build server that sent the +command. REPLY is a procedure that can be used to reply to this server." + (match (zmq-read-message command) + (('build ('drv drv) ('system system)) + (info (G_ "Building `~a' derivation.~%") drv) + (run-build drv service #:reply reply)) + (('no-build) + #t))) + +(define (start-worker worker service) + "Start a worker thread named NAME, reading commands from the DEALER sock= et +and executing them. The worker can reply on the same socket." + (define (reply socket client) + (lambda (message) + (zmq-send-msg-parts-bytevector + socket + (list (zmq-empty-delimiter) client + (zmq-empty-delimiter) (string->bv message))))) + + (define (ready socket) + (zmq-send-msg-parts-bytevector + socket + (list (make-bytevector 0) + (string->bv + (zmq-worker-ready-message (worker->sexp worker)))))) + + (define (request-work socket) + (let ((name (worker-name worker))) + (zmq-send-msg-parts-bytevector + socket + (list (make-bytevector 0) + (string->bv (zmq-worker-request-work-message name)))))) + + (call-with-new-thread + (lambda () + (set-thread-name (worker-name worker)) + (let* ((socket (zmq-dealer-socket)) + (address (avahi-service-address service)) + (port (avahi-service-port service)) + (endpoint (zmq-backend-endpoint address port))) + (zmq-connect socket endpoint) + (ready socket) + (let loop () + (request-work socket) + (match (zmq-get-msg-parts-bytevector socket '()) + ((empty client empty command) + (run-command (bv->string command) service + #:reply (reply socket client)))) + (sleep 1) + (loop)))))) + + +;;; +;;; Entry point. +;;; + +;; The PID of the publish process. +(define %publish-pid + (make-atomic-box #f)) + +(define (signal-handler) + "Catch SIGINT to stop the Avahi event loop and the publish process before +exiting." + (sigaction SIGINT + (lambda (signum) + (let ((publish-pid (atomic-box-ref %publish-pid))) + (atomic-box-set! %stop-process? #t) + + (and publish-pid + (begin + (kill publish-pid SIGHUP) + (waitpid publish-pid))) + + (exit 1))))) + +(define (remote-worker args) + (with-error-handling + (let* ((opts (args-fold* args %options + (lambda (opt name arg result) + (leave (G_ "~A: unrecognized option~%") nam= e)) + (lambda (arg result) + (leave (G_ "~A: extraneous argument~%") arg= )) + %default-options)) + (workers (assoc-ref opts 'workers)) + (publish-port (assoc-ref opts 'publish-port)) + (public-key + (read-file-sexp + (assoc-ref opts 'public-key-file))) + (private-key + (read-file-sexp + (assoc-ref opts 'private-key-file)))) + + (atomic-box-set! %local-publish-port publish-port) + + (atomic-box-set! + %publish-pid + (publish-server publish-port + #:public-key public-key + #:private-key private-key)) + + (avahi-browse-service-thread + (lambda (action service) + (case action + ((new-service) + (for-each (lambda (n) + (start-worker (worker + (name (generate-worker-name)) + (systems '("x86_64-linux"))) + service)) + (iota workers))))) + #:types (list remote-server-service-type) + #:stop-loop? (lambda () + (atomic-box-ref %stop-process?)))))) diff --git a/src/cuirass/remote.scm b/src/cuirass/remote.scm new file mode 100644 index 0000000..7a71391 --- /dev/null +++ b/src/cuirass/remote.scm @@ -0,0 +1,292 @@ +;;; remote.scm -- Build on remote machines. +;;; Copyright =C2=A9 2020 Mathieu Othacehe +;;; +;;; This file is part of Cuirass. +;;; +;;; GNU Guix is free software; you can redistribute it and/or modify it +;;; under the terms of the GNU General Public License as published by +;;; the Free Software Foundation; either version 3 of the License, or (at +;;; your option) any later version. +;;; +;;; GNU Guix is distributed in the hope that it will be useful, but +;;; WITHOUT ANY WARRANTY; without even the implied warranty of +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;; GNU General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with GNU Guix. If not, see . + +(define-module (cuirass remote) + #:use-module (guix config) + #:use-module (guix derivations) + #:use-module (guix records) + #:use-module (guix store) + #:use-module (guix ui) + #:use-module (guix build download) + #:use-module ((guix build utils) #:select (mkdir-p)) + #:use-module (guix scripts publish) + #:use-module (simple-zmq) + #:use-module (rnrs bytevectors) + #:use-module (srfi srfi-1) + #:use-module (srfi srfi-26) + #:use-module (ice-9 match) + #:use-module (ice-9 rdelim) + #:export (worker + worker? + worker-name + worker-systems + worker->sexp + sexp->worker + generate-worker-name + + publish-server + add-substitute-url + + zmq-frontend-socket-name + zmq-frontend-endpoint + zmq-poll* + zmq-socket-ready? + zmq-empty-delimiter + + zmq-build-request-message + zmq-no-build-message + zmq-build-started-message + zmq-build-failed-message + zmq-build-succeeded-message + zmq-worker-ready-message + zmq-worker-request-work-message + zmq-read-message + + remote-server-service-type + remote-build-socket + remote-build + remote-build-poll)) + + +;;; +;;; Workers. +;;; + +(define-record-type* + worker make-worker + worker? + (name worker-name) + (systems worker-systems)) + +(define (worker->sexp worker) + "Return an sexp describing WORKER." + (let ((name (worker-name worker)) + (systems (worker-systems worker))) + `(worker + (name ,name) + (systems ,systems)))) + +(define (sexp->worker sexp) + "Turn SEXP, an sexp as returned by 'worker->sexp', into a recor= d." + (match sexp + (('worker ('name name) ('systems systems)) + (worker + (name name) + (systems systems))))) + + +(define %seed + (seed->random-state + (logxor (getpid) (car (gettimeofday))))) + +(define (integer->alphanumeric-char n) + "Map N, an integer in the [0..62] range, to an alphanumeric character." + (cond ((< n 10) + (integer->char (+ (char->integer #\0) n))) + ((< n 36) + (integer->char (+ (char->integer #\A) (- n 10)))) + ((< n 62) + (integer->char (+ (char->integer #\a) (- n 36)))) + (else + (error "integer out of bounds" n)))) + +(define (random-string len) + "Compute a random string of size LEN where each character is alphanumeri= c." + (let loop ((chars '()) + (len len)) + (if (zero? len) + (list->string chars) + (let ((n (random 62 %seed))) + (loop (cons (integer->alphanumeric-char n) chars) + (- len 1)))))) + +(define (generate-worker-name) + "Return the service name of the server." + (string-append (gethostname) "-" (random-string 4))) + + +;;; +;;; Store publishing. +;;; + +(define (add-substitute-url store url) + "Add URL to the list of STORE substitutes-urls." + (set-build-options store + #:use-substitutes? #t + #:fallback? #f + #:keep-going? #t + #:print-build-trace #t + #:build-verbosity 1 + #:substitute-urls + (cons url %default-substitute-urls))) + +(define* (publish-server port + #:key + public-key + private-key) + "This procedure starts a publishing server listening on PORT in a new +process and returns the pid of the forked process. Use PUBLIC-KEY and +PRIVATE-KEY to sign narinfos." + (match (primitive-fork) + (0 + (parameterize ((%public-key public-key) + (%private-key private-key)) + (with-store store + (let* ((address (make-socket-address AF_INET INADDR_ANY 0)) + (socket-address + (make-socket-address (sockaddr:fam address) + (sockaddr:addr address) + port)) + (socket (open-server-socket socket-address))) + (run-publish-server socket store + #:compressions + (list %default-gzip-compression)))))) + (pid pid))) + + +;;; +;;; ZMQ. +;;; + +(define %zmq-context + (zmq-create-context)) + +(define (zmq-frontend-socket-name) + "Return the name of the ZMQ frontend socket." + (string-append %state-directory "/remote-build-socket")) + +(define (zmq-frontend-endpoint) + "Return a ZMQ endpoint allowing client connections using the IPC transpo= rt." + (string-append "ipc://" (zmq-frontend-socket-name))) + +(define (EINTR-safe proc) + "Return a variant of PROC that catches EINTR 'zmq-error' exceptions and +retries a call to PROC." + (define (safe . args) + (catch 'zmq-error + (lambda () + (apply proc args)) + (lambda (key errno . rest) + (if (=3D errno EINTR) + (apply safe args) + (apply throw key errno rest))))) + + safe) + +(define zmq-poll* + ;; Return a variant of ZMQ-POLL that catches EINTR errors. + (EINTR-safe zmq-poll)) + +(define (zmq-socket-ready? items socket) + "Return #t if the given SOCKET is part of ITEMS, a list returned by a +'zmq-poll' call, return #f otherwise." + (find (lambda (item) + (eq? (poll-item-socket item) socket)) + items)) + +(define (zmq-read-message msg) + (call-with-input-string msg read)) + +(define (zmq-empty-delimiter) + "Return an empty ZMQ delimiter used to format message envelopes." + (make-bytevector 0)) + +;; ZMQ Messages. +(define* (zmq-build-request-message drv #:optional system) + "Return a message requesting the build of DRV for SYSTEM." + (format #f "~s" `(build (drv ,drv) (system ,system)))) + +(define (zmq-no-build-message) + "Return a message that indicates that no builds are available." + (format #f "~s" `(no-build))) + +(define (zmq-build-started-message drv) + "Return a message that indicates that the build of DRV has started." + (format #f "~s" `(build-started (drv ,drv)))) + +(define (zmq-build-failed-message drv) + "Return a message that indicates that the build of DRV has failed." + (format #f "~s" `(build-failed (drv ,drv)))) + +(define (zmq-build-succeeded-message drv url) + "Return a message that indicates that the build of DRV is done." + (format #f "~s" `(build-succeeded (drv ,drv) (url ,url)))) + +(define (zmq-worker-ready-message worker) + "Return a message that indicates that WORKER is ready." + (format #f "~s" `(worker-ready ,worker))) + +(define (zmq-worker-request-work-message name) + "Return a message that indicates that WORKER is requesting work." + (format #f "~s" `(worker-request-work ,name))) + + +;;; +;;; Remote builds. +;;; + +(define remote-server-service-type + "_remote-server._tcp") + +(define (remote-build-socket) + "Return a socket used to communicate with the remote build server." + (let ((socket (zmq-create-socket %zmq-context ZMQ_DEALER)) + (endpoint (zmq-frontend-endpoint))) + (zmq-connect socket endpoint) + socket)) + +(define* (remote-build socket drvs systems) + "Builds DRVS using the remote build mechanism. A build command is sent = on +SOCKET to the build server for each derivation. + +SYSTEMS is a list describing the systems of each derivations in the DRVS l= ist. +It is used for performance reasons, so that the remote server doesn't need= to +call 'read-derivation-from-file' for each derivation, which can be an +expensive operation." + (for-each + (lambda (drv system) + ;; We need to prefix the command with an empty delimiter + ;; because the DEALER socket is connected to a ROUTER + ;; socket. See "zmq-start-proxy" procedure. + (zmq-send-msg-parts-bytevector + socket + (list (make-bytevector 0) + (string->bv (zmq-build-request-message drv system))))) + drvs systems)) + +(define* (remote-build-poll socket event-proc + #:key + (timeout 1000)) + "Poll SOCKET for messages and call EVENT-PROC each time a build event is +received, return if no event occured for TIMEOUT milliseconds." + (define (parse-result result) + (match (zmq-read-message result) + (('build-started ('drv drv)) + (event-proc (list 'build-started drv))) + (('build-succeeded ('drv drv) ('url url)) + (event-proc (list 'build-succeeded drv))) + (('build-failed ('drv drv)) + (event-proc (list 'build-failed drv))))) + + (let* ((poll-items (list + (poll-item socket ZMQ_POLLIN))) + (items (zmq-poll* poll-items timeout))) + (when (zmq-socket-ready? items socket) + (match (zmq-get-msg-parts-bytevector socket '()) + ((empty result) + (parse-result (bv->string result))))))) --=20 2.29.2 --=-=-= Content-Type: image/png Content-Disposition: attachment; filename=remote.png Content-Transfer-Encoding: base64 iVBORw0KGgoAAAANSUhEUgAAA98AAAFWCAYAAABw0y6JAAAIjHRFWHRteGZpbGUAJTNDbXhmaWxl JTIwaG9zdCUzRCUyMmFwcC5kaWFncmFtcy5uZXQlMjIlMjBtb2RpZmllZCUzRCUyMjIwMjAtMTIt MDJUMTAlM0E0OSUzQTAzLjk5MVolMjIlMjBhZ2VudCUzRCUyMjUuMCUyMChYMTEpJTIyJTIwdmVy c2lvbiUzRCUyMjEzLjkuMiUyMiUyMGV0YWclM0QlMjJBS3h0Y2JVZnQyS0hkeWlKV0lmRCUyMiUy MHR5cGUlM0QlMjJkZXZpY2UlMjIlM0UlM0NkaWFncmFtJTIwaWQlM0QlMjI5YkJIZ28tWGlDbDB3 WHFHc1J1LSUyMiUzRTdWdmZjNXM0RVA1ciUyRk5nTWt2Z2hIbU1uYVRyVHp2bWFkSHAzTHpjRVpK c3B0and5cnUzNzYwOEVBVWFJSUR2Q2RaM2tKV2dSa3ZoMnRmdnRDZyUyRlFhTDc5eUlMbDdBdU5T REtBVnJRZG9Kc0JoTURCbVAlMkZMSkx0Y2dqMDNGMHhaSElsT2xlQWglMkZvOElvU1drNnpnaXEx ckhsTklralpkMVlVZ1hDeEttTlZuQUdOM1V1MDFvVXA5MUdVeEpRJTJGQVFCa2xUJTJCajJPMHBt UUF0ZXZidHlUZURvVFUyUG81VGZtUWRGWnZNbHFGa1Iwc3lkQ3R3TTBZcFNtJTJCZFY4T3lKSkJs NkJTJTJGN2NYY3ZkY21HTUxGS2RCMkQlMkJ3TThnV1l0M0UlMkJ0S2Q4WExNcnBlUkNUcmJ3M1Fj RE9MVSUyRkt3RE1MczdvYXJsOHRtNlR6aExjQXZWeW1qUDhpSUpwUTlQNDNjRUpPbkNiOHppWk5r VHg0RkJFOUNMaGNMSUN3bDI5YVhBQ1UwM0tZSW5aT1U3WGlYNGdGYm9Dbk15UlBOVGFVYnU1RE45 dFNDaXVjQ1lRJTJGVGN1Z0tNbjRoVUZNamlCUUl1Z21mWWZqRUw2YlpCU056bXBJUEs4TDRheFoz JTJCYmhsQjhPWVIlMkI2VDY3aE56Q2VUQ1F3TllRNVJIWE5nTlVHSFdBRTZ3QVpBdHklMkZCYkQy M0RpRnVJb2hVWmx0NHo5Y0E2R2hiN1lheUgzMVliWnR0dGx1ekNjaHhIWExmYjBBT29BSnl4NERO dXQyUTg3aTIlMkZjQk5iRTRYRndLNGoyVTNBVTZIdU5mdEpjZ2l1czRJQVclMkJGU2JCYXhUa2lB VXViNG9hJTJGS0dLJTJGZlFpNmQzZER4N05LZEVuVW9CcWQyTzVoNTd3QUhTTkprTVklMkY2OE9y OEJRempHbk1KNjVVNSUyRmxYJTJGdDZmVTFja3hGZE9mY2dWWGJPUWlGSDJPWWMwTVBZN0JxNFB5 NVV4SldsajJHZmxsNkJvMlFQdTNvSCUyRmZQbnozNXZiNjglMkIzWDN2ZmdBUkVEdkZVSnVLN0hn b01iVURYNnd3eXZzS0diQU14eHRlRCUyQiUyQnNmM3g0dkJtNEhkTk9pVWxhalJTYTRhREg1UzVD UHZ3MCUyRmYzcTQ3eDF2amphT2JCWGVHRDRodHlmekJnVTE3TEp2MndEYW9EdSUyQiUyRkg0c0ZQ aGVBOEhlYUNoUTVaOXZrSWRDNERiZFJGJTJCMENHaWtySmZQUkhraWVrTElOUkxXZHlwNkpCV0Z0 dE1QRmVVRDE0YzFSMFdCUmdKJTJCOFZ4VUZXcjZJcU5BSSUyRjIlMkJiR29FN1dicTNSczElMkJv V3B0N1l0bjYlMkIlMkZjJTJCVXNvZ2paaCUyRnEzeG80RFVIYVZCbjJhS3I5JTJCViUyRnF4U3Vj dTY5aXcxdGo2aGZmclFla2FXZjdqYUt6bFU3bXZTenN5b2xLYmJmb1Brbmk2eU15SUs1V25DMmlZ JTJCZEE0REpKcmNXTWVSMUUyb2RKbjE3MjZDU2NNNjVwUWxGOXNoVWxCQTV3VGFoUUQzalZUdWNa VHFrWlZPYmowMHBoOFNxc3FqYW40aUpFRHd5SVV2UjM2SjFjaVMyUHVxa1NhS0VUcW5JcUg2NWp4 VUg4cDUlMkJGT0hXNkVtbkNYWiUyQmFteXdzNjUlMkJIdnpLdVZlVFYwSiUyQnRFbDNjQlg5cHpz dXRxNFYxY0E4RnVyOXN5NjdCcVg3QThUN0hneWx6eUVZOGxkVHFmQjN3YWo5NHFkWUNXQkQ4NElY WFFxR05NU0JyTyUyQmklMkZiQnBFZk9TcDlCc1FHQ0pyQkdqV3FHS2NNWTBiS0dBdTZJSWZXYkx1 eGZaN2lMczdXZnZNNzFIQnQ2WU14NkVsVlBWMG42OWxlYlNEZzZuMHdjS2lUbFJlTTVFOHBwZjRP UHF5JTJGbktSMzlmZmtLQVVOTyUyRjNEeWpmQ3FvOWhFSHlVUGNNOTFxMmNyNm03RFJwd2JCMUhq dk55QkRGWHg5SDVXdVAlMkI4ZEZZdWFCVVpwdjZ6eXpteSUyQlVDRmI5WG1aU0ptRjlNMWYlMkZP UER4VDVmQ3gzVjhaemxkTzBmeGJqUCUyRmN1TmtLSmVTdFhkbUt1b05YYnQ1Q2hNNXNsenVPZEZy b2VjZnRjdXhCS2FCSkF4a0thUEtDQzJyVkduQ2tDa0pYZjJEWnIzMUFWSk5OaFRTazhZVk94OGJS SVc4eUZYdlZMc0slMkJYZHRIVnhaME92Y1NiNDBKaXprMG1hczhib081WjdhJTJGT0t0JTJGNlhD JTJCVEF3T1R0SXQlMkJadGtWeSUyQnFIcHlsVzNMNDlzd3lOZ1JQRzdOTGMyMHo4RE9MMlJoSUpx TW84UnVLMmJ4WiUyRldnclYyWDEwemQwJTJCejglM0QlM0MlMkZkaWFncmFtJTNFJTNDJTJGbXhm aWxlJTNF+aKNuwAAIABJREFUeJzsnXdUVEfDxgcE6SgqFqRYsBN7i4WILXaNJvZYYl57hxiM3aiJ xlgSJRprxJbYG2jErqiwKiJrxbI2NDExWRO/vCnv8/1B9sqFZXdRdu9leZ5zfifHu7v3zg0zs/Nj Zi5i5IIjIIQQQkjuwTAMwzAMkzli5IIjuP7ob0IIIYTkApRvhmEYhmGMhfJNCCGE5CKUb4ZhGIZh jIXyTQghhOQilG+GYRiGYYyF8k0IIYTkIpRvhmEYhmGMhfJNCCGE5CKUb4ZhGIZhjIXyTQghhOQi lG+GYRiGYYyF8k0IIYTkIpRvhmEYhmGMhfJNCCGE5CKUb4ZhGIZhjIXyTQghhOQilG+GYRiGYYyF 8k0IIYTkIpbKd1T4RtVjL/dhL4xccIQQQkgehvJNCCGE5CIjF1gu3zptmmrJiXwrXdb8QFT4RsXr NiGEkJeH8k0IIYTkMpRvQvkmhBCSGco3IYQQkstQvgnlmxBCSGYo34QQQkguk1vy3bdHPwghjDJz 0hzotGnYvGYbXq/fCN7ehVCuTHn06zUQVxJvQqdNw5L5y2SfcXJyQoXyFbFs0SrKdx6E8k0IIXkb yjchhBCSy+SWfK+Jisa4EREYNyICLi6uKFzIR/r37s2xWBMVDUdHRxTyLoy3O3dHw3qvw8HBAdUq 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 fmi1bc8P4pv4Rptqp/g+sWWTxE2fIkop6dH2FTmUuU5+yVkvvdq3leuvrSk1qlWV7q+0kZNbc6RD q5aenc16f6wtp/JyJSduiTS+/+9SOTxc6tSqJZ+9+44W351at5IHGtSXmjVqyOsvtJBfctYbXzPx rVsU333+F+G5rdWrL4hSSibOmWApvrN3Z3m+pvaNteWaa6+R3AJr8V3j6hpy/R+u99y3auNKuevu O6V9l3aSW0B8X2qIb+vGdmyhxXfGysiAjiviGwPVjfHN+eFOR8R20+L72CH/n51OfLtA4hutaKf4 3rN6lfZy4YHdusiQnm+KUkr6dnxderVvK0opef+tCClIS5UGt98u1atWkW+SEuX45o1yS906UrNG DRnVv588+LcGUq5cOUmLmuuJ70oVK0rnl1rL7X/+syilZNboEcbXTHzrFo/vxOwEufNvd4pSSiIT Ii3Fd+eIztKtbzd5tsUzUq5cOXlr8FuSW2Atvv9y119EKSVP/OsJeXfsUIlMiJRN+zZ6vrYovtt1 bivd+3WX7v26y0ONHyK+y4hgHn9uucQhrktrLb7XJXxNfGNQJb45P9ziyLg3tfj+mfhG4hutaqf4 PpWXK+sXL/SE96m8XLnpxhulTq1a8k1SouxclSC1b7jes9N9/svOiwK792vt5FRermxZHiudX2ot 8TOne+7r0KqlnMrL9eyuv9Orh/H1Et++g6jIho0bXnAQKaWkYsWKMi1qmuUgikuLlceefkwqV6ns +VnXXHuNfDRtrBZEpUl8B5+yiG+nX+IQ1+1lPb6XfUF8Y1Alvjk/3OLopT20+P7pYJJtzw/im/hG m2r3+K545ZUloiYsLEyOb96oxfe8T8aJUkomvTe0xM8sfs130b8H9+hufL3Et+8g6tqni4ycOEI2 7F2vxfdXcV+eC6IBPXy+7Dw+c7ncesetUrFSRcnenWUpiIrM2b9JlqxeLAOGDZDyFcpL5SqVJWf/ plKDaMCwAcR3GVEW8e30SxyW9nxVi++02JnENwZV4pvzwy2OWdZTi++jP6yw7flBfBPfaFPtHt9/ /P0N8sh993ru/35dquxNTZJTeb/tfF9drZqcysuVlK+/0Ha+8xKWSq/2bWXlnJnEdxA1cc33+XaO 6CxKKfnfB4M8tzV5uokopWTmopleg6hZ6+dFKSULVs4POIiydmVKvZvqyUONH/K89Lfo54eFhUn2 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 --=-=-=-- From debbugs-submit-bounces@debbugs.gnu.org Wed Dec 02 06:26:50 2020 Received: (at 45006) by debbugs.gnu.org; 2 Dec 2020 11:26:50 +0000 Received: from localhost ([127.0.0.1]:34536 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1kkQHa-0002RG-5h for submit@debbugs.gnu.org; Wed, 02 Dec 2020 06:26:50 -0500 Received: from mail-wr1-f49.google.com ([209.85.221.49]:44857) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1kkQHX-0002Qj-07 for 45006@debbugs.gnu.org; Wed, 02 Dec 2020 06:26:47 -0500 Received: by mail-wr1-f49.google.com with SMTP id 64so3397755wra.11 for <45006@debbugs.gnu.org>; Wed, 02 Dec 2020 03:26:46 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=from:to:subject:in-reply-to:references:date:message-id:mime-version :content-transfer-encoding; bh=rjlsCWayrcUa+/BHeXCTEa2eIiPGoFe/XKXUwDM0VYM=; b=TnAENC2nlRhI/zAiq0AOx/mhYJUNMw3fbEEB3TjIboWdGVVflzaapnXFvX735C5H51 hPsxMzfRpjcoCZRaaCD5Y+4UnJECEgYGGQKLfWcQpT7nXWFmeJ9iNzyR6ZsmqeUEdk5j yXZweWmIg42OJG1QCcITNqLZXEe+E7KS0YmwHY3nRD31LOFZtJ/mU0GsO51PcJhAfiaZ gXnBQxnBqcQFB5t+1qwK5yuzxLCyk76IuhAMqEUjZ867ITjdYfG557bX/lEzoezahPso EBhw4p9QDFj5PFmi8I2vAtgt633EMnE+vMEVsH/L7VGA/ZrdlqqZPqUCX7tiH8ttWNDS /GOA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:subject:in-reply-to:references:date :message-id:mime-version:content-transfer-encoding; bh=rjlsCWayrcUa+/BHeXCTEa2eIiPGoFe/XKXUwDM0VYM=; b=preHnnCngT8S+xVobhyt8O03cEUskT9sBH6WtvFqr/zdaT9is6L7fB5YDxrj/Y7vK7 ReQttWkfrnpjEKtgV7DRC8At3FJJYZPhA0DdHeo2GzlcGGpMYEIfRLJtMZo+2gLgoZ8P qo++OLPXxg0qDBOfKa7LA4pYjPk0kPkZlSEvh9pxuOoakeQd0uVTFy3xO+Bk7l4vbzsb qTtBo0evzbYFKno9TTL9cEVNQzHvRkIuQKXbiQenruRfRrnOpz1FGMcitrLk0XESarGs hV9tNmNpPgGvrK2iNvUa+B6S9dM53qljePsb15AyaYHnPfdYUEmNp4wUNrraQQuXFdyO zsXg== X-Gm-Message-State: AOAM532PMPy5nyU+0F7QeBPR2L2Ol6iFXiXnRe69Kmuuo8MMfR8Sj1gJ CqR9NBX7/oq+GH83P9/oHx9fUClPOhJGVw== X-Google-Smtp-Source: ABdhPJwuurIkDfBavxyiqngj9GDYfdvolyiwm2qwPjuTUrxsn7Z1Aus3azs/QRA5mnJ/ceg+ccJbLg== X-Received: by 2002:adf:ff8e:: with SMTP id j14mr2926670wrr.48.1606908400927; Wed, 02 Dec 2020 03:26:40 -0800 (PST) Received: from lili ([2a01:e0a:59b:9120:65d2:2476:f637:db1e]) by smtp.gmail.com with ESMTPSA id e3sm1807286wro.90.2020.12.02.03.26.40 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 02 Dec 2020 03:26:40 -0800 (PST) From: zimoun To: Mathieu Othacehe , 45006@debbugs.gnu.org Subject: Re: [bug#45006] cuirass: Add remote build support. In-Reply-To: <87czzso4dj.fsf@gnu.org> References: <87czzso4dj.fsf@gnu.org> Date: Wed, 02 Dec 2020 12:25:41 +0100 Message-ID: <86pn3s1mbu.fsf@gmail.com> MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable X-Spam-Score: 0.0 (/) X-Debbugs-Envelope-To: 45006 X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.0 (-) Hi Mathieu, On Wed, 02 Dec 2020 at 12:04, Mathieu Othacehe wrote: > Here's a patch adding remote build support to Cuirass, as presented > during Guix Days[1]. The concept is the following: Neat! You implemented the =E2=80=9Cdynamic offloading=E2=80=9D in Cuirass.= \o/ What about the store? And the outputs? > This is still a bit rough on the edges, but I have tested it on berlin > spawning ~30 workers and building ~10K derivations, it seems to work > fine. ~30 workers on ~30 different machines? Or are some workers running on the same node? All the best, simon From debbugs-submit-bounces@debbugs.gnu.org Mon Dec 21 08:40:58 2020 Received: (at 45006) by debbugs.gnu.org; 21 Dec 2020 13:40:58 +0000 Received: from localhost ([127.0.0.1]:46530 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1krLQi-0006OU-9w for submit@debbugs.gnu.org; Mon, 21 Dec 2020 08:40:58 -0500 Received: from eggs.gnu.org ([209.51.188.92]:35590) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1krLQa-0006NZ-5p for 45006@debbugs.gnu.org; Mon, 21 Dec 2020 08:40:50 -0500 Received: from fencepost.gnu.org ([2001:470:142:3::e]:38000) by eggs.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1krLQU-0001so-Ul; Mon, 21 Dec 2020 08:40:38 -0500 Received: from [2a01:e0a:19b:d9a0:ac2a:8ddd:1dd0:ed79] (port=54484 helo=cervin) by fencepost.gnu.org with esmtpsa (TLS1.2:RSA_AES_256_CBC_SHA1:256) (Exim 4.82) (envelope-from ) id 1krLQT-00063y-Q1; Mon, 21 Dec 2020 08:40:38 -0500 From: Mathieu Othacehe To: zimoun Subject: Re: [bug#45006] cuirass: Add remote build support. References: <87czzso4dj.fsf@gnu.org> <86pn3s1mbu.fsf@gmail.com> Date: Mon, 21 Dec 2020 14:40:36 +0100 In-Reply-To: <86pn3s1mbu.fsf@gmail.com> (zimoun's message of "Wed, 02 Dec 2020 12:25:41 +0100") Message-ID: <87wnxb2s6j.fsf@gnu.org> User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/27.1 (gnu/linux) MIME-Version: 1.0 Content-Type: text/plain X-Spam-Score: -2.3 (--) X-Debbugs-Envelope-To: 45006 Cc: 45006@debbugs.gnu.org X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -3.3 (---) Hello, There's a new variant of this patch on the wip-offload branch of Cuirass. Quite a few things have changed since the first version: * The "remote-server" no longer communicates directly with Cuirass, all the exchanges are done through the database. * The "remote-worker" now honors "timeout" and "max-silent-time" package parameters. * I have added build priorities support. The build priority is computed this way: build_priority = specification_priority * 10 + package_priority * There's a new "worker" table that stores what workers are currently building, and an associated "/workers" page. * The "remote-worker" can connect to a "remote-server" specified on the command line. * The substitutes are downloaded and stored in the publish cache directory. I have deployed another Cuirass instance on berlin using this mechanism and workers on all build machines. It's been building master/core-updated/staging/modular specifications since a few days for x86_64-linux and i686-linux architectures. The results are really better than using the current implementation and it should be possible to transition to this new architecture soon. Thanks, Mathieu From debbugs-submit-bounces@debbugs.gnu.org Mon Dec 21 09:22:31 2020 Received: (at 45006) by debbugs.gnu.org; 21 Dec 2020 14:22:31 +0000 Received: from localhost ([127.0.0.1]:46607 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1krM51-0000eL-Ii for submit@debbugs.gnu.org; Mon, 21 Dec 2020 09:22:31 -0500 Received: from mail-wr1-f53.google.com ([209.85.221.53]:40206) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1krM4z-0000dp-Ko for 45006@debbugs.gnu.org; Mon, 21 Dec 2020 09:22:30 -0500 Received: by mail-wr1-f53.google.com with SMTP id 91so11237930wrj.7 for <45006@debbugs.gnu.org>; Mon, 21 Dec 2020 06:22:29 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=from:to:cc:subject:in-reply-to:references:date:message-id :mime-version; bh=PX53m6mVsnV11FJlHRaPBkznztrHCqqdqyhFisbJRpM=; b=EHgIzMZgSSusRVjHzMTmTtqwUtL7DNPpaoV6sINUGdHnHh+DTykjx47Ut5mbJm0mXL q7Gb1e2DoToJ9xQ2VECPX/Vh4wd6YA+H279tMZxYtjuSJMg0qdSYHuF994bBFr6U1WcO DPaaIqLMj5QWuNFKv8rYcwRWN3eonjRvEFMVDu0xi8nS1UjbAamTqQhz/e4ICHIc+MfR IiYbHIKdBDCKrPDodx33ja++D/HbZGnhpP+7g4JyjZjwo5d2YRG5PPFarC992Q4DArc8 7xwdTYnMJ26T4QUz34HS545UPu6dyMvK8xysBjEwV+jkeUPeQ8YtYLvoDb6sXgTJA3nq TKtw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:cc:subject:in-reply-to:references:date :message-id:mime-version; bh=PX53m6mVsnV11FJlHRaPBkznztrHCqqdqyhFisbJRpM=; b=dIsQ4zIED2v2UpxkZwN8YiGJEye9LZQXMjLD2gudK8kwZvsm+f/yl/sWL1OnSrzpwW 0xXxWG11Wur8T2KtLZZL2SyHuCp8rnTHAKVkaeulHfZdrfuqVCe0+pXKfDea2Z/Ery8X GGa08pZEJCQMvQBhfJAeOToqLd6ASxWLvHGgfP9zuJQqQAnREDzYj2xVgIZGaTCO6TDu aievsZa1lgCvcGAmu8wiBR2/1e/rajdLzIhGMwk9wwQpxlfnNs7tbW35jtr1iNNZIjxS 1P8Gfga4yIM/kt4eAx6+kosHPKnYpu2zuDD2EZS0ZZYCnouN8b027Lf0YLNEVJFymMy6 P+6A== X-Gm-Message-State: AOAM530miCi3PmBOg02m4cizPfdQeWW1stmpnE4OTqGhSdCFv+ZkX0cd hboj84wkIi+xtLuGAvTa4uxjGhrI9yw= X-Google-Smtp-Source: ABdhPJz3ZVRF5jR/hAQcrPyciibDV2eLtylyJq7S3KwCeK1QgJlG6+8Ns1iyD8hYPX02X7eydNl8aw== X-Received: by 2002:a5d:4104:: with SMTP id l4mr19248768wrp.340.1608560543543; Mon, 21 Dec 2020 06:22:23 -0800 (PST) Received: from lili (oul69-1-82-232-2-3.fbx.proxad.net. [82.232.2.3]) by smtp.gmail.com with ESMTPSA id j7sm23679760wmb.40.2020.12.21.06.22.22 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 21 Dec 2020 06:22:23 -0800 (PST) From: zimoun To: Mathieu Othacehe Subject: Re: [bug#45006] cuirass: Add remote build support. In-Reply-To: <87wnxb2s6j.fsf@gnu.org> References: <87czzso4dj.fsf@gnu.org> <86pn3s1mbu.fsf@gmail.com> <87wnxb2s6j.fsf@gnu.org> Date: Mon, 21 Dec 2020 15:13:47 +0100 Message-ID: <86czz39rhg.fsf@gmail.com> MIME-Version: 1.0 Content-Type: text/plain X-Spam-Score: 0.0 (/) X-Debbugs-Envelope-To: 45006 Cc: 45006@debbugs.gnu.org X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.0 (-) Hi Mathieu, On Mon, 21 Dec 2020 at 14:40, Mathieu Othacehe wrote: > * I have added build priorities support. The build priority is computed > this way: > > build_priority = specification_priority * 10 + package_priority Where are these specification_priority and package_priority configured? > * The substitutes are downloaded and stored in the publish cache > directory. This removes part of issues about GC on The Big Store, right? > The results are really better than using the current implementation and > it should be possible to transition to this new architecture soon. Cool! Thanks for working on this. :-) Cheers, simon From debbugs-submit-bounces@debbugs.gnu.org Mon Dec 21 10:08:18 2020 Received: (at 45006) by debbugs.gnu.org; 21 Dec 2020 15:08:18 +0000 Received: from localhost ([127.0.0.1]:47996 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1krMnJ-0003mh-O3 for submit@debbugs.gnu.org; Mon, 21 Dec 2020 10:08:17 -0500 Received: from eggs.gnu.org ([209.51.188.92]:54122) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1krMnH-0003mR-7m for 45006@debbugs.gnu.org; Mon, 21 Dec 2020 10:08:16 -0500 Received: from fencepost.gnu.org ([2001:470:142:3::e]:39045) by eggs.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1krMnC-0006ul-3F; Mon, 21 Dec 2020 10:08:10 -0500 Received: from [2a01:e0a:19b:d9a0:ac2a:8ddd:1dd0:ed79] (port=54878 helo=cervin) by fencepost.gnu.org with esmtpsa (TLS1.2:RSA_AES_256_CBC_SHA1:256) (Exim 4.82) (envelope-from ) id 1krMnB-0003Wq-IZ; Mon, 21 Dec 2020 10:08:09 -0500 From: Mathieu Othacehe To: zimoun Subject: Re: [bug#45006] cuirass: Add remote build support. References: <87czzso4dj.fsf@gnu.org> <86pn3s1mbu.fsf@gmail.com> <87wnxb2s6j.fsf@gnu.org> <86czz39rhg.fsf@gmail.com> Date: Mon, 21 Dec 2020 16:08:07 +0100 In-Reply-To: <86czz39rhg.fsf@gmail.com> (zimoun's message of "Mon, 21 Dec 2020 15:13:47 +0100") Message-ID: <87pn332o4o.fsf@gnu.org> User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/27.1 (gnu/linux) MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable X-Spam-Score: -2.3 (--) X-Debbugs-Envelope-To: 45006 Cc: 45006@debbugs.gnu.org X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -3.3 (---) Hey zimoun, > Where are these specification_priority and package_priority configured? specification_priority comes from the new "#:priority" field in the Cuirass specification file and package_priority comes from the new "#:priority" field in the job structure. specification_priority =E2=88=88 [0, 9] package_priority =E2=88=88 [0, 9] =E2=87=92 build_priority =E2=88=88 [0, 99] where 0 is the maximal priority. When both specification_priority and package_priority are unset, the priority defaults to 99. I'm currently using the following priorities: modular: 1 guix-master:2 staging:3 core-updates:4 The builds are picked according to their priority and then their timestamp, so that the most recent builds are picked first when the priorities are identical. I don't have a strategy regarding package_priority yet. > This removes part of issues about GC on The Big Store, right? Yes, this should allow to garbage collect way more aggressively the store on berlin and hopefully reduce the GC duration. Thanks, Mathieu From debbugs-submit-bounces@debbugs.gnu.org Mon Dec 21 11:45:37 2020 Received: (at 45006) by debbugs.gnu.org; 21 Dec 2020 16:45:37 +0000 Received: from localhost ([127.0.0.1]:48151 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1krOJU-0006nS-QC for submit@debbugs.gnu.org; Mon, 21 Dec 2020 11:45:37 -0500 Received: from mail-wr1-f50.google.com ([209.85.221.50]:41989) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1krOJS-0006nC-FS for 45006@debbugs.gnu.org; Mon, 21 Dec 2020 11:45:35 -0500 Received: by mail-wr1-f50.google.com with SMTP id m5so11678724wrx.9 for <45006@debbugs.gnu.org>; Mon, 21 Dec 2020 08:45:34 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=from:to:cc:subject:in-reply-to:references:date:message-id :mime-version:content-transfer-encoding; bh=vw0/pAtRCSer0g1gLDt3NqekypynCv88iwtBRrUDa3s=; b=c2u9F8tkxKXAC0SivIH8b7AzOsVZOiLi22RnCrpeymDTieDI+a4YttvT90cQeJxgZF uZSxXxhES2YWWmepEP+jmHPMUuDomaWTgZKZhsuUl9w45i/xzhc/JKe5dyrA+rlQxibl qMymNuVVSqyoEdD2W5bREY3o7T0vL1W3j6GLKlm4lJ8abiXwoky35vkNoIXr975SDyht mbNP89OOfdYwGLMyQYRFCGpY11ZWaRcB1RgIjeKEuU6kFbzPDvORa4fIMFSHLY9kWP9K A70ACZXgW+NCH0qgzwN+Hfoa2OgJPIDHAr8IBjwoH1RZP1zyWggx0/sT2fTv5DV3g4M5 /ezg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:cc:subject:in-reply-to:references:date :message-id:mime-version:content-transfer-encoding; bh=vw0/pAtRCSer0g1gLDt3NqekypynCv88iwtBRrUDa3s=; b=IcS7sgE4yPJDaY6gnJWdw8xKhoFOWsV1P943KGoVDk6zxt3tqHNY/F4TAMRERgSIg6 5Rx/ggoTeVk9D3rUzLXBIt8nq8960EcGnCp02iQoyxq5OFxoHgo6VF7LXzv3tpnOHCLK XcuCBtlQbMJuyUs0hSdoUmBVcuzOWSi70SXRE79PyVlpK0JvvGr2B6laNIwKMNmxolYR SL+QzIqgF6iq5yz89Hr6ODtZF1x/yyey6u6Geq0QHQToRdxbIPdaM3nXCNn3UcF/sLai w4vjujveyufWmA4JzuoXUfTESbZq1Ws0/j31+X6cq9iY4Pt7VWYQ4nTyTshB+2fJhqAA X1mA== X-Gm-Message-State: AOAM532yhGkNLen4atBNZLPyo6Hsjl/d7BC14Q1e2gKw9WXg3Anl/+sC e+UclTq+w/il3uX8J2BaJlZ18KjlyIE= X-Google-Smtp-Source: ABdhPJwKN4xrctA5vKLwVuSN3ZURfoQRlwaIIHfqVBGuOQ8u7OsVWE2klawt0bS6Mct01ljXjICD/g== X-Received: by 2002:a5d:68ce:: with SMTP id p14mr19529845wrw.386.1608569128481; Mon, 21 Dec 2020 08:45:28 -0800 (PST) Received: from lili (oul69-1-82-232-2-3.fbx.proxad.net. [82.232.2.3]) by smtp.gmail.com with ESMTPSA id i18sm28166115wrp.74.2020.12.21.08.45.27 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 21 Dec 2020 08:45:28 -0800 (PST) From: zimoun To: Mathieu Othacehe Subject: Re: [bug#45006] cuirass: Add remote build support. In-Reply-To: <87pn332o4o.fsf@gnu.org> References: <87czzso4dj.fsf@gnu.org> <86pn3s1mbu.fsf@gmail.com> <87wnxb2s6j.fsf@gnu.org> <86czz39rhg.fsf@gmail.com> <87pn332o4o.fsf@gnu.org> Date: Mon, 21 Dec 2020 17:41:21 +0100 Message-ID: <867dpb9kni.fsf@gmail.com> MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable X-Spam-Score: 0.0 (/) X-Debbugs-Envelope-To: 45006 Cc: 45006@debbugs.gnu.org X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.0 (-) Hi Mathieu, Thanks for the explanations. On Mon, 21 Dec 2020 at 16:08, Mathieu Othacehe wrote: >> Where are these specification_priority and package_priority configured? > > specification_priority comes from the new "#:priority" field in the > Cuirass specification file and package_priority comes from the new > "#:priority" field in the job structure. > > specification_priority =E2=88=88 [0, 9] > package_priority =E2=88=88 [0, 9] > > =E2=87=92 build_priority =E2=88=88 [0, 99] > > where 0 is the maximal priority. When both specification_priority and > package_priority are unset, the priority defaults to 99. > > I'm currently using the following priorities: > > modular: 1 > guix-master:2 > staging:3 > core-updates:4 Is this the specification_priority? Where would the package_priority be defined? A file mapping the package name to the priority number? Something else? > The builds are picked according to their priority and then their > timestamp, so that the most recent builds are picked first when the > priorities are identical. First are high priority, whatever the timestamp is, right? > I don't have a strategy regarding package_priority yet. Why do you need a package_priority strategy? You only need a #:priority strategy and a formula to compute it with the relevant parameters, here specification_priority and package_priority. Or I miss something. In my understanding, the priority is given by something as: #:priority =3D f(specification_priority, package_priority, timestamp) where, for example, once a week the queue is reevaluated to increase the old; otherwise some could be blocked. >> This removes part of issues about GC on The Big Store, right? > > Yes, this should allow to garbage collect way more aggressively the > store on berlin and hopefully reduce the GC duration. Really cool! Cheers, simon From debbugs-submit-bounces@debbugs.gnu.org Fri Jan 29 05:58:19 2021 Received: (at control) by debbugs.gnu.org; 29 Jan 2021 10:58:19 +0000 Received: from localhost ([127.0.0.1]:51236 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1l5RTm-0003h5-Qr for submit@debbugs.gnu.org; Fri, 29 Jan 2021 05:58:19 -0500 Received: from eggs.gnu.org ([209.51.188.92]:41140) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1l5RTi-0003gn-IC for control@debbugs.gnu.org; Fri, 29 Jan 2021 05:58:17 -0500 Received: from fencepost.gnu.org ([2001:470:142:3::e]:39569) by eggs.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1l5RTa-0005h8-76 for control@debbugs.gnu.org; Fri, 29 Jan 2021 05:58:09 -0500 Received: from [2a01:e0a:19b:d9a0:10b6:a2c3:9f4e:6dae] (port=55468 helo=cervin) by fencepost.gnu.org with esmtpsa (TLS1.2:RSA_AES_256_CBC_SHA1:256) (Exim 4.82) (envelope-from ) id 1l5RTX-0003tI-NG for control@debbugs.gnu.org; Fri, 29 Jan 2021 05:58:04 -0500 Date: Fri, 29 Jan 2021 11:58:02 +0100 Message-Id: <87a6ssj9hh.fsf@cervin.i-did-not-set--mail-host-address--so-tickle-me> To: control@debbugs.gnu.org From: Mathieu Othacehe Subject: control message for bug #45006 X-Spam-Score: -0.3 (/) X-Debbugs-Envelope-To: control X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: debbugs-submit-bounces@debbugs.gnu.org Sender: "Debbugs-submit" X-Spam-Score: -1.3 (-) close 45006 quit From unknown Sun Aug 17 01:22:15 2025 Received: (at fakecontrol) by fakecontrolmessage; To: internal_control@debbugs.gnu.org From: Debbugs Internal Request Subject: Internal Control Message-Id: bug archived. Date: Fri, 26 Feb 2021 12:24:04 +0000 User-Agent: Fakemail v42.6.9 # This is a fake control message. # # The action: # bug archived. thanks # This fakemail brought to you by your local debbugs # administrator