Skip to content

Commit 894e80c

Browse files
committed
Implement temporary directory
Fixes: https://issues.redhat.com/browse/RUN-3104 Signed-off-by: Jan Rodák <[email protected]>
1 parent e1679c1 commit 894e80c

File tree

2 files changed

+529
-0
lines changed

2 files changed

+529
-0
lines changed

internal/tempdir/tempdir.go

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
package tempdir
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
10+
"github.com/containers/storage/internal/staging_lockfile"
11+
"github.com/sirupsen/logrus"
12+
)
13+
14+
/*
15+
Locking rules and invariants for TempDir and its recovery mechanism:
16+
17+
1. TempDir Instance Locks:
18+
- Path: 'RootDir/lock-XYZ' (in the root directory)
19+
- Each TempDir instance creates and holds an exclusive lock on this file immediately
20+
during NewTempDir() initialization.
21+
- This lock signifies that the temporary directory is in active use by the
22+
process/goroutine that holds the TempDir object.
23+
24+
2. Stale Directory Recovery (separate operation):
25+
- RecoverStaleDirs() can be called independently to identify and clean up stale
26+
temporary directories.
27+
- For each potential stale directory (found by listPotentialStaleDirs), it
28+
attempts to TryLockPath() its instance lock file.
29+
- If TryLockPath() succeeds: The directory is considered stale, and both the
30+
directory and lock file are removed.
31+
- If TryLockPath() fails: The directory is considered in active use by another
32+
process/goroutine, and it's skipped.
33+
34+
3. TempDir Usage:
35+
- NewTempDir() immediately creates both the instance lock and the temporary directory.
36+
- TempDir.Add() moves files into the existing temporary directory with counter-based naming.
37+
- Files moved into the temporary directory are renamed with a counter-based prefix
38+
to ensure uniqueness (e.g., "0-filename", "1-filename").
39+
- Once cleaned up, the TempDir instance cannot be reused - Add() will return an error.
40+
41+
4. Cleanup Process:
42+
- TempDir.Cleanup() removes both the temporary directory and its lock file.
43+
- The instance lock is unlocked and deleted after cleanup operations are complete.
44+
- The TempDir instance becomes inactive after cleanup (internal fields are reset).
45+
- The TempDir instance cannot be reused after Cleanup() - Add() will fail.
46+
47+
5. TempDir Lifetime:
48+
- NewTempDir() creates both the TempDir manager and the actual temporary directory immediately.
49+
- The temporary directory is created eagerly during NewTempDir().
50+
- During its lifetime, the temporary directory is protected by its instance lock.
51+
- The temporary directory exists until Cleanup() is called, which removes both
52+
the directory and its lock file.
53+
- Multiple TempDir instances can coexist in the same RootDir, each with its own
54+
unique subdirectory and lock.
55+
- After cleanup, the TempDir instance cannot be reused.
56+
57+
6. Example Directory Structure:
58+
59+
RootDir/
60+
lock-ABC (instance lock for temp-dir-ABC)
61+
temp-dir-ABC/
62+
0-file1
63+
1-file3
64+
lock-XYZ (instance lock for temp-dir-XYZ)
65+
temp-dir-XYZ/
66+
0-file2
67+
*/
68+
const (
69+
// tempDirPrefix is the prefix used for creating temporary directories.
70+
tempDirPrefix = "temp-dir-"
71+
// tempdirLockPrefix is the prefix used for creating lock files for temporary directories.
72+
tempdirLockPrefix = "lock-"
73+
)
74+
75+
// TempDir represents a temporary directory that is created in a specified root directory.
76+
// It manages the lifecycle of the temporary directory, including creation, locking, and cleanup.
77+
// Each TempDir instance is associated with a unique subdirectory in the root directory.
78+
// Warning: The TempDir instance should be used in a single goroutine.
79+
type TempDir struct {
80+
RootDir string
81+
82+
tempDirPath string
83+
// tempDirLock is a lock file (e.g., RootDir/lock-XYZ) specific to this
84+
// TempDir instance, indicating it's in active use.
85+
tempDirLock *staging_lockfile.StagingLockFile
86+
tempDirLockPath string
87+
88+
// counter is used to generate unique filenames for added files.
89+
counter uint64
90+
}
91+
92+
// CleanupTempDirFunc is a function type that can be returned by operations
93+
// which need to perform cleanup actions later.
94+
type CleanupTempDirFunc func() error
95+
96+
// listPotentialStaleDirs scans the RootDir for directories that might be stale temporary directories.
97+
// It identifies directories with the tempDirPrefix and their corresponding lock files with the tempdirLockPrefix.
98+
// The function returns a map of IDs that correspond to both directories and lock files found.
99+
// These IDs are extracted from the filenames by removing their respective prefixes.
100+
func listPotentialStaleDirs(rootDir string) (map[string]struct{}, error) {
101+
ids := make(map[string]struct{})
102+
103+
dirContent, err := os.ReadDir(rootDir)
104+
if err != nil {
105+
if os.IsNotExist(err) {
106+
return nil, nil
107+
}
108+
return nil, fmt.Errorf("error reading temp dir %s: %w", rootDir, err)
109+
}
110+
111+
for _, entry := range dirContent {
112+
if id, ok := strings.CutPrefix(entry.Name(), tempDirPrefix); ok {
113+
ids[id] = struct{}{}
114+
continue
115+
}
116+
117+
if id, ok := strings.CutPrefix(entry.Name(), tempdirLockPrefix); ok {
118+
ids[id] = struct{}{}
119+
}
120+
}
121+
return ids, nil
122+
}
123+
124+
// RecoverStaleDirs identifies and removes stale temporary directories in the root directory.
125+
// A directory is considered stale if its lock file can be acquired (indicating no active use).
126+
// The function attempts to remove both the directory and its lock file.
127+
// If a directory's lock cannot be acquired, it is considered in use and is skipped.
128+
func RecoverStaleDirs(rootDir string) error {
129+
potentialStaleDirs, err := listPotentialStaleDirs(rootDir)
130+
if err != nil {
131+
return fmt.Errorf("error listing potential stale temp dirs in %s: %w", rootDir, err)
132+
}
133+
134+
if len(potentialStaleDirs) == 0 {
135+
return nil
136+
}
137+
138+
var recoveryErrors []error
139+
140+
for id := range potentialStaleDirs {
141+
lockPath := filepath.Join(rootDir, tempdirLockPrefix+id)
142+
tempDirPath := filepath.Join(rootDir, tempDirPrefix+id)
143+
144+
// Try to lock the lock file. If it can be locked, the directory is stale.
145+
instanceLock, err := staging_lockfile.TryLockPath(lockPath)
146+
if err != nil {
147+
continue
148+
}
149+
150+
if rmErr := os.RemoveAll(tempDirPath); rmErr != nil && !os.IsNotExist(rmErr) {
151+
recoveryErrors = append(recoveryErrors, fmt.Errorf("error removing stale temp dir %s: %w", tempDirPath, rmErr))
152+
}
153+
if unlockErr := instanceLock.UnlockAndDelete(); unlockErr != nil {
154+
recoveryErrors = append(recoveryErrors, fmt.Errorf("error unlocking and deleting stale lock file %s: %w", lockPath, unlockErr))
155+
}
156+
}
157+
158+
return errors.Join(recoveryErrors...)
159+
}
160+
161+
// NewTempDir creates a TempDir and immediately creates both the temporary directory
162+
// and its corresponding lock file in the specified RootDir.
163+
// The RootDir itself will be created if it doesn't exist.
164+
// Note: The caller MUST ensure that returned TempDir instance is cleaned up with .Cleanup().
165+
func NewTempDir(rootDir string) (*TempDir, error) {
166+
if err := os.MkdirAll(rootDir, 0o700); err != nil {
167+
return nil, fmt.Errorf("creating root temp directory %s failed: %w", rootDir, err)
168+
}
169+
170+
td := &TempDir{
171+
RootDir: rootDir,
172+
}
173+
tempDirLock, tempDirLockFileName, err := staging_lockfile.CreateAndLock(td.RootDir, tempdirLockPrefix)
174+
if err != nil {
175+
return nil, fmt.Errorf("creating and locking temp dir instance lock in %s failed: %w", td.RootDir, err)
176+
}
177+
td.tempDirLock = tempDirLock
178+
td.tempDirLockPath = filepath.Join(td.RootDir, tempDirLockFileName)
179+
180+
// Create the temporary directory that corresponds to the lock file
181+
id := strings.TrimPrefix(tempDirLockFileName, tempdirLockPrefix)
182+
actualTempDirPath := filepath.Join(td.RootDir, tempDirPrefix+id)
183+
if err := os.MkdirAll(actualTempDirPath, 0o700); err != nil {
184+
return nil, fmt.Errorf("creating temp directory %s failed: %w", actualTempDirPath, err)
185+
}
186+
td.tempDirPath = actualTempDirPath
187+
td.counter = 0
188+
return td, nil
189+
}
190+
191+
// Add moves the specified file into the instance's temporary directory.
192+
// The temporary directory must already exist (created during NewTempDir).
193+
// Files are renamed with a counter-based prefix (e.g., "0-filename", "1-filename") to ensure uniqueness.
194+
// Note: 'path' must be on the same filesystem as the TempDir for os.Rename to work.
195+
// The caller MUST ensure .Cleanup() is called.
196+
// If the TempDir has been cleaned up, this method will return an error.
197+
func (td *TempDir) Add(path string) error {
198+
if td.tempDirLock == nil {
199+
return fmt.Errorf("temp dir instance not initialized or already cleaned up")
200+
}
201+
fileName := fmt.Sprintf("%d-", td.counter) + filepath.Base(path)
202+
destPath := filepath.Join(td.tempDirPath, fileName)
203+
td.counter++
204+
return os.Rename(path, destPath)
205+
}
206+
207+
// Cleanup removes the temporary directory and releases its instance lock.
208+
// After cleanup, the TempDir instance becomes inactive and cannot be reused.
209+
// Subsequent calls to Add() will fail.
210+
// Multiple calls to Cleanup() are safe and will not return an error.
211+
// Callers should typically defer Cleanup() to run after any application-level
212+
// global locks are released to avoid holding those locks during potentially
213+
// slow disk I/O.
214+
func (td *TempDir) Cleanup() error {
215+
if td.tempDirLock == nil {
216+
logrus.Debug("Temp dir already cleaned up")
217+
return nil
218+
}
219+
220+
if err := os.RemoveAll(td.tempDirPath); err != nil && !os.IsNotExist(err) {
221+
return fmt.Errorf("removing temp dir %s failed: %w", td.tempDirPath, err)
222+
}
223+
224+
lock := td.tempDirLock
225+
td.tempDirPath = ""
226+
td.tempDirLock = nil
227+
td.tempDirLockPath = ""
228+
return lock.UnlockAndDelete()
229+
}
230+
231+
// CleanupTemporaryDirectories cleans up multiple temporary directories by calling their cleanup functions.
232+
func CleanupTemporaryDirectories(cleanFuncs ...CleanupTempDirFunc) error {
233+
var cleanupErrors []error
234+
for _, cleanupFunc := range cleanFuncs {
235+
if cleanupFunc == nil {
236+
continue
237+
}
238+
if err := cleanupFunc(); err != nil {
239+
cleanupErrors = append(cleanupErrors, err)
240+
}
241+
}
242+
return errors.Join(cleanupErrors...)
243+
}

0 commit comments

Comments
 (0)