fix(client): make ReconnectingOutbox KMP-native-safe (@Volatile→AtomicReference, Math.pow→kotlin.math.pow)
ci / JVM build + tests (push) Successful in 5m48s
release / Publish KMP libraries → caffeine Nexus (release) Successful in 35s

CI red on tag 10 release #1983: client:compileCommonMainKotlinMetadata
and :compileKotlinLinuxArm64 both failed with:
  e: ReconnectingOutbox.kt:154 Unresolved reference 'Volatile'
  e: ReconnectingOutbox.kt:227 Unresolved reference 'Math'

@Volatile is JVM-only annotation; java.lang.Math is JVM-only API. On
linuxArm64/macosArm64 they don't resolve.

Fix:
- @Volatile private var lastSeen: Instant? → AtomicReference<Instant?>
  (kotlin.concurrent.atomics, same module as the AtomicBoolean already
  used for ). .load() / .store() / @OptIn(ExperimentalAtomicApi::class).
- Math.pow(m, e) → m.pow(e) via kotlin.math.pow import.

commonMain stays KMP-clean; jvmTest green (95 tasks); linuxX64 / linuxArm64
/ mingwX64 / macosX64 / macosArm64 compile green.
This commit is contained in:
2026-09-22 03:13:02 +03:00
parent c0a933d251
commit acb4ee6186
@@ -13,8 +13,10 @@ import kotlinx.coroutines.launch
import pw.binom.agentik.outbox.CommonEvent
import pw.binom.agentik.outbox.OutboxStore
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.AtomicReference
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.math.min
import kotlin.math.pow
import kotlin.random.Random
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@@ -151,8 +153,8 @@ class ReconnectingOutbox(
private val started = AtomicBoolean(false)
private var job: Job? = null
@Volatile
private var lastSeen: Instant? = null
@OptIn(ExperimentalAtomicApi::class)
private val lastSeen: AtomicReference<Instant?> = AtomicReference(null)
/**
* Live-события из [outbox] с авто-reconnect. [after] — начальный курсор;
@@ -183,10 +185,11 @@ class ReconnectingOutbox(
@OptIn(ExperimentalAtomicApi::class)
private fun ensureStarted(initialCursor: Instant?) {
if (!started.compareAndSet(false, true)) return
lastSeen = initialCursor
lastSeen.store(initialCursor)
job = scope.launch { runLoop() }
}
@OptIn(ExperimentalAtomicApi::class)
private suspend fun runLoop() {
var attempt = 0
var connected = false
@@ -194,12 +197,12 @@ class ReconnectingOutbox(
attempt++
_status.emit(ConnectionStatus.Connecting(attempt))
val error: Throwable? = try {
outbox.events(after = lastSeen).collect { event ->
lastSeen = event.date
outbox.events(after = lastSeen.load()).collect { event ->
lastSeen.store(event.date)
_events.emit(event)
if (!connected) {
connected = true
_status.emit(ConnectionStatus.Connected(lastSeen!!))
_status.emit(ConnectionStatus.Connected(event.date))
}
}
null
@@ -224,7 +227,7 @@ class ReconnectingOutbox(
private fun computeBackoff(attempt: Int): Duration {
// attempt 1 → initial, 2 → initial * m, 3 → initial * m^2, ...
val base = (policy.initial.inWholeMilliseconds.toDouble() *
Math.pow(policy.multiplier, (attempt - 1).toDouble()))
policy.multiplier.pow((attempt - 1).toDouble()))
.toLong()
val capped = min(base, policy.max.inWholeMilliseconds)
val jitterMs = (capped * policy.jitter * random.nextDouble()).toLong()