-
Notifications
You must be signed in to change notification settings - Fork 3
jnats-server-runner 4.0.2 across the repo, chaos-runner fixes #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |||||||||||||||||||||
| import io.nats.ClusterDefaults; | ||||||||||||||||||||||
| import io.nats.ClusterInsert; | ||||||||||||||||||||||
| import io.nats.ClusterNode; | ||||||||||||||||||||||
| import io.nats.JsConfig; | ||||||||||||||||||||||
| import io.nats.NatsRunnerUtils; | ||||||||||||||||||||||
| import io.nats.NatsServerRunner; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -93,11 +94,10 @@ private NatsServerRunner createRunner(int index) throws Exception { | |||||||||||||||||||||
| ClusterInsert ci = clusterInserts.get(index); | ||||||||||||||||||||||
| NatsServerRunner.Builder b = NatsServerRunner.builder() | ||||||||||||||||||||||
| .debug(false) | ||||||||||||||||||||||
| .jetstream(true) | ||||||||||||||||||||||
| .jetstream(js) | ||||||||||||||||||||||
| .configInserts(ci.configInserts) | ||||||||||||||||||||||
| .port(ci.node.port) | ||||||||||||||||||||||
| .skipConnectValidate() | ||||||||||||||||||||||
| ; | ||||||||||||||||||||||
| .skipConnectValidate(); | ||||||||||||||||||||||
| return b.build(); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -110,11 +110,19 @@ private void scheduleUp() { | |||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private void downTask() { | ||||||||||||||||||||||
| // natsServerRunners is an ArrayList and shutdownServers() iterates it under this | ||||||||||||||||||||||
| // lock. executor.shutdown() does not interrupt a task already running, so without | ||||||||||||||||||||||
| // the lock a structural change here can race that iteration. | ||||||||||||||||||||||
| INSTANCE_LOCK.lock(); | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| if (INSTANCE == null) { | ||||||||||||||||||||||
| // shut down before this task got to run | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if (specificPort != -1) { | ||||||||||||||||||||||
| for (int i = 0; i < natsServerRunners.size(); i++) { | ||||||||||||||||||||||
| NatsServerRunner nsr = natsServerRunners.get(i); | ||||||||||||||||||||||
| if (nsr.getPort() == specificPort) { | ||||||||||||||||||||||
| if (nsr.getNatsPort() == specificPort) { | ||||||||||||||||||||||
| downIx = i; | ||||||||||||||||||||||
| break; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
@@ -125,30 +133,47 @@ else if (random) { | |||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| NatsServerRunner runner = natsServerRunners.remove(downIx); | ||||||||||||||||||||||
| printer.out(CR_LABEL, "DOWN", runner.getPort()); | ||||||||||||||||||||||
| printer.out(CR_LABEL, "DOWN", runner.getNatsPort()); | ||||||||||||||||||||||
| clusterInserts.add(clusterInserts.remove(downIx)); | ||||||||||||||||||||||
| runner.close(); | ||||||||||||||||||||||
| scheduleUp(); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| catch (Throwable e) { | ||||||||||||||||||||||
| printer.out(CR_LABEL, "DOWN/EX", e); | ||||||||||||||||||||||
| printer.out(CR_LABEL, "DOWN/EX", e); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| finally { | ||||||||||||||||||||||
| INSTANCE_LOCK.unlock(); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private void upTask() { | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| NatsServerRunner runner = createRunner(servers - 1); | ||||||||||||||||||||||
| printer.out(CR_LABEL, "UP", runner.getPort()); | ||||||||||||||||||||||
| natsServerRunners.add(runner); | ||||||||||||||||||||||
| scheduleDown(delay); | ||||||||||||||||||||||
| INSTANCE_LOCK.lock(); | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| if (INSTANCE == null) { | ||||||||||||||||||||||
| // Shut down while this server was starting. executor.shutdown() does not | ||||||||||||||||||||||
| // interrupt a task already running, so we got here after shutdownServers() | ||||||||||||||||||||||
| // had already closed out the list and the jvm hook was removed. Close it | ||||||||||||||||||||||
| // here, or it outlives the jvm still holding its port. | ||||||||||||||||||||||
| try { runner.close(); } catch (Exception ignore) {} | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| printer.out(CR_LABEL, "UP", runner.getNatsPort()); | ||||||||||||||||||||||
| natsServerRunners.add(runner); | ||||||||||||||||||||||
| scheduleDown(delay); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| finally { | ||||||||||||||||||||||
| INSTANCE_LOCK.unlock(); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| catch (Throwable e) { | ||||||||||||||||||||||
| printer.out(CR_LABEL, "UP/EX: ", e); | ||||||||||||||||||||||
| scheduleUp(); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private static void deleteDirContents(Path dir, boolean alsoDeleteSelf) throws IOException { | ||||||||||||||||||||||
| private static void deleteDirContents(Path dir, boolean alsoDeleteSelf) { | ||||||||||||||||||||||
| File fDir = dir.toFile(); | ||||||||||||||||||||||
| if (fDir.exists()) { | ||||||||||||||||||||||
| File[] items = fDir.listFiles(); | ||||||||||||||||||||||
|
|
@@ -208,9 +233,10 @@ else if (!a.workDirectory.toFile().exists()) { | |||||||||||||||||||||
| throw new IllegalArgumentException("Invalid specific port"); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| List<String> inserts = new ArrayList<>(); | ||||||||||||||||||||||
| ClusterNode cn; | ||||||||||||||||||||||
| Path jsStorePath = Paths.get(jsStoreDirBase.toString(), "" + port); | ||||||||||||||||||||||
| cn = ClusterNode.builder() | ||||||||||||||||||||||
| // jsStoreDirBase is only set when js is on, and ClusterNode takes a null | ||||||||||||||||||||||
| // jsStoreDir, which is what the cluster branch ends up with in that case too | ||||||||||||||||||||||
| Path jsStorePath = js ? Paths.get(jsStoreDirBase.toString(), "" + port) : null; | ||||||||||||||||||||||
| ClusterNode cn = ClusterNode.builder() | ||||||||||||||||||||||
| .port(port) | ||||||||||||||||||||||
| .listen(listen) | ||||||||||||||||||||||
| .monitor(monitor < 1 ? null : monitor) | ||||||||||||||||||||||
|
|
@@ -221,16 +247,9 @@ else if (!a.workDirectory.toFile().exists()) { | |||||||||||||||||||||
| inserts.add("http: " + monitor); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if (js) { | ||||||||||||||||||||||
| String storeDir = jsStorePath.toString(); | ||||||||||||||||||||||
| if (File.separatorChar == '\\') { | ||||||||||||||||||||||
| storeDir = storeDir.replace("\\", "\\\\").replace("/", "\\\\"); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| else { | ||||||||||||||||||||||
| storeDir = storeDir.replace("\\", "/"); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| inserts.add("jetstream {"); | ||||||||||||||||||||||
| inserts.add(" store_dir=" + storeDir); | ||||||||||||||||||||||
| inserts.add("}"); | ||||||||||||||||||||||
| // as of jnats-server-runner 4.0.2 JsConfig cleans and escapes the dir it is | ||||||||||||||||||||||
| // given, the same as the cluster branch gets by way of createClusterInserts | ||||||||||||||||||||||
| inserts.addAll(new JsConfig(jsStorePath).configInserts); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| inserts.add("server_name=" + serverNamePrefix); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -266,7 +285,10 @@ else if (!a.workDirectory.toFile().exists()) { | |||||||||||||||||||||
| // delete jsStoreDirs for clean start | ||||||||||||||||||||||
| if (js) { | ||||||||||||||||||||||
| for (ClusterInsert ci : clusterInserts) { | ||||||||||||||||||||||
| deleteDirContents(ci.node.jsStoreDir, false); | ||||||||||||||||||||||
| // jsStoreDir is nullable on ClusterNode, so it might not have been given one | ||||||||||||||||||||||
| if (ci.node.jsStoreDir != null) { | ||||||||||||||||||||||
| deleteDirContents(ci.node.jsStoreDir, false); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -307,12 +329,13 @@ public static ChaosRunner start(ChaosArguments a, ChaosPrinter printer) throws E | |||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| INSTANCE = new ChaosRunner(a, finalPrinter); | ||||||||||||||||||||||
| INSTANCE_ARGUMENTS = a; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| APP_SHUTDOWN_HOOK_THREAD = new Thread("app-shutdown-hook") { | ||||||||||||||||||||||
| @Override | ||||||||||||||||||||||
| public void run() { | ||||||||||||||||||||||
| shutdownServers(); | ||||||||||||||||||||||
| shutdownExecutor(); | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The same issue is reachable if
Suggested change
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. |
||||||||||||||||||||||
| shutdownServers(); | ||||||||||||||||||||||
| finalPrinter.out(CR_LABEL, "EXIT"); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
@@ -356,7 +379,11 @@ public static void shutdown() { | |||||||||||||||||||||
| public static void shutdownExecutor() { | ||||||||||||||||||||||
| INSTANCE_LOCK.lock(); | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| INSTANCE.executor.shutdown(); | ||||||||||||||||||||||
| // guard matches shutdownServers(). This is public and is also reachable a | ||||||||||||||||||||||
| // second time when the jvm hook and an explicit shutdown() overlap. | ||||||||||||||||||||||
| if (INSTANCE != null) { | ||||||||||||||||||||||
| INSTANCE.executor.shutdown(); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| finally { | ||||||||||||||||||||||
| INSTANCE_LOCK.unlock(); | ||||||||||||||||||||||
|
|
@@ -384,6 +411,7 @@ private static void shutdownServers() { | |||||||||||||||||||||
| try { runner.close(); } catch (Exception ignore) {} | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| INSTANCE = null; | ||||||||||||||||||||||
| INSTANCE_ARGUMENTS = null; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| finally { | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
downTaskaccessesnatsServerRunnerswithoutINSTANCE_LOCK, creating the same class of race that was fixed forupTask.executor.shutdown()(called inshutdownExecutor()) prevents new tasks from being scheduled but does not interrupt a task already executing. IfdownTaskis mid-execution when shutdown fires:shutdownServers()acquiresINSTANCE_LOCKand iteratesINSTANCE.natsServerRunnersdownTaskconcurrently callsnatsServerRunners.remove(downIx)with no lock heldArrayListis not thread-safe; the concurrent structural modification can causeConcurrentModificationException(or index corruption). This is the symmetric counterpart of theupTaskbug described in the PR. TheupTaskfix (acquire lock → null-checkINSTANCE→ operate → release) is the right template to apply here too.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.