Skip to content

Commit 03380a0

Browse files
committed
Add test with external locking process
1 parent 4d7f2c4 commit 03380a0

File tree

5 files changed

+155
-17
lines changed

5 files changed

+155
-17
lines changed

internal/pkg/agent/application/upgrade/step_mark.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,9 +263,12 @@ func markerFilePath(dataDirPath string) string {
263263

264264
func lockMarkerFile(markerFileName string) (*flock.Flock, error) {
265265
fLock := flock.New(markerFileName + ".lock")
266-
_, err := fLock.TryLock()
266+
locked, err := fLock.TryLock()
267267
if err != nil {
268-
return nil, err
268+
return nil, fmt.Errorf("locking %s: %w", fLock, err)
269+
}
270+
if !locked {
271+
return nil, fmt.Errorf("failed locking %s", fLock)
269272
}
270273

271274
return fLock, nil

internal/pkg/agent/application/upgrade/step_mark_test.go

Lines changed: 96 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,16 @@
55
package upgrade
66

77
import (
8+
"bufio"
9+
"context"
810
"fmt"
911
"os"
12+
"os/exec"
13+
"path/filepath"
14+
"runtime"
15+
"strings"
1016
"testing"
17+
"time"
1118

1219
"github.com/stretchr/testify/assert"
1320
"github.com/stretchr/testify/require"
@@ -52,11 +59,11 @@ func Test_markUpgradeLocking(t *testing.T) {
5259
}{
5360
{
5461
name: "Lock file is created when writing update marker", args: args{
55-
agent: newAgent456,
56-
previousAgent: prevAgent123,
57-
action: nil,
58-
upgradeDetails: nil,
59-
},
62+
agent: newAgent456,
63+
previousAgent: prevAgent123,
64+
action: nil,
65+
upgradeDetails: nil,
66+
},
6067
afterUpdateMarkerCreation: func(t *testing.T, dataDir string) {
6168
assert.FileExists(t, markerFilePath(dataDir), "Update marker file must exist")
6269
assert.FileExists(t, markerFilePath(dataDir)+".lock", "Update marker lock file must exist")
@@ -69,11 +76,11 @@ func Test_markUpgradeLocking(t *testing.T) {
6976
},
7077
{
7178
name: "Update marker is re-lockable after writing", args: args{
72-
agent: newAgent456,
73-
previousAgent: prevAgent123,
74-
action: nil,
75-
upgradeDetails: nil,
76-
},
79+
agent: newAgent456,
80+
previousAgent: prevAgent123,
81+
action: nil,
82+
upgradeDetails: nil,
83+
},
7784
afterUpdateMarkerCreation: func(t *testing.T, dataDir string) {
7885
assert.FileExists(t, markerFilePath(dataDir), "Update marker file must exist")
7986
assert.FileExists(t, markerFilePath(dataDir)+".lock", "Update marker lock file must exist")
@@ -88,11 +95,11 @@ func Test_markUpgradeLocking(t *testing.T) {
8895
},
8996
{
9097
name: "Update marker creation should not fail if marker is already locked by the same process", args: args{
91-
agent: newAgent456,
92-
previousAgent: prevAgent123,
93-
action: nil,
94-
upgradeDetails: nil,
95-
},
98+
agent: newAgent456,
99+
previousAgent: prevAgent123,
100+
action: nil,
101+
upgradeDetails: nil,
102+
},
96103
beforeUpdateMarkerCreation: func(t *testing.T, dataDir string) {
97104
// write some fake data in update marker file
98105
updateMarkerFilePath := markerFilePath(dataDir)
@@ -115,6 +122,62 @@ func Test_markUpgradeLocking(t *testing.T) {
115122
},
116123
wantErr: assert.NoError,
117124
},
125+
{
126+
name: "Update marker creation should fail if marker is already locked by another process", args: args{
127+
agent: newAgent456,
128+
previousAgent: prevAgent123,
129+
action: nil,
130+
upgradeDetails: nil,
131+
},
132+
beforeUpdateMarkerCreation: func(t *testing.T, dataDir string) {
133+
// write some fake data in update marker file
134+
updateMarkerFilePath := markerFilePath(dataDir)
135+
err := os.WriteFile(updateMarkerFilePath, []byte("this: is not a real update marker"), 0o664)
136+
require.NoError(t, err, "error creating fake update marker")
137+
138+
// lock the fake update marker using an external process
139+
lockFilePath := updateMarkerFilePath + ".lock"
140+
cmdCancel, lockFileCmd := createFileLockerCmd(t, lockFilePath)
141+
142+
fileLockerStdErr, err := lockFileCmd.StderrPipe()
143+
require.NoError(t, err, "Error getting stderr pipe from filelocker")
144+
145+
fileLockedCh := make(chan struct{})
146+
147+
// consume stderr to check for locking
148+
go func() {
149+
scanner := bufio.NewScanner(fileLockerStdErr)
150+
for scanner.Scan() {
151+
line := scanner.Text()
152+
if strings.Contains(line, "Acquired lock on file") {
153+
fileLockedCh <- struct{}{}
154+
}
155+
}
156+
}()
157+
158+
err = lockFileCmd.Start()
159+
require.NoError(t, err, "running filelocker should not fail")
160+
161+
t.Cleanup(func() {
162+
cmdCancel()
163+
_ = lockFileCmd.Wait()
164+
})
165+
166+
select {
167+
case <-fileLockedCh:
168+
// file was locked from the external process: all good
169+
t.Log("external filelocker acquired the lock!")
170+
case <-time.After(30 * time.Second):
171+
t.Fatalf("timed out waiting for file locker to lock the file")
172+
}
173+
},
174+
afterUpdateMarkerCreation: func(t *testing.T, dataDir string) {
175+
// verify we can't read the actual update marker since it's still locked
176+
_, err := LoadMarker(dataDir)
177+
require.Error(t, err, "loading update marker should fail")
178+
},
179+
wantErr: assert.Error,
180+
},
118181
}
119182
for _, tt := range tests {
120183
t.Run(tt.name, func(t *testing.T) {
@@ -131,6 +194,24 @@ func Test_markUpgradeLocking(t *testing.T) {
131194
}
132195
}
133196

197+
func createFileLockerCmd(t *testing.T, lockFilePath string) (context.CancelFunc, *exec.Cmd) {
198+
executableName := "filelocker"
199+
if runtime.GOOS == "windows" {
200+
executableName += ".exe"
201+
}
202+
filelockerExecutablePath := filepath.Join("test", "filelocker", executableName)
203+
require.FileExistsf(
204+
t,
205+
filelockerExecutablePath,
206+
"filelocker executable %s should exist. Please ensure that mage build:testbinaries has been executed.",
207+
filelockerExecutablePath,
208+
)
209+
210+
cmdCtx, cmdCancel := context.WithCancel(t.Context())
211+
lockFileCmd := exec.CommandContext(cmdCtx, filelockerExecutablePath, "-lockfile", lockFilePath)
212+
return cmdCancel, lockFileCmd
213+
}
214+
134215
func checkUpgradeMarker(t *testing.T, updateMarker *UpdateMarker, prevAgent agentInstall, newAgent agentInstall) {
135216
t.Helper()
136217
require.NotNil(t, updateMarker, "update marker should not be nil")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Ignore test binary
2+
filelocker*
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License 2.0;
3+
// you may not use this file except in compliance with the Elastic License 2.0.
4+
5+
// This is a simple program that will lock a file passed using the -lockfile option, used for testing file lock works properly.
6+
// os.Interrupt or signal.SIGTERM will make the program release the lock and exit
7+
package main
8+
9+
import (
10+
"flag"
11+
"log"
12+
"os"
13+
"os/signal"
14+
"syscall"
15+
16+
"github.com/gofrs/flock"
17+
)
18+
19+
const AcquiredLockLogFmt = "Acquired lock on file %s\n"
20+
21+
const lockFileFlagName = "lockfile"
22+
23+
var lockFile = flag.String(lockFileFlagName, "", "path to lock file")
24+
25+
func main() {
26+
27+
signalCh := make(chan os.Signal, 1)
28+
signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
29+
30+
flag.Parse()
31+
if *lockFile == "" {
32+
log.Fatalf("No lockfile specified. Please run %s -%s <path to lockfile>", os.Args[0], lockFileFlagName)
33+
}
34+
35+
fLock := flock.New(*lockFile)
36+
37+
locked, err := fLock.TryLock()
38+
if err != nil {
39+
log.Fatalf("Error locking %s: %s", *lockFile, err.Error())
40+
}
41+
42+
if !locked {
43+
log.Fatalf("Failed acquiring lock on %s", *lockFile)
44+
}
45+
defer fLock.Unlock()
46+
47+
log.Printf(AcquiredLockLogFmt, *lockFile)
48+
49+
s := <-signalCh
50+
log.Printf("Received signal: %s, exiting", s.String())
51+
}

magefile.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ func getTestBinariesPath() ([]string, error) {
435435
filepath.Join(wd, "pkg", "component", "fake", "component"),
436436
filepath.Join(wd, "internal", "pkg", "agent", "install", "testblocking"),
437437
filepath.Join(wd, "pkg", "core", "process", "testsignal"),
438+
filepath.Join(wd, "internal", "pkg", "agent", "application", "upgrade", "test", "filelocker"),
438439
}
439440
return testBinaryPkgs, nil
440441
}

0 commit comments

Comments
 (0)