Repository navigation
SOLR-18531: Reserve a stopped test runner's port until its next start - #5101
Open
nick-boss-tech wants to merge 4 commits into
Open
nick-boss-tech wants to merge 4 commits into
nick-boss-tech wants to merge 4 commits into
Conversation
…on it JettySolrRunner released its port on stop and rebound the same pinned port on start, so a process outside the test JVM could take the port in the gap and the restart failed with BindException once the holder outlived the bind retry budget (seen in LeaderElectionIntegrationTest on CI). stop() now binds a reservation socket to the runner port and keeps it in a static map; start() releases the reservation for the port it is about to bind. The map is static so a replacement runner created for the same port sees the reservation. MiniSolrCloudCluster.shutdown() releases its runners reservations, since they will not start again. Adds a deterministic test: after stop(), a foreign bind to the port is refused, and a restart on the same port succeeds. The test uses only the existing public API, so it also runs against the pre-fix framework, where the foreign bind succeeds.
…correct the release rules Review of the first version found that a runner stopped and removed from its cluster is never released at cluster shutdown, and that close() only called stop(), which reserves the port again. close() now releases the reservation after stopping: close is the end of a runner life, while stop() stays one half of the stop and restart cycle the reservation protects. The reservation documentation now states the three release routes exactly (a start on the port, cluster shutdown for runners still in the cluster, or close) and that anything else holds the port until the JVM exits, including a runner restarted on a different port than the one it reserved. The changelog title no longer claims a restart cannot fail with BindException; it states what the change does. The new test also covers the close() release.
…servation bind it exposed The new testStoppedRunnerThatServedTrafficKeepsItsPortUntilRestart serves a real request through its own client before stopping the runner, then checks the port stays reserved against foreign binds with and without address reuse, and that a restart on the same port serves again. Run at the gate seed, that test showed the reservation itself could fail: connections the stopped server closed can leave the port in TIME_WAIT, and the reservation socket bound without address reuse, so it could not bind over them and the port went unprotected. Jetty connectors bind with reuse set. The reservation socket now binds with address reuse as well; the listening socket it holds still refuses every later bind, so the protection is unchanged.
A second stop() on a runner whose port is already reserved (close() after stop(), or a stop followed by cluster shutdown) tried to bind the port again, failed against the held socket, and logged a warning claiming a restart on the port is not protected when it is. Return early when a reservation for the port is already held. Also qualify the bind comment: the held socket refusing later binds is verified on Linux; address-reuse bind semantics differ on Windows.
nick-boss-tech
marked this pull request as ready for review
October 11, 2026 01:11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 AI text below 🤖 (posted on behalf of Nick Shanin)
https://issues.apache.org/jira/browse/SOLR-18531
What happens today
A stopped test node can lose its port to another process before it restarts.
JettySolrRunnergives up its port when it stops and holds nothing in its place. If another process takes that port during the gap, the restart fails withjava.net.BindException: Address already in use. The retry only tries the same port, and it stops after 60 seconds.LeaderElectionIntegrationTest.testSimpleSliceLeaderElectionhit this once in CI, on an unrelated pull request. No assertion ran.What this change does
A stopped runner holds its port until it starts again, is closed, or its cluster shuts down while the runner is still in the cluster.
When a runner stops, it binds a socket to its port on 127.0.0.1 and keeps it. The next start on that port closes the socket before Jetty binds the port. The sockets live in a static map, so a new runner for the same port sees them.
MiniSolrCloudCluster.shutdown()closes the sockets for the runners that were in its list when shutdown began, after the stops finish, and a runner'sclose()releases the reservation on its current port, whichever runner holds it (see Limits).The reservation socket binds with address reuse set. A node that served traffic can leave its port in TIME_WAIT when it stops; without reuse, the reservation bind fails there and the port stays unprotected apart from a log warning, while the held socket still refuses every later bind, with or without reuse, on Linux.
This is a test-framework change only. Behavior changes: a stopped runner keeps its port bound, so nothing else can bind that port on Linux until the node starts on it again, the runner is closed (a runner's
close()releases its port), or, for runners still in the cluster's list, the cluster shuts down.Proof
New tests pin the reservation down, and the existing restart suites still pass at this head.
New tests in
TestJettySolrRunner:testStoppedRunnerKeepsItsPortUntilRestartandtestStoppedRunnerThatServedTrafficKeepsItsPortUntilRestart. The first checks that a stopped runner's port stays reserved until a restart on it, and thatclose()gives the port back. The second serves a real request before the stop, then checks that outside binds fail with address reuse off and on, and that a restart on the same port serves again.With the two framework source files reverted to the base commit and the new tests kept, both reservation tests fail on their reservation checks: 4 tests run, 2 failures, each the reservation assertion (a foreign socket binds the port that a stopped runner released). At this head,
TestJettySolrRunnerpasses 4 of 4. Verified 2026-10-10 at c8a67c7.Restart regression runs at the same head:
LeaderElectionIntegrationTest: 1 test, 0 failures.TestCoordinatorRole: 9 tests, 0 failures.TestPullReplica: 39 tests, 1 skipped, 0 failures, run with nightly tests enabled (the class is marked@Nightly, so a plain run skips it; the one skip is an@Ignored method).These runs show the existing restart tests still pass; they do not show the flake is gone. It was seen once, so one passing run says little about it.
A choice to check
Hold the port in the framework, or restart the one affected test on a fresh port.
This change holds the port in the framework, so restarts keep the same port, node names and test code. Or the one affected test could restart its node on a fresh port. That needs no reservation, but node names then change with the port, and the test compares node names. The cost of this change is one bound socket per stopped node. Was holding the port the right call?
Limits
Only the Jetty port is reserved, and some stopped runners hold it for a long time.
stopJettySolrRunnerand never restarted is removed from the cluster's list, so cluster shutdown does not release its port. Closing the runner releases it. Otherwise the port stays reserved until a later start on that port or the JVM exits.start()releases the reservation before it binds the port, and a failure after that release does not put the reservation back.Changelog: changelog/unreleased/SOLR-18531-jetty-runner-port-reservation.yml
AI assistance
AI agents assisted with research, implementation, review, and drafting. Nick Shanin directed the work and takes responsibility for this contribution.