Summary
MajordomoRestPluginTests fails intermittently with either:
java.util.ConcurrentModificationException
at io.javalin.http.PathMatcher.add(PathMatcher.kt:26)
at io.opencmw.server.rest.MajordomoRestPlugin.lambda$registerEndPoint$10(MajordomoRestPlugin.java:361)
or
java.lang.IllegalArgumentException: Handler with type='POST' and path='/PrimaryBroker/mmi.openapi' already exists.
at io.javalin.http.PathMatcher.add(PathMatcher.kt:27)
Observed on CI in an unrelated PR (#273) and reproduced locally, reliably within ~10
back-to-back runs of MajordomoRestPluginTests.
Root cause
MajordomoRestPlugin.start() registers endpoints from two independent threads: the
synchronous initial service enumeration, and the async getServiceSubscriptionTask()
listener reacting to the same broker registrations as they happen. Both call
registerEndPoint(), which de-duplicates repeated registration of the same endpoint via
registeredEndpoints.computeIfAbsent(...) — but that guard does nothing to stop two
different endpoints from being registered concurrently. Both paths mutate the same Javalin
route table (RestServer.getInstance(), a JVM-wide singleton, backed by a plain,
non-thread-safe ArrayList in PathMatcher), so a burst of near-simultaneous service
announcements (as happens during startup) can trigger a genuine data race on that shared,
unsynchronized list.
The failed registration then leaves providedServices without an entry for the affected
service, which can surface downstream as an unrelated-looking NullPointerException on
MajordomoBroker.DnsServiceItem.getUri().
Fix
Will submit a PR adding a dedicated lock around the Javalin route-mutation call, scoped
static since the underlying Javalin instance is shared across every MajordomoRestPlugin
instance in the JVM, not just one instance's own threads.
Summary
MajordomoRestPluginTestsfails intermittently with either:or
Observed on CI in an unrelated PR (#273) and reproduced locally, reliably within ~10
back-to-back runs of
MajordomoRestPluginTests.Root cause
MajordomoRestPlugin.start()registers endpoints from two independent threads: thesynchronous initial service enumeration, and the async
getServiceSubscriptionTask()listener reacting to the same broker registrations as they happen. Both call
registerEndPoint(), which de-duplicates repeated registration of the same endpoint viaregisteredEndpoints.computeIfAbsent(...)— but that guard does nothing to stop twodifferent endpoints from being registered concurrently. Both paths mutate the same Javalin
route table (
RestServer.getInstance(), a JVM-wide singleton, backed by a plain,non-thread-safe
ArrayListinPathMatcher), so a burst of near-simultaneous serviceannouncements (as happens during startup) can trigger a genuine data race on that shared,
unsynchronized list.
The failed registration then leaves
providedServiceswithout an entry for the affectedservice, which can surface downstream as an unrelated-looking
NullPointerExceptiononMajordomoBroker.DnsServiceItem.getUri().Fix
Will submit a PR adding a dedicated lock around the Javalin route-mutation call, scoped
staticsince the underlying Javalin instance is shared across everyMajordomoRestPlugininstance in the JVM, not just one instance's own threads.