Exchange.kt

package eu.inqudium.legatium.restclient.logging

import eu.inqudium.legatium.common.TraceMdcKeys
import org.springframework.http.HttpHeaders
import org.springframework.http.client.ClientHttpResponse
import java.nio.charset.Charset
import java.util.concurrent.atomic.AtomicBoolean

/**
 * All state one outbound exchange accumulates between interceptor entry and log emission at response
 * close: the request-side coordinates captured EAGERLY at wiring time (the emission runs from the
 * response's `close`, possibly on another thread than the one that sent the request), the body captures,
 * and what the wire call produced. Fields written after construction are `@Volatile`: the call, the body
 * read and the close can happen on different threads when a host hands the response around.
 */
internal class Exchange(
    val method: String,
    /** The request TARGET, `scheme://host[:port]/path` without the query - the message and MDC coordinate. */
    val target: String,
    /** The peer host with an explicit port when the URI names one; null for a URI without an authority. */
    val host: String?,
    /** The raw request path as sent (`/` for an empty path). */
    val path: String,
    val query: String?,
    /**
     * The exchange identity (`adapter_request_id`, ADR-0002): the `traceparent` trace id when the outgoing
     * request carried a conformant one, otherwise the accepted or generated-and-sent correlation id.
     */
    val requestId: String,
    val requestHeaders: List<Pair<String, String>>,
    /** The URI template the client recorded for the request (`RestClient.uri(String, ...)`); null for an expanded URI. */
    val uriTemplate: String?,
    val requestCapture: BoundedBodyCapture?,
    val responseCapture: BoundedBodyCapture?,
    /** Charset of the request body for the logged value, resolved from the Content-Type at wiring time. */
    val requestCharset: Charset,
    val startNanos: Long,
    /**
     * Trace context parsed from the outgoing W3C `traceparent` header: the trace id is the client span's
     * trace id; the parent-id is the local client span the peer will see as its parent (see
     * [TraceMdcKeys]). Null without the header.
     */
    val traceId: String? = null,
    val spanId: String? = null,
) {
    /**
     * The exactly-once guard of the COMPLETION (gauge close + emission): the response close and the
     * no-response failure path can never both happen for one exchange, but a response closed twice can -
     * whoever wins this CAS completes, and the emitter trusts it (one guard, like the reactive twin's
     * state machine).
     */
    val completed = AtomicBoolean(false)

    /**
     * The failure of the call - the exception the wire call threw (no response), or the exception the
     * application met on the response it was handed: opening or reading the body, asking for status or
     * headers, closing the body stream or the response (a response exists, its status may be known).
     */
    @Volatile
    var failure: Throwable? = null

    /**
     * The REAL response the wire call produced. Null when the call produced no response. Status and
     * headers are SNAPSHOTTED at handover ([responseStatus], [responseHeaders]) - they are final there,
     * and the emission runs after the client closed the response, when an engine need not answer.
     */
    @Volatile
    var response: ClientHttpResponse? = null

    /** The status code read at handover; null when the call produced no response or the engine could not say. */
    @Volatile
    var responseStatus: Int? = null

    /** The response headers read at handover; null when the call produced no response or the engine could not say. */
    @Volatile
    var responseHeaders: HttpHeaders? = null
}