Skip to content

Commit

Permalink
fix: avoid data races when deleting in background
Browse files Browse the repository at this point in the history
  • Loading branch information
dundee committed Mar 31, 2024
1 parent 755b20d commit cd14e9d
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 2 deletions.
31 changes: 31 additions & 0 deletions pkg/analyze/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package analyze
import (
"os"
"path/filepath"
"sync"
"time"

"github.com/dundee/gdu/v5/pkg/fs"
Expand Down Expand Up @@ -112,6 +113,16 @@ func (f *File) GetFiles() fs.Files {
return fs.Files{}
}

// GetFilesLocked returns all files in directory
func (f *File) GetFilesLocked() fs.Files {
return f.GetFiles()

Check warning on line 118 in pkg/analyze/file.go

View check run for this annotation

Codecov / codecov/patch

pkg/analyze/file.go#L117-L118

Added lines #L117 - L118 were not covered by tests
}

// RLock panics on file
func (f *File) RLock() func() {
panic("SetFiles should not be called on file")

Check warning on line 123 in pkg/analyze/file.go

View check run for this annotation

Codecov / codecov/patch

pkg/analyze/file.go#L122-L123

Added lines #L122 - L123 were not covered by tests
}

// SetFiles panics on file
func (f *File) SetFiles(files fs.Files) {
panic("SetFiles should not be called on file")
Expand All @@ -133,6 +144,7 @@ type Dir struct {
BasePath string
Files fs.Files
ItemCount int
m sync.RWMutex
}

// AddFile add item fo files
Expand All @@ -145,6 +157,14 @@ func (f *Dir) GetFiles() fs.Files {
return f.Files
}

// GetFilesLocked returns all files in directory
// It is safe to call this function from multiple goroutines
func (f *Dir) GetFilesLocked() fs.Files {
f.m.RLock()
defer f.m.RUnlock()
return f.GetFiles()[:]

Check warning on line 165 in pkg/analyze/file.go

View check run for this annotation

Codecov / codecov/patch

pkg/analyze/file.go#L162-L165

Added lines #L162 - L165 were not covered by tests
}

// SetFiles sets files in directory
func (f *Dir) SetFiles(files fs.Files) {
f.Files = files
Expand All @@ -157,6 +177,8 @@ func (f *Dir) GetType() string {

// GetItemCount returns number of files in dir
func (f *Dir) GetItemCount() int {
f.m.RLock()
defer f.m.RUnlock()
return f.ItemCount
}

Expand Down Expand Up @@ -211,6 +233,9 @@ func (f *Dir) UpdateStats(linkedItems fs.HardLinkedItems) {

// RemoveFile panics on file
func (f *Dir) RemoveFile(item fs.Item) {
f.m.Lock()
defer f.m.Unlock()

f.SetFiles(f.GetFiles().Remove(item))

cur := f
Expand All @@ -226,6 +251,12 @@ func (f *Dir) RemoveFile(item fs.Item) {
}
}

// RLock read locks dir
func (f *Dir) RLock() func() {
f.m.RLock()
return f.m.RUnlock

Check warning on line 257 in pkg/analyze/file.go

View check run for this annotation

Codecov / codecov/patch

pkg/analyze/file.go#L255-L257

Added lines #L255 - L257 were not covered by tests
}

// RemoveItemFromDir removes item from dir
func RemoveItemFromDir(dir fs.Item, item fs.Item) error {
err := os.RemoveAll(item.GetPath())
Expand Down
2 changes: 2 additions & 0 deletions pkg/analyze/stored.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,8 @@ func (p *ParentDir) EncodeJSON(writer io.Writer, topLevel bool) error { panic("m
func (p *ParentDir) UpdateStats(linkedItems fs.HardLinkedItems) { panic("must not be called") }
func (p *ParentDir) AddFile(fs.Item) { panic("must not be called") }
func (p *ParentDir) GetFiles() fs.Files { panic("must not be called") }
func (p *ParentDir) GetFilesLocked() fs.Files { panic("must not be called") }
func (p *ParentDir) RLock() func() { panic("must not be called") }

Check warning on line 377 in pkg/analyze/stored.go

View check run for this annotation

Codecov / codecov/patch

pkg/analyze/stored.go#L376-L377

Added lines #L376 - L377 were not covered by tests
func (p *ParentDir) SetFiles(fs.Files) { panic("must not be called") }
func (f *ParentDir) RemoveFile(item fs.Item) { panic("must not be called") }
func (p *ParentDir) GetItemStats(
Expand Down
2 changes: 2 additions & 0 deletions pkg/fs/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ type Item interface {
UpdateStats(linkedItems HardLinkedItems)
AddFile(Item)
GetFiles() Files
GetFilesLocked() Files
SetFiles(Files)
RemoveFile(Item)
RLock() func()
}

// Files - slice of pointers to File
Expand Down
2 changes: 1 addition & 1 deletion tui/background.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (ui *UI) deleteItem(item fs.Item, shouldEmpty bool) {
var deleteItems []fs.Item
if shouldEmpty && item.IsDir() {
parentDir = item.(*analyze.Dir)
for _, file := range item.GetFiles() {
for _, file := range item.GetFilesLocked() {
deleteItems = append(deleteItems, file)
}
} else {
Expand Down
3 changes: 3 additions & 0 deletions tui/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ func (ui *UI) showDir() {

ui.sortItems()

unlock := ui.currentDir.RLock()
defer unlock()

if ui.ShowRelativeSize {
for _, item := range ui.currentDir.GetFiles() {
if item.GetUsage() > maxUsage {
Expand Down
4 changes: 3 additions & 1 deletion tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ type UI struct {
activeWorkers int
workersMut sync.Mutex
statusMut sync.RWMutex
deleteWorkersCount int
}

type deleteQueueItem struct {
Expand Down Expand Up @@ -117,6 +118,7 @@ func CreateUI(
exportName: "export.json",
noDelete: false,
deleteQueue: make(chan deleteQueueItem, 1000),
deleteWorkersCount: 3 * runtime.GOMAXPROCS(0),
}
for _, o := range opts {
o(ui)
Expand Down Expand Up @@ -233,7 +235,7 @@ func (ui *UI) SetNoDelete() {
func (ui *UI) SetDeleteInBackground() {
ui.deleteInBackground = true

for i := 0; i < 3*runtime.GOMAXPROCS(0); i++ {
for i := 0; i < ui.deleteWorkersCount; i++ {
go ui.deleteWorker()
}
go ui.updateStatusWorker()
Expand Down
28 changes: 28 additions & 0 deletions tui/tui_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,34 @@ func TestDeleteMarkedWithErr(t *testing.T) {
assert.DirExists(t, "test_dir/nested")
}

func TestDeleteMarkedInBackground(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()

ui := getAnalyzedPathMockedApp(t, false, true, false)
ui.SetDeleteInBackground()

assert.Equal(t, 1, ui.table.GetRowCount())

ui.fileItemSelected(0, 0) // nested

ui.markedRows[1] = struct{}{} // subnested
ui.markedRows[2] = struct{}{} // file2

ui.deleteMarked(false)

<-ui.done // wait for deletion of subnested
<-ui.done // wait for deletion of file2

for _, f := range ui.app.(*testapp.MockedApp).GetUpdateDraws() {
f()
}

assert.DirExists(t, "test_dir/nested")
assert.NoDirExists(t, "test_dir/nested/subnested")
assert.NoFileExists(t, "test_dir/nested/file2")
}

func TestDeleteMarkedInBackgroundWithErr(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
Expand Down

0 comments on commit cd14e9d

Please sign in to comment.