substitute-binary: Avoid dangling connections to the server.

* guix/web.scm (open-socket-for-uri): New procedure.
  (http-fetch): Add `port' keyword parameter; use it.
* guix/scripts/substitute-binary.scm (%random-state): New variable.
  (with-timeout): Wait a little before retrying.
  (fetch): Use `open-socket-for-uri', and keep a copy of the socket in
  variable `port'.  Close PORT upon timeout.
This commit is contained in:
Ludovic Courtès 2013-06-29 22:10:06 +02:00
parent 013ce67b19
commit bb7dcaea57
2 changed files with 84 additions and 66 deletions

View file

@ -124,6 +124,9 @@ (define %fetch-timeout
;; Number of seconds after which networking is considered "slow". ;; Number of seconds after which networking is considered "slow".
3) 3)
(define %random-state
(seed->random-state (+ (ash (cdr (gettimeofday)) 32) (getpid))))
(define-syntax-rule (with-timeout duration handler body ...) (define-syntax-rule (with-timeout duration handler body ...)
"Run BODY; when DURATION seconds have expired, call HANDLER, and run BODY "Run BODY; when DURATION seconds have expired, call HANDLER, and run BODY
again." again."
@ -140,11 +143,15 @@ (define-syntax-rule (with-timeout duration handler body ...)
(lambda () (lambda ()
body ...) body ...)
(lambda args (lambda args
;; The SIGALRM triggers EINTR. When that happens, try again. ;; The SIGALRM triggers EINTR, because of the bug at
;; Note: SA_RESTART cannot be used because of ;; <http://lists.gnu.org/archive/html/guile-devel/2013-06/msg00050.html>.
;; <http://bugs.gnu.org/14640>. ;; When that happens, try again. Note: SA_RESTART cannot be
;; used because of <http://bugs.gnu.org/14640>.
(if (= EINTR (system-error-errno args)) (if (= EINTR (system-error-errno args))
(try) (begin
;; Wait a little to avoid bursts.
(usleep (random 3000000 %random-state))
(try))
(apply throw args)))))) (apply throw args))))))
(lambda result (lambda result
(alarm 0) (alarm 0)
@ -168,14 +175,19 @@ (define* (fetch uri #:key (buffered? #t) (timeout? #t))
;; sudo tc qdisc add dev eth0 root netem delay 1500ms ;; sudo tc qdisc add dev eth0 root netem delay 1500ms
;; and then cancel with: ;; and then cancel with:
;; sudo tc qdisc del dev eth0 root ;; sudo tc qdisc del dev eth0 root
(let ((port #f))
(with-timeout (if (or timeout? (version>? (version) "2.0.5")) (with-timeout (if (or timeout? (version>? (version) "2.0.5"))
%fetch-timeout %fetch-timeout
0) 0)
(begin (begin
(warning (_ "while fetching ~a: server is unresponsive~%") (warning (_ "while fetching ~a: server is unresponsive~%")
(uri->string uri)) (uri->string uri))
(warning (_ "try `--no-substitutes' if the problem persists~%"))) (warning (_ "try `--no-substitutes' if the problem persists~%"))
(http-fetch uri #:text? #f #:buffered? buffered?))))) (when port
(close-port port)))
(begin
(set! port (open-socket-for-uri uri #:buffered? buffered?))
(http-fetch uri #:text? #f #:port port)))))))
(define-record-type <cache> (define-record-type <cache>
(%make-cache url store-directory wants-mass-query?) (%make-cache url store-directory wants-mass-query?)
@ -535,7 +547,7 @@ (define (guix-substitute-binary . args)
(show-version-and-exit "guix substitute-binary"))))) (show-version-and-exit "guix substitute-binary")))))
;;; Local Variable: ;;; Local Variables:
;;; eval: (put 'with-atomic-file-output 'scheme-indent-function 1) ;;; eval: (put 'with-atomic-file-output 'scheme-indent-function 1)
;;; eval: (put 'with-timeout 'scheme-indent-function 1) ;;; eval: (put 'with-timeout 'scheme-indent-function 1)
;;; End: ;;; End:

View file

@ -27,7 +27,8 @@ (define-module (guix web)
#:use-module (rnrs bytevectors) #:use-module (rnrs bytevectors)
#:use-module (guix ui) #:use-module (guix ui)
#:use-module (guix utils) #:use-module (guix utils)
#:export (http-fetch)) #:export (open-socket-for-uri
http-fetch))
;;; Commentary: ;;; Commentary:
;;; ;;;
@ -141,18 +142,23 @@ (define bad-response
(module-define! (resolve-module '(web client)) (module-define! (resolve-module '(web client))
'shutdown (const #f)) 'shutdown (const #f))
(define* (http-fetch uri #:key (text? #f) (buffered? #t)) (define* (open-socket-for-uri uri #:key (buffered? #t))
"Return an open port for URI. When BUFFERED? is false, the returned port is
unbuffered."
(let ((s ((@ (web client) open-socket-for-uri) uri)))
(unless buffered?
(setvbuf s _IONBF))
s))
(define* (http-fetch uri #:key port (text? #f) (buffered? #t))
"Return an input port containing the data at URI, and the expected number of "Return an input port containing the data at URI, and the expected number of
bytes available or #f. If TEXT? is true, the data at URI is considered to be bytes available or #f. If TEXT? is true, the data at URI is considered to be
textual. Follow any HTTP redirection. When BUFFERED? is #f, return an textual. Follow any HTTP redirection. When BUFFERED? is #f, return an
unbuffered port, suitable for use in `filtered-port'." unbuffered port, suitable for use in `filtered-port'."
(let loop ((uri uri)) (let loop ((uri uri))
(define port (let ((port (or port
(let ((s (open-socket-for-uri uri))) (open-socket-for-uri uri
(unless buffered? #:buffered? buffered?))))
(setvbuf s _IONBF))
s))
(let*-values (((resp data) (let*-values (((resp data)
;; Try hard to use the API du jour to get an input port. ;; Try hard to use the API du jour to get an input port.
;; On Guile 2.0.5 and before, we can only get a string or ;; On Guile 2.0.5 and before, we can only get a string or
@ -197,6 +203,6 @@ (define port
(loop uri))) (loop uri)))
(else (else
(error "download failed" uri code (error "download failed" uri code
(response-reason-phrase resp))))))) (response-reason-phrase resp))))))))
;;; web.scm ends here ;;; web.scm ends here