jnats-server-runner 4.0.2 across the repo, chaos-runner fixes - #53
Conversation
All eleven declarations move from 3.1.1 to 4.0.2. The ten test-only modules needed the version string and nothing else. 4.0.2 rather than 4.0.1 because 4.0.1 did not escape a store dir it was handed, which is a startup failure on Windows rather than a wrong directory, and that was fixed upstream in nats-io/java-nats-server-runner#47 instead of worked around here. chaos-runner, adapting to 4.x: - getPort() is gone. The three call sites use getNatsPort(), the resolved client port, rather than getReadyPort(), which is only the port asked for or generated. getConnectionUrls() already reported node.port, so this is the consistent one, and getNatsPort() existed in 3.1.1 too. - The single server branch no longer hand rolls its jetstream block and escapes the store dir inline. It calls JsConfig, the same as the cluster branch gets by way of createClusterInserts, so neither branch escapes anything itself. chaos-runner bugs found while verifying, none of them version related: - upTask leaked a server when the runner was shut down while that task was starting one. It never took INSTANCE_LOCK, and executor.shutdown() does not interrupt a running task, so the new server was added to a list that had already been closed out and nothing ever closed it. It outlived the jvm still holding its port, which is what made a later run fail to start. Reproduced 5 of 5 by timing the shutdown to land while a server was starting. Now takes the lock after createRunner and closes the runner instead of adding it when the instance is gone. - INSTANCE_ARGUMENTS was declared and read but never assigned, so a second start() threw NullPointerException instead of the intended message. - The jvm shutdown hook called shutdownServers() then shutdownExecutor(), which dereferences the instance the first one nulls. Order now matches the public shutdown(). - deleteDirContents was called on ClusterNode.jsStoreDir, which is nullable. - --nojs --servers 1 died on a null jsStoreDirBase, and --nojs did not actually disable JetStream since createRunner passed jetstream(true). pcgroups-cli tracked a wrapper jar and properties but no gradlew, and cannot build standalone anyway since implementation project(':') makes it its own root. Both CI workflows build it from ./pcgroups. The half wrapper is removed. Verified: twelve projects compile, 131 tests pass against nats-server v2.15.0-dev, and chaos-runner runs cluster and single server on both Linux and Windows with JetStream state landing in the configured directories. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YRbGPiFEXpwnU9BEy8mXb7
|
Claude finished @scottf's task in 4m 31s —— View job PR Review: jnats-server-runner 4.0.2 upgrade + chaos-runner fixes
SummaryThe version bumps across all eleven modules are clean — version string only, no API changes needed. The The chaos-runner fixes are generally well-reasoned and the PR description is unusually thorough about reproducing each bug. Two issues remain: 1. 2. Everything else looks correct: the |
|
|
||
| NatsServerRunner runner = natsServerRunners.remove(downIx); | ||
| printer.out(CR_LABEL, "DOWN", runner.getPort()); | ||
| printer.out(CR_LABEL, "DOWN", runner.getNatsPort()); |
There was a problem hiding this comment.
downTask accesses natsServerRunners without INSTANCE_LOCK, creating the same class of race that was fixed for upTask.
executor.shutdown() (called in shutdownExecutor()) prevents new tasks from being scheduled but does not interrupt a task already executing. If downTask is mid-execution when shutdown fires:
shutdownServers()acquiresINSTANCE_LOCKand iteratesINSTANCE.natsServerRunnersdownTaskconcurrently callsnatsServerRunners.remove(downIx)with no lock held
ArrayList is not thread-safe; the concurrent structural modification can cause ConcurrentModificationException (or index corruption). This is the symmetric counterpart of the upTask bug described in the PR. The upTask fix (acquire lock → null-check INSTANCE → operate → release) is the right template to apply here too.
There was a problem hiding this comment.
Fixed in 741cd1e.
Correct, and worse than I had assessed. When I looked at this earlier I concluded downTask "leaks nothing, a late run just throws IndexOutOfBounds into its own catch(Throwable)" and deferred it as tidiness. That was wrong: ArrayList is not thread safe, so a structural change racing the iteration in shutdownServers() is corruption or ConcurrentModificationException, not a bounded exception.
Applied the same template as upTask: acquire INSTANCE_LOCK, return if INSTANCE is null, operate, release in a finally. The lock is held across runner.close(), which matches what shutdownServers() already does.
Verified with the shutdown race harness across ten offsets on Windows, including 1900-2100ms, which brackets the first DOWN at initialDelay=2000 and so exercises this path specifically. No orphaned servers at any offset.
| @Override | ||
| public void run() { | ||
| shutdownServers(); | ||
| shutdownExecutor(); |
There was a problem hiding this comment.
shutdownExecutor() can NPE if INSTANCE is already null.
shutdownServers() guards with if (INSTANCE != null) before dereferencing, but shutdownExecutor() (the public sibling) does not. A narrow concurrent path:
- The JVM shutdown hook fires while an explicit
shutdown()call is also in flight. - The hook wins
INSTANCE_LOCKfirst, runsshutdownExecutor()+shutdownServers()(nullingINSTANCE). - The explicit
shutdown()then re-entersshutdownExecutor()→ NPE atINSTANCE.executor.shutdown().
The same issue is reachable if shutdownExecutor() is ever called as a standalone public API after shutdown() has completed. A guard matching shutdownServers() is all that's needed:
| shutdownExecutor(); | |
| INSTANCE_LOCK.lock(); | |
| try { | |
| if (INSTANCE != null) { | |
| INSTANCE.executor.shutdown(); | |
| } | |
| } | |
| finally { | |
| INSTANCE_LOCK.unlock(); | |
| } |
There was a problem hiding this comment.
Fixed in 741cd1e, using the suggested guard.
I noticed this while reading the file earlier and left it alone as out of scope for the version upgrade. That was the wrong call given the shutdown hook order change in this same PR touches the adjacent code.
Two reachable paths, not one: the concurrent one you describe, and the simpler case of shutdownExecutor() being called a second time after shutdown() has completed, since it is public API.
The guard now matches shutdownServers() exactly.
Both from review on #53. downTask took no lock while shutdownServers() iterates natsServerRunners under INSTANCE_LOCK. executor.shutdown() does not interrupt a task already running, so a structural change in downTask could race that iteration. ArrayList is not thread safe, so this is corruption or ConcurrentModificationException rather than the IndexOutOfBounds I assumed when I first looked at it. Same template as the upTask fix: take the lock, return if the instance is gone, operate, release. shutdownExecutor() dereferenced INSTANCE without the null check its sibling shutdownServers() has. It is public, and it is reachable a second time when the jvm hook and an explicit shutdown() overlap. Verified: cluster and single server still run on Linux and Windows, --nojs starts in both modes, and the shutdown race sweep is clean across ten offsets including 1900-2100ms, which brackets the first DOWN and exercises downTask directly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YRbGPiFEXpwnU9BEy8mXb7
All eleven
jnats-server-runnerdeclarations move from 3.1.1 to 4.0.2. The ten test-only modules needed the version string and nothing else.4.0.2 rather than 4.0.1 because 4.0.1 did not escape a store dir it was handed. That is a startup failure rather than a wrong directory — the nats-server conf parser treats
\as an escape even in an unquoted value, sostore_dir=C:\temp\xfails to parse on\t. It was fixed upstream in nats-io/java-nats-server-runner#47 rather than worked around here, so chaos-runner carries nothing for it.chaos-runner, adapting to 4.x
getPort()is gone. The three call sites usegetNatsPort(), the resolved client port, rather thangetReadyPort(), which is only the port that was asked for or generated — they differ when there is no top level port and the server falls back to 4222, or when the ready port turns out to be a non-nats port.getConnectionUrls()already reportednode.port, so the resolved port is the consistent one.getNatsPort()existed in 3.1.1 too, so this is a correction rather than a rename.JsConfig, the same as the cluster branch gets by way ofcreateClusterInserts, so neither branch escapes anything itself.chaos-runner bugs found while verifying
None of these are version related; they were all reachable before.
upTaskleaked a server on shutdown. It never tookINSTANCE_LOCK, andexecutor.shutdown()does not interrupt a task already running, so an in-flightupTaskfinishedcreateRunneraftershutdownServers()had closed out the list and the jvm hook was removed. The new server went into a list nobody reads again and nothing ever closed it — it outlived the jvm still holding its port, which is what made a later run fail to start. Reproduced 5 of 5 on Windows by timing the shutdown to land while a server was starting, each time leaving anats-server.exelistening on 4222. Now takes the lock aftercreateRunnerreturns and closes the runner instead of adding it when the instance is gone; verified across ten shutdown offsets with no leftovers.INSTANCE_ARGUMENTSwas declared and read but never assigned, so a secondstart()threwNullPointerExceptionfromINSTANCE_ARGUMENTS.equals(a)instead of the intended "Instance already started with different arguments."shutdownServers()(which nulls the instance) thenshutdownExecutor()(which dereferences it). Order now matches the publicshutdown().deleteDirContentswas called onClusterNode.jsStoreDir, which is@Nullable. Nothing reaches it with a null today, but the guarantee is spread across three places.--nojs --servers 1died on a nulljsStoreDirBase, and--nojsdid not actually disable JetStream becausecreateRunnerpassedjetstream(true). Confirmed off the process command line:--nojsnow runsnats-server --config <conf>and the default runs it with-js.pcgroups-cli
It tracked
gradle/wrapper/gradle-wrapper.jarand.propertiesbut nogradlew, so the wrapper could not be used, and it cannot build standalone anyway —implementation project(':')makes it its own root project, giving a circular:jar→:classes→:compileJavadependency. Both CI workflows build it from./pcgroupsas:pcgroups-cli:dist. The half wrapper is removed;:pcgroups-cli:diststill builds.Verification
nats-server v2.15.0-dev: counters 6, direct-batch 4, encoded-kv 19, js-publish-extensions 2, pcgroups 67, request-many 18, retrier 2, schedule-message 13.store_dir=C:\\temp\\...\\4222, escaped exactly once by the library.--nojsstarts in both modes with no store dir written.🤖 Generated with Claude Code
https://claude.ai/code/session_01YRbGPiFEXpwnU9BEy8mXb7