Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 3 additions & 2 deletions src/stateMachines/activation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,8 @@ describe('Activation State Machine', () => {
devices[clientId].nonce = PasswordHelper.generateNonce()
await activation.sendAdminSetup({ input: context })
expect(createSignedStringSpy).toHaveBeenCalled()
expect(invokeWsmanCallSpy).toHaveBeenCalled()
// AdminSetup is one-shot: AMT drops the session on success without replying.
expect(invokeWsmanCallSpy).toHaveBeenCalledWith(context, 0, undefined, true)
})
it('should return null when signature in null', async () => {
context.certChainPfx = { provisioningCertificateObj: { certChain: [
Expand Down Expand Up @@ -496,7 +497,7 @@ describe('Activation State Machine', () => {
null }, fingerprint: { sha256: '82f2ed575db4abe462499cf550dbff9584980d70a0272894639c3653b9ad932c', sha384: 'bb00173b0fb55bc1b24fff5a32a02d210d2bbe16dc6ba4f8300729c1d545313a66930bcd1bcf9ed5a76e82ce602ef04a', sha1: '47d7b7db23f3e300189f54802482b1bd18b945ef' }, hashAlgorithm: 'sha256' }
await activation.sendUpgradeClientToAdmin({ input: context })
expect(createSignedStringSpy).toHaveBeenCalled()
expect(invokeWsmanCallSpy).toHaveBeenCalled()
expect(invokeWsmanCallSpy).toHaveBeenCalledWith(context, 0, undefined, true)
})
it('should send WSMan to change AMT password', async () => {
await activation.changeAMTPassword({ input: context })
Expand Down
6 changes: 4 additions & 2 deletions src/stateMachines/activation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,8 @@ export class Activation {
2,
clientObj.signature
)
return await invokeWsmanCall(input)
// One-shot: ACM activation drops the session on success; don't retry (state machine re-checks status).
return await invokeWsmanCall(input, 0, undefined, true)
}
return null
}
Expand All @@ -300,7 +301,8 @@ export class Activation {
2,
clientObj.signature
)
return await invokeWsmanCall(input)
// One-shot: CCM->ACM upgrade drops the session on success; don't retry (state machine re-checks status).
return await invokeWsmanCall(input, 0, undefined, true)
}
return null
}
Expand Down
33 changes: 33 additions & 0 deletions src/stateMachines/common.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,39 @@ describe('Common', () => {
expect(sendSpy).toHaveBeenCalledTimes(1)
await expect(wsmanPromise).rejects.toBeInstanceOf(UNEXPECTED_PARSE_ERROR)
})
it('should cap oneShot calls to a single attempt even when wsman_max_attempts > 1', async () => {
Environment.Config.wsman_max_attempts = 3
sendSpy = vi.spyOn(devices[clientId].ClientSocket, 'send')
sendSpy.mockImplementation(async () => devices[clientId].reject(new UNEXPECTED_PARSE_ERROR()))
const wsmanPromise = invokeWsmanCall(context, 0, undefined, true)
await expect(wsmanPromise).rejects.toBeInstanceOf(UNEXPECTED_PARSE_ERROR)
expect(sendSpy).toHaveBeenCalledTimes(1)
})
it('should honor the wsman_max_attempts floor when oneShot is false', async () => {
Environment.Config.wsman_max_attempts = 3
sendSpy = vi.spyOn(devices[clientId].ClientSocket, 'send')
sendSpy.mockImplementation(async () => devices[clientId].reject(new UNEXPECTED_PARSE_ERROR()))
const wsmanPromise = invokeWsmanCall(context)
await expect(wsmanPromise).rejects.toBeInstanceOf(UNEXPECTED_PARSE_ERROR)
expect(sendSpy).toHaveBeenCalledTimes(3)
})
it('should not log exhaustion for the expected oneShot timeout', async () => {
const errorLogSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => {})
const wsmanPromise = invokeWsmanCall(context, 0, undefined, true)
vi.advanceTimersByTime(Environment.Config.delay_timer * 1000)
await expect(wsmanPromise).rejects.toBeInstanceOf(GATEWAY_TIMEOUT_ERROR)
expect(errorLogSpy).not.toHaveBeenCalledWith(expect.stringContaining('Max WSMAN attempts'))
errorLogSpy.mockRestore()
})
it('should log exhaustion for a non-timeout oneShot failure', async () => {
const errorLogSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => {})
sendSpy = vi.spyOn(devices[clientId].ClientSocket, 'send')
sendSpy.mockImplementation(async () => devices[clientId].reject(new UNEXPECTED_PARSE_ERROR()))
const wsmanPromise = invokeWsmanCall(context, 0, undefined, true)
await expect(wsmanPromise).rejects.toBeInstanceOf(UNEXPECTED_PARSE_ERROR)
expect(errorLogSpy).toHaveBeenCalledWith(expect.stringContaining('Max WSMAN attempts'))
errorLogSpy.mockRestore()
})
it('should not retry when error is not UNEXPECTED_PARSE_ERROR', async () => {
const expected = {
statusCode: 401,
Expand Down
13 changes: 7 additions & 6 deletions src/stateMachines/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ const timeout = async (ms: number): Promise<void> => {
})
}

const invokeWsmanCall = async <T>(context: any, maxRetries = 0, timeoutMs?: number): Promise<T> => {
const invokeWsmanCall = async <T>(context: any, maxRetries = 0, timeoutMs?: number, oneShot = false): Promise<T> => {
const { clientId } = context
const clientObj = devices[clientId]

Expand All @@ -293,7 +293,8 @@ const invokeWsmanCall = async <T>(context: any, maxRetries = 0, timeoutMs?: numb
}

let retriesUsed = 0
const maxAttempts = Math.max(maxRetries + 1, Environment.Config.wsman_max_attempts)
// One-shot calls (ACM activation/upgrade) drop the session on success; never re-issue them.
const maxAttempts = oneShot ? 1 : Math.max(maxRetries + 1, Environment.Config.wsman_max_attempts)
const timeoutValue = timeoutMs ?? Environment.Config.delay_timer * 1000
Comment thread
madhavilosetty-intel marked this conversation as resolved.
const retryDelayMs = Environment.Config.delay_tls_timer * 1000

Expand Down Expand Up @@ -353,7 +354,9 @@ const invokeWsmanCall = async <T>(context: any, maxRetries = 0, timeoutMs?: numb
continue
}

if (isRetryableError && retriesUsed >= maxAttempts - 1) {
// Skip the exhaustion log only for the expected one-shot timeout; still log real failures.
const isExpectedOneShotTimeout = oneShot && error instanceof GATEWAY_TIMEOUT_ERROR
if (isRetryableError && retriesUsed >= maxAttempts - 1 && !isExpectedOneShotTimeout) {
const errorType = (error as any)?.constructor?.name ?? typeof error
const tunnelState = clientObj?.tlsTunnelManager != null ? 'present' : 'none'
const sessionId = clientObj?.tlsTunnelSessionId ?? 'none'
Expand All @@ -363,10 +366,8 @@ const invokeWsmanCall = async <T>(context: any, maxRetries = 0, timeoutMs?: numb
invokeWsmanLogger.error(
`WSMAN final failure context: attempts=${maxAttempts}, retriesUsed=${retriesUsed}, errorType=${errorType}, tunnelState=${tunnelState}, sessionId=${sessionId}, tunnelNeedsReset=${clientObj?.tlsTunnelNeedsReset === true}, amtReconfiguring=${clientObj?.amtReconfiguring === true}`
)
throw error
} else {
throw error
}
throw error
}
}
return await Promise.reject(new Error('Max retries reached'))
Expand Down
Loading