Skip to content

Lock upgrade marker #8254

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jun 11, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,33 @@ import (
"fmt"
"os"
"path/filepath"

"github.com/gofrs/flock"
)

// ErrAppAlreadyRunning error returned when another elastic-agent is already holding the lock.
var ErrAppAlreadyRunning = fmt.Errorf("another elastic-agent is already running")

// AppLocker locks the agent.lock file inside the provided directory.
type AppLocker struct {
lock *flock.Flock
*FileLocker
}

// NewAppLocker creates an AppLocker that locks the agent.lock file inside the provided directory.
func NewAppLocker(dir, lockFileName string) *AppLocker {
if _, err := os.Stat(dir); os.IsNotExist(err) {
_ = os.Mkdir(dir, 0755)
}
return &AppLocker{
lock: flock.New(filepath.Join(dir, lockFileName)),
}
}

// TryLock tries to grab the lock file and returns error if it cannot.
func (a *AppLocker) TryLock() error {
locked, err := a.lock.TryLock()
lockFilePath := filepath.Join(dir, lockFileName)
lock, err := NewFileLocker(lockFilePath, WithCustomNotLockedError(ErrAppAlreadyRunning))
if err != nil {
return err
// should never happen, if it does something is seriously wrong. Better to abort here and let a human take over.
panic(fmt.Errorf("creating new file locker %s: %w", lockFilePath, err))
}
if !locked {
return ErrAppAlreadyRunning
}
return nil

return &AppLocker{FileLocker: lock}
}

// Unlock releases the lock file.
func (a *AppLocker) Unlock() error {
return a.lock.Unlock()
// TryLock tries to grab the lock file and returns error if it cannot.
func (a *AppLocker) TryLock() error {
return a.Lock()
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ func TestAppLocker(t *testing.T) {
locker2 := NewAppLocker(tmp, testLockFile)

require.NoError(t, locker1.TryLock())
assert.Error(t, locker2.TryLock())
assert.ErrorIs(t, locker2.TryLock(), ErrAppAlreadyRunning)
require.NoError(t, locker1.Unlock())
require.NoError(t, locker2.TryLock())
assert.Error(t, locker1.TryLock())
assert.ErrorIs(t, locker1.TryLock(), ErrAppAlreadyRunning)
require.NoError(t, locker2.Unlock())
}
111 changes: 111 additions & 0 deletions internal/pkg/agent/application/filelock/file_locker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.

package filelock

import (
"context"
"errors"
"fmt"
"time"

"github.com/gofrs/flock"
)

var (
ErrZeroTimeout = errors.New("must specify a non-zero timeout for a blocking file locker")
ErrNotLocked = errors.New("file not locked")
)

// FileLocker is a thin wrapper around "github.com/gofrs/flock" Flock providing both blocking and non-blocking file locking.
// It exposes a simplified Lock*/Unlock interface and by default is non-blocking.
// If it's not possible to acquire a lock on the specified file ErrNotLocked (directly or wrapped in another error) is returned by default.
// It's possible to customize FileLocker behavior specifying one or more FileLockerOption at creation time.
type FileLocker struct {
fileLock *flock.Flock
blocking bool
timeout time.Duration
customNotLockedError error
}

func NewFileLocker(lockFilePath string, opts ...FileLockerOption) (*FileLocker, error) {
flocker := &FileLocker{fileLock: flock.New(lockFilePath)}
for _, opt := range opts {
if err := opt(flocker); err != nil {
return nil, fmt.Errorf("applying options to new file locker: %w", err)
}
}
return flocker, nil
}

// Lock() will attempt to lock the configured lockfile. Depending on the options specified at FileLocker creation this
// call can be blocking or non-blocking. In order to use a blocking FileLocker a timeout must be specified at creation
// specifying WithTimeout() option.
// Even in case of a blocking FileLocker the maximum duration of the locking attempt will be the timeout specified at creation.
// If no lock can be acquired ErrNotLocked error will be returned by default, unless a custom "not locked" error has been
// specified with WithCustomNotLockedError at creation.
func (fl *FileLocker) Lock() error {
return fl.LockContext(context.Background())
}

// LockWithContext() will attempt to lock the configured lockfile. It has the same semantics as Lock(), additionally it
// allows passing a context as an argument to back out of locking attempts when context expires (useful in case of a
// blocking FileLocker)
func (fl *FileLocker) LockContext(ctx context.Context) error {
var locked bool
var err error

if fl.blocking {
timeoutCtx, cancel := context.WithTimeout(ctx, fl.timeout)
defer cancel()
locked, err = fl.fileLock.TryLockContext(timeoutCtx, time.Second)
} else {
locked, err = fl.fileLock.TryLock()
}

if err != nil {
return fmt.Errorf("locking %s: %w", fl.fileLock.Path(), err)
}
if !locked {
if fl.customNotLockedError != nil {
return fmt.Errorf("failed locking %s: %w", fl.fileLock.Path(), fl.customNotLockedError)
}
return fmt.Errorf("failed locking %s: %w", fl.fileLock.Path(), ErrNotLocked)
}
return nil
}

func (fl *FileLocker) Unlock() error {
return fl.fileLock.Unlock()
}

func (fl *FileLocker) Locked() bool {
return fl.fileLock.Locked()
}

type FileLockerOption func(locker *FileLocker) error

// WithCustomNotLockedError will set a custom error to be returned when it's not possible to acquire a lock
func WithCustomNotLockedError(customError error) FileLockerOption {
return func(locker *FileLocker) error {
locker.customNotLockedError = customError
return nil
}
}

// WithTimeout will set the FileLocker to be blocking and will enforce a non-zero timeout.
// If a zero timeout is passed this option will error out, failing the FileLocker creation.
func WithTimeout(timeout time.Duration) FileLockerOption {
return func(locker *FileLocker) error {

if timeout == 0 {
return ErrZeroTimeout
}

locker.blocking = true
locker.timeout = timeout

return nil
}
}
Loading
Loading