Skip to content

Commit 225c626

Browse files
committed
memfs: Add thread-safety to Memory
Make Memory thread safe for operations such as Create, Rename and Delete. When compared with Memory without a mutex, the cost per op is roughly 50ns, as per benchmark below: goos: linux goarch: amd64 pkg: github.com/go-git/go-billy/v6/test cpu: AMD Ryzen 7 PRO 8840HS w/ Radeon 780M Graphics BenchmarkCompare/stdlib_open-16 341277 3303 ns/op 152 B/op 3 allocs/op BenchmarkCompare/osfs.chrootOS_open-16 286580 3806 ns/op 360 B/op 9 allocs/op BenchmarkCompare/osfs.boundOS_open-16 209929 5311 ns/op 984 B/op 20 allocs/op BenchmarkCompare/memfs_open-16 1503024 967.4 ns/op 224 B/op 10 allocs/op BenchmarkCompare/memfs_mutex_open-16 1272890 911.6 ns/op 224 B/op 10 allocs/op BenchmarkCompare/stdlib_read-16 184956 5957 ns/op 171.88 MB/s 0 B/op 0 allocs/op BenchmarkCompare/osfs.chrootOS_read-16 180552 6207 ns/op 164.97 MB/s 0 B/op 0 allocs/op BenchmarkCompare/osfs.boundOS_read-16 179314 6131 ns/op 167.02 MB/s 0 B/op 0 allocs/op BenchmarkCompare/memfs_read-16 792040 1568 ns/op 653.02 MB/s 0 B/op 0 allocs/op BenchmarkCompare/memfs_mutex_read-16 783422 1609 ns/op 636.27 MB/s 0 B/op 0 allocs/op BenchmarkCompare/stdlib_create-16 216717 5232 ns/op 152 B/op 3 allocs/op BenchmarkCompare/osfs.chrootOS_create-16 157268 7017 ns/op 616 B/op 11 allocs/op BenchmarkCompare/osfs.boundOS_create-16 123038 9242 ns/op 1335 B/op 22 allocs/op BenchmarkCompare/memfs_create-16 880813 1954 ns/op 693 B/op 12 allocs/op BenchmarkCompare/memfs_mutex_create-16 559534 1901 ns/op 631 B/op 12 allocs/op PASS ok github.com/go-git/go-billy/v6/test 118.368s This change enables users to opt-out (memfs.WithoutMutex), in case speed on a single thread is more important than being thread-safe. Additional thread-safety will be required for memfs.file. Signed-off-by: Paulo Gomes <[email protected]>
1 parent 07d505c commit 225c626

File tree

10 files changed

+463
-273
lines changed

10 files changed

+463
-273
lines changed

.github/workflows/test.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ name: Test
22

33
on:
44
push:
5-
branches: [ "master", "main" ]
65
pull_request:
76

87
permissions: {}

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ $(GOLANGCI):
1414

1515
.PHONY: test
1616
test:
17-
$(GOTEST) -race -timeout 60s ./...
17+
$(GOTEST) -race -timeout 300s ./...
1818

1919
test-coverage:
2020
echo "" > $(COVERAGE_REPORT); \

memfs/file.go

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
package memfs
2+
3+
import (
4+
"errors"
5+
"io"
6+
"io/fs"
7+
"os"
8+
"sync"
9+
"time"
10+
11+
"github.com/go-git/go-billy/v6"
12+
)
13+
14+
type file struct {
15+
name string
16+
content *content
17+
position int64
18+
flag int
19+
mode os.FileMode
20+
modTime time.Time
21+
22+
isClosed bool
23+
}
24+
25+
func (f *file) Name() string {
26+
return f.name
27+
}
28+
29+
func (f *file) Read(b []byte) (int, error) {
30+
n, err := f.ReadAt(b, f.position)
31+
f.position += int64(n)
32+
33+
if errors.Is(err, io.EOF) && n != 0 {
34+
err = nil
35+
}
36+
37+
return n, err
38+
}
39+
40+
func (f *file) ReadAt(b []byte, off int64) (int, error) {
41+
if f.isClosed {
42+
return 0, os.ErrClosed
43+
}
44+
45+
if !isReadAndWrite(f.flag) && !isReadOnly(f.flag) {
46+
return 0, errors.New("read not supported")
47+
}
48+
49+
n, err := f.content.ReadAt(b, off)
50+
51+
return n, err
52+
}
53+
54+
func (f *file) Seek(offset int64, whence int) (int64, error) {
55+
if f.isClosed {
56+
return 0, os.ErrClosed
57+
}
58+
59+
switch whence {
60+
case io.SeekCurrent:
61+
f.position += offset
62+
case io.SeekStart:
63+
f.position = offset
64+
case io.SeekEnd:
65+
f.position = int64(f.content.Len()) + offset
66+
}
67+
68+
return f.position, nil
69+
}
70+
71+
func (f *file) Write(p []byte) (int, error) {
72+
return f.WriteAt(p, f.position)
73+
}
74+
75+
func (f *file) WriteAt(p []byte, off int64) (int, error) {
76+
if f.isClosed {
77+
return 0, os.ErrClosed
78+
}
79+
80+
if !isReadAndWrite(f.flag) && !isWriteOnly(f.flag) {
81+
return 0, errors.New("write not supported")
82+
}
83+
84+
f.modTime = time.Now()
85+
n, err := f.content.WriteAt(p, off)
86+
f.position = off + int64(n)
87+
88+
return n, err
89+
}
90+
91+
func (f *file) Close() error {
92+
if f.isClosed {
93+
return os.ErrClosed
94+
}
95+
96+
f.isClosed = true
97+
return nil
98+
}
99+
100+
func (f *file) Truncate(size int64) error {
101+
if size < int64(len(f.content.bytes)) {
102+
f.content.bytes = f.content.bytes[:size]
103+
} else if more := int(size) - len(f.content.bytes); more > 0 {
104+
f.content.bytes = append(f.content.bytes, make([]byte, more)...)
105+
}
106+
107+
return nil
108+
}
109+
110+
func (f *file) Duplicate(filename string, mode fs.FileMode, flag int) billy.File {
111+
n := &file{
112+
name: filename,
113+
content: f.content,
114+
mode: mode,
115+
flag: flag,
116+
modTime: f.modTime,
117+
}
118+
119+
if isTruncate(flag) {
120+
n.content.Truncate()
121+
}
122+
123+
if isAppend(flag) {
124+
n.position = int64(n.content.Len())
125+
}
126+
127+
return n
128+
}
129+
130+
func (f *file) Stat() (os.FileInfo, error) {
131+
return &fileInfo{
132+
name: f.Name(),
133+
mode: f.mode,
134+
size: f.content.Len(),
135+
modTime: f.modTime,
136+
}, nil
137+
}
138+
139+
// Lock is a no-op in memfs.
140+
func (f *file) Lock() error {
141+
return nil
142+
}
143+
144+
// Unlock is a no-op in memfs.
145+
func (f *file) Unlock() error {
146+
return nil
147+
}
148+
149+
type fileInfo struct {
150+
name string
151+
size int
152+
mode os.FileMode
153+
modTime time.Time
154+
}
155+
156+
func (fi *fileInfo) Name() string {
157+
return fi.name
158+
}
159+
160+
func (fi *fileInfo) Size() int64 {
161+
return int64(fi.size)
162+
}
163+
164+
func (fi *fileInfo) Mode() fs.FileMode {
165+
return fi.mode
166+
}
167+
168+
func (fi *fileInfo) ModTime() time.Time {
169+
return fi.modTime
170+
}
171+
172+
func (fi *fileInfo) IsDir() bool {
173+
return fi.mode.IsDir()
174+
}
175+
176+
func (*fileInfo) Sys() interface{} {
177+
return nil
178+
}
179+
180+
type content struct {
181+
name string
182+
bytes []byte
183+
184+
m sync.RWMutex
185+
}
186+
187+
func (c *content) WriteAt(p []byte, off int64) (int, error) {
188+
if off < 0 {
189+
return 0, &os.PathError{
190+
Op: "writeat",
191+
Path: c.name,
192+
Err: errors.New("negative offset"),
193+
}
194+
}
195+
196+
c.m.Lock()
197+
prev := len(c.bytes)
198+
199+
diff := int(off) - prev
200+
if diff > 0 {
201+
c.bytes = append(c.bytes, make([]byte, diff)...)
202+
}
203+
204+
c.bytes = append(c.bytes[:off], p...)
205+
if len(c.bytes) < prev {
206+
c.bytes = c.bytes[:prev]
207+
}
208+
c.m.Unlock()
209+
210+
return len(p), nil
211+
}
212+
213+
func (c *content) ReadAt(b []byte, off int64) (n int, err error) {
214+
if off < 0 {
215+
return 0, &os.PathError{
216+
Op: "readat",
217+
Path: c.name,
218+
Err: errors.New("negative offset"),
219+
}
220+
}
221+
222+
c.m.RLock()
223+
size := int64(len(c.bytes))
224+
if off >= size {
225+
c.m.RUnlock()
226+
return 0, io.EOF
227+
}
228+
229+
l := int64(len(b))
230+
if off+l > size {
231+
l = size - off
232+
}
233+
234+
btr := c.bytes[off : off+l]
235+
n = copy(b, btr)
236+
237+
if len(btr) < len(b) {
238+
err = io.EOF
239+
}
240+
c.m.RUnlock()
241+
242+
return
243+
}

0 commit comments

Comments
 (0)