Repository navigation
Open
Description
Example code:
object ScexFlakyValidation {
private class DefaultJavaScexCompiler(val settings: ScexSettings)
extends ScexCompiler
with ScexPresentationCompiler
with ClassfileReusingScexCompiler
with TemplateOptimizingScexCompiler
with CachingScexCompiler // removing this fixes it, but requires a lower count of tests to not crash (1000 is too much)
with CachingScexPresentationCompiler
with WeakReferenceWrappingScexCompiler
with JavaScexCompiler
def main(args: Array[String]): Unit = {
val symbolValidator = SymbolValidator(PredefinedAccessSpecs.basicOperations)
val symbolAttributes = SymbolAttributes(Nil)
val syntaxValidator = SyntaxValidator.SimpleExpressions
val utils = NamedSource("test", "")
val profile = new ExpressionProfile("test", syntaxValidator, symbolValidator, symbolAttributes, "", utils)
val settings = new ScexSettings
// settings.noGetterAdapters.value = true //adding this option fixes it too
val scexCompiler = new DefaultJavaScexCompiler(settings)
val completer = scexCompiler.getCompleter[SimpleContext[Any], String](profile, template = false)
def validateExpr(expr: String): Boolean = completer.getErrors(expr).isEmpty
val invalidExpression = "symbolMissing" //basically any invalid expression works
val totalCount = 1000
val fpCount = (0 until totalCount)
.map(i => invalidExpression + " " * i) //spaces added to avoid hitting the cache
.count(validateExpr) // <- this is what gives false positives sometimes (completer.getErrors)
if (fpCount != 0) {
println(s"There were $fpCount/$totalCount false positives in validation")
} else {
println("all good")
}
}
}This code tries to check for errors on the same invalid expression 1000 times (with added spaces to avoid hitting the cache). The number and order of FPs is nondeterministic, and seems to get higher the longer the code runs, e.g. for 1k times it's around 50-70%:
There were 695/1000 false positives in validation
But for 100 times it's between 10-30%:
There were 12/100 false positives in validation
It seems to not happen with either setting the noGetterAdapters = true in ScexSettings, or when using a ScexCompiler without CachingScexCompiler mixed in, even though those seem to be completely unrelated.
Some other findings:
- it seems it doesn't matter in what way the expression is invalid, unclosed strings, invalid syntax, missing symbols seem to get false positives at the same rate
- on the same note, random strings can also be used to check the validation (or something like
s"missingSymbol$i") - while the false positives are nondeterministic between runs, on the same code run, the same expression always gets the same result (might be some caching tho)
- The specific caches that seem to affect this problem are
profileCompilationResultsCacheandjavaGetterAdaptersCache. With either of them being removed, and the rest of caches still in place, there are no more FPs ScexPresentationCompiler#Completer.workaroundAssertionErrorlooks like the culprit, but it doesn't affect this at all
Metadata
Metadata
Assignees
Labels
No labels
Activity
anetaporebska commentedon May 21, 2024
It seems that there is a problem with
com.avsystem.scex.compiler.ScexCompiler.Reporter#errorsBuilderand the fact that errors are cleared both by scex code and the Scala Presentation Compiler code (scala.tools.nsc.interactive.Global#backgroundCompile) by callingreporter.reset(). The latter runs arbitrarily and sometimes interferes with accumulated errors and clears them.Since I didn't find any direct connection between cache/getterAdapters and
errorsBuilderI assume they affect validation time and thus contribute to different results.Workaround is presented here: #36
sebaciv commentedon Oct 28, 2026
I reproduced this on current master and confirmed the explanation in the comment above.
Mechanism.
scala.tools.nsc.interactive.Global#backgroundCompilestarts every pass withreporter.reset(), andScexCompiler.Reporter.reset()also clearserrorsBuilder. IngetErrors, the expression is typechecked on the presentation compiler thread, and then the caller thread readsreporter.compileErrors(). If a background pass starts in between, the errors are wiped and the expression looks valid.Measurements. I ran the repro from this issue with 300 invalid expressions:
taskset -c 0)reset()keeps errors; scex clears them explicitly per request)Getter adapters and caching. Turning off
noGetterAdaptersor the caches only changes timing. Both settings affect how many shared units stay loaded in the presentation compiler, and therefore how long each background pass takes. I saw the same correlation with a different race in this area, and it wasn't causal there either.Related. #190 fixes a sibling race with the same root cause: scex treats the presentation compiler as synchronous, while its background compile keeps changing shared state between scex's request steps. In that case, the shared state is
unit.synthetics, which causes the flakyno synthetic tree found for bean accessorerror inScopeAndTypeCompletionTest. #190 does not fix this issue. With #190 applied, the one-CPU repro above still gives 128–129/300.Things to handle if #36 is revived:
errorsBuilderis a plainListBuffer. The presentation compiler thread writes to it and the caller thread reads it, with no synchronization.ParsePollingTestin Fix flaky presentation compiler crash on lazy accessor completion #190) would guard this better than a stress loop, because the original failure only shows up reliably on slow or single-core machines.🤖 Written by Claude.