deduplication: Gracefully handle ENOSPC raised by 'link' calls.

Reported by Andreas Enge <andreas@enge.fr>
in <https://bugs.gnu.org/33676>.

* guix/store/deduplication.scm (replace-with-link): Catch ENOSPC around
'get-temp-link'.  Do nothing when 'get-temp-link' throws ENOSPC.  Move
code to restore PARENT's permissions outside of 'catch'.
* tests/store-deduplication.scm ("deduplicate, ENOSPC"): New test.
This commit is contained in:
Ludovic Courtès 2018-12-14 11:10:25 +01:00
parent ea49fbdea3
commit adb158b739
No known key found for this signature in database
GPG Key ID: 090B11993D9AEBB5
2 changed files with 70 additions and 14 deletions

View File

@ -99,24 +99,38 @@ LINK-PREFIX."
(define* (replace-with-link target to-replace (define* (replace-with-link target to-replace
#:key (swap-directory (dirname target))) #:key (swap-directory (dirname target)))
"Atomically replace the file TO-REPLACE with a link to TARGET. Use "Atomically replace the file TO-REPLACE with a link to TARGET. Use
SWAP-DIRECTORY as the directory to store temporary hard links. SWAP-DIRECTORY as the directory to store temporary hard links. Upon ENOSPC
and EMLINK, TO-REPLACE is left unchanged.
Note: TARGET, TO-REPLACE, and SWAP-DIRECTORY must be on the same file system." Note: TARGET, TO-REPLACE, and SWAP-DIRECTORY must be on the same file system."
(let* ((temp-link (get-temp-link target swap-directory)) (define temp-link
(parent (dirname to-replace))
(stat (stat parent)))
(make-file-writable parent)
(catch 'system-error (catch 'system-error
(lambda () (lambda ()
(rename-file temp-link to-replace) (get-temp-link target swap-directory))
;; Restore PARENT's mtime and permissions.
(set-file-time parent stat)
(chmod parent (stat:mode stat)))
(lambda args (lambda args
(delete-file temp-link) ;; We get ENOSPC when we can't fit an additional entry in
(unless (= EMLINK (system-error-errno args)) ;; SWAP-DIRECTORY.
(apply throw args)))))) (if (= ENOSPC (system-error-errno args))
#f
(apply throw args)))))
;; If we couldn't create TEMP-LINK, that's OK: just don't do the
;; replacement, which means TO-REPLACE won't be deduplicated.
(when temp-link
(let* ((parent (dirname to-replace))
(stat (stat parent)))
(make-file-writable parent)
(catch 'system-error
(lambda ()
(rename-file temp-link to-replace))
(lambda args
(delete-file temp-link)
(unless (= EMLINK (system-error-errno args))
(apply throw args))))
;; Restore PARENT's mtime and permissions.
(set-file-time parent stat)
(chmod parent (stat:mode stat)))))
(define* (deduplicate path hash #:key (store %store-directory)) (define* (deduplicate path hash #:key (store %store-directory))
"Check if a store item with sha256 hash HASH already exists. If so, "Check if a store item with sha256 hash HASH already exists. If so,

View File

@ -48,7 +48,7 @@
(put-bytevector port data)))) (put-bytevector port data))))
identical) identical)
;; Make the parent of IDENTICAL read-only. This should not prevent ;; Make the parent of IDENTICAL read-only. This should not prevent
;; deduplication for inserting its hard link. ;; deduplication from inserting its hard link.
(chmod (dirname (second identical)) #o544) (chmod (dirname (second identical)) #o544)
(call-with-output-file unique (call-with-output-file unique
@ -64,4 +64,46 @@
(stat:nlink (stat unique)) (stat:nlink (stat unique))
(map (compose stat:nlink stat) identical)))))) (map (compose stat:nlink stat) identical))))))
(test-equal "deduplicate, ENOSPC"
(cons* #f ;inode comparison
(append (make-list 3 4)
(make-list 7 1))) ;'nlink' values
;; In this scenario the first 3 files are properly deduplicated and then we
;; simulate a full '.links' directory where link(2) gets ENOSPC, thereby
;; preventing deduplication of the subsequent files.
(call-with-temporary-directory
(lambda (store)
(let ((true-link link)
(links 0)
(data1 (string->utf8 "Hello, world!"))
(data2 (string->utf8 "Hi, world!"))
(identical (map (lambda (n)
(string-append store "/" (number->string n)
"/a/b/c"))
(iota 10)))
(populate (lambda (data)
(lambda (file)
(mkdir-p (dirname file))
(call-with-output-file file
(lambda (port)
(put-bytevector port data)))))))
(for-each (populate data1) (take identical 5))
(for-each (populate data2) (drop identical 5))
(dynamic-wind
(lambda ()
(set! link (lambda (old new)
(set! links (+ links 1))
(if (<= links 3)
(true-link old new)
(throw 'system-error "link" "~A" '("Whaaat?!")
(list ENOSPC))))))
(lambda ()
(deduplicate store (nar-sha256 store) #:store store))
(lambda ()
(set! link true-link)))
(cons (apply = (map (compose stat:ino stat) identical))
(map (compose stat:nlink stat) identical))))))
(test-end "store-deduplication") (test-end "store-deduplication")