aboutsummaryrefslogtreecommitdiff
path: root/internal/lock/lock_test.go
blob: 04023aab2f995f28855b0fd67d8c33696c8094b1 (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
// SPDX-License-Identifier: GPL-3.0-or-later

package lock

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"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")
	}
}

// TestStaleLockIsTakenOver: a lock naming a pid that is not running must not
// wedge krino - a machine that lost power mid-run would need manual cleanup.
func TestStaleLockIsTakenOver(t *testing.T) {
	path := filepath.Join(t.TempDir(), "dl.lock")
	if err := os.WriteFile(path, []byte("pid 4294967000\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 stale lock blocked Acquire: %v", err)
	}
	defer l.Release()
	if !l.TookOverStale {
		t.Error("the takeover was not reported to the caller")
	}
}

// TestRunningRejectsPidsBeyondTheKernelsRange: a pid wider than 32 bits in a
// damaged lock file is cut to its low bits by kill(2), so without a range
// check it names some other process - here this very one - and a stale lock
// would never be taken over.
func TestRunningRejectsPidsBeyondTheKernelsRange(t *testing.T) {
	if running(os.Getpid() + 1<<32) {
		t.Error("running(pid + 2^32) is true: the pid was truncated to this process")
	}
	if !running(os.Getpid()) {
		t.Error("running(own pid) is false")
	}
}