aboutsummaryrefslogtreecommitdiff
path: root/internal/lock/lock_test.go
blob: c756bb2a5eee8b241847fd6ea85f8937a383bbfc (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
// SPDX-License-Identifier: GPL-3.0-or-later

package lock

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"
)

func TestAcquireAndRelease(t *testing.T) {
	path := filepath.Join(t.TempDir(), "state", "dl.lock")
	l, err := Acquire(context.Background(), path, false)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(path); err != nil {
		t.Errorf("lock file missing: %v", err)
	}
	if b, _ := os.ReadFile(path); !strings.Contains(string(b), fmt.Sprint(os.Getpid())) {
		t.Errorf("lock file does not name the holder's pid: %q", b)
	}
	if err := l.Release(); err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(path); !os.IsNotExist(err) {
		t.Error("Release left the lock file behind")
	}
	if err := l.Release(); err != nil {
		t.Errorf("a second Release must be harmless: %v", err)
	}
}

func TestAcquireFailsWhenHeldAndNotWaiting(t *testing.T) {
	path := filepath.Join(t.TempDir(), "dl.lock")
	first, err := Acquire(context.Background(), path, false)
	if err != nil {
		t.Fatal(err)
	}
	defer first.Release()
	if _, err := Acquire(context.Background(), path, false); !errors.Is(err, ErrHeld) {
		t.Fatalf("second Acquire err = %v, want ErrHeld", err)
	}
}

func TestAcquireWaitsUntilReleased(t *testing.T) {
	path := filepath.Join(t.TempDir(), "dl.lock")
	first, err := Acquire(context.Background(), path, false)
	if err != nil {
		t.Fatal(err)
	}
	go func() {
		time.Sleep(150 * time.Millisecond)
		first.Release()
	}()
	start := time.Now()
	second, err := Acquire(context.Background(), path, true)
	if err != nil {
		t.Fatalf("waiting Acquire failed: %v", err)
	}
	defer second.Release()
	if time.Since(start) < 100*time.Millisecond {
		t.Error("Acquire returned before the first holder released")
	}
}

// TestAcquireRespectsContextCancellation: a waiting Acquire must not
// ignore an interrupt - a cancelled ctx must return promptly with
// ctx.Err(), not poll forever. A regression here would HANG rather than
// fail, so the wait for Acquire's result is itself bounded with its own
// hard timeout: a regression must fail this test, not hang the whole
// suite.
func TestAcquireRespectsContextCancellation(t *testing.T) {
	path := filepath.Join(t.TempDir(), "dl.lock")
	held, err := Acquire(context.Background(), path, false)
	if err != nil {
		t.Fatal(err)
	}
	defer held.Release()

	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
	defer cancel()

	result := make(chan error, 1)
	start := time.Now()
	go func() {
		_, err := Acquire(ctx, path, true)
		result <- err
	}()

	select {
	case err := <-result:
		if !errors.Is(err, context.DeadlineExceeded) {
			t.Fatalf("Acquire err = %v, want context.DeadlineExceeded", err)
		}
		if elapsed := time.Since(start); elapsed > 500*time.Millisecond {
			t.Errorf("Acquire took %v to notice cancellation, want well under a second", elapsed)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("Acquire ignored context cancellation and is still blocked")
	}
}

// TestALockFileWithoutAHolderIsFree: a machine that lost power mid-run
// leaves the lock file behind, but nothing holding it. The text in that
// file is for a human; it must not wedge the next run, whatever it says.
func TestALockFileWithoutAHolderIsFree(t *testing.T) {
	path := filepath.Join(t.TempDir(), "dl.lock")
	if err := os.WriteFile(path, []byte("pid 1\nstarted 2020-01-01T00:00:00Z\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	l, err := Acquire(context.Background(), path, false)
	if err != nil {
		t.Fatalf("a lock file nobody holds blocked Acquire: %v", err)
	}
	defer l.Release()
	if b, _ := os.ReadFile(path); !strings.Contains(string(b), fmt.Sprint(os.Getpid())) {
		t.Errorf("the lock file still names the old holder: %q", b)
	}
}

// TestALockDiesWithItsProcess: the kernel drops an flock when the last
// descriptor closes, however the process ended. A holder that vanishes
// without unlinking - a crash, a kill -9 - must leave the directory usable
// at once. This is the reason the lock is the kernel's rather than a pid
// written into a file and believed.
func TestALockDiesWithItsProcess(t *testing.T) {
	path := filepath.Join(t.TempDir(), "dl.lock")
	l, err := Acquire(context.Background(), path, false)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := Acquire(context.Background(), path, false); !errors.Is(err, ErrHeld) {
		t.Fatalf("while held, Acquire err = %v, want ErrHeld", err)
	}
	// Close the descriptor without unlinking, as a crash would leave it.
	if err := l.f.Close(); err != nil {
		t.Fatal(err)
	}
	l.f = nil
	if _, err := os.Stat(path); err != nil {
		t.Fatalf("the lock file should still be there: %v", err)
	}
	second, err := Acquire(context.Background(), path, false)
	if err != nil {
		t.Fatalf("a lock file left by a dead holder still blocks Acquire: %v", err)
	}
	second.Release()
}

// TestOnlyOneAcquireWinsAtOnce: the promise is that a directory is locked
// while krino works in it. Judging a lock stale and recreating it cannot be
// made atomic, so two runs reaching that conclusion together could both
// believe they held it; the kernel's lock has one winner by construction.
func TestOnlyOneAcquireWinsAtOnce(t *testing.T) {
	path := filepath.Join(t.TempDir(), "dl.lock")
	for round := 0; round < 20; round++ {
		const n = 8
		start := make(chan struct{})
		var mu sync.Mutex
		var won []*Lock
		var wg sync.WaitGroup
		for i := 0; i < n; i++ {
			wg.Add(1)
			go func() {
				defer wg.Done()
				<-start
				if l, err := Acquire(context.Background(), path, false); err == nil {
					mu.Lock()
					won = append(won, l)
					mu.Unlock()
				}
			}()
		}
		close(start)
		wg.Wait()
		if len(won) != 1 {
			t.Fatalf("round %d: %d callers hold the same lock at once, want 1", round, len(won))
		}
		for _, l := range won {
			if err := l.Release(); err != nil {
				t.Fatal(err)
			}
		}
	}
}

// TestHeldErrorNamesTheLockAndHolder: "another krino is working in this
// directory" leaves nothing to act on when the holder is a pid that was
// reused after a crash - the lock is then live for ever as far as krino can
// tell. The error must name the file to remove and the pid it blames.
func TestHeldErrorNamesTheLockAndHolder(t *testing.T) {
	path := filepath.Join(t.TempDir(), "dl.lock")
	first, err := Acquire(context.Background(), path, false)
	if err != nil {
		t.Fatal(err)
	}
	defer first.Release()

	_, err = Acquire(context.Background(), path, false)
	if !errors.Is(err, ErrHeld) {
		t.Fatalf("second Acquire err = %v, want ErrHeld", err)
	}
	if got := err.Error(); !strings.Contains(got, path) {
		t.Errorf("the error does not name the lock file %q: %s", path, got)
	}
	if got := err.Error(); !strings.Contains(got, fmt.Sprint(os.Getpid())) {
		t.Errorf("the error does not name the holder's pid %d: %s", os.Getpid(), got)
	}
}