Skip to content

Commit 201297d

Browse files
committed
Use temporary file when compressing rotated logs and atomically rename to prevent reading incomplete files
If another process is watching for `*.gz` files then it's possible to begin reading the archive before it has been completely created, resulting in corruption if the other process is copying the archive to another location (for example: archival to s3). To resolve this, we can use a different suffix when writing the file so that other programs do not read it while it's being created. Once the archive has been completely created, we atomically rename it to the desired file name with the `*.gz` extension, ensuring external programs only ever see the finished archive. Signed-off-by: Chance Zibolski <[email protected]>
1 parent 47ffae2 commit 201297d

File tree

1 file changed

+29
-3
lines changed

1 file changed

+29
-3
lines changed

lumberjack.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
const (
3939
backupTimeFormat = "2006-01-02T15-04-05.000"
4040
compressSuffix = ".gz"
41+
tmpSuffix = ".tmp"
4142
defaultMaxSize = 100
4243
)
4344

@@ -477,13 +478,17 @@ func compressLogFile(src, dst string) (err error) {
477478
return fmt.Errorf("failed to stat log file: %v", err)
478479
}
479480

480-
if err := chown(dst, fi); err != nil {
481+
// Use a different filename to write the file, so that anything looking for
482+
// "*.gz" only sees the compressed file after it's been finished writing to.
483+
tmpDst := dst + tmpSuffix
484+
485+
if err := chown(tmpDst, fi); err != nil {
481486
return fmt.Errorf("failed to chown compressed log file: %v", err)
482487
}
483488

484489
// If this file already exists, we presume it was created by
485490
// a previous attempt to compress the log file.
486-
gzf, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, fi.Mode())
491+
gzf, err := os.OpenFile(tmpDst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, fi.Mode())
487492
if err != nil {
488493
return fmt.Errorf("failed to open compressed log file: %v", err)
489494
}
@@ -493,24 +498,45 @@ func compressLogFile(src, dst string) (err error) {
493498

494499
defer func() {
495500
if err != nil {
496-
os.Remove(dst)
501+
os.Remove(tmpDst)
497502
err = fmt.Errorf("failed to compress log file: %v", err)
498503
}
499504
}()
500505

501506
if _, err := io.Copy(gz, f); err != nil {
502507
return err
503508
}
509+
510+
// Flush the gz writer before Sync()ing
511+
if err := gz.Flush(); err != nil {
512+
return err
513+
}
514+
515+
// fsync is important, otherwise os.Rename could rename a zero-length file
516+
if err := gzf.Sync(); err != nil {
517+
return err
518+
}
519+
520+
// Close the gzip writer
504521
if err := gz.Close(); err != nil {
505522
return err
506523
}
524+
525+
// close the underlying gzip file
507526
if err := gzf.Close(); err != nil {
508527
return err
509528
}
510529

530+
// close the source file we copied from
511531
if err := f.Close(); err != nil {
512532
return err
513533
}
534+
535+
// Atomically replace the destination file
536+
if err := os.Rename(tmpDst, dst); err != nil {
537+
return err
538+
}
539+
514540
if err := os.Remove(src); err != nil {
515541
return err
516542
}

0 commit comments

Comments
 (0)