summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/guix.texi46
-rw-r--r--guix/scripts/publish.scm197
-rw-r--r--tests/publish.scm54
3 files changed, 280 insertions, 17 deletions
diff --git a/doc/guix.texi b/doc/guix.texi
index fd3483ee5d4..bbb2ba732d4 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -6522,6 +6522,13 @@ archive}), the daemon may download substitutes from it:
6522guix-daemon --substitute-urls=http://example.org:8080 6522guix-daemon --substitute-urls=http://example.org:8080
6523@end example 6523@end example
6524 6524
6525By default, @command{guix publish} compresses archives on the fly as it
6526serves them. This ``on-the-fly'' mode is convenient in that it requires
6527no setup and is immediately available. However, when serving lots of
6528clients, we recommend using the @option{--cache} option, which enables
6529caching of the archives before they are sent to clients---see below for
6530details.
6531
6525As a bonus, @command{guix publish} also serves as a content-addressed 6532As a bonus, @command{guix publish} also serves as a content-addressed
6526mirror for source files referenced in @code{origin} records 6533mirror for source files referenced in @code{origin} records
6527(@pxref{origin Reference}). For instance, assuming @command{guix 6534(@pxref{origin Reference}). For instance, assuming @command{guix
@@ -6559,10 +6566,43 @@ disable compression. The range 1 to 9 corresponds to different gzip
6559compression levels: 1 is the fastest, and 9 is the best (CPU-intensive). 6566compression levels: 1 is the fastest, and 9 is the best (CPU-intensive).
6560The default is 3. 6567The default is 3.
6561 6568
6562Compression occurs on the fly and the compressed streams are not 6569Unless @option{--cache} is used, compression occurs on the fly and
6570the compressed streams are not
6563cached. Thus, to reduce load on the machine that runs @command{guix 6571cached. Thus, to reduce load on the machine that runs @command{guix
6564publish}, it may be a good idea to choose a low compression level, or to 6572publish}, it may be a good idea to choose a low compression level, to
6565run @command{guix publish} behind a caching proxy. 6573run @command{guix publish} behind a caching proxy, or to use
6574@option{--cache}. Using @option{--cache} has the advantage that it
6575allows @command{guix publish} to add @code{Content-Length} HTTP header
6576to its responses.
6577
6578@item --cache=@var{directory}
6579@itemx -c @var{directory}
6580Cache archives and meta-data (@code{.narinfo} URLs) to @var{directory}
6581and only serve archives that are in cache.
6582
6583When this option is omitted, archives and meta-data are created
6584on-the-fly. This can reduce the available bandwidth, especially when
6585compression is enabled, since this may become CPU-bound. Another
6586drawback of the default mode is that the length of archives is not known
6587in advance, so @command{guix publish} does not add a
6588@code{Content-Length} HTTP header to its responses, which in turn
6589prevents clients from knowing the amount of data being downloaded.
6590
6591Conversely, when @option{--cache} is used, the first request for a store
6592item (@i{via} a @code{.narinfo} URL) returns 404 and triggers a
6593background process to @dfn{bake} the archive---computing its
6594@code{.narinfo} and compressing the archive, if needed. Once the
6595archive is cached in @var{directory}, subsequent requests succeed and
6596are served directly from the cache, which guarantees that clients get
6597the best possible bandwidth.
6598
6599The ``baking'' process is performed by worker threads. By default, one
6600thread per CPU core is created, but this can be customized. See
6601@option{--workers} below.
6602
6603@item --workers=@var{N}
6604When @option{--cache} is used, request the allocation of @var{N} worker
6605threads to ``bake'' archives.
6566 6606
6567@item --ttl=@var{ttl} 6607@item --ttl=@var{ttl}
6568Produce @code{Cache-Control} HTTP headers that advertise a time-to-live 6608Produce @code{Cache-Control} HTTP headers that advertise a time-to-live
diff --git a/guix/scripts/publish.scm b/guix/scripts/publish.scm
index f54757b4c90..70d914d60c4 100644
--- a/guix/scripts/publish.scm
+++ b/guix/scripts/publish.scm
@@ -24,6 +24,7 @@
24 #:use-module (ice-9 match) 24 #:use-module (ice-9 match)
25 #:use-module (ice-9 regex) 25 #:use-module (ice-9 regex)
26 #:use-module (ice-9 rdelim) 26 #:use-module (ice-9 rdelim)
27 #:use-module (ice-9 threads)
27 #:use-module (rnrs bytevectors) 28 #:use-module (rnrs bytevectors)
28 #:use-module (srfi srfi-1) 29 #:use-module (srfi srfi-1)
29 #:use-module (srfi srfi-2) 30 #:use-module (srfi srfi-2)
@@ -45,13 +46,15 @@
45 #:use-module (guix hash) 46 #:use-module (guix hash)
46 #:use-module (guix pki) 47 #:use-module (guix pki)
47 #:use-module (guix pk-crypto) 48 #:use-module (guix pk-crypto)
49 #:use-module (guix workers)
48 #:use-module (guix store) 50 #:use-module (guix store)
49 #:use-module ((guix serialization) #:select (write-file)) 51 #:use-module ((guix serialization) #:select (write-file))
50 #:use-module (guix zlib) 52 #:use-module (guix zlib)
51 #:use-module (guix ui) 53 #:use-module (guix ui)
52 #:use-module (guix scripts) 54 #:use-module (guix scripts)
53 #:use-module ((guix utils) #:select (compressed-file?)) 55 #:use-module ((guix utils)
54 #:use-module ((guix build utils) #:select (dump-port)) 56 #:select (with-atomic-file-output compressed-file?))
57 #:use-module ((guix build utils) #:select (dump-port mkdir-p))
55 #:export (%public-key 58 #:export (%public-key
56 %private-key 59 %private-key
57 60
@@ -70,6 +73,10 @@ Publish ~a over HTTP.\n") %store-directory)
70 -C, --compression[=LEVEL] 73 -C, --compression[=LEVEL]
71 compress archives at LEVEL")) 74 compress archives at LEVEL"))
72 (display (_ " 75 (display (_ "
76 -c, --cache=DIRECTORY cache published items to DIRECTORY"))
77 (display (_ "
78 --workers=N use N workers to bake items"))
79 (display (_ "
73 --ttl=TTL announce narinfos can be cached for TTL seconds")) 80 --ttl=TTL announce narinfos can be cached for TTL seconds"))
74 (display (_ " 81 (display (_ "
75 --nar-path=PATH use PATH as the prefix for nar URLs")) 82 --nar-path=PATH use PATH as the prefix for nar URLs"))
@@ -154,6 +161,13 @@ if ITEM is already compressed."
154 (warning (_ "zlib support is missing; \ 161 (warning (_ "zlib support is missing; \
155compression disabled~%")) 162compression disabled~%"))
156 result)))))) 163 result))))))
164 (option '(#\c "cache") #t #f
165 (lambda (opt name arg result)
166 (alist-cons 'cache arg result)))
167 (option '("workers") #t #f
168 (lambda (opt name arg result)
169 (alist-cons 'workers (string->number* arg)
170 result)))
157 (option '("ttl") #t #f 171 (option '("ttl") #t #f
158 (lambda (opt name arg result) 172 (lambda (opt name arg result)
159 (let ((duration (string->duration arg))) 173 (let ((duration (string->duration arg)))
@@ -190,6 +204,9 @@ compression disabled~%"))
190 %default-gzip-compression 204 %default-gzip-compression
191 %no-compression)) 205 %no-compression))
192 206
207 ;; Default number of workers when caching is enabled.
208 (workers . ,(current-processor-count))
209
193 (address . ,(make-socket-address AF_INET INADDR_ANY 0)) 210 (address . ,(make-socket-address AF_INET INADDR_ANY 0))
194 (repl . #f))) 211 (repl . #f)))
195 212
@@ -308,6 +325,121 @@ appropriate duration. NAR-PATH specifies the prefix for nar URLs."
308 #:compression compression) 325 #:compression compression)
309 <>))))) 326 <>)))))
310 327
328(define* (nar-cache-file directory item
329 #:key (compression %no-compression))
330 (string-append directory "/"
331 (symbol->string (compression-type compression))
332 "/" (basename item) ".nar"))
333
334(define* (narinfo-cache-file directory item
335 #:key (compression %no-compression))
336 (string-append directory "/"
337 (symbol->string (compression-type compression))
338 "/" (basename item)
339 ".narinfo"))
340
341(define run-single-baker
342 (let ((baking (make-weak-value-hash-table))
343 (mutex (make-mutex)))
344 (lambda (item thunk)
345 "Run THUNK, which is supposed to bake ITEM, but make sure only one
346thread is baking ITEM at a given time."
347 (define selected?
348 (with-mutex mutex
349 (and (not (hash-ref baking item))
350 (begin
351 (hash-set! baking item (current-thread))
352 #t))))
353
354 (when selected?
355 (dynamic-wind
356 (const #t)
357 thunk
358 (lambda ()
359 (with-mutex mutex
360 (hash-remove! baking item))))))))
361
362(define-syntax-rule (single-baker item exp ...)
363 "Bake ITEM by evaluating EXP, but make sure there's only one baker for ITEM
364at a time."
365 (run-single-baker item (lambda () exp ...)))
366
367
368(define* (render-narinfo/cached store request hash
369 #:key ttl (compression %no-compression)
370 (nar-path "nar")
371 cache pool)
372 "Respond to the narinfo request for REQUEST. If the narinfo is available in
373CACHE, then send it; otherwise, return 404 and \"bake\" that nar and narinfo
374requested using POOL."
375 (let* ((item (hash-part->path store hash))
376 (compression (actual-compression item compression))
377 (cached (and (not (string-null? item))
378 (narinfo-cache-file cache item
379 #:compression compression))))
380 (cond ((string-null? item)
381 (not-found request))
382 ((file-exists? cached)
383 ;; Narinfo is in cache, send it.
384 (values `((content-type . (application/x-nix-narinfo))
385 ,@(if ttl
386 `((cache-control (max-age . ,ttl)))
387 '()))
388 (lambda (port)
389 (display (call-with-input-file cached
390 read-string)
391 port))))
392 ((valid-path? store item)
393 ;; Nothing in cache: bake the narinfo and nar in the background and
394 ;; return 404.
395 (eventually pool
396 (single-baker item
397 ;; (format #t "baking ~s~%" item)
398 (bake-narinfo+nar cache item
399 #:ttl ttl
400 #:compression compression
401 #:nar-path nar-path)))
402 (not-found request))
403 (else
404 (not-found request)))))
405
406(define* (bake-narinfo+nar cache item
407 #:key ttl (compression %no-compression)
408 (nar-path "/nar"))
409 "Write the narinfo and nar for ITEM to CACHE."
410 (let* ((compression (actual-compression item compression))
411 (nar (nar-cache-file cache item
412 #:compression compression))
413 (narinfo (narinfo-cache-file cache item
414 #:compression compression)))
415
416 (mkdir-p (dirname nar))
417 (match (compression-type compression)
418 ('gzip
419 ;; Note: the file port gets closed along with the gzip port.
420 (call-with-gzip-output-port (open-output-file (string-append nar ".tmp"))
421 (lambda (port)
422 (write-file item port))
423 #:level (compression-level compression))
424 (rename-file (string-append nar ".tmp") nar))
425 ('none
426 ;; When compression is disabled, we retrieve files directly from the
427 ;; store; no need to cache them.
428 #t))
429
430 (mkdir-p (dirname narinfo))
431 (with-atomic-file-output narinfo
432 (lambda (port)
433 ;; Open a new connection to the store. We cannot reuse the main
434 ;; thread's connection to the store since we would end up sending
435 ;; stuff concurrently on the same channel.
436 (with-store store
437 (display (narinfo-string store item
438 (%private-key)
439 #:nar-path nar-path
440 #:compression compression)
441 port))))))
442
311;; XXX: Declare the 'Guix-Compression' HTTP header, which is in fact for 443;; XXX: Declare the 'Guix-Compression' HTTP header, which is in fact for
312;; internal consumption: it allows us to pass the compression info to 444;; internal consumption: it allows us to pass the compression info to
313;; 'http-write', as part of the workaround to <http://bugs.gnu.org/21093>. 445;; 'http-write', as part of the workaround to <http://bugs.gnu.org/21093>.
@@ -339,6 +471,21 @@ appropriate duration. NAR-PATH specifies the prefix for nar URLs."
339 store-path) 471 store-path)
340 (not-found request)))) 472 (not-found request))))
341 473
474(define* (render-nar/cached store cache request store-item
475 #:key (compression %no-compression))
476 "Respond to REQUEST with a nar for STORE-ITEM. If the nar is in CACHE,
477return it; otherwise, return 404."
478 (let ((cached (nar-cache-file cache store-item
479 #:compression compression)))
480 (if (file-exists? cached)
481 (values `((content-type . (application/octet-stream
482 (charset . "ISO-8859-1"))))
483 ;; XXX: We're not returning the actual contents, deferring
484 ;; instead to 'http-write'. This is a hack to work around
485 ;; <http://bugs.gnu.org/21093>.
486 cached)
487 (not-found request))))
488
342(define (render-content-addressed-file store request 489(define (render-content-addressed-file store request
343 name algo hash) 490 name algo hash)
344 "Return the content of the result of the fixed-output derivation NAME that 491 "Return the content of the result of the fixed-output derivation NAME that
@@ -495,6 +642,7 @@ blocking."
495 642
496(define* (make-request-handler store 643(define* (make-request-handler store
497 #:key 644 #:key
645 cache pool
498 narinfo-ttl 646 narinfo-ttl
499 (nar-path "nar") 647 (nar-path "nar")
500 (compression %no-compression)) 648 (compression %no-compression))
@@ -515,10 +663,17 @@ blocking."
515 (((= extract-narinfo-hash (? string? hash))) 663 (((= extract-narinfo-hash (? string? hash)))
516 ;; TODO: Register roots for HASH that will somehow remain for 664 ;; TODO: Register roots for HASH that will somehow remain for
517 ;; NARINFO-TTL. 665 ;; NARINFO-TTL.
518 (render-narinfo store request hash 666 (if cache
519 #:ttl narinfo-ttl 667 (render-narinfo/cached store request hash
520 #:nar-path nar-path 668 #:cache cache
521 #:compression compression)) 669 #:pool pool
670 #:ttl narinfo-ttl
671 #:nar-path nar-path
672 #:compression compression)
673 (render-narinfo store request hash
674 #:ttl narinfo-ttl
675 #:nar-path nar-path
676 #:compression compression)))
522 ;; /nar/file/NAME/sha256/HASH 677 ;; /nar/file/NAME/sha256/HASH
523 (("file" name "sha256" hash) 678 (("file" name "sha256" hash)
524 (guard (c ((invalid-base32-character? c) 679 (guard (c ((invalid-base32-character? c)
@@ -534,13 +689,16 @@ blocking."
534 ;; /nar/gzip/<store-item> 689 ;; /nar/gzip/<store-item>
535 ((components ... "gzip" store-item) 690 ((components ... "gzip" store-item)
536 (if (and (nar-path? components) (zlib-available?)) 691 (if (and (nar-path? components) (zlib-available?))
537 (render-nar store request store-item 692 (let ((compression (match compression
538 #:compression 693 (($ <compression> 'gzip)
539 (match compression 694 compression)
540 (($ <compression> 'gzip) 695 (_
541 compression) 696 %default-gzip-compression))))
542 (_ 697 (if cache
543 %default-gzip-compression))) 698 (render-nar/cached store cache request store-item
699 #:compression compression)
700 (render-nar store request store-item
701 #:compression compression)))
544 (not-found request))) 702 (not-found request)))
545 703
546 ;; /nar/<store-item> 704 ;; /nar/<store-item>
@@ -555,8 +713,11 @@ blocking."
555 713
556(define* (run-publish-server socket store 714(define* (run-publish-server socket store
557 #:key (compression %no-compression) 715 #:key (compression %no-compression)
558 (nar-path "nar") narinfo-ttl) 716 (nar-path "nar") narinfo-ttl
717 cache pool)
559 (run-server (make-request-handler store 718 (run-server (make-request-handler store
719 #:cache cache
720 #:pool pool
560 #:nar-path nar-path 721 #:nar-path nar-path
561 #:narinfo-ttl narinfo-ttl 722 #:narinfo-ttl narinfo-ttl
562 #:compression compression) 723 #:compression compression)
@@ -606,6 +767,8 @@ blocking."
606 (socket (open-server-socket address)) 767 (socket (open-server-socket address))
607 (nar-path (assoc-ref opts 'nar-path)) 768 (nar-path (assoc-ref opts 'nar-path))
608 (repl-port (assoc-ref opts 'repl)) 769 (repl-port (assoc-ref opts 'repl))
770 (cache (assoc-ref opts 'cache))
771 (workers (assoc-ref opts 'workers))
609 772
610 ;; Read the key right away so that (1) we fail early on if we can't 773 ;; Read the key right away so that (1) we fail early on if we can't
611 ;; access them, and (2) we can then drop privileges. 774 ;; access them, and (2) we can then drop privileges.
@@ -631,6 +794,12 @@ consider using the '--user' option!~%")))
631 (repl:spawn-server (repl:make-tcp-server-socket #:port repl-port))) 794 (repl:spawn-server (repl:make-tcp-server-socket #:port repl-port)))
632 (with-store store 795 (with-store store
633 (run-publish-server socket store 796 (run-publish-server socket store
797 #:cache cache
798 #:pool (and cache (make-pool workers))
634 #:nar-path nar-path 799 #:nar-path nar-path
635 #:compression compression 800 #:compression compression
636 #:narinfo-ttl ttl)))))) 801 #:narinfo-ttl ttl))))))
802
803;;; Local Variables:
804;;; eval: (put 'single-baker 'scheme-indent-function 1)
805;;; End:
diff --git a/tests/publish.scm b/tests/publish.scm
index ea0f4a34776..233b71ce93a 100644
--- a/tests/publish.scm
+++ b/tests/publish.scm
@@ -314,4 +314,58 @@ References: ~%"
314 (call-with-input-string "" port-sha256)))))) 314 (call-with-input-string "" port-sha256))))))
315 (response-code (http-get uri)))) 315 (response-code (http-get uri))))
316 316
317(unless (zlib-available?)
318 (test-skip 1))
319(test-equal "with cache"
320 (list #t
321 `(("StorePath" . ,%item)
322 ("URL" . ,(string-append "nar/gzip/" (basename %item)))
323 ("Compression" . "gzip"))
324 200 ;nar/gzip/…
325 #t ;Content-Length
326 200) ;nar/…
327 (call-with-temporary-directory
328 (lambda (cache)
329 (define (wait-for-file file)
330 (let loop ((i 20))
331 (or (file-exists? file)
332 (begin
333 (pk 'wait-for-file file)
334 (sleep 1)
335 (loop (- i 1))))))
336
337 (let ((thread (with-separate-output-ports
338 (call-with-new-thread
339 (lambda ()
340 (guix-publish "--port=6797" "-C2"
341 (string-append "--cache=" cache)))))))
342 (wait-until-ready 6797)
343 (let* ((base "http://localhost:6797/")
344 (part (store-path-hash-part %item))
345 (url (string-append base part ".narinfo"))
346 (nar-url (string-append base "/nar/gzip/" (basename %item)))
347 (cached (string-append cache "/gzip/" (basename %item)
348 ".narinfo"))
349 (nar (string-append cache "/gzip/"
350 (basename %item) ".nar"))
351 (response (http-get url)))
352 (and (= 404 (response-code response))
353 (wait-for-file cached)
354 (let ((body (http-get-port url))
355 (compressed (http-get nar-url))
356 (uncompressed (http-get (string-append base "nar/"
357 (basename %item)))))
358 (list (file-exists? nar)
359 (filter (lambda (item)
360 (match item
361 (("Compression" . _) #t)
362 (("StorePath" . _) #t)
363 (("URL" . _) #t)
364 (_ #f)))
365 (recutils->alist body))
366 (response-code compressed)
367 (= (response-content-length compressed)
368 (stat:size (stat nar)))
369 (response-code uncompressed)))))))))
370
317(test-end "publish") 371(test-end "publish")