@@ -9,6 +9,8 @@ export const CROSS_HOST_OWNER_DIRECTORY = "primary.owner.json"
99const OWNER_FILENAME = "owner.json"
1010const PARTICIPANT_PREFIX = "participant."
1111const PARTICIPANT_SUFFIX = ".json"
12+ const RECOVERY_PREFIX = "recovery."
13+ const RECOVERY_SUFFIX = ".claim"
1214const RETIRED_PREFIX = "retired."
1315const ACQUIRE_ATTEMPTS = 10
1416
@@ -130,6 +132,10 @@ function participantPath(directory: string, owner: ProcessOwner): string {
130132 return join ( directory , `${ PARTICIPANT_PREFIX } ${ owner . pid } .${ owner . runToken } ${ PARTICIPANT_SUFFIX } ` )
131133}
132134
135+ function recoveryPath ( directory : string , owner : ProcessOwner ) : string {
136+ return join ( directory , `${ RECOVERY_PREFIX } ${ owner . pid } .${ owner . runToken } ${ RECOVERY_SUFFIX } ` )
137+ }
138+
133139function publishParticipant ( path : string , owner : ProcessOwner ) : void {
134140 const value = serializeOwner ( owner )
135141 try { publishFile ( path , value ) } catch ( error ) {
@@ -156,22 +162,37 @@ function removeParticipantIfOwned(path: string, owner: ProcessOwner): void {
156162 }
157163}
158164
159- function hasLiveParticipants ( directory : string , current : ProcessOwner , dependencies : CrossHostLeaseDependencies ) : boolean {
165+ function recoveryClaimants (
166+ directory : string ,
167+ current : ProcessOwner ,
168+ observedOwner : string ,
169+ dependencies : CrossHostLeaseDependencies ,
170+ ) : ProcessOwner [ ] | undefined {
171+ const claimants = [ current ]
160172 for ( const name of readdirSync ( directory ) ) {
161173 if ( ! name . startsWith ( PARTICIPANT_PREFIX ) || ! name . endsWith ( PARTICIPANT_SUFFIX ) ) continue
162174 const path = join ( directory , name )
163175 const participant = parseOwner ( readIfExists ( path ) ?? "" )
164- if ( ! participant ) return true
176+ if ( ! participant ) return undefined
165177 if ( sameOwner ( participant , current ) ) continue
166178 const stale = ownerIsStale ( participant , dependencies )
167- if ( stale !== true ) return true
168- removeParticipantIfOwned ( path , participant )
179+ if ( stale === true ) {
180+ removeParticipantIfOwned ( path , participant )
181+ try { unlinkSync ( recoveryPath ( directory , participant ) ) } catch { }
182+ continue
183+ }
184+ if ( readIfExists ( recoveryPath ( directory , participant ) ) !== observedOwner ) return undefined
185+ claimants . push ( participant )
169186 }
170- return false
187+ return claimants
171188}
172189
173190function retireOwner ( directory : string , observed : string , owner : ProcessOwner , claimant : ProcessOwner , dependencies : CrossHostLeaseDependencies ) : boolean {
174- if ( ownerIsStale ( owner , dependencies ) !== true || hasLiveParticipants ( directory , claimant , dependencies ) ) return false
191+ if ( ownerIsStale ( owner , dependencies ) !== true ) return false
192+ const claimants = recoveryClaimants ( directory , claimant , observed , dependencies )
193+ if ( ! claimants ) return false
194+ claimants . sort ( ( left , right ) => serializeOwner ( left ) < serializeOwner ( right ) ? - 1 : 1 )
195+ if ( ! sameOwner ( claimants [ 0 ] ! , claimant ) ) return false
175196 if ( readIfExists ( ownerPath ( directory ) ) !== observed ) return false
176197 const retired = join ( directory , `${ RETIRED_PREFIX } ${ owner . pid } .${ owner . runToken } ` )
177198 try {
@@ -220,6 +241,7 @@ export class CrossHostRegistration {
220241 private readonly directory : string ,
221242 readonly owner : ProcessOwner ,
222243 private readonly participant : string ,
244+ private readonly recoveryClaim : string | undefined ,
223245 private primary : boolean ,
224246 ) { }
225247
@@ -237,6 +259,7 @@ export class CrossHostRegistration {
237259 publishParticipant ( participant , owner )
238260 dependencies . onParticipantPublished ?.( )
239261 let primary = false
262+ let recoveryClaim : string | undefined
240263 try {
241264 if ( typeof primaryCandidate === "function" ? primaryCandidate ( ) : primaryCandidate ) {
242265 for ( let attempt = 0 ; attempt < ACQUIRE_ATTEMPTS ; attempt += 1 ) {
@@ -246,12 +269,19 @@ export class CrossHostRegistration {
246269 const existing = parseOwner ( observed )
247270 if ( ! existing ) break
248271 if ( sameOwner ( existing , owner ) ) { primary = true ; break }
272+ if ( ownerIsStale ( existing , dependencies ) === true ) {
273+ recoveryClaim ??= recoveryPath ( directory , owner )
274+ try { publishFile ( recoveryClaim , observed ) } catch ( error ) {
275+ if ( ! hasErrorCode ( error , "EEXIST" ) || readIfExists ( recoveryClaim ) !== observed ) throw error
276+ }
277+ }
249278 if ( ! retireOwner ( directory , observed , existing , owner , dependencies ) ) break
250279 }
251280 }
252- return new CrossHostRegistration ( directory , owner , participant , primary )
281+ return new CrossHostRegistration ( directory , owner , participant , recoveryClaim , primary )
253282 } catch ( error ) {
254283 removeParticipantIfOwned ( participant , owner )
284+ if ( recoveryClaim ) try { unlinkSync ( recoveryClaim ) } catch { }
255285 throw error
256286 }
257287 }
@@ -265,6 +295,7 @@ export class CrossHostRegistration {
265295 release ( ) : boolean {
266296 if ( this . released ) return false
267297 removeParticipantIfOwned ( this . participant , this . owner )
298+ if ( this . recoveryClaim ) try { unlinkSync ( this . recoveryClaim ) } catch { }
268299 this . primary = false
269300 this . released = true
270301 return true
0 commit comments