Skip to content

Commit 721a193

Browse files
authored
Merge pull request #4256 from apostasie/2025-05-fs-2
[REFACTOR] Filesystem, part 2: revamp locks
2 parents 3c8411b + 721e285 commit 721a193

File tree

10 files changed

+524
-153
lines changed

10 files changed

+524
-153
lines changed

pkg/internal/filesystem/consts.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@
1717
package filesystem
1818

1919
const (
20+
lockPermission = 0o600
2021
pathComponentMaxLength = 255
2122
)

pkg/internal/filesystem/errors.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,8 @@ package filesystem
1919
import "errors"
2020

2121
var (
22+
ErrLockFail = errors.New("failed to acquire lock")
23+
ErrUnlockFail = errors.New("failed to release lock")
24+
ErrLockIsNil = errors.New("nil lock")
2225
ErrInvalidPath = errors.New("invalid path")
2326
)

pkg/internal/filesystem/lock.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Portions from internal go
18+
//
19+
// Copyright 2018 The Go Authors. All rights reserved.
20+
// Use of this source code is governed by a BSD-style
21+
// license that can be found in the LICENSE file.
22+
//
23+
// https://cs.opensource.google/go/go/+/refs/tags/go1.24.3:LICENSE
24+
25+
// https://cs.opensource.google/go/go/+/refs/tags/go1.24.3:src/cmd/go/internal/lockedfile/internal/filelock/filelock.go
26+
27+
package filesystem
28+
29+
import (
30+
"errors"
31+
"os"
32+
"runtime"
33+
)
34+
35+
// Lock places an advisory write lock on the file, blocking until it can be locked.
36+
//
37+
// If Lock returns nil, no other process will be able to place a read or write lock on the file until
38+
// this process exits, closes f, or calls Unlock on it.
39+
func Lock(path string) (file *os.File, err error) {
40+
return commonlock(path, writeLock)
41+
}
42+
43+
// ReadOnlyLock places an advisory read lock on the file, blocking until it can be locked.
44+
//
45+
// If ReadOnlyLock returns nil, no other process will be able to place a write lock on
46+
// the file until this process exits, closes f, or calls Unlock on it.
47+
func ReadOnlyLock(path string) (file *os.File, err error) {
48+
return commonlock(path, readLock)
49+
}
50+
51+
func commonlock(path string, mode lockType) (file *os.File, err error) {
52+
defer func() {
53+
if err != nil {
54+
err = errors.Join(ErrLockFail, err, file.Close())
55+
}
56+
}()
57+
58+
if runtime.GOOS == "windows" {
59+
// LockFileEx does not work on directories, so check what we have first.
60+
// If that is a dir, swap out the path for a sidecar file instead (not inside the directory).
61+
// Note that this cannot be done in platform specific implementation without moving all the fd Open and Close
62+
// logic over there, which is undesirable.
63+
if sl, err := os.Stat(path); err == nil && sl.IsDir() {
64+
path = path + ".nerdctl.lock"
65+
}
66+
}
67+
68+
file, err = os.Open(path)
69+
if errors.Is(err, os.ErrNotExist) {
70+
file, err = os.OpenFile(path, os.O_RDONLY|os.O_CREATE, lockPermission)
71+
}
72+
73+
if err != nil {
74+
return nil, err
75+
}
76+
77+
if err = platformSpecificLock(file, mode); err != nil {
78+
return nil, errors.Join(err, file.Close())
79+
}
80+
81+
return file, nil
82+
}
83+
84+
// Unlock removes an advisory lock placed on f by this process.
85+
func Unlock(lock *os.File) error {
86+
if lock == nil {
87+
return ErrLockIsNil
88+
}
89+
90+
if err := errors.Join(platformSpecificUnlock(lock), lock.Close()); err != nil {
91+
return errors.Join(ErrUnlockFail, err)
92+
}
93+
94+
return nil
95+
}
96+
97+
// WithLock executes the provided function after placing a write lock on `path`.
98+
// The lock is released once the function has been run, regardless of outcome.
99+
func WithLock(path string, function func() error) (err error) {
100+
file, err := Lock(path)
101+
if err != nil {
102+
return err
103+
}
104+
105+
defer func() {
106+
err = errors.Join(Unlock(file), err)
107+
}()
108+
109+
return function()
110+
}
111+
112+
// WithReadOnlyLock executes the provided function after placing a read lock on `path`.
113+
// The lock is released once the function has been run, regardless of outcome.
114+
func WithReadOnlyLock(path string, function func() error) (err error) {
115+
file, err := ReadOnlyLock(path)
116+
if err != nil {
117+
return err
118+
}
119+
120+
defer func() {
121+
err = errors.Join(Unlock(file), err)
122+
}()
123+
124+
return function()
125+
}

0 commit comments

Comments
 (0)