Answer to the core question
When a servlet is executed with servlet-loop, the same continuation instance is reused for every retry. Any mutable state that the handler has already altered – for example a database row that was updated before a transient failure – remains in that continuation. Consequently, a retry that re‑enters the same servlet will re‑execute the write, leading to duplicate side‑effects.
In contrast, servlet-per-request spawns a brand‑new servlet procedure for each invocation. The request bindings, environment, and any mutable objects created during the previous attempt are discarded. Each retry therefore starts with a clean slate, and the write operation is performed only once per retry attempt.
Reconstructing the original request
An external retry wrapper can capture the full request context before the first invocation:
(define (capture-request req)
(list (servlet/request-params req)
(servlet/request-headers req)
(servlet/request-method req)))
On a retry, the wrapper passes the stored parameters back to a fresh servlet-per-request instance:
(apply servlet-per-request my-servlet captured-params)
This guarantees that the retry receives exactly the same input as the original call, but with no carried‑over mutable state.
Recommended pattern for idempotent writes
- Use
servlet-per-request for handlers that perform writes which must not be duplicated.
- Wrap the handler in a retry loop that catches transient errors (e.g., database timeouts) and re‑invokes the servlet with the captured request.
- Ensure that any side‑effects performed before the error are rolled back or made idempotent (e.g., using a database transaction or a unique constraint).
- Document the retry policy and the idempotency guarantees so that future maintainers understand the isolation boundaries.
When to keep servlet-loop
If the servlet relies on loop‑specific continuation state – such as a long‑running computation that must preserve partial results across retries – then servlet-loop is appropriate. In that case, you must explicitly guard writes with idempotent logic or transactional rollbacks to avoid duplication.
Next diagnostic step
Does your servlet use any loop‑specific continuation features (e.g., a mutable counter stored in the continuation’s environment)? If so, switching to servlet-per-request may alter its behavior. Confirm whether such state exists before applying the retry wrapper.