Skip to content

Commit 2c010af

Browse files
committed
Fix races between removing backups and creating periodic backups
Micro's logic for periodic backup creation is racy and may cause spurious backups of unmodified buffers, at least for the following reasons: 1. When a buffer is closed, its backup is removed by the main goroutine, without any synchronization with the backup/save goroutine which creates periodic backups in the background. A part of the problem here is that the main goroutine removes the backup before setting b.fini to true, not after it, so the backup/save goroutine may start creating a new backup even after it has been removed by the main goroutine. But even if we move the b.RemoveBackup() call after setting b.fini, it will not solve the problem, since the backup/save goroutine may have already started creating a new periodic backup just before b.fini was set to true. 2. When a buffer is successfully saved and thus its backup is removed, if there was a periodic backup for this buffer requested by the main goroutine but not saved by the backup/save goroutine yet (i.e. this request is still pending in backupRequestChan), micro doesn't cancel this pending request, so a backup is unexpectedly saved a couple of seconds after the file itself was saved. Although usually this erroneous backup is removed later, when the buffer is closed. But if micro terminates abnormally and the buffer is not properly closed, this backup is not removed. Also if this issue occurs in combination with the race issue zyedidia#1 described above, this backup may not be successfully removed either. So, to fix these issues: 1. Do the backup removal in the backup/save goroutine (at requests from the main goroutine), not directly in the main goroutine. 2. Make the communication between these goroutines fully synchronous: 2a. Instead of using the buffered channel backupRequestChan as a storage for pending requests for periodic backups, let the backup/save goroutine itself store this information, in the requestesBackups map. Then, backupRequestChan can be made non-buffered. 2b. Make saveRequestChan a non-buffered channel as well. (There was no point in making it buffered in the first place, actually.) Once both channels are non-buffered, the backup/save goroutine receives both backup and save requests from the main goroutine in exactly the same order as the main goroutine sends them, so we can guarantee that saving the buffer will cancel the previous pending backup request for this buffer.
1 parent e84d44d commit 2c010af

File tree

4 files changed

+50
-33
lines changed

4 files changed

+50
-33
lines changed

cmd/micro/micro.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -489,8 +489,6 @@ func DoEvent() {
489489
}
490490
case f := <-timerChan:
491491
f()
492-
case b := <-buffer.BackupCompleteChan:
493-
b.RequestedBackup = false
494492
case <-sighup:
495493
exit(0)
496494
case <-util.Sigterm:

internal/buffer/backup.go

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,49 @@ Options: [r]ecover, [i]gnore, [a]bort: `
3434

3535
const backupSeconds = 8
3636

37-
var BackupCompleteChan chan *SharedBuffer
37+
type backupRequestType int
38+
39+
const (
40+
backupCreate = iota
41+
backupRemove
42+
)
43+
44+
type backupRequest struct {
45+
buf *SharedBuffer
46+
reqType backupRequestType
47+
}
48+
49+
var requestedBackups map[*SharedBuffer]bool
3850

3951
func init() {
40-
BackupCompleteChan = make(chan *SharedBuffer, 10)
52+
requestedBackups = make(map[*SharedBuffer]bool)
4153
}
4254

4355
func (b *SharedBuffer) RequestBackup() {
44-
if !b.RequestedBackup {
45-
select {
46-
case backupRequestChan <- b:
47-
default:
48-
// channel is full
56+
backupRequestChan <- backupRequest{buf: b, reqType: backupCreate}
57+
}
58+
59+
func (b *SharedBuffer) CancelBackup() {
60+
backupRequestChan <- backupRequest{buf: b, reqType: backupRemove}
61+
}
62+
63+
func handleBackupRequest(br backupRequest) {
64+
switch br.reqType {
65+
case backupCreate:
66+
// schedule periodic backup
67+
requestedBackups[br.buf] = true
68+
case backupRemove:
69+
br.buf.RemoveBackup()
70+
delete(requestedBackups, br.buf)
71+
}
72+
}
73+
74+
func periodicBackup() {
75+
for buf := range requestedBackups {
76+
err := buf.Backup()
77+
if err == nil {
78+
delete(requestedBackups, buf)
4979
}
50-
b.RequestedBackup = true
5180
}
5281
}
5382

@@ -77,9 +106,6 @@ func (b *SharedBuffer) Backup() error {
77106
name := util.DetermineEscapePath(backupdir, b.AbsPath)
78107
if _, err := os.Stat(name); errors.Is(err, fs.ErrNotExist) {
79108
_, err = b.overwriteFile(name)
80-
if err == nil {
81-
BackupCompleteChan <- b
82-
}
83109
return err
84110
}
85111

@@ -95,8 +121,6 @@ func (b *SharedBuffer) Backup() error {
95121
return err
96122
}
97123

98-
BackupCompleteChan <- b
99-
100124
return err
101125
}
102126

internal/buffer/buffer.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"strconv"
1414
"strings"
1515
"sync"
16-
"sync/atomic"
1716
"time"
1817

1918
luar "layeh.com/gopher-luar"
@@ -101,7 +100,6 @@ type SharedBuffer struct {
101100
diffLock sync.RWMutex
102101
diff map[int]DiffStatus
103102

104-
RequestedBackup bool
105103
forceKeepBackup bool
106104

107105
// ReloadDisabled allows the user to disable reloads if they
@@ -123,8 +121,6 @@ type SharedBuffer struct {
123121

124122
// Hash of the original buffer -- empty if fastdirty is on
125123
origHash [md5.Size]byte
126-
127-
fini int32
128124
}
129125

130126
func (b *SharedBuffer) insert(pos Loc, value []byte) {
@@ -495,13 +491,11 @@ func (b *Buffer) Fini() {
495491
if !b.Modified() {
496492
b.Serialize()
497493
}
498-
b.RemoveBackup()
494+
b.CancelBackup()
499495

500496
if b.Type == BTStdout {
501497
fmt.Fprint(util.Stdout, string(b.Bytes()))
502498
}
503-
504-
atomic.StoreInt32(&(b.fini), int32(1))
505499
}
506500

507501
// GetName returns the name that should be displayed in the statusline

internal/buffer/save.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"os/signal"
1212
"path/filepath"
1313
"runtime"
14-
"sync/atomic"
1514
"time"
1615
"unicode"
1716

@@ -47,11 +46,14 @@ type saveRequest struct {
4746
}
4847

4948
var saveRequestChan chan saveRequest
50-
var backupRequestChan chan *SharedBuffer
49+
var backupRequestChan chan backupRequest
5150

5251
func init() {
53-
saveRequestChan = make(chan saveRequest, 10)
54-
backupRequestChan = make(chan *SharedBuffer, 10)
52+
// Both saveRequestChan and backupRequestChan need to be non-buffered
53+
// so the save/backup goroutine receives both save and backup requests
54+
// in the same order the main goroutine sends them.
55+
saveRequestChan = make(chan saveRequest)
56+
backupRequestChan = make(chan backupRequest)
5557

5658
go func() {
5759
duration := backupSeconds * float64(time.Second)
@@ -62,14 +64,10 @@ func init() {
6264
case sr := <-saveRequestChan:
6365
size, err := sr.buf.safeWrite(sr.path, sr.withSudo, sr.newFile)
6466
sr.saveResponseChan <- saveResponse{size, err}
67+
case br := <-backupRequestChan:
68+
handleBackupRequest(br)
6569
case <-backupTicker.C:
66-
for len(backupRequestChan) > 0 {
67-
b := <-backupRequestChan
68-
bfini := atomic.LoadInt32(&(b.fini)) != 0
69-
if !bfini {
70-
b.Backup()
71-
}
72-
}
70+
periodicBackup()
7371
}
7472
}
7573
}()
@@ -380,6 +378,9 @@ func (b *SharedBuffer) safeWrite(path string, withSudo bool, newFile bool) (int,
380378
return 0, err
381379
}
382380

381+
// Backup saved, so cancel pending periodic backup, if any
382+
delete(requestedBackups, b)
383+
383384
b.forceKeepBackup = true
384385
size := 0
385386
{

0 commit comments

Comments
 (0)