CeresCrossThreadCloseSmoke.kt
src/jvmMain/kotlin/afield/examples/ceres/CeresCrossThreadCloseSmoke.kt · 63 lines
package afield.examples.ceres
import afield.math.optimization.ceres.AxisSet
import afield.math.optimization.ceres.FfmCeresBackend
import afield.math.optimization.ceres.GeometrySolveSession
import afield.math.optimization.ceres.SolveOptions
import afield.math.optimization.ceres.geometryModel
import java.util.concurrent.Executors
fun main()
A native session built on one thread and solved and closed on another.
The house sketch builds its first session on its own executor and rebuilds it later from the runloop. With the
solver's buffers in a confined arena, closing from the runloop freed the native problem and then threw
WrongThreadException before marking the solver closed, so the caller's retry freed it again and the JVM died in
ceres::Problem::~Problem() (house host, 2026-10-22). Run before the fix, this smoke fails at the first foreign
close.
body — 44 lines, from line 19
fun main() {
val worker = Executors.newSingleThreadExecutor { r -> Thread(r, "ceres-session-owner").apply { isDaemon = true } }
try {
// Closed from a thread that did not create it: the house sketch's rebuild.
val session = worker.submit<GeometrySolveSession> { openSolvedSession() }.get()
val failure = runCatching { session.close() }.exceptionOrNull()
check(failure == null) { "closing on a foreign thread threw ${failure!!::class.simpleName}: ${failure.message}" }
// A second close, on either thread, must not free the native solver again.
session.close()
worker.submit { session.close() }.get()
// Solved, then closed, from a thread that did not create it.
val moved = worker.submit<GeometrySolveSession> { openSolvedSession() }.get()
check(moved.solve(SolveOptions(maxIterations = 20))) { "solve on a foreign thread failed" }
moved.close()
// And the other way round: built here, closed on the worker.
val local = openSolvedSession()
worker.submit { local.close() }.get()
local.close()
println("ok cross-thread close")
} finally {
worker.shutdown()
}
}
private fun openSolvedSession(): GeometrySolveSession {
val model = geometryModel {
val a = point("a", 0.0, 0.0, 0.0)
val b = point("b", 1.2, 0.1, 0.0)
val c = point("c", 1.1, 0.9, 0.0)
constraints {
a.lock(AxisSet.XYZ, 1e9)
listOf(a, b, c).forEach { it.lock(AxisSet.Z, 1e8) }
line(a, b).minLength(.3, 1e7)
line(b, c).minLength(.3, 1e7)
same(listOf(a, b), AxisSet.Y, 1e8)
}
}
val session = model.openSession(backend = FfmCeresBackend())
check(session.solve(SolveOptions(maxIterations = 20))) { "solve on the owning thread failed" }
return session
}