@@ -26,59 +26,105 @@ type Worker struct {
2626 vcsOpts vcs.Options
2727 pollInterval time.Duration
2828 uuid string
29+ slotCleanup func ()
2930 log * logrus.Entry
3031
3132 metricsMu sync.Mutex
3233 lastCounters * RawCounters
3334}
3435
3536func NewWorker (client * Client , docker DockerConfig , vcsOpts vcs.Options ) * Worker {
37+ uuid , cleanup := acquireAgentSlot ()
38+ client .SetUUID (uuid )
3639 return & Worker {
3740 client : client ,
3841 docker : docker ,
3942 vcsOpts : vcsOpts ,
4043 pollInterval : 1 * time .Second ,
41- uuid : loadOrGenerateUUID (),
44+ uuid : uuid ,
45+ slotCleanup : cleanup ,
4246 log : logrus .WithField ("component" , "agent" ),
4347 }
4448}
4549
46- // uuidFilePath returns the path to the persistent UUID file in the user's config directory .
47- func uuidFilePath () string {
50+ // agentSlotDir returns the directory for agent UUID slot files .
51+ func agentSlotDir () string {
4852 dir , err := os .UserConfigDir ()
4953 if err != nil {
5054 dir = os .TempDir ()
5155 }
52- return filepath .Join (dir , "actionforge" , "agent-uuid" )
56+ return filepath .Join (dir , "actionforge" )
5357}
5458
55- // loadOrGenerateUUID loads a persistent UUID from disk, or generates and saves a new one.
56- func loadOrGenerateUUID () string {
57- path := uuidFilePath ()
58- if data , err := os .ReadFile (path ); err == nil {
59- if id := strings .TrimSpace (string (data )); len (id ) == 36 {
60- return id
59+ // acquireAgentSlot finds and locks the lowest available agent slot.
60+ // Each slot has a persistent UUID file and a lock file. When the process
61+ // exits, the lock is released so the next process can reuse that slot
62+ // (and its UUID/metrics history).
63+ // Returns the UUID and a cleanup function that releases the lock.
64+ func acquireAgentSlot () (string , func ()) {
65+ dir := agentSlotDir ()
66+ _ = os .MkdirAll (dir , 0700 )
67+
68+ const maxSlots = 256
69+ for i := 0 ; i < maxSlots ; i ++ {
70+ lockPath := filepath .Join (dir , fmt .Sprintf ("agent-%d.lock" , i ))
71+ uuidPath := filepath .Join (dir , fmt .Sprintf ("agent-%d.uuid" , i ))
72+
73+ lockFile , err := os .OpenFile (lockPath , os .O_CREATE | os .O_RDWR , 0600 )
74+ if err != nil {
75+ continue
76+ }
77+
78+ if err := lockFileExclusive (lockFile ); err != nil {
79+ if cerr := lockFile .Close (); cerr != nil {
80+ logrus .WithError (cerr ).Warn ("failed to close lock file" )
81+ }
82+ continue
6183 }
84+
85+ // Slot acquired — read or generate UUID
86+ uuid := ""
87+ if data , err := os .ReadFile (uuidPath ); err == nil {
88+ if id := strings .TrimSpace (string (data )); len (id ) == 36 {
89+ uuid = id
90+ }
91+ }
92+ if uuid == "" {
93+ var buf [16 ]byte
94+ _ , _ = rand .Read (buf [:])
95+ buf [6 ] = (buf [6 ] & 0x0f ) | 0x40 // version 4
96+ buf [8 ] = (buf [8 ] & 0x3f ) | 0x80 // variant 1
97+ uuid = fmt .Sprintf ("%08x-%04x-%04x-%04x-%012x" ,
98+ buf [0 :4 ], buf [4 :6 ], buf [6 :8 ], buf [8 :10 ], buf [10 :16 ])
99+ _ = os .WriteFile (uuidPath , []byte (uuid + "\n " ), 0600 )
100+ }
101+
102+ cleanup := func () {
103+ unlockFile (lockFile )
104+ if cerr := lockFile .Close (); cerr != nil {
105+ logrus .WithError (cerr ).Warn ("failed to close lock file" )
106+ }
107+ }
108+ return uuid , cleanup
62109 }
63110
64- // Generate UUID v4
111+ // Fallback: all slots taken, generate ephemeral UUID with no lock
65112 var buf [16 ]byte
66113 _ , _ = rand .Read (buf [:])
67- buf [6 ] = (buf [6 ] & 0x0f ) | 0x40 // version 4
68- buf [8 ] = (buf [8 ] & 0x3f ) | 0x80 // variant 1
69- id := fmt .Sprintf ("%08x-%04x-%04x-%04x-%012x" ,
70- buf [0 :4 ], buf [4 :6 ], buf [6 :8 ], buf [8 :10 ], buf [10 :16 ])
71-
72- _ = os .MkdirAll (filepath .Dir (path ), 0700 )
73- _ = os .WriteFile (path , []byte (id + "\n " ), 0600 )
74- return id
114+ buf [6 ] = (buf [6 ] & 0x0f ) | 0x40
115+ buf [8 ] = (buf [8 ] & 0x3f ) | 0x80
116+ return fmt .Sprintf ("%08x-%04x-%04x-%04x-%012x" ,
117+ buf [0 :4 ], buf [4 :6 ], buf [6 :8 ], buf [8 :10 ], buf [10 :16 ]), func () {}
75118}
76119
77120// maxConsecutiveErrors is the number of consecutive connection errors before
78121// Run returns ErrConnectionLost so the caller can decide to restart.
79122const maxConsecutiveErrors = 10
80123
81124func (w * Worker ) Run (ctx context.Context ) error {
125+ if w .slotCleanup != nil {
126+ defer w .slotCleanup ()
127+ }
82128 w .log .Info ("starting" )
83129
84130 // Take initial snapshot for delta computation
@@ -358,12 +404,16 @@ func (w *Worker) execute(ctx context.Context, job *ClaimResponse) {
358404 env = append (env , "BUILD_TMPDIR=" + tmpDir )
359405 env = append (env , "BUILD_VCS_TYPE=" + job .VCSType )
360406 env = append (env , "BUILD_VCS_URL=" + job .VCSURL )
407+ env = append (env , "BUILD_REF=" + ref )
361408 if job .RepoID != "" {
362409 env = append (env , "BUILD_REPO_ID=" + job .RepoID )
363410 }
364411 if checkout .SHA != "" {
365412 env = append (env , "BUILD_COMMIT_SHA=" + checkout .SHA )
366413 }
414+ if checkout .P4Client != "" {
415+ env = append (env , "P4CLIENT=" + checkout .P4Client )
416+ }
367417
368418 // Resolve env mappings from trigger config (if present)
369419 if len (job .EnvMappings ) > 0 && job .MatrixValues != nil {
0 commit comments