@@ -23,7 +23,9 @@ import (
2323 "github.com/opentracing/opentracing-go"
2424
2525 "github.com/buildkite/agent/v3/env"
26+ "github.com/buildkite/agent/v3/experiments"
2627 "github.com/buildkite/agent/v3/internal/shellscript"
28+ "github.com/buildkite/agent/v3/lock"
2729 "github.com/buildkite/agent/v3/logger"
2830 "github.com/buildkite/agent/v3/process"
2931 "github.com/buildkite/agent/v3/tracetools"
@@ -67,23 +69,45 @@ type Shell struct {
6769 cmd * command
6870 cmdLock sync.Mutex
6971
72+ // Lock service client, if available
73+ lockClient * lock.Client
74+
7075 // The signal to use to interrupt the command
7176 InterruptSignal process.Signal
7277}
7378
74- // New returns a new Shell
75- func New () (* Shell , error ) {
79+ // Config contains configuration options for the
80+ type Config struct {
81+ SocketsPath string
82+ }
83+
84+ // New returns a new Shell.
85+ func New (ctx context.Context , cfg Config ) (* Shell , error ) {
7686 wd , err := os .Getwd ()
7787 if err != nil {
7888 return nil , fmt .Errorf ("Failed to find current working directory: %w" , err )
7989 }
8090
81- return & Shell {
91+ sh := & Shell {
8292 Logger : StderrLogger ,
8393 Env : env .FromSlice (os .Environ ()),
8494 Writer : os .Stdout ,
8595 wd : wd ,
86- }, nil
96+ }
97+
98+ // Use the Agent API for locking?
99+ if cfg .SocketsPath != "" && experiments .IsEnabled (experiments .AgentAPI ) {
100+ ctx , canc := context .WithTimeout (ctx , 10 * time .Second )
101+ defer canc ()
102+ lc , err := lock .NewClient (ctx , cfg .SocketsPath )
103+ if err != nil {
104+ sh .Logger .Errorf ("Couldn't use Agent API for locking, so falling back to using flock-based locks: %v" , err )
105+ lc = nil
106+ }
107+ sh .lockClient = lc
108+ }
109+
110+ return sh , nil
87111}
88112
89113// WithStdin returns a copy of the Shell with the provided io.Reader set as the
@@ -181,8 +205,8 @@ func (s *Shell) WaitStatus() (process.WaitStatus, error) {
181205 return s .cmd .proc .WaitStatus (), nil
182206}
183207
184- // LockFile is a pid-based lock for cross-process locking
185- type LockFile interface {
208+ // Unlocker types can unlock a cross-process lock (such as an flock).
209+ type Unlocker interface {
186210 Unlock () error
187211}
188212
@@ -222,8 +246,44 @@ func (s *Shell) flock(ctx context.Context, path string, timeout time.Duration) (
222246 return lock , err
223247}
224248
249+ // agentAPILock contains all the information required to unlock an Agent API
250+ // lock-service lock.
251+ type agentAPILock struct {
252+ client * lock.Client
253+ key , token string
254+ }
255+
256+ func (l * agentAPILock ) Unlock () error {
257+ return l .client .Unlock (context .Background (), l .key , l .token )
258+ }
259+
260+ // lockWithAgentAPI acquires a lock in the Agent API lock service.
261+ func (s * Shell ) lockWithAgentAPI (ctx context.Context , path string , timeout time.Duration ) (* agentAPILock , error ) {
262+ absolutePathToLock , err := filepath .Abs (path )
263+ if err != nil {
264+ return nil , fmt .Errorf ("Failed to find absolute path to lock \" %s\" (%v)" , path , err )
265+ }
266+
267+ ctx , cancel := context .WithTimeout (ctx , timeout )
268+ defer cancel ()
269+
270+ token , err := s .lockClient .Lock (ctx , absolutePathToLock )
271+ if err != nil {
272+ return nil , fmt .Errorf ("Failed to acquire lock for %q: %v" , path , err )
273+ }
274+
275+ return & agentAPILock {
276+ client : s .lockClient ,
277+ key : absolutePathToLock ,
278+ token : token ,
279+ }, err
280+ }
281+
225282// Create a cross-process file-based lock based on pid files
226- func (s * Shell ) LockFile (ctx context.Context , path string , timeout time.Duration ) (LockFile , error ) {
283+ func (s * Shell ) LockFile (ctx context.Context , path string , timeout time.Duration ) (Unlocker , error ) {
284+ if s .lockClient != nil {
285+ return s .lockWithAgentAPI (ctx , path , timeout )
286+ }
227287 return s .flock (ctx , path , timeout )
228288}
229289
0 commit comments