| 1 | package workspacelease |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "testing" |
| 6 | "time" |
| 7 | ) |
| 8 | |
| 9 | func newOwnerWithGrace(t *testing.T, root, lockDir string, grace time.Duration) *Owner { |
| 10 | t.Helper() |
| 11 | owner, err := New(root, lockDir, nil) |
| 12 | if err != nil { |
| 13 | t.Fatalf("New: %v", err) |
| 14 | } |
| 15 | owner.graceAfter = grace |
| 16 | return owner |
| 17 | } |
| 18 | |
| 19 | func waitForRelease(t *testing.T, owner *Owner) { |
| 20 | t.Helper() |
| 21 | deadline := time.Now().Add(2 * time.Second) |
| 22 | for time.Now().Before(deadline) { |
| 23 | if !owner.State().Acquired { |
| 24 | return |
| 25 | } |
| 26 | time.Sleep(time.Millisecond) |
| 27 | } |
| 28 | t.Fatal("workspace lease was never released") |
| 29 | } |
| 30 | |
| 31 | // A resident background job (dev server, watcher) whose channel never closes |
| 32 | // must not own the workspace indefinitely once the session is idle. |
| 33 | func TestResidentBackgroundJobReleasesLeaseAfterGrace(t *testing.T) { |
| 34 | root, lockDir := t.TempDir(), t.TempDir() |
| 35 | owner := newOwnerWithGrace(t, root, lockDir, 20*time.Millisecond) |
| 36 | |
| 37 | owner.BeginRun() |
| 38 | if err := owner.AcquireWrite(context.Background()); err != nil { |
| 39 | t.Fatalf("AcquireWrite: %v", err) |
| 40 | } |
| 41 | resident := make(chan struct{}) |
| 42 | owner.RetainUntil(resident) |
| 43 | owner.EndRun() |
| 44 | |
| 45 | waitForRelease(t, owner) |
| 46 | |
| 47 | other, err := New(root, lockDir, nil) |
| 48 | if err != nil { |
| 49 | t.Fatalf("New other: %v", err) |
| 50 | } |
| 51 | other.BeginRun() |
| 52 | defer other.EndRun() |
| 53 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) |
| 54 | defer cancel() |
| 55 | if err := other.AcquireWrite(ctx); err != nil { |
| 56 | t.Fatalf("second session could not acquire the freed lease: %v", err) |
| 57 | } |
| 58 | |
| 59 | // The job ending after the grace release must not release a second time. |
| 60 | close(resident) |
| 61 | time.Sleep(20 * time.Millisecond) |
| 62 | if !other.State().Acquired { |
| 63 | t.Fatal("the retained job released a lease it no longer owned") |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | // The grace release must never fire underneath a run that has already started, |
| 68 | // which is the path that would let two sessions write concurrently. |
| 69 | func TestRunCancelsPendingGraceRelease(t *testing.T) { |
| 70 | owner := newOwnerWithGrace(t, t.TempDir(), t.TempDir(), 20*time.Millisecond) |
| 71 | |
| 72 | owner.BeginRun() |
| 73 | if err := owner.AcquireWrite(context.Background()); err != nil { |
| 74 | t.Fatalf("AcquireWrite: %v", err) |
| 75 | } |
| 76 | resident := make(chan struct{}) |
| 77 | defer close(resident) |
| 78 | owner.RetainUntil(resident) |
| 79 | owner.EndRun() |
| 80 | owner.BeginRun() |
| 81 | defer owner.EndRun() |
| 82 | |
| 83 | time.Sleep(200 * time.Millisecond) |
| 84 | if !owner.State().Acquired { |
| 85 | t.Fatal("grace release fired while a run was active") |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | // A background job that finishes on its own still releases immediately; the |
| 90 | // grace window is a ceiling, not an added delay. |
| 91 | func TestFinishedBackgroundJobReleasesWithoutWaitingForGrace(t *testing.T) { |
| 92 | owner := newOwnerWithGrace(t, t.TempDir(), t.TempDir(), 30*time.Second) |
| 93 | |
| 94 | owner.BeginRun() |
| 95 | if err := owner.AcquireWrite(context.Background()); err != nil { |
| 96 | t.Fatalf("AcquireWrite: %v", err) |
| 97 | } |
| 98 | job := make(chan struct{}) |
| 99 | owner.RetainUntil(job) |
| 100 | owner.EndRun() |
| 101 | |
| 102 | if !owner.State().Acquired { |
| 103 | t.Fatal("lease released while the background job was still running") |
| 104 | } |
| 105 | close(job) |
| 106 | waitForRelease(t, owner) |
| 107 | } |
| 108 | |
| 109 | // The grace release must leave the owner able to reacquire, so two sessions |
| 110 | // sharing a workspace alternate instead of deadlocking each other. |
| 111 | func TestGraceReleaseAllowsReacquireAndAlternation(t *testing.T) { |
| 112 | root, lockDir := t.TempDir(), t.TempDir() |
| 113 | a := newOwnerWithGrace(t, root, lockDir, 20*time.Millisecond) |
| 114 | b := newOwnerWithGrace(t, root, lockDir, 20*time.Millisecond) |
| 115 | |
| 116 | a.BeginRun() |
| 117 | if err := a.AcquireWrite(context.Background()); err != nil { |
| 118 | t.Fatalf("a acquire: %v", err) |
| 119 | } |
| 120 | resident := make(chan struct{}) |
| 121 | defer close(resident) |
| 122 | a.RetainUntil(resident) |
| 123 | a.EndRun() |
| 124 | waitForRelease(t, a) |
| 125 | |
| 126 | // b takes the workspace while a's resident job is still running. |
| 127 | b.BeginRun() |
| 128 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) |
| 129 | defer cancel() |
| 130 | if err := b.AcquireWrite(ctx); err != nil { |
| 131 | t.Fatalf("b acquire after grace: %v", err) |
| 132 | } |
| 133 | b.EndRun() |
| 134 | waitForRelease(t, b) |
| 135 | |
| 136 | a.BeginRun() |
| 137 | defer a.EndRun() |
| 138 | reacquire, cancelReacquire := context.WithTimeout(context.Background(), time.Second) |
| 139 | defer cancelReacquire() |
| 140 | if err := a.AcquireWrite(reacquire); err != nil { |
| 141 | t.Fatalf("a could not reacquire after releasing: %v", err) |
| 142 | } |
| 143 | if st := a.State(); !st.Acquired { |
| 144 | t.Fatalf("a state after reacquire: %+v", st) |
| 145 | } |
| 146 | } |
| 147 |