Skip to content

Commit 6b56ffe

Browse files
Saloedclaude
andcommitted
Bound Z3 memory in testTimeout
Z3 only checks its `timeout` parameter at internal checkpoints. On the quantified array query in this test it allocates roughly 400 MB/s of native memory before reaching one, so the check grew to ~14 GB and the test JVM was killed by the OS (`finished with non-zero exit value 137`). The test worker runs with -Xmx512m, so none of that is heap. On CI this consumed the whole runner - 16 GB plus swap - and GitHub reported it as "The operation was canceled" on whichever task Gradle had last printed, which made it look like an infrastructure problem rather than a test. This is not specific to the recent Z3 upgrade: measured over 8 runs at 4 CPU / 6 GB, the previous Z3 dist failed 1/8 and the current one 2/8. Bound Z3's memory for the duration of the check so it gives up cleanly with UNKNOWN instead of taking the process down. The limit has to be the global parameter: a solver parameter would not survive, because every check replaces the solver parameters with the ones carrying the timeout. 12/12 runs pass at 4 CPU / 6 GB after this change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ebeb932 commit 6b56ffe

1 file changed

Lines changed: 38 additions & 2 deletions

File tree

ksmt-z3/ksmt-z3-core/src/test/kotlin/io/ksmt/solver/z3/IncrementalApiTest.kt

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.ksmt.solver.z3
22

3+
import com.microsoft.z3.Global
34
import io.ksmt.KContext
45
import io.ksmt.solver.KSolverStatus
56
import io.ksmt.utils.mkConst
@@ -82,8 +83,43 @@ class IncrementalApiTest {
8283
val query = mkUniversalQuantifier(queryBody, listOf(x.decl))
8384
solver.assert(query)
8485

85-
val status = solver.checkWithAssumptions(emptyList(), timeout = 1.milliseconds)
86+
val status = withZ3MemoryLimit(MEMORY_LIMIT_MB) {
87+
solver.checkWithAssumptions(emptyList(), timeout = 1.milliseconds)
88+
}
8689
assertEquals(KSolverStatus.UNKNOWN, status)
87-
assertEquals("timeout", solver.reasonOfUnknown())
90+
91+
/**
92+
* Normally Z3 reports the timeout. If it happens to reach the memory limit before
93+
* its next timeout checkpoint it reports [Z3_OUT_OF_MEMORY_REASON] instead, which is
94+
* an equally valid "gave up on the resource limit" outcome.
95+
*/
96+
val reason = solver.reasonOfUnknown()
97+
assertTrue(
98+
reason == TIMEOUT_REASON || reason == Z3_OUT_OF_MEMORY_REASON,
99+
"Unexpected reason of unknown: $reason"
100+
)
101+
}
102+
103+
/**
104+
* Z3 only checks the [timeout] parameter at its internal checkpoints. On this query it
105+
* allocates memory far faster than it reaches one, so without a memory limit the check
106+
* grows to tens of gigabytes and the test process gets killed by the OS.
107+
*
108+
* The limit is a global Z3 parameter because a solver parameter would not survive:
109+
* every check replaces the solver parameters with the ones holding the timeout.
110+
*/
111+
private inline fun <T> withZ3MemoryLimit(limitMb: Int, body: () -> T): T = try {
112+
Global.setParameter(Z3_MAX_MEMORY, limitMb.toString())
113+
body()
114+
} finally {
115+
Global.setParameter(Z3_MAX_MEMORY, UNLIMITED_MEMORY)
116+
}
117+
118+
companion object {
119+
private const val Z3_MAX_MEMORY = "memory_max_size"
120+
private const val UNLIMITED_MEMORY = "0"
121+
private const val MEMORY_LIMIT_MB = 2048
122+
private const val TIMEOUT_REASON = "timeout"
123+
private const val Z3_OUT_OF_MEMORY_REASON = "out of memory"
88124
}
89125
}

0 commit comments

Comments
 (0)