Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ The context system carries metadata through the request/response lifecycle:
| Type | Role |
|-------------------|----------------------------------------------------------------------------|
| `CallContext` | Base interface — provides `instrumentationContext` and a per-call `callKey`; `AutoCloseable` |
| `DispatchContext` | Head of the promotion chain — mints the `callKey` for the call |
| `DispatchContext` | Head of the promotion chain — generates the `callKey` for the call |
| `RequestContext` | Adds the outgoing `Request` to the chain |
| `ExchangeContext` | Full exchange context — carries both request and response |
| `ContextStore` | Thread-safe store keyed by `callKey` for retrieving a call's live context |
Expand Down
4 changes: 2 additions & 2 deletions docs/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ data class DispatchContext(

**Default factory**: `DispatchContext.default()` creates a context with
`NoopInstrumentationContext` for non-instrumented calls. Because the no-op context's trace and
span ids are shared constants, `default()` mints a process-unique `callKey` so two untraced
span ids are shared constants, `default()` generates a process-unique `callKey` so two untraced
calls cannot collide in the store.

### RequestContext
Expand Down Expand Up @@ -635,7 +635,7 @@ afterwards.
### Context Flow

```
1. DispatchContext.default() → DispatchContext created (mints a unique callKey)
1. DispatchContext.default() → DispatchContext created (generates a unique callKey)
2. dispatchCtx.toRequestContext(request) → RequestContext stored in ContextStore
3. httpClient.execute(request) → HTTP call happens
4. requestCtx.toExchangeContext(response) → ExchangeContext replaces it in ContextStore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ import java.util.Locale
import java.util.function.Consumer
import java.util.function.Function

// Top-level — deliberately NOT in the companion. The companion initializes its `global` slot by
// constructing a Configuration (which reads these defaults); keeping the seams at file scope makes
// their initialization independent of companion init order, sidestepping that ordering hazard.
// `@get:` targets the getter — a bare @JvmSynthetic on a top-level val lands on the backing field
// and would leave the (mangled) getter Java-visible.

/** Default environment-variable lookup seam: delegates to [System.getenv]. */
@get:JvmSynthetic
internal val DEFAULT_ENV_SOURCE: Function<String, String?> = Function { name -> System.getenv(name) }

/** Default system-property lookup seam: delegates to [System.getProperty]. */
@get:JvmSynthetic
internal val DEFAULT_PROPS_SOURCE: Function<String, String?> = Function { name -> System.getProperty(name) }

/**
* Layered runtime configuration: explicit override -> environment variable -> system property -> default.
*
Expand Down Expand Up @@ -51,9 +65,9 @@ public class Configuration internal constructor(
@get:JvmSynthetic
internal val overrides: Map<String, String>,
@get:JvmSynthetic
internal val envSource: Function<String, String?> = Function { name -> System.getenv(name) },
internal val envSource: Function<String, String?> = DEFAULT_ENV_SOURCE,
@get:JvmSynthetic
internal val propsSource: Function<String, String?> = Function { name -> System.getProperty(name) },
internal val propsSource: Function<String, String?> = DEFAULT_PROPS_SOURCE,
) {
/**
* Returns a fresh [ConfigurationBuilder] preloaded with this instance's overrides and lookup
Expand Down Expand Up @@ -122,14 +136,7 @@ public class Configuration internal constructor(
public fun getBoolean(
name: String,
default: Boolean,
): Boolean {
val raw = get(name) ?: return default
return when (raw.lowercase(Locale.US)) {
"true" -> true
"false" -> false
else -> default
}
}
): Boolean = get(name)?.lowercase(Locale.US)?.toBooleanStrictOrNull() ?: default

/**
* Duration accessor. Supports ISO-8601 (`PT5S`, `P1D`) and shorthand (`500ms`, `5s`, `1m`, `2h`, `1d`).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import java.util.function.Function
*/
public class ConfigurationBuilder : Builder<Configuration> {
private val overrides = mutableMapOf<String, String>()
private var envSource: Function<String, String?> = Function { name -> System.getenv(name) }
private var propsSource: Function<String, String?> = Function { name -> System.getProperty(name) }
private var envSource: Function<String, String?> = DEFAULT_ENV_SOURCE
private var propsSource: Function<String, String?> = DEFAULT_PROPS_SOURCE

/** Creates an empty builder with the default env / system-property lookup sources. */
public constructor()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import org.dexpace.sdk.core.instrumentation.InstrumentationContext
* shares one constant trace id across every untraced call, and an inbound W3C trace
* legitimately shares a single trace id across many spans. Keying by the trace id would
* let concurrent calls overwrite and evict each other's live entries. [DispatchContext]
* mints a unique [callKey] once at the head of the chain, and each promotion carries that
* generates a unique [callKey] once at the head of the chain, and each promotion carries that
* same key forward so the whole chain shares one stable, call-unique store slot.
*
* Implements [AutoCloseable] so users can `use { ... }` the context; the default [close]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import java.util.concurrent.atomic.AtomicLong
*/
public data class DispatchContext(
override val instrumentationContext: InstrumentationContext,
override val callKey: String = mintCallKey(instrumentationContext),
override val callKey: String = generateCallKey(instrumentationContext),
) : CallContext {
/**
* Promotes this dispatch context into a [RequestContext] bound to [request] and stores
Expand All @@ -51,36 +51,29 @@ public data class DispatchContext(
}

public companion object {
private val mintCounter: AtomicLong = AtomicLong()

/**
* Derives the trace/span portion of a store key for [instrumentationContext]
* (`traceId:spanId`). This portion is not call-unique on its own — the no-op context
* shares constant ids, an inbound trace shares a trace id across spans, and a span id
* may be reused across sibling calls — so [mintCallKey] appends a process-unique
* counter to it for the actual key.
*/
private fun deriveCallKey(instrumentationContext: InstrumentationContext): String =
"${instrumentationContext.traceId.value}:${instrumentationContext.spanId.value}"
private val generateCounter: AtomicLong = AtomicLong()

/**
* A dispatch context with a no-op instrumentation context; used when tracing is
* disabled. The primary constructor's default [callKey] already mints a process-unique
* disabled. The primary constructor's default [callKey] already generates a process-unique
* key, so this just constructs one with the no-op context.
*/
public fun default(): DispatchContext = DispatchContext(NoopInstrumentationContext)

/**
* Mints a call-unique store key by appending a monotonically increasing,
* process-unique counter to [deriveCallKey]'s trace/span derivation
* (`traceId:spanId:n`). The counter disambiguates calls that would otherwise share a
* trace/span pair, so distinct calls never collide in [ContextStore].
* Generates a call-unique store key by appending a monotonically increasing, process-unique
* counter to the `traceId:spanId` derivation of [instrumentationContext], yielding
* `traceId:spanId:n`. The counter disambiguates calls that would otherwise share a
* trace/span pair (see the class KDoc for why that pair alone is not call-unique), so
* distinct calls never collide in [ContextStore].
*
* Shared with [RequestContext] and [ExchangeContext], which mint the same call-unique
* Shared with [RequestContext] and [ExchangeContext], which generate the same call-unique
* default key when constructed directly off-chain (rather than promoted from a
* [DispatchContext]), so every link in the chain is collision-safe by default.
*/
internal fun mintCallKey(instrumentationContext: InstrumentationContext): String =
"${deriveCallKey(instrumentationContext)}:${mintCounter.incrementAndGet()}"
internal fun generateCallKey(instrumentationContext: InstrumentationContext): String {
val traceSpan = "${instrumentationContext.traceId.value}:${instrumentationContext.spanId.value}"
return "$traceSpan:${generateCounter.incrementAndGet()}"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ import org.dexpace.sdk.core.instrumentation.InstrumentationContext
* As the terminal link this is the context whose [close] should be called to evict the
* chain's [ContextStore] entry. In the normal flow the [callKey] is supplied by
* [RequestContext.toExchangeContext]. When this context is constructed directly off-chain it
* defaults to a freshly minted, call-unique key (`traceId:spanId:n`) via
* [DispatchContext.mintCallKey] — see [DispatchContext] for why the trace/span pair alone is not
* a collision-safe store key. One consequence of the minted default: two default-constructed
* instances are not structurally equal, since each mints a distinct key — pin an explicit
* defaults to a freshly generated, call-unique key (`traceId:spanId:n`) via
* [DispatchContext.generateCallKey] — see [DispatchContext] for why the trace/span pair alone is not
* a collision-safe store key. One consequence of the generated default: two default-constructed
* instances are not structurally equal, since each generates a distinct key — pin an explicit
* [callKey] if you need equality.
*/
public data class ExchangeContext(
override val instrumentationContext: InstrumentationContext,
val request: Request,
val response: Response,
override val callKey: String = DispatchContext.mintCallKey(instrumentationContext),
override val callKey: String = DispatchContext.generateCallKey(instrumentationContext),
) : CallContext
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ import org.dexpace.sdk.core.instrumentation.InstrumentationContext
*
* In the normal flow the [callKey] is supplied by [DispatchContext.toRequestContext] so the
* whole chain shares one store slot. When this context is constructed directly off-chain it
* defaults to a freshly minted, call-unique key (`traceId:spanId:n`) via
* [DispatchContext.mintCallKey] — see [DispatchContext] for why the trace/span pair alone is not
* a collision-safe store key. One consequence of the minted default: two default-constructed
* instances are not structurally equal, since each mints a distinct key — pin an explicit
* defaults to a freshly generated, call-unique key (`traceId:spanId:n`) via
* [DispatchContext.generateCallKey] — see [DispatchContext] for why the trace/span pair alone is not
* a collision-safe store key. One consequence of the generated default: two default-constructed
* instances are not structurally equal, since each generates a distinct key — pin an explicit
* [callKey] if you need equality.
*/
public data class RequestContext(
override val instrumentationContext: InstrumentationContext,
val request: Request,
override val callKey: String = DispatchContext.mintCallKey(instrumentationContext),
override val callKey: String = DispatchContext.generateCallKey(instrumentationContext),
) : CallContext {
/**
* Promotes this request context into an [ExchangeContext] bound to [response] and stores
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ public open class AsyncBearerTokenAuthStep
* Validates a freshly fetched token: rejects a `null` (a Java provider may hand one back
* despite the non-null Kotlin signature when null-check intrinsics are disabled — hence the
* nullable parameter) and a token already expired at fetch time (no margin applied — a
* provider minting an effectively-expired token is misbehaving).
* provider generating an effectively-expired token is misbehaving).
*/
private fun validateFresh(token: BearerToken?): BearerToken {
val nonNull = token ?: error("BearerTokenProvider returned null")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ public open class BearerTokenAuthStep
cachedToken?.takeIf { !it.isExpiredAt(now, refreshMargin) }?.let { return@withLock it }
val fresh = fetchFresh()
// The fresh-token validation does NOT apply `refreshMargin`: a provider that just
// minted a token returning expiration < margin is misbehaving (returning an
// generated a token returning expiration < margin is misbehaving (returning an
// effectively-expired token), and the IllegalStateException must fire regardless of margin.
check(!fresh.isExpiredAt(now)) {
"BearerTokenProvider returned an already-expired token"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ public interface InstrumentationContext {
/** `true` when the context contains real (non-sentinel) identifiers suitable for propagation. */
public val isValid: Boolean

/** `true` when this context was extracted from an inbound request rather than minted locally. */
/** `true` when this context was extracted from an inbound request rather than generated locally. */
public val isRemote: Boolean

/** Currently active span associated with this context; [Span.NOOP] when tracing is disabled. */
public val span: Span

/**
* Factory used to mint an [HttpTracer] for each operation that runs within this
* Factory used to generate an [HttpTracer] for each operation that runs within this
* context. Defaults to [NoopHttpTracerFactory] so existing implementations of
* [InstrumentationContext] do not need to declare anything — adding this property
* is a non-breaking change. Real tracing backends override this with a factory that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public object NoopInstrumentationContext : InstrumentationContext {
override val span: Span = Span.NOOP

/**
* Always [NoopHttpTracerFactory] — the no-op context cannot mint a recording tracer.
* Always [NoopHttpTracerFactory] — the no-op context cannot generate a recording tracer.
* Explicit override (rather than inheriting the interface default) so the no-op
* identity is part of the type's contract and is asserted by tests.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import java.util.concurrent.ThreadLocalRandom
* Trace-id encoding flavours supported by the SDK.
*
* Different tracing backends expect different on-the-wire trace-id formats; [generate] is the
* factory used by [InstrumentationContext] to mint a fresh id of the chosen variant.
* factory used by [InstrumentationContext] to generate a fresh id of the chosen variant.
*
* - [DATADOG] — 64-bit unsigned integer rendered as a decimal string (Datadog wire format).
* - [W3C] — 128-bit value rendered as a 32-character lowercase hex string per the W3C Trace
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import java.util.function.Supplier
* ## Default behaviour
*
* For requests whose [Request.method] is in [methods] (default: `POST`, `PUT`, `PATCH`),
* the step adds an `Idempotency-Key` header carrying a freshly minted [UUID]. Requests for
* the step adds an `Idempotency-Key` header carrying a freshly generated [UUID]. Requests for
* other methods (`GET`, `HEAD`, `OPTIONS`, etc.) pass through untouched — these are safe by
* HTTP semantics and the key carries no useful meaning.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ public sealed class Tristate<out T> {

/** Returns [Null]. Convenience factory for Java callers / generic call-sites. */
@JvmStatic
@JvmName("nullValue")
public fun <T> nullValue(): Tristate<T> = Null

/**
Expand Down
27 changes: 6 additions & 21 deletions sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/ProxyOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -167,23 +167,7 @@ public class ProxyOptions

if (!sysHost.isNullOrEmpty()) {
val port = parsePort(sysPortRaw) ?: return null
return try {
ProxyOptions(
type = Type.HTTP,
address = InetSocketAddress(sysHost, port),
nonProxyHosts = nonProxyHosts,
username = sysUser,
password = sysPassword,
)
} catch (t: IllegalArgumentException) {
logger.atWarning()
.event("proxy.config.invalid")
.field("host", sysHost)
.field("port", port)
.cause(t)
.field("message", "Invalid proxy address; ignoring").log()
null
}
return buildOptions(sysHost, port, sysUser, sysPassword, nonProxyHosts)
}

// 2. Env var layer: HTTPS_PROXY then HTTP_PROXY.
Expand Down Expand Up @@ -292,15 +276,16 @@ public class ProxyOptions
* — the caller returns `null` in that case so the consumer routes directly.
*/
private fun resolveNonProxyHosts(config: Configuration): Pair<List<String>, Boolean> {
fun classify(parts: List<String>): Pair<List<String>, Boolean> =
if (parts.size == 1 && parts[0] == "*") emptyList<String>() to true else parts to false

val sysProp = config.getProperty("http.nonProxyHosts")
if (!sysProp.isNullOrEmpty()) {
val parts = splitAndUnescape(sysProp, PROP_SPLIT, '|')
return if (parts.size == 1 && parts[0] == "*") emptyList<String>() to true else parts to false
return classify(splitAndUnescape(sysProp, PROP_SPLIT, '|'))
}
val envVar = config.get(Configuration.NO_PROXY)
if (!envVar.isNullOrEmpty()) {
val parts = splitAndUnescape(envVar, ENV_SPLIT, ',')
return if (parts.size == 1 && parts[0] == "*") emptyList<String>() to true else parts to false
return classify(splitAndUnescape(envVar, ENV_SPLIT, ','))
}
return emptyList<String>() to false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import kotlin.test.assertTrue

/**
* Stable, equality-safe [InstrumentationContext] for tests that need a deterministic trace id.
* Production contexts mint random ids; tests need to predict the key into [ContextStore].
* Production contexts generate random ids; tests need to predict the key into [ContextStore].
*/
internal data class FakeInstrumentationContext(
override val traceId: TraceId,
Expand Down Expand Up @@ -196,7 +196,7 @@ class ContextStoreTest {
@Test
fun `concurrent untraced calls keep independent entries`() {
// Two concurrent default()/NOOP-trace calls share a constant trace+span id, but each
// mints a unique call key, so neither overwrites nor evicts the other's live entry.
// generates a unique call key, so neither overwrites nor evicts the other's live entry.
val calls = 16
val barrier = CountDownLatch(1)
val keys = java.util.concurrent.ConcurrentHashMap.newKeySet<String>()
Expand All @@ -217,7 +217,7 @@ class ContextStoreTest {
barrier.countDown()
for (t in ts) t.join()

assertEquals(calls, keys.size, "every concurrent call should mint a distinct key")
assertEquals(calls, keys.size, "every concurrent call should generate a distinct key")
assertEquals(calls, survivors.get(), "no call should have its entry overwritten by another")
}

Expand Down
Loading
Loading