|
| 1 | +# Timeout Functionality: Client Perspective |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +The response timeout feature is primarily designed to prevent the client from blocking indefinitely when the server fails to respond to a request. This is most relevant for **blocking operations** such as wolfCrypt cryptographic calls, where the client sends a request and polls for a response in a tight loop. Without a timeout, a non-responsive server would cause the client to hang forever. |
| 6 | + |
| 7 | +Since the timeout is checked inside `wh_CommClient_RecvResponse`, it **can** also apply to the split (async) API where the caller manually polls `RecvResponse`. However, in the async case the timeout is only evaluated each time the caller invokes `RecvResponse` -- it does not proactively notify the caller or fire asynchronously. If the caller is not actively polling, the timeout has no effect. |
| 8 | + |
| 9 | +## 1. Configuration at Init Time |
| 10 | + |
| 11 | +The timeout feature uses a callback-based abstraction (similar to the lock feature) that allows platform-specific timer implementations without introducing OS dependencies in core wolfHSM code. A platform port provides a callback table implementing the timer operations, and the core timeout module delegates to these callbacks. |
| 12 | + |
| 13 | +The timeout lives in the comm layer. When creating a client, you provide a `whTimeoutConfig` in the `whCommClientConfig`: |
| 14 | +```c |
| 15 | +/* Platform-specific setup (e.g. POSIX) */ |
| 16 | +posixTimeoutContext posixCtx = {0}; |
| 17 | +posixTimeoutConfig posixCfg = {.timeoutUs = WH_SEC_TO_USEC(5)}; |
| 18 | +whTimeoutCb timeoutCbTable = POSIX_TIMEOUT_CB; |
| 19 | + |
| 20 | +/* NOTE: The callback table, platform context, and expiredCtx must remain valid |
| 21 | + * for the lifetime of the whCommClient/whTimeout instance. Do not use stack |
| 22 | + * locals that go out of scope while the client is still in use. */ |
| 23 | +whTimeoutConfig timeoutCfg = { |
| 24 | + .cb = &timeoutCbTable, /* platform callback table */ |
| 25 | + .context = &posixCtx, /* platform context */ |
| 26 | + .config = &posixCfg, /* platform-specific config */ |
| 27 | + .expiredCb = myTimeoutHandler, /* optional app callback on expiry */ |
| 28 | + .expiredCtx = myAppContext, /* context passed to app callback */ |
| 29 | +}; |
| 30 | +whCommClientConfig commConfig = { |
| 31 | + .transport_cb = &transportCb, |
| 32 | + .transport_context = &transportCtx, |
| 33 | + .transport_config = &transportCfg, |
| 34 | + .client_id = 1, |
| 35 | + .respTimeoutConfig = &timeoutCfg, /* attach timeout config */ |
| 36 | +}; |
| 37 | +whClientConfig clientCfg = { |
| 38 | + .comm = &commConfig, |
| 39 | +}; |
| 40 | +wh_Client_Init(&clientCtx, &clientCfg); |
| 41 | +``` |
| 42 | +
|
| 43 | +During `wh_CommClient_Init`, the timeout is initialized via `wh_Timeout_Init()`. This calls the platform `init` callback to set up timer resources but doesn't start any timer yet. |
| 44 | +If `respTimeoutConfig` is NULL (or `cb` is NULL), the timeout enters no-op mode and never expires. |
| 45 | +
|
| 46 | +## 2. How the Timeout Works |
| 47 | +
|
| 48 | +The timeout is handled transparently in the comm layer: |
| 49 | +
|
| 50 | +1. **`wh_CommClient_SendRequest`**: After a successful send, starts the response timer via `wh_Timeout_Start()`. |
| 51 | +2. **`wh_CommClient_RecvResponse`**: When the transport returns `WH_ERROR_NOTREADY`, checks `wh_Timeout_Expired()`. If expired, returns `WH_ERROR_TIMEOUT`. On successful receive, stops the timer via `wh_Timeout_Stop()`. |
| 52 | +
|
| 53 | +For blocking (synchronous) client APIs, this means the internal `do { ... } while (ret == WH_ERROR_NOTREADY)` polling loop automatically gets timeout support -- the client will return `WH_ERROR_TIMEOUT` instead of spinning forever if the server does not respond within the configured deadline. |
| 54 | +
|
| 55 | +For split (async) APIs where the application calls `SendRequest` and `RecvResponse` separately, the timeout check occurs each time `RecvResponse` is called and returns `WH_ERROR_NOTREADY`. The timeout does **not** interrupt the caller or provide out-of-band notification -- it is purely poll-based. |
| 56 | +
|
| 57 | +``` |
| 58 | +Client App CommClient whTimeout |
| 59 | + | | | |
| 60 | + |-- wh_Client_AesCbc() -------->| | |
| 61 | + | |-- SendRequest ------> cb->start() |
| 62 | + | | | |
| 63 | + | |-- RecvResponse (NOTREADY) | |
| 64 | + | |-- Expired? -------> cb->expired() -> no |
| 65 | + | |-- RecvResponse (NOTREADY) | |
| 66 | + | |-- Expired? -------> cb->expired() -> no |
| 67 | + | | ... | |
| 68 | + | |-- RecvResponse (NOTREADY) | |
| 69 | + | |-- Expired? -------> cb->expired() -> YES |
| 70 | + | | |-- expiredCb() |
| 71 | + |<-- WH_ERROR_TIMEOUT -----------| | |
| 72 | +``` |
| 73 | +
|
| 74 | +## 3. What the Client Sees |
| 75 | +
|
| 76 | +From the application's perspective, any client API that waits for a server response can now return `WH_ERROR_TIMEOUT` (-2010) instead of hanging indefinitely. The application can then decide how to handle it -- retry, log, fail gracefully, etc. |
| 77 | +The `expiredCb` fires *before* the error is returned, so you can use it for logging or cleanup without needing to check the return code first. |
| 78 | +
|
| 79 | +## 4. Overriding Expiration via the Callback |
| 80 | +
|
| 81 | +The application expired callback receives a pointer to the `isExpired` flag and can override it by setting `*isExpired = 0`. This suppresses the expiration for the current check, allowing the polling loop to continue. A common use case is to extend the timeout deadline: clear the flag, then call `wh_Timeout_Start()` to restart the timer. |
| 82 | +
|
| 83 | +The callback can also return a non-zero error code to signal a failure. When it does, `wh_Timeout_Expired()` propagates that error directly to the caller instead of returning the expired flag. |
| 84 | +
|
| 85 | +```c |
| 86 | +static int myOverrideCb(whTimeout* timeout, int* isExpired) |
| 87 | +{ |
| 88 | + int* retryCount = (int*)timeout->expiredCtx; |
| 89 | + if (retryCount == NULL) { |
| 90 | + return WH_ERROR_BADARGS; |
| 91 | + } |
| 92 | +
|
| 93 | + (*retryCount)++; |
| 94 | +
|
| 95 | + if (*retryCount <= 1) { |
| 96 | + /* First expiration: suppress and restart the timer */ |
| 97 | + *isExpired = 0; |
| 98 | + wh_Timeout_Start(timeout); |
| 99 | + } |
| 100 | + /* Subsequent expirations: allow the timeout to fire */ |
| 101 | + return WH_ERROR_OK; |
| 102 | +} |
| 103 | +
|
| 104 | +int retryCount = 0; |
| 105 | +posixTimeoutContext posixCtx = {0}; |
| 106 | +posixTimeoutConfig posixCfg = {.timeoutUs = WH_SEC_TO_USEC(5)}; |
| 107 | +whTimeoutCb timeoutCbTable = POSIX_TIMEOUT_CB; |
| 108 | +
|
| 109 | +whTimeoutConfig timeoutCfg = { |
| 110 | + .cb = &timeoutCbTable, |
| 111 | + .context = &posixCtx, |
| 112 | + .config = &posixCfg, |
| 113 | + .expiredCb = myOverrideCb, |
| 114 | + .expiredCtx = &retryCount, |
| 115 | +}; |
| 116 | +``` |
| 117 | + |
| 118 | +## 5. Design Notes |
| 119 | +- **Primary use case: blocking wolfCrypt operations.** The timeout is designed to prevent indefinite hangs when the server fails to respond to blocking client API calls, which currently only exist when using the wolfCrypt API for crypto. These calls internally poll `RecvResponse` in a tight loop, and the timeout provides automatic protection against a non-responsive server. |
| 120 | +- **Async API compatibility.** The timeout mechanism also works with the split wolfHSM `SendRequest`/`RecvResponse` API, but only checks for expiration when `RecvResponse` is called by the application. It is purely poll-driven, and there is no callback, signal, or interrupt that fires independently. If the application stops calling `RecvResponse`, the timeout will not trigger. |
| 121 | +- **The timeout is per-comm-client, not per-call.** All operations for a given client share the same `respTimeout` context with the same duration. You can call `wh_Timeout_Set()` to change the duration between calls, but there's no per-operation override. |
| 122 | +- **Timer starts on send, checks on receive.** The timer window begins when a request is successfully sent, measuring the full round-trip wait. |
0 commit comments