Skip to content

Commit f3313af

Browse files
committed
pre-allocate storage for metadata json files, see containers/podman#13967
Keeping a temporary file of at least the same size as the target file for atomic writes helps reduce the probability of running out of space when deleting entities from corresponding metadata in a disk full scenario. This strategy is applied to writing containers.json, layers.json, images.json and mountpoints.json. Signed-off-by: Denys Knertser <[email protected]>
1 parent bf2534b commit f3313af

File tree

6 files changed

+119
-28
lines changed

6 files changed

+119
-28
lines changed

containers.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -541,13 +541,13 @@ func (r *containerStore) save(saveLocations containerLocations) error {
541541
if err != nil {
542542
return err
543543
}
544-
var opts *ioutils.AtomicFileWriterOptions
544+
opts := ioutils.AtomicFileWriterOptions{
545+
PreAllocate: true,
546+
}
545547
if location == volatileContainerLocation {
546-
opts = &ioutils.AtomicFileWriterOptions{
547-
NoSync: true,
548-
}
548+
opts.NoSync = true
549549
}
550-
if err := ioutils.AtomicWriteFileWithOpts(rpath, jdata, 0600, opts); err != nil {
550+
if err := ioutils.AtomicWriteFileWithOpts(rpath, jdata, 0600, &opts); err != nil {
551551
return err
552552
}
553553
}

images.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,10 @@ func (r *imageStore) Save() error {
536536
if err != nil {
537537
return err
538538
}
539-
if err := ioutils.AtomicWriteFile(rpath, jdata, 0600); err != nil {
539+
opts := ioutils.AtomicFileWriterOptions{
540+
PreAllocate: true,
541+
}
542+
if err := ioutils.AtomicWriteFileWithOpts(rpath, jdata, 0600, &opts); err != nil {
540543
return err
541544
}
542545
lw, err := r.lockfile.RecordWrite()

layers.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -942,7 +942,9 @@ func (r *layerStore) saveLayers(saveLocations layerLocations) error {
942942
if err != nil {
943943
return err
944944
}
945-
opts := ioutils.AtomicFileWriterOptions{}
945+
opts := ioutils.AtomicFileWriterOptions{
946+
PreAllocate: true,
947+
}
946948
if location == volatileLayerLocation {
947949
opts.NoSync = true
948950
}
@@ -984,7 +986,10 @@ func (r *layerStore) saveMounts() error {
984986
if err != nil {
985987
return err
986988
}
987-
if err = ioutils.AtomicWriteFile(mpath, jmdata, 0600); err != nil {
989+
opts := ioutils.AtomicFileWriterOptions{
990+
PreAllocate: true,
991+
}
992+
if err = ioutils.AtomicWriteFileWithOpts(mpath, jmdata, 0600, &opts); err != nil {
988993
return err
989994
}
990995
lw, err := r.mountsLockfile.RecordWrite()

pkg/ioutils/fswriters.go

Lines changed: 90 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package ioutils
22

33
import (
44
"io"
5+
"math/rand"
56
"os"
67
"path/filepath"
8+
"strconv"
79
"time"
810
)
911

@@ -17,6 +19,9 @@ type AtomicFileWriterOptions struct {
1719
// On successful return from Close() this is set to the mtime of the
1820
// newly written file.
1921
ModTime time.Time
22+
// Whenever an atomic file is successfully written create a temporary
23+
// file of the same size in order to pre-allocate storage for the next write
24+
PreAllocate bool
2025
}
2126

2227
var defaultWriterOptions = AtomicFileWriterOptions{}
@@ -38,22 +43,33 @@ func NewAtomicFileWriterWithOpts(filename string, perm os.FileMode, opts *Atomic
3843
// temporary file and closing it atomically changes the temporary file to
3944
// destination path. Writing and closing concurrently is not allowed.
4045
func newAtomicFileWriter(filename string, perm os.FileMode, opts *AtomicFileWriterOptions) (*atomicFileWriter, error) {
41-
f, err := os.CreateTemp(filepath.Dir(filename), ".tmp-"+filepath.Base(filename))
42-
if err != nil {
43-
return nil, err
44-
}
46+
dir := filepath.Dir(filename)
47+
base := filepath.Base(filename)
4548
if opts == nil {
4649
opts = &defaultWriterOptions
4750
}
51+
random := ""
52+
// need predictable name when pre-allocated
53+
if !opts.PreAllocate {
54+
random = strconv.FormatUint(rand.Uint64(), 36)
55+
}
56+
tmp := filepath.Join(dir, ".tmp"+random+"-"+base)
57+
// if pre-allocated the temporary file exists and contains some data
58+
// do not truncate it here, instead truncate at Close()
59+
f, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE, perm)
60+
if err != nil {
61+
return nil, err
62+
}
4863
abspath, err := filepath.Abs(filename)
4964
if err != nil {
5065
return nil, err
5166
}
5267
return &atomicFileWriter{
53-
f: f,
54-
fn: abspath,
55-
perm: perm,
56-
noSync: opts.NoSync,
68+
f: f,
69+
fn: abspath,
70+
perm: perm,
71+
noSync: opts.NoSync,
72+
preAllocate: opts.PreAllocate,
5773
}, nil
5874
}
5975

@@ -91,12 +107,13 @@ func AtomicWriteFile(filename string, data []byte, perm os.FileMode) error {
91107
}
92108

93109
type atomicFileWriter struct {
94-
f *os.File
95-
fn string
96-
writeErr error
97-
perm os.FileMode
98-
noSync bool
99-
modTime time.Time
110+
f *os.File
111+
fn string
112+
writeErr error
113+
perm os.FileMode
114+
noSync bool
115+
preAllocate bool
116+
modTime time.Time
100117
}
101118

102119
func (w *atomicFileWriter) Write(dt []byte) (int, error) {
@@ -108,22 +125,35 @@ func (w *atomicFileWriter) Write(dt []byte) (int, error) {
108125
}
109126

110127
func (w *atomicFileWriter) Close() (retErr error) {
111-
defer func() {
112-
if retErr != nil || w.writeErr != nil {
113-
os.Remove(w.f.Name())
128+
if !w.preAllocate {
129+
defer func() {
130+
if retErr != nil || w.writeErr != nil {
131+
os.Remove(w.f.Name())
132+
}
133+
}()
134+
} else {
135+
truncateAt, err := w.f.Seek(0, io.SeekCurrent)
136+
if err != nil {
137+
return err
138+
}
139+
err = w.f.Truncate(truncateAt)
140+
if err != nil {
141+
return err
114142
}
115-
}()
143+
}
116144
if !w.noSync {
117145
if err := fdatasync(w.f); err != nil {
118146
w.f.Close()
119147
return err
120148
}
121149
}
122150

151+
var size int64 = 0
123152
// fstat before closing the fd
124153
info, statErr := w.f.Stat()
125154
if statErr == nil {
126155
w.modTime = info.ModTime()
156+
size = info.Size()
127157
}
128158
// We delay error reporting until after the real call to close()
129159
// to match the traditional linux close() behaviour that an fd
@@ -138,11 +168,51 @@ func (w *atomicFileWriter) Close() (retErr error) {
138168
return statErr
139169
}
140170

141-
if err := os.Chmod(w.f.Name(), w.perm); err != nil {
171+
tmpName := w.f.Name()
172+
if err := os.Chmod(tmpName, w.perm); err != nil {
142173
return err
143174
}
144175
if w.writeErr == nil {
145-
return os.Rename(w.f.Name(), w.fn)
176+
if w.preAllocate {
177+
err := swapOrMove(tmpName, w.fn)
178+
if err != nil {
179+
return err
180+
}
181+
// ignore errors, this is a best effort operation
182+
preAllocate(tmpName, size, w.perm)
183+
return nil
184+
} else {
185+
return os.Rename(tmpName, w.fn)
186+
}
187+
}
188+
return nil
189+
}
190+
191+
// ensure that the file is of at least indicated size
192+
func preAllocate(filename string, size int64, perm os.FileMode) error {
193+
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, perm)
194+
if err != nil {
195+
return err
196+
}
197+
defer f.Close()
198+
info, err := f.Stat()
199+
if err != nil {
200+
return err
201+
}
202+
extendBytes := size - info.Size()
203+
if extendBytes > 0 {
204+
var blockSize int64 = 65536
205+
block := make([]byte, blockSize)
206+
for extendBytes > 0 {
207+
if blockSize > extendBytes {
208+
blockSize = extendBytes
209+
}
210+
_, err := f.Write(block[:blockSize])
211+
if err != nil {
212+
return err
213+
}
214+
extendBytes -= blockSize
215+
}
146216
}
147217
return nil
148218
}

pkg/ioutils/fswriters_linux.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,12 @@ import (
99
func fdatasync(f *os.File) error {
1010
return unix.Fdatasync(int(f.Fd()))
1111
}
12+
13+
func swapOrMove(oldpath string, newpath string) error {
14+
err := unix.Renameat2(unix.AT_FDCWD, oldpath, unix.AT_FDCWD, newpath, unix.RENAME_EXCHANGE)
15+
if err != nil {
16+
// unlikely that rename will succeed if renameat2 failed, but just in case
17+
err = os.Rename(oldpath, newpath)
18+
}
19+
return err
20+
}

pkg/ioutils/fswriters_unsupported.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ import (
1010
func fdatasync(f *os.File) error {
1111
return f.Sync()
1212
}
13+
14+
func swapOrMove(oldpath string, newpath string) error {
15+
return os.Rename(oldpath, newpath)
16+
}

0 commit comments

Comments
 (0)