SOLR-18378: remove CoreContainer.getCores() and SolrCores.getCores(), 26 sites - #4764

Merged
dsmiley merged 9 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18378-remove-corecontainer-getcores
Aug 25, 2026
Merged

SOLR-18378: remove CoreContainer.getCores() and SolrCores.getCores(), 26 sites#4764
dsmiley merged 9 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18378-remove-corecontainer-getcores

Conversation

@serhiy-bzhezytskyy

@serhiy-bzhezytskyyserhiy-bzhezytskyy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/SOLR-18378

Removes CoreContainer.getCores()/SolrCores.getCores() — 26 sites. Not a rename: the removed methods handed out unreserved cores; the replacement (getLoadedCoreNames() + getCore(String)) reserves, so every site now acquires and releases.

Where to look: two shutdown paths (cancelCoreRecoveries, pauseUpdatesAndAwaitInflightRequests) can't use getCore — it can load a core, wrong during shutdown — so they use getCoreFromAnyList(name, true) instead, which never loads. TestTlogReplica's two helpers deliberately return already-released cores, matching the old contract, to avoid an 11-site refactor out of scope here.

Also fixed a dangling {@link #getCores()} — only ecjLintMain catches that, not the compiler or renderJavadoc.

93 tests / 15 classes / 0 failures. Simulated merge order against SOLR-18382 (#4760) and SOLR-18380 (#4762), which also touch shared files — applies cleanly either way.

AI-assisted (Claude Sonnet 5)

… 26 sites
Not a rename. The removed methods handed out cores that were NOT
reference counted - their own javadoc said both "rather dangerous API
because each core is not reserved" and "Don't need to close them". The
replacement, getLoadedCoreNames() plus getCore(String), hands out a
reserved core that the caller must close. So every migrated site now
acquires a refcount and every site has to release it, and a mechanical
replace here would leak one. A leaked refcount does not fail a test;
it hangs a shutdown.
The documented replacement is also subtly wrong in two places, and
reading it was the only way to find out. getCore(name) is not a pure
lookup - its own comment says "Do this in two phases since we don't
want to lock access to the cores over a load", and in standalone mode
it can create a core from its descriptor. The removed method returned
only already-loaded cores. Both CoreContainer.cancelCoreRecoveries and
pauseUpdatesAndAwaitInflightRequests are shutdown paths, where loading
a core would be actively wrong, so those two use
solrCores.getCoreFromAnyList(name, true), which reserves an
already-loaded core and never loads.
Three treatments, chosen per site rather than applied uniformly:
- pure count (3 sites): getLoadedCoreNames().size() - reserves nothing at all
- iterate all cores (5 sites): for (name : getLoadedCoreNames()) try (core = getCore(name)),
with a null guard because the core may be unloaded between the two calls
- the reference escapes the expression (8 sites): restructured so the use sits inside the
try. Where a site only needed the core's name, it now takes the name and opens no core.
getLoadedCoreNames() is unsorted exactly as getCores() was, so sites
picking .get(0) or iterator().next() keep the same arbitrary choice.
Nothing was sorted.
One deliberate exception, stated in the code. TestTlogReplica's two
private helpers return SolrCore references consumed at 11 call sites
combined, one of which uses the cores to stop the jettys hosting them;
propagating reservation there is exactly the hang hazard above. Those
helpers reserve-then-release inside their own try, returning already-
closed instances - the removed API's own contract - with a comment
saying so. The alternative is an 11-site refactor of a test, which is
not this ticket.
The census took four compile rounds - 5 production sites, then 3 in
test-framework/src/java which ships, then 16 in tests, then 2 more in
modules/ltr - because each round's failures stopped the next module
from compiling at all. 26 total.
Also fixed: a dangling {@link #getCores()} on
SolrCores.getNumLoadedPermanentCores, and a commented-out sketch in
TimeAllowedTest that named the removed method. Measured which gate
catches the dangling link, because it is not the obvious one:
compileJava passes, renderJavadoc passes, and ecjLintMain is what
fails.
Verified: compileJava and compileTestJava for the whole build with
zero errors, spotlessCheck, ecjLintMain and ecjLintTest on core,
ecjLintMain on test-framework and ltr, renderJavadoc, and 15 changed
test classes (counting TestTlogReplica's helper subclasses via their
own suites) - 93 tests, 0 failures.
Simulated merge order against SOLR-18382 (open PR, shares
test-framework cloud base classes and TestTlogReplica) and SOLR-18380
(open PR, shares the same test-framework files) - applying this diff
on top of either merged first, and both merged together, succeeds
cleanly.
AI-assisted (Claude Sonnet 5)
@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

@dsmiley you deprecated getCores() on both CoreContainer and SolrCores back in 2023. Worth flagging directly: the javadoc's own suggested replacement (getCore(name)) turns out to load a core if it isn't already loaded -- wrong on 2 shutdown paths, where that's exactly what must not happen. Used getCoreFromAnyList(name, true) instead. Given it's your deprecation and the fix touches a subtlety in the reference-counting contract, a review here would carry real weight.

AI-assisted (Claude Sonnet 5)

@dsmiley
dsmiley self-requested a review August 20, 2026 00:26

@dsmileydsmiley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I wonder if we should have a CoreContainer.forEachCore(Consumer<SolrCore>))

Comment on lines +1388 to +1389
// getCoreFromAnyList, not getCore: never loads, safe during shutdown
try (SolrCore core = solrCores.getCoreFromAnyList(coreName, true)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how could this be correct, using try-with-resources on a getCore that has not been inc-ref'ed, and thus we shouldn't close it.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getCoreFromAnyList(name, true) does inc-ref -- the second param is literally named incRefCount, and its body calls core.open(), whose own javadoc says "expert: increments the core reference count". So try-with-resources's close() here releases exactly that reference, not an un-reserved one. Same pattern is already used elsewhere in this file (SolrCore.java:3447/3482).

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went further and wrote a concurrency stress test for this exact concern (production, not just the single-threaded mechanism) -- 3 threads acquiring/releasing via getCoreFromAnyList(name, true) while a 4th concurrently unloads/reloads the same core. 0 failures across ~730k acquisitions in 4 runs. Added as TestCoreContainer.testGetCoreFromAnyListSafeUnderConcurrentUnload, pushed.

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I misunderstood your other PR comments where I thought you/AI communicated solrCores.getCoreFromAnyList doesn't incref which is why you were using it. Now I actually looked at our code to confirm.


public void cancelCoreRecoveries() {

List<SolrCore> cores = solrCores.getCores();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguably, SolrCores.getCores() shouldn't be deprecated because it's hidden one layer deep on a class only used by CoreContainer.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair -- checked, and SolrCores is package-private and only ever referenced from CoreContainer, so the deprecation was arguably unnecessary ceremony from the start. Not making a case for the original deprecation though -- the ticket scope was to remove both getCores() methods together, so that's what this PR does.

AI-assisted (Claude Sonnet 5)

* _and_ not yet loaded it will _not_ be returned by this call.
* <p>This list is a new copy, it can be modified by the caller (e.g. it can be sorted).
*/
@Deprecated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe shouldn't be deprecated after all (as I say above

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the CoreContainer.java:1386 thread -- fair point, no argument.

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either we bring this back, or we add the method I suggested to CoreContainer forEachLoadedCore(Consumer<SolrCore>))

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added CoreContainer.forEachLoadedCore(Consumer<SolrCore>), migrated cancelCoreRecoveries() to it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice; this is safer as we avoid race condition on a core closing with inspecting its health

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again; added safety :-)

David Smiley's review claimed the core in cancelCoreRecoveries() has
"not been inc-ref'ed", so try-with-resources closing it would be wrong.
Verified false: incRefCount=true calls core.open(), same pattern already
used at SolrCore.java:3447/3482. Proven live on a single-threaded probe
(openCount 1 -> 2 -> 1 across acquire/close).
This test extends that to the actual concern raised -- production
concurrency: 3 threads repeatedly acquire/use/release via
getCoreFromAnyList(name, true) while a 4th thread concurrently
unloads/reloads the same core name. No existing test exercises this;
the one similar-looking loop (SimpleCollectionCreateDeleteTest.java, also
touched by this PR) uses the unrelated CoreContainer.getCore(name),
not SolrCores.getCoreFromAnyList.
Ran 4x before formatting, ~730k total acquisitions, 0 failures.
AI-assisted (Claude Sonnet 5)
@epugh

Copy link
Copy Markdown
Contributor

Just retriggered the CI processes. There is enough change here with ramifications, I'll wait for approved review from someone like @dsmiley before merging.

Comment on lines +1388 to +1389
// getCoreFromAnyList, not getCore: never loads, safe during shutdown
try (SolrCore core = solrCores.getCoreFromAnyList(coreName, true)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I misunderstood your other PR comments where I thought you/AI communicated solrCores.getCoreFromAnyList doesn't incref which is why you were using it. Now I actually looked at our code to confirm.

* _and_ not yet loaded it will _not_ be returned by this call.
* <p>This list is a new copy, it can be modified by the caller (e.g. it can be sorted).
*/
@Deprecated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either we bring this back, or we add the method I suggested to CoreContainer forEachLoadedCore(Consumer<SolrCore>))

@dsmiley

Copy link
Copy Markdown
Contributor

The changelog is debatable but I think the more important (value producing aspect) here is that this reduced risks of SolrCore closing while being used. The methods themselves are internal stuff a reader shouldn't care about.

David's suggestion on SolrCores.java:152 -- migrates
cancelCoreRecoveries() to it, the only site requested in review.
NodeHealth/SolrPackageLoader keep their current getCore()-based loops
unchanged (different semantics: getCore() can lazy-reload on a race,
forEachLoadedCore deliberately never does).

@dsmileydsmiley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the new method but why aren't you calling it nearly everywhere possible?

Comment threadsolr/core/src/java/org/apache/solr/pkg/SolrPackageLoader.java Outdated
Comment threadsolr/core/src/java/org/apache/solr/handler/admin/api/NodeHealth.java Outdated
…EachLoadedCore
dsmiley asked on the PR why these two call sites, which iterate
getLoadedCoreNames() and reopen each via getCore(coreName), weren't
migrated when forEachLoadedCore was added. Turns out they carry the
same bug as the one this ticket already fixed elsewhere:
getCore(name) LOADS the core if it was unloaded between the
getLoadedCoreNames() snapshot and the getCore() call, whereas
forEachLoadedCore's getCoreFromAnyList(name, true) never does.
SolrPackageLoader: a package-update notification or a health check
should never have the side effect of force-loading an unloaded core.
NodeHealth's healthCheckStandaloneMode needed a mutable
AtomicBoolean instead of a plain local, since it's now captured by
the forEachLoadedCore lambda.
Verified: NodeHealthTest, NodeHealthStandaloneTest, TestPackages
green; full compileJava clean.
@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

Fair — pushed a follow-up commit migrating SolrPackageLoader and NodeHealth too. Both had the same footgun this ticket already fixed elsewhere: getCore(coreName) after a getLoadedCoreNames() snapshot reloads the core if it was unloaded in between, which is wrong for a package-update notification or a health check. forEachLoadedCore's getCoreFromAnyList(name, true) never does that. Agree the new method should be the default everywhere this pattern shows up -- these were the only other two call sites left in this PR's diff.

AI-assisted (Claude Sonnet 5)

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

To be fair, the reason cancelCoreRecoveries() was the only one migrated originally is that I was trying to keep the change as small as possible -- didn't weigh that against the correctness gap it left open in the other two spots.

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

Removed the stress test.

@dsmiley

Copy link
Copy Markdown
Contributor

Yesterday I committed changes to use forEachLoadedCore much more. I anticipate merging today.

@dsmileydsmiley added this to the 10.x milestone Aug 24, 2026
dsmileyand others added 2 commits August 24, 2026 22:57
Left dangling by an earlier commit on this branch that replaced its one use
site; failed ecjLint + spotlessJavaCheck in CI.
@dsmiley
dsmiley merged commit 1400dbc into apache:mainAug 25, 2026
6 checks passed
dsmiley added a commit that referenced this pull request Aug 29, 2026
…4764)
semi-replaced with forEachLoadedCore(lambda)
Co-authored-by: David Smiley <dsmiley@apache.org>
(cherry picked from commit 1400dbc)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@serhiy-bzhezytskyy@epugh@dsmiley
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

SOLR-18378: remove CoreContainer.getCores() and SolrCores.getCores(), 26 sites - #4764

Merged
dsmiley merged 9 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18378-remove-corecontainer-getcores
Aug 25, 2026
Merged

SOLR-18378: remove CoreContainer.getCores() and SolrCores.getCores(), 26 sites#4764
dsmiley merged 9 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18378-remove-corecontainer-getcores

Conversation

@serhiy-bzhezytskyy

@serhiy-bzhezytskyyserhiy-bzhezytskyy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/SOLR-18378

Removes CoreContainer.getCores()/SolrCores.getCores() — 26 sites. Not a rename: the removed methods handed out unreserved cores; the replacement (getLoadedCoreNames() + getCore(String)) reserves, so every site now acquires and releases.

Where to look: two shutdown paths (cancelCoreRecoveries, pauseUpdatesAndAwaitInflightRequests) can't use getCore — it can load a core, wrong during shutdown — so they use getCoreFromAnyList(name, true) instead, which never loads. TestTlogReplica's two helpers deliberately return already-released cores, matching the old contract, to avoid an 11-site refactor out of scope here.

Also fixed a dangling {@link #getCores()} — only ecjLintMain catches that, not the compiler or renderJavadoc.

93 tests / 15 classes / 0 failures. Simulated merge order against SOLR-18382 (#4760) and SOLR-18380 (#4762), which also touch shared files — applies cleanly either way.

AI-assisted (Claude Sonnet 5)

… 26 sites
Not a rename. The removed methods handed out cores that were NOT
reference counted - their own javadoc said both "rather dangerous API
because each core is not reserved" and "Don't need to close them". The
replacement, getLoadedCoreNames() plus getCore(String), hands out a
reserved core that the caller must close. So every migrated site now
acquires a refcount and every site has to release it, and a mechanical
replace here would leak one. A leaked refcount does not fail a test;
it hangs a shutdown.
The documented replacement is also subtly wrong in two places, and
reading it was the only way to find out. getCore(name) is not a pure
lookup - its own comment says "Do this in two phases since we don't
want to lock access to the cores over a load", and in standalone mode
it can create a core from its descriptor. The removed method returned
only already-loaded cores. Both CoreContainer.cancelCoreRecoveries and
pauseUpdatesAndAwaitInflightRequests are shutdown paths, where loading
a core would be actively wrong, so those two use
solrCores.getCoreFromAnyList(name, true), which reserves an
already-loaded core and never loads.
Three treatments, chosen per site rather than applied uniformly:
- pure count (3 sites): getLoadedCoreNames().size() - reserves nothing at all
- iterate all cores (5 sites): for (name : getLoadedCoreNames()) try (core = getCore(name)),
with a null guard because the core may be unloaded between the two calls
- the reference escapes the expression (8 sites): restructured so the use sits inside the
try. Where a site only needed the core's name, it now takes the name and opens no core.
getLoadedCoreNames() is unsorted exactly as getCores() was, so sites
picking .get(0) or iterator().next() keep the same arbitrary choice.
Nothing was sorted.
One deliberate exception, stated in the code. TestTlogReplica's two
private helpers return SolrCore references consumed at 11 call sites
combined, one of which uses the cores to stop the jettys hosting them;
propagating reservation there is exactly the hang hazard above. Those
helpers reserve-then-release inside their own try, returning already-
closed instances - the removed API's own contract - with a comment
saying so. The alternative is an 11-site refactor of a test, which is
not this ticket.
The census took four compile rounds - 5 production sites, then 3 in
test-framework/src/java which ships, then 16 in tests, then 2 more in
modules/ltr - because each round's failures stopped the next module
from compiling at all. 26 total.
Also fixed: a dangling {@link #getCores()} on
SolrCores.getNumLoadedPermanentCores, and a commented-out sketch in
TimeAllowedTest that named the removed method. Measured which gate
catches the dangling link, because it is not the obvious one:
compileJava passes, renderJavadoc passes, and ecjLintMain is what
fails.
Verified: compileJava and compileTestJava for the whole build with
zero errors, spotlessCheck, ecjLintMain and ecjLintTest on core,
ecjLintMain on test-framework and ltr, renderJavadoc, and 15 changed
test classes (counting TestTlogReplica's helper subclasses via their
own suites) - 93 tests, 0 failures.
Simulated merge order against SOLR-18382 (open PR, shares
test-framework cloud base classes and TestTlogReplica) and SOLR-18380
(open PR, shares the same test-framework files) - applying this diff
on top of either merged first, and both merged together, succeeds
cleanly.
AI-assisted (Claude Sonnet 5)
@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

@dsmiley you deprecated getCores() on both CoreContainer and SolrCores back in 2023. Worth flagging directly: the javadoc's own suggested replacement (getCore(name)) turns out to load a core if it isn't already loaded -- wrong on 2 shutdown paths, where that's exactly what must not happen. Used getCoreFromAnyList(name, true) instead. Given it's your deprecation and the fix touches a subtlety in the reference-counting contract, a review here would carry real weight.

AI-assisted (Claude Sonnet 5)

@dsmiley
dsmiley self-requested a review August 20, 2026 00:26

@dsmileydsmiley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I wonder if we should have a CoreContainer.forEachCore(Consumer<SolrCore>))

Comment on lines +1388 to +1389
// getCoreFromAnyList, not getCore: never loads, safe during shutdown
try (SolrCore core = solrCores.getCoreFromAnyList(coreName, true)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how could this be correct, using try-with-resources on a getCore that has not been inc-ref'ed, and thus we shouldn't close it.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getCoreFromAnyList(name, true) does inc-ref -- the second param is literally named incRefCount, and its body calls core.open(), whose own javadoc says "expert: increments the core reference count". So try-with-resources's close() here releases exactly that reference, not an un-reserved one. Same pattern is already used elsewhere in this file (SolrCore.java:3447/3482).

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went further and wrote a concurrency stress test for this exact concern (production, not just the single-threaded mechanism) -- 3 threads acquiring/releasing via getCoreFromAnyList(name, true) while a 4th concurrently unloads/reloads the same core. 0 failures across ~730k acquisitions in 4 runs. Added as TestCoreContainer.testGetCoreFromAnyListSafeUnderConcurrentUnload, pushed.

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I misunderstood your other PR comments where I thought you/AI communicated solrCores.getCoreFromAnyList doesn't incref which is why you were using it. Now I actually looked at our code to confirm.


public void cancelCoreRecoveries() {

List<SolrCore> cores = solrCores.getCores();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguably, SolrCores.getCores() shouldn't be deprecated because it's hidden one layer deep on a class only used by CoreContainer.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair -- checked, and SolrCores is package-private and only ever referenced from CoreContainer, so the deprecation was arguably unnecessary ceremony from the start. Not making a case for the original deprecation though -- the ticket scope was to remove both getCores() methods together, so that's what this PR does.

AI-assisted (Claude Sonnet 5)

* _and_ not yet loaded it will _not_ be returned by this call.
* <p>This list is a new copy, it can be modified by the caller (e.g. it can be sorted).
*/
@Deprecated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe shouldn't be deprecated after all (as I say above

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the CoreContainer.java:1386 thread -- fair point, no argument.

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either we bring this back, or we add the method I suggested to CoreContainer forEachLoadedCore(Consumer<SolrCore>))

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added CoreContainer.forEachLoadedCore(Consumer<SolrCore>), migrated cancelCoreRecoveries() to it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice; this is safer as we avoid race condition on a core closing with inspecting its health

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again; added safety :-)

David Smiley's review claimed the core in cancelCoreRecoveries() has
"not been inc-ref'ed", so try-with-resources closing it would be wrong.
Verified false: incRefCount=true calls core.open(), same pattern already
used at SolrCore.java:3447/3482. Proven live on a single-threaded probe
(openCount 1 -> 2 -> 1 across acquire/close).
This test extends that to the actual concern raised -- production
concurrency: 3 threads repeatedly acquire/use/release via
getCoreFromAnyList(name, true) while a 4th thread concurrently
unloads/reloads the same core name. No existing test exercises this;
the one similar-looking loop (SimpleCollectionCreateDeleteTest.java, also
touched by this PR) uses the unrelated CoreContainer.getCore(name),
not SolrCores.getCoreFromAnyList.
Ran 4x before formatting, ~730k total acquisitions, 0 failures.
AI-assisted (Claude Sonnet 5)
@epugh

Copy link
Copy Markdown
Contributor

Just retriggered the CI processes. There is enough change here with ramifications, I'll wait for approved review from someone like @dsmiley before merging.

Comment on lines +1388 to +1389
// getCoreFromAnyList, not getCore: never loads, safe during shutdown
try (SolrCore core = solrCores.getCoreFromAnyList(coreName, true)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I misunderstood your other PR comments where I thought you/AI communicated solrCores.getCoreFromAnyList doesn't incref which is why you were using it. Now I actually looked at our code to confirm.

* _and_ not yet loaded it will _not_ be returned by this call.
* <p>This list is a new copy, it can be modified by the caller (e.g. it can be sorted).
*/
@Deprecated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either we bring this back, or we add the method I suggested to CoreContainer forEachLoadedCore(Consumer<SolrCore>))

@dsmiley

Copy link
Copy Markdown
Contributor

The changelog is debatable but I think the more important (value producing aspect) here is that this reduced risks of SolrCore closing while being used. The methods themselves are internal stuff a reader shouldn't care about.

David's suggestion on SolrCores.java:152 -- migrates
cancelCoreRecoveries() to it, the only site requested in review.
NodeHealth/SolrPackageLoader keep their current getCore()-based loops
unchanged (different semantics: getCore() can lazy-reload on a race,
forEachLoadedCore deliberately never does).

@dsmileydsmiley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the new method but why aren't you calling it nearly everywhere possible?

Comment threadsolr/core/src/java/org/apache/solr/pkg/SolrPackageLoader.java Outdated
Comment threadsolr/core/src/java/org/apache/solr/handler/admin/api/NodeHealth.java Outdated
…EachLoadedCore
dsmiley asked on the PR why these two call sites, which iterate
getLoadedCoreNames() and reopen each via getCore(coreName), weren't
migrated when forEachLoadedCore was added. Turns out they carry the
same bug as the one this ticket already fixed elsewhere:
getCore(name) LOADS the core if it was unloaded between the
getLoadedCoreNames() snapshot and the getCore() call, whereas
forEachLoadedCore's getCoreFromAnyList(name, true) never does.
SolrPackageLoader: a package-update notification or a health check
should never have the side effect of force-loading an unloaded core.
NodeHealth's healthCheckStandaloneMode needed a mutable
AtomicBoolean instead of a plain local, since it's now captured by
the forEachLoadedCore lambda.
Verified: NodeHealthTest, NodeHealthStandaloneTest, TestPackages
green; full compileJava clean.
@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

Fair — pushed a follow-up commit migrating SolrPackageLoader and NodeHealth too. Both had the same footgun this ticket already fixed elsewhere: getCore(coreName) after a getLoadedCoreNames() snapshot reloads the core if it was unloaded in between, which is wrong for a package-update notification or a health check. forEachLoadedCore's getCoreFromAnyList(name, true) never does that. Agree the new method should be the default everywhere this pattern shows up -- these were the only other two call sites left in this PR's diff.

AI-assisted (Claude Sonnet 5)

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

To be fair, the reason cancelCoreRecoveries() was the only one migrated originally is that I was trying to keep the change as small as possible -- didn't weigh that against the correctness gap it left open in the other two spots.

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

Removed the stress test.

@dsmiley

Copy link
Copy Markdown
Contributor

Yesterday I committed changes to use forEachLoadedCore much more. I anticipate merging today.

@dsmileydsmiley added this to the 10.x milestone Aug 24, 2026
dsmileyand others added 2 commits August 24, 2026 22:57
Left dangling by an earlier commit on this branch that replaced its one use
site; failed ecjLint + spotlessJavaCheck in CI.
@dsmiley
dsmiley merged commit 1400dbc into apache:mainAug 25, 2026
6 checks passed
dsmiley added a commit that referenced this pull request Aug 29, 2026
…4764)
semi-replaced with forEachLoadedCore(lambda)
Co-authored-by: David Smiley <dsmiley@apache.org>
(cherry picked from commit 1400dbc)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@serhiy-bzhezytskyy@epugh@dsmiley
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

SOLR-18378: remove CoreContainer.getCores() and SolrCores.getCores(), 26 sites - #4764

Merged
dsmiley merged 9 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18378-remove-corecontainer-getcores
Aug 25, 2026
Merged

SOLR-18378: remove CoreContainer.getCores() and SolrCores.getCores(), 26 sites#4764
dsmiley merged 9 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18378-remove-corecontainer-getcores

Conversation

@serhiy-bzhezytskyy

@serhiy-bzhezytskyyserhiy-bzhezytskyy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/SOLR-18378

Removes CoreContainer.getCores()/SolrCores.getCores() — 26 sites. Not a rename: the removed methods handed out unreserved cores; the replacement (getLoadedCoreNames() + getCore(String)) reserves, so every site now acquires and releases.

Where to look: two shutdown paths (cancelCoreRecoveries, pauseUpdatesAndAwaitInflightRequests) can't use getCore — it can load a core, wrong during shutdown — so they use getCoreFromAnyList(name, true) instead, which never loads. TestTlogReplica's two helpers deliberately return already-released cores, matching the old contract, to avoid an 11-site refactor out of scope here.

Also fixed a dangling {@link #getCores()} — only ecjLintMain catches that, not the compiler or renderJavadoc.

93 tests / 15 classes / 0 failures. Simulated merge order against SOLR-18382 (#4760) and SOLR-18380 (#4762), which also touch shared files — applies cleanly either way.

AI-assisted (Claude Sonnet 5)

… 26 sites
Not a rename. The removed methods handed out cores that were NOT
reference counted - their own javadoc said both "rather dangerous API
because each core is not reserved" and "Don't need to close them". The
replacement, getLoadedCoreNames() plus getCore(String), hands out a
reserved core that the caller must close. So every migrated site now
acquires a refcount and every site has to release it, and a mechanical
replace here would leak one. A leaked refcount does not fail a test;
it hangs a shutdown.
The documented replacement is also subtly wrong in two places, and
reading it was the only way to find out. getCore(name) is not a pure
lookup - its own comment says "Do this in two phases since we don't
want to lock access to the cores over a load", and in standalone mode
it can create a core from its descriptor. The removed method returned
only already-loaded cores. Both CoreContainer.cancelCoreRecoveries and
pauseUpdatesAndAwaitInflightRequests are shutdown paths, where loading
a core would be actively wrong, so those two use
solrCores.getCoreFromAnyList(name, true), which reserves an
already-loaded core and never loads.
Three treatments, chosen per site rather than applied uniformly:
- pure count (3 sites): getLoadedCoreNames().size() - reserves nothing at all
- iterate all cores (5 sites): for (name : getLoadedCoreNames()) try (core = getCore(name)),
with a null guard because the core may be unloaded between the two calls
- the reference escapes the expression (8 sites): restructured so the use sits inside the
try. Where a site only needed the core's name, it now takes the name and opens no core.
getLoadedCoreNames() is unsorted exactly as getCores() was, so sites
picking .get(0) or iterator().next() keep the same arbitrary choice.
Nothing was sorted.
One deliberate exception, stated in the code. TestTlogReplica's two
private helpers return SolrCore references consumed at 11 call sites
combined, one of which uses the cores to stop the jettys hosting them;
propagating reservation there is exactly the hang hazard above. Those
helpers reserve-then-release inside their own try, returning already-
closed instances - the removed API's own contract - with a comment
saying so. The alternative is an 11-site refactor of a test, which is
not this ticket.
The census took four compile rounds - 5 production sites, then 3 in
test-framework/src/java which ships, then 16 in tests, then 2 more in
modules/ltr - because each round's failures stopped the next module
from compiling at all. 26 total.
Also fixed: a dangling {@link #getCores()} on
SolrCores.getNumLoadedPermanentCores, and a commented-out sketch in
TimeAllowedTest that named the removed method. Measured which gate
catches the dangling link, because it is not the obvious one:
compileJava passes, renderJavadoc passes, and ecjLintMain is what
fails.
Verified: compileJava and compileTestJava for the whole build with
zero errors, spotlessCheck, ecjLintMain and ecjLintTest on core,
ecjLintMain on test-framework and ltr, renderJavadoc, and 15 changed
test classes (counting TestTlogReplica's helper subclasses via their
own suites) - 93 tests, 0 failures.
Simulated merge order against SOLR-18382 (open PR, shares
test-framework cloud base classes and TestTlogReplica) and SOLR-18380
(open PR, shares the same test-framework files) - applying this diff
on top of either merged first, and both merged together, succeeds
cleanly.
AI-assisted (Claude Sonnet 5)
@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

@dsmiley you deprecated getCores() on both CoreContainer and SolrCores back in 2023. Worth flagging directly: the javadoc's own suggested replacement (getCore(name)) turns out to load a core if it isn't already loaded -- wrong on 2 shutdown paths, where that's exactly what must not happen. Used getCoreFromAnyList(name, true) instead. Given it's your deprecation and the fix touches a subtlety in the reference-counting contract, a review here would carry real weight.

AI-assisted (Claude Sonnet 5)

@dsmiley
dsmiley self-requested a review August 20, 2026 00:26

@dsmileydsmiley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I wonder if we should have a CoreContainer.forEachCore(Consumer<SolrCore>))

Comment on lines +1388 to +1389
// getCoreFromAnyList, not getCore: never loads, safe during shutdown
try (SolrCore core = solrCores.getCoreFromAnyList(coreName, true)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how could this be correct, using try-with-resources on a getCore that has not been inc-ref'ed, and thus we shouldn't close it.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getCoreFromAnyList(name, true) does inc-ref -- the second param is literally named incRefCount, and its body calls core.open(), whose own javadoc says "expert: increments the core reference count". So try-with-resources's close() here releases exactly that reference, not an un-reserved one. Same pattern is already used elsewhere in this file (SolrCore.java:3447/3482).

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went further and wrote a concurrency stress test for this exact concern (production, not just the single-threaded mechanism) -- 3 threads acquiring/releasing via getCoreFromAnyList(name, true) while a 4th concurrently unloads/reloads the same core. 0 failures across ~730k acquisitions in 4 runs. Added as TestCoreContainer.testGetCoreFromAnyListSafeUnderConcurrentUnload, pushed.

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I misunderstood your other PR comments where I thought you/AI communicated solrCores.getCoreFromAnyList doesn't incref which is why you were using it. Now I actually looked at our code to confirm.


public void cancelCoreRecoveries() {

List<SolrCore> cores = solrCores.getCores();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguably, SolrCores.getCores() shouldn't be deprecated because it's hidden one layer deep on a class only used by CoreContainer.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair -- checked, and SolrCores is package-private and only ever referenced from CoreContainer, so the deprecation was arguably unnecessary ceremony from the start. Not making a case for the original deprecation though -- the ticket scope was to remove both getCores() methods together, so that's what this PR does.

AI-assisted (Claude Sonnet 5)

* _and_ not yet loaded it will _not_ be returned by this call.
* <p>This list is a new copy, it can be modified by the caller (e.g. it can be sorted).
*/
@Deprecated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe shouldn't be deprecated after all (as I say above

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the CoreContainer.java:1386 thread -- fair point, no argument.

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either we bring this back, or we add the method I suggested to CoreContainer forEachLoadedCore(Consumer<SolrCore>))

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added CoreContainer.forEachLoadedCore(Consumer<SolrCore>), migrated cancelCoreRecoveries() to it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice; this is safer as we avoid race condition on a core closing with inspecting its health

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again; added safety :-)

David Smiley's review claimed the core in cancelCoreRecoveries() has
"not been inc-ref'ed", so try-with-resources closing it would be wrong.
Verified false: incRefCount=true calls core.open(), same pattern already
used at SolrCore.java:3447/3482. Proven live on a single-threaded probe
(openCount 1 -> 2 -> 1 across acquire/close).
This test extends that to the actual concern raised -- production
concurrency: 3 threads repeatedly acquire/use/release via
getCoreFromAnyList(name, true) while a 4th thread concurrently
unloads/reloads the same core name. No existing test exercises this;
the one similar-looking loop (SimpleCollectionCreateDeleteTest.java, also
touched by this PR) uses the unrelated CoreContainer.getCore(name),
not SolrCores.getCoreFromAnyList.
Ran 4x before formatting, ~730k total acquisitions, 0 failures.
AI-assisted (Claude Sonnet 5)
@epugh

Copy link
Copy Markdown
Contributor

Just retriggered the CI processes. There is enough change here with ramifications, I'll wait for approved review from someone like @dsmiley before merging.

Comment on lines +1388 to +1389
// getCoreFromAnyList, not getCore: never loads, safe during shutdown
try (SolrCore core = solrCores.getCoreFromAnyList(coreName, true)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I misunderstood your other PR comments where I thought you/AI communicated solrCores.getCoreFromAnyList doesn't incref which is why you were using it. Now I actually looked at our code to confirm.

* _and_ not yet loaded it will _not_ be returned by this call.
* <p>This list is a new copy, it can be modified by the caller (e.g. it can be sorted).
*/
@Deprecated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either we bring this back, or we add the method I suggested to CoreContainer forEachLoadedCore(Consumer<SolrCore>))

@dsmiley

Copy link
Copy Markdown
Contributor

The changelog is debatable but I think the more important (value producing aspect) here is that this reduced risks of SolrCore closing while being used. The methods themselves are internal stuff a reader shouldn't care about.

David's suggestion on SolrCores.java:152 -- migrates
cancelCoreRecoveries() to it, the only site requested in review.
NodeHealth/SolrPackageLoader keep their current getCore()-based loops
unchanged (different semantics: getCore() can lazy-reload on a race,
forEachLoadedCore deliberately never does).

@dsmileydsmiley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the new method but why aren't you calling it nearly everywhere possible?

Comment threadsolr/core/src/java/org/apache/solr/pkg/SolrPackageLoader.java Outdated
Comment threadsolr/core/src/java/org/apache/solr/handler/admin/api/NodeHealth.java Outdated
…EachLoadedCore
dsmiley asked on the PR why these two call sites, which iterate
getLoadedCoreNames() and reopen each via getCore(coreName), weren't
migrated when forEachLoadedCore was added. Turns out they carry the
same bug as the one this ticket already fixed elsewhere:
getCore(name) LOADS the core if it was unloaded between the
getLoadedCoreNames() snapshot and the getCore() call, whereas
forEachLoadedCore's getCoreFromAnyList(name, true) never does.
SolrPackageLoader: a package-update notification or a health check
should never have the side effect of force-loading an unloaded core.
NodeHealth's healthCheckStandaloneMode needed a mutable
AtomicBoolean instead of a plain local, since it's now captured by
the forEachLoadedCore lambda.
Verified: NodeHealthTest, NodeHealthStandaloneTest, TestPackages
green; full compileJava clean.
@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

Fair — pushed a follow-up commit migrating SolrPackageLoader and NodeHealth too. Both had the same footgun this ticket already fixed elsewhere: getCore(coreName) after a getLoadedCoreNames() snapshot reloads the core if it was unloaded in between, which is wrong for a package-update notification or a health check. forEachLoadedCore's getCoreFromAnyList(name, true) never does that. Agree the new method should be the default everywhere this pattern shows up -- these were the only other two call sites left in this PR's diff.

AI-assisted (Claude Sonnet 5)

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

To be fair, the reason cancelCoreRecoveries() was the only one migrated originally is that I was trying to keep the change as small as possible -- didn't weigh that against the correctness gap it left open in the other two spots.

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

Removed the stress test.

@dsmiley

Copy link
Copy Markdown
Contributor

Yesterday I committed changes to use forEachLoadedCore much more. I anticipate merging today.

@dsmileydsmiley added this to the 10.x milestone Aug 24, 2026
dsmileyand others added 2 commits August 24, 2026 22:57
Left dangling by an earlier commit on this branch that replaced its one use
site; failed ecjLint + spotlessJavaCheck in CI.
@dsmiley
dsmiley merged commit 1400dbc into apache:mainAug 25, 2026
6 checks passed
dsmiley added a commit that referenced this pull request Aug 29, 2026
…4764)
semi-replaced with forEachLoadedCore(lambda)
Co-authored-by: David Smiley <dsmiley@apache.org>
(cherry picked from commit 1400dbc)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@serhiy-bzhezytskyy@epugh@dsmiley
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

SOLR-18378: remove CoreContainer.getCores() and SolrCores.getCores(), 26 sites - #4764

Merged
dsmiley merged 9 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18378-remove-corecontainer-getcores
Aug 25, 2026
Merged

SOLR-18378: remove CoreContainer.getCores() and SolrCores.getCores(), 26 sites#4764
dsmiley merged 9 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18378-remove-corecontainer-getcores

Conversation

@serhiy-bzhezytskyy

@serhiy-bzhezytskyyserhiy-bzhezytskyy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/SOLR-18378

Removes CoreContainer.getCores()/SolrCores.getCores() — 26 sites. Not a rename: the removed methods handed out unreserved cores; the replacement (getLoadedCoreNames() + getCore(String)) reserves, so every site now acquires and releases.

Where to look: two shutdown paths (cancelCoreRecoveries, pauseUpdatesAndAwaitInflightRequests) can't use getCore — it can load a core, wrong during shutdown — so they use getCoreFromAnyList(name, true) instead, which never loads. TestTlogReplica's two helpers deliberately return already-released cores, matching the old contract, to avoid an 11-site refactor out of scope here.

Also fixed a dangling {@link #getCores()} — only ecjLintMain catches that, not the compiler or renderJavadoc.

93 tests / 15 classes / 0 failures. Simulated merge order against SOLR-18382 (#4760) and SOLR-18380 (#4762), which also touch shared files — applies cleanly either way.

AI-assisted (Claude Sonnet 5)

… 26 sites
Not a rename. The removed methods handed out cores that were NOT
reference counted - their own javadoc said both "rather dangerous API
because each core is not reserved" and "Don't need to close them". The
replacement, getLoadedCoreNames() plus getCore(String), hands out a
reserved core that the caller must close. So every migrated site now
acquires a refcount and every site has to release it, and a mechanical
replace here would leak one. A leaked refcount does not fail a test;
it hangs a shutdown.
The documented replacement is also subtly wrong in two places, and
reading it was the only way to find out. getCore(name) is not a pure
lookup - its own comment says "Do this in two phases since we don't
want to lock access to the cores over a load", and in standalone mode
it can create a core from its descriptor. The removed method returned
only already-loaded cores. Both CoreContainer.cancelCoreRecoveries and
pauseUpdatesAndAwaitInflightRequests are shutdown paths, where loading
a core would be actively wrong, so those two use
solrCores.getCoreFromAnyList(name, true), which reserves an
already-loaded core and never loads.
Three treatments, chosen per site rather than applied uniformly:
- pure count (3 sites): getLoadedCoreNames().size() - reserves nothing at all
- iterate all cores (5 sites): for (name : getLoadedCoreNames()) try (core = getCore(name)),
with a null guard because the core may be unloaded between the two calls
- the reference escapes the expression (8 sites): restructured so the use sits inside the
try. Where a site only needed the core's name, it now takes the name and opens no core.
getLoadedCoreNames() is unsorted exactly as getCores() was, so sites
picking .get(0) or iterator().next() keep the same arbitrary choice.
Nothing was sorted.
One deliberate exception, stated in the code. TestTlogReplica's two
private helpers return SolrCore references consumed at 11 call sites
combined, one of which uses the cores to stop the jettys hosting them;
propagating reservation there is exactly the hang hazard above. Those
helpers reserve-then-release inside their own try, returning already-
closed instances - the removed API's own contract - with a comment
saying so. The alternative is an 11-site refactor of a test, which is
not this ticket.
The census took four compile rounds - 5 production sites, then 3 in
test-framework/src/java which ships, then 16 in tests, then 2 more in
modules/ltr - because each round's failures stopped the next module
from compiling at all. 26 total.
Also fixed: a dangling {@link #getCores()} on
SolrCores.getNumLoadedPermanentCores, and a commented-out sketch in
TimeAllowedTest that named the removed method. Measured which gate
catches the dangling link, because it is not the obvious one:
compileJava passes, renderJavadoc passes, and ecjLintMain is what
fails.
Verified: compileJava and compileTestJava for the whole build with
zero errors, spotlessCheck, ecjLintMain and ecjLintTest on core,
ecjLintMain on test-framework and ltr, renderJavadoc, and 15 changed
test classes (counting TestTlogReplica's helper subclasses via their
own suites) - 93 tests, 0 failures.
Simulated merge order against SOLR-18382 (open PR, shares
test-framework cloud base classes and TestTlogReplica) and SOLR-18380
(open PR, shares the same test-framework files) - applying this diff
on top of either merged first, and both merged together, succeeds
cleanly.
AI-assisted (Claude Sonnet 5)
@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

@dsmiley you deprecated getCores() on both CoreContainer and SolrCores back in 2023. Worth flagging directly: the javadoc's own suggested replacement (getCore(name)) turns out to load a core if it isn't already loaded -- wrong on 2 shutdown paths, where that's exactly what must not happen. Used getCoreFromAnyList(name, true) instead. Given it's your deprecation and the fix touches a subtlety in the reference-counting contract, a review here would carry real weight.

AI-assisted (Claude Sonnet 5)

@dsmiley
dsmiley self-requested a review August 20, 2026 00:26

@dsmileydsmiley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I wonder if we should have a CoreContainer.forEachCore(Consumer<SolrCore>))

Comment on lines +1388 to +1389
// getCoreFromAnyList, not getCore: never loads, safe during shutdown
try (SolrCore core = solrCores.getCoreFromAnyList(coreName, true)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how could this be correct, using try-with-resources on a getCore that has not been inc-ref'ed, and thus we shouldn't close it.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getCoreFromAnyList(name, true) does inc-ref -- the second param is literally named incRefCount, and its body calls core.open(), whose own javadoc says "expert: increments the core reference count". So try-with-resources's close() here releases exactly that reference, not an un-reserved one. Same pattern is already used elsewhere in this file (SolrCore.java:3447/3482).

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went further and wrote a concurrency stress test for this exact concern (production, not just the single-threaded mechanism) -- 3 threads acquiring/releasing via getCoreFromAnyList(name, true) while a 4th concurrently unloads/reloads the same core. 0 failures across ~730k acquisitions in 4 runs. Added as TestCoreContainer.testGetCoreFromAnyListSafeUnderConcurrentUnload, pushed.

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I misunderstood your other PR comments where I thought you/AI communicated solrCores.getCoreFromAnyList doesn't incref which is why you were using it. Now I actually looked at our code to confirm.


public void cancelCoreRecoveries() {

List<SolrCore> cores = solrCores.getCores();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguably, SolrCores.getCores() shouldn't be deprecated because it's hidden one layer deep on a class only used by CoreContainer.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair -- checked, and SolrCores is package-private and only ever referenced from CoreContainer, so the deprecation was arguably unnecessary ceremony from the start. Not making a case for the original deprecation though -- the ticket scope was to remove both getCores() methods together, so that's what this PR does.

AI-assisted (Claude Sonnet 5)

* _and_ not yet loaded it will _not_ be returned by this call.
* <p>This list is a new copy, it can be modified by the caller (e.g. it can be sorted).
*/
@Deprecated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe shouldn't be deprecated after all (as I say above

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the CoreContainer.java:1386 thread -- fair point, no argument.

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either we bring this back, or we add the method I suggested to CoreContainer forEachLoadedCore(Consumer<SolrCore>))

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added CoreContainer.forEachLoadedCore(Consumer<SolrCore>), migrated cancelCoreRecoveries() to it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice; this is safer as we avoid race condition on a core closing with inspecting its health

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again; added safety :-)

David Smiley's review claimed the core in cancelCoreRecoveries() has
"not been inc-ref'ed", so try-with-resources closing it would be wrong.
Verified false: incRefCount=true calls core.open(), same pattern already
used at SolrCore.java:3447/3482. Proven live on a single-threaded probe
(openCount 1 -> 2 -> 1 across acquire/close).
This test extends that to the actual concern raised -- production
concurrency: 3 threads repeatedly acquire/use/release via
getCoreFromAnyList(name, true) while a 4th thread concurrently
unloads/reloads the same core name. No existing test exercises this;
the one similar-looking loop (SimpleCollectionCreateDeleteTest.java, also
touched by this PR) uses the unrelated CoreContainer.getCore(name),
not SolrCores.getCoreFromAnyList.
Ran 4x before formatting, ~730k total acquisitions, 0 failures.
AI-assisted (Claude Sonnet 5)
@epugh

Copy link
Copy Markdown
Contributor

Just retriggered the CI processes. There is enough change here with ramifications, I'll wait for approved review from someone like @dsmiley before merging.

Comment on lines +1388 to +1389
// getCoreFromAnyList, not getCore: never loads, safe during shutdown
try (SolrCore core = solrCores.getCoreFromAnyList(coreName, true)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I misunderstood your other PR comments where I thought you/AI communicated solrCores.getCoreFromAnyList doesn't incref which is why you were using it. Now I actually looked at our code to confirm.

* _and_ not yet loaded it will _not_ be returned by this call.
* <p>This list is a new copy, it can be modified by the caller (e.g. it can be sorted).
*/
@Deprecated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either we bring this back, or we add the method I suggested to CoreContainer forEachLoadedCore(Consumer<SolrCore>))

@dsmiley

Copy link
Copy Markdown
Contributor

The changelog is debatable but I think the more important (value producing aspect) here is that this reduced risks of SolrCore closing while being used. The methods themselves are internal stuff a reader shouldn't care about.

David's suggestion on SolrCores.java:152 -- migrates
cancelCoreRecoveries() to it, the only site requested in review.
NodeHealth/SolrPackageLoader keep their current getCore()-based loops
unchanged (different semantics: getCore() can lazy-reload on a race,
forEachLoadedCore deliberately never does).

@dsmileydsmiley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the new method but why aren't you calling it nearly everywhere possible?

Comment threadsolr/core/src/java/org/apache/solr/pkg/SolrPackageLoader.java Outdated
Comment threadsolr/core/src/java/org/apache/solr/handler/admin/api/NodeHealth.java Outdated
…EachLoadedCore
dsmiley asked on the PR why these two call sites, which iterate
getLoadedCoreNames() and reopen each via getCore(coreName), weren't
migrated when forEachLoadedCore was added. Turns out they carry the
same bug as the one this ticket already fixed elsewhere:
getCore(name) LOADS the core if it was unloaded between the
getLoadedCoreNames() snapshot and the getCore() call, whereas
forEachLoadedCore's getCoreFromAnyList(name, true) never does.
SolrPackageLoader: a package-update notification or a health check
should never have the side effect of force-loading an unloaded core.
NodeHealth's healthCheckStandaloneMode needed a mutable
AtomicBoolean instead of a plain local, since it's now captured by
the forEachLoadedCore lambda.
Verified: NodeHealthTest, NodeHealthStandaloneTest, TestPackages
green; full compileJava clean.
@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

Fair — pushed a follow-up commit migrating SolrPackageLoader and NodeHealth too. Both had the same footgun this ticket already fixed elsewhere: getCore(coreName) after a getLoadedCoreNames() snapshot reloads the core if it was unloaded in between, which is wrong for a package-update notification or a health check. forEachLoadedCore's getCoreFromAnyList(name, true) never does that. Agree the new method should be the default everywhere this pattern shows up -- these were the only other two call sites left in this PR's diff.

AI-assisted (Claude Sonnet 5)

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

To be fair, the reason cancelCoreRecoveries() was the only one migrated originally is that I was trying to keep the change as small as possible -- didn't weigh that against the correctness gap it left open in the other two spots.

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

Removed the stress test.

@dsmiley

Copy link
Copy Markdown
Contributor

Yesterday I committed changes to use forEachLoadedCore much more. I anticipate merging today.

@dsmileydsmiley added this to the 10.x milestone Aug 24, 2026
dsmileyand others added 2 commits August 24, 2026 22:57
Left dangling by an earlier commit on this branch that replaced its one use
site; failed ecjLint + spotlessJavaCheck in CI.
@dsmiley
dsmiley merged commit 1400dbc into apache:mainAug 25, 2026
6 checks passed
dsmiley added a commit that referenced this pull request Aug 29, 2026
…4764)
semi-replaced with forEachLoadedCore(lambda)
Co-authored-by: David Smiley <dsmiley@apache.org>
(cherry picked from commit 1400dbc)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@serhiy-bzhezytskyy@epugh@dsmiley
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

SOLR-18378: remove CoreContainer.getCores() and SolrCores.getCores(), 26 sites - #4764

Merged
dsmiley merged 9 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18378-remove-corecontainer-getcores
Aug 25, 2026
Merged

SOLR-18378: remove CoreContainer.getCores() and SolrCores.getCores(), 26 sites#4764
dsmiley merged 9 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18378-remove-corecontainer-getcores

Conversation

@serhiy-bzhezytskyy

@serhiy-bzhezytskyyserhiy-bzhezytskyy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/SOLR-18378

Removes CoreContainer.getCores()/SolrCores.getCores() — 26 sites. Not a rename: the removed methods handed out unreserved cores; the replacement (getLoadedCoreNames() + getCore(String)) reserves, so every site now acquires and releases.

Where to look: two shutdown paths (cancelCoreRecoveries, pauseUpdatesAndAwaitInflightRequests) can't use getCore — it can load a core, wrong during shutdown — so they use getCoreFromAnyList(name, true) instead, which never loads. TestTlogReplica's two helpers deliberately return already-released cores, matching the old contract, to avoid an 11-site refactor out of scope here.

Also fixed a dangling {@link #getCores()} — only ecjLintMain catches that, not the compiler or renderJavadoc.

93 tests / 15 classes / 0 failures. Simulated merge order against SOLR-18382 (#4760) and SOLR-18380 (#4762), which also touch shared files — applies cleanly either way.

AI-assisted (Claude Sonnet 5)

… 26 sites
Not a rename. The removed methods handed out cores that were NOT
reference counted - their own javadoc said both "rather dangerous API
because each core is not reserved" and "Don't need to close them". The
replacement, getLoadedCoreNames() plus getCore(String), hands out a
reserved core that the caller must close. So every migrated site now
acquires a refcount and every site has to release it, and a mechanical
replace here would leak one. A leaked refcount does not fail a test;
it hangs a shutdown.
The documented replacement is also subtly wrong in two places, and
reading it was the only way to find out. getCore(name) is not a pure
lookup - its own comment says "Do this in two phases since we don't
want to lock access to the cores over a load", and in standalone mode
it can create a core from its descriptor. The removed method returned
only already-loaded cores. Both CoreContainer.cancelCoreRecoveries and
pauseUpdatesAndAwaitInflightRequests are shutdown paths, where loading
a core would be actively wrong, so those two use
solrCores.getCoreFromAnyList(name, true), which reserves an
already-loaded core and never loads.
Three treatments, chosen per site rather than applied uniformly:
- pure count (3 sites): getLoadedCoreNames().size() - reserves nothing at all
- iterate all cores (5 sites): for (name : getLoadedCoreNames()) try (core = getCore(name)),
with a null guard because the core may be unloaded between the two calls
- the reference escapes the expression (8 sites): restructured so the use sits inside the
try. Where a site only needed the core's name, it now takes the name and opens no core.
getLoadedCoreNames() is unsorted exactly as getCores() was, so sites
picking .get(0) or iterator().next() keep the same arbitrary choice.
Nothing was sorted.
One deliberate exception, stated in the code. TestTlogReplica's two
private helpers return SolrCore references consumed at 11 call sites
combined, one of which uses the cores to stop the jettys hosting them;
propagating reservation there is exactly the hang hazard above. Those
helpers reserve-then-release inside their own try, returning already-
closed instances - the removed API's own contract - with a comment
saying so. The alternative is an 11-site refactor of a test, which is
not this ticket.
The census took four compile rounds - 5 production sites, then 3 in
test-framework/src/java which ships, then 16 in tests, then 2 more in
modules/ltr - because each round's failures stopped the next module
from compiling at all. 26 total.
Also fixed: a dangling {@link #getCores()} on
SolrCores.getNumLoadedPermanentCores, and a commented-out sketch in
TimeAllowedTest that named the removed method. Measured which gate
catches the dangling link, because it is not the obvious one:
compileJava passes, renderJavadoc passes, and ecjLintMain is what
fails.
Verified: compileJava and compileTestJava for the whole build with
zero errors, spotlessCheck, ecjLintMain and ecjLintTest on core,
ecjLintMain on test-framework and ltr, renderJavadoc, and 15 changed
test classes (counting TestTlogReplica's helper subclasses via their
own suites) - 93 tests, 0 failures.
Simulated merge order against SOLR-18382 (open PR, shares
test-framework cloud base classes and TestTlogReplica) and SOLR-18380
(open PR, shares the same test-framework files) - applying this diff
on top of either merged first, and both merged together, succeeds
cleanly.
AI-assisted (Claude Sonnet 5)
@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

@dsmiley you deprecated getCores() on both CoreContainer and SolrCores back in 2023. Worth flagging directly: the javadoc's own suggested replacement (getCore(name)) turns out to load a core if it isn't already loaded -- wrong on 2 shutdown paths, where that's exactly what must not happen. Used getCoreFromAnyList(name, true) instead. Given it's your deprecation and the fix touches a subtlety in the reference-counting contract, a review here would carry real weight.

AI-assisted (Claude Sonnet 5)

@dsmiley
dsmiley self-requested a review August 20, 2026 00:26

@dsmileydsmiley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I wonder if we should have a CoreContainer.forEachCore(Consumer<SolrCore>))

Comment on lines +1388 to +1389
// getCoreFromAnyList, not getCore: never loads, safe during shutdown
try (SolrCore core = solrCores.getCoreFromAnyList(coreName, true)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how could this be correct, using try-with-resources on a getCore that has not been inc-ref'ed, and thus we shouldn't close it.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getCoreFromAnyList(name, true) does inc-ref -- the second param is literally named incRefCount, and its body calls core.open(), whose own javadoc says "expert: increments the core reference count". So try-with-resources's close() here releases exactly that reference, not an un-reserved one. Same pattern is already used elsewhere in this file (SolrCore.java:3447/3482).

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went further and wrote a concurrency stress test for this exact concern (production, not just the single-threaded mechanism) -- 3 threads acquiring/releasing via getCoreFromAnyList(name, true) while a 4th concurrently unloads/reloads the same core. 0 failures across ~730k acquisitions in 4 runs. Added as TestCoreContainer.testGetCoreFromAnyListSafeUnderConcurrentUnload, pushed.

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I misunderstood your other PR comments where I thought you/AI communicated solrCores.getCoreFromAnyList doesn't incref which is why you were using it. Now I actually looked at our code to confirm.


public void cancelCoreRecoveries() {

List<SolrCore> cores = solrCores.getCores();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguably, SolrCores.getCores() shouldn't be deprecated because it's hidden one layer deep on a class only used by CoreContainer.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair -- checked, and SolrCores is package-private and only ever referenced from CoreContainer, so the deprecation was arguably unnecessary ceremony from the start. Not making a case for the original deprecation though -- the ticket scope was to remove both getCores() methods together, so that's what this PR does.

AI-assisted (Claude Sonnet 5)

* _and_ not yet loaded it will _not_ be returned by this call.
* <p>This list is a new copy, it can be modified by the caller (e.g. it can be sorted).
*/
@Deprecated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe shouldn't be deprecated after all (as I say above

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the CoreContainer.java:1386 thread -- fair point, no argument.

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either we bring this back, or we add the method I suggested to CoreContainer forEachLoadedCore(Consumer<SolrCore>))

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added CoreContainer.forEachLoadedCore(Consumer<SolrCore>), migrated cancelCoreRecoveries() to it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice; this is safer as we avoid race condition on a core closing with inspecting its health

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again; added safety :-)

David Smiley's review claimed the core in cancelCoreRecoveries() has
"not been inc-ref'ed", so try-with-resources closing it would be wrong.
Verified false: incRefCount=true calls core.open(), same pattern already
used at SolrCore.java:3447/3482. Proven live on a single-threaded probe
(openCount 1 -> 2 -> 1 across acquire/close).
This test extends that to the actual concern raised -- production
concurrency: 3 threads repeatedly acquire/use/release via
getCoreFromAnyList(name, true) while a 4th thread concurrently
unloads/reloads the same core name. No existing test exercises this;
the one similar-looking loop (SimpleCollectionCreateDeleteTest.java, also
touched by this PR) uses the unrelated CoreContainer.getCore(name),
not SolrCores.getCoreFromAnyList.
Ran 4x before formatting, ~730k total acquisitions, 0 failures.
AI-assisted (Claude Sonnet 5)
@epugh

Copy link
Copy Markdown
Contributor

Just retriggered the CI processes. There is enough change here with ramifications, I'll wait for approved review from someone like @dsmiley before merging.

Comment on lines +1388 to +1389
// getCoreFromAnyList, not getCore: never loads, safe during shutdown
try (SolrCore core = solrCores.getCoreFromAnyList(coreName, true)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I misunderstood your other PR comments where I thought you/AI communicated solrCores.getCoreFromAnyList doesn't incref which is why you were using it. Now I actually looked at our code to confirm.

* _and_ not yet loaded it will _not_ be returned by this call.
* <p>This list is a new copy, it can be modified by the caller (e.g. it can be sorted).
*/
@Deprecated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either we bring this back, or we add the method I suggested to CoreContainer forEachLoadedCore(Consumer<SolrCore>))

@dsmiley

Copy link
Copy Markdown
Contributor

The changelog is debatable but I think the more important (value producing aspect) here is that this reduced risks of SolrCore closing while being used. The methods themselves are internal stuff a reader shouldn't care about.

David's suggestion on SolrCores.java:152 -- migrates
cancelCoreRecoveries() to it, the only site requested in review.
NodeHealth/SolrPackageLoader keep their current getCore()-based loops
unchanged (different semantics: getCore() can lazy-reload on a race,
forEachLoadedCore deliberately never does).

@dsmileydsmiley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the new method but why aren't you calling it nearly everywhere possible?

Comment threadsolr/core/src/java/org/apache/solr/pkg/SolrPackageLoader.java Outdated
Comment threadsolr/core/src/java/org/apache/solr/handler/admin/api/NodeHealth.java Outdated
…EachLoadedCore
dsmiley asked on the PR why these two call sites, which iterate
getLoadedCoreNames() and reopen each via getCore(coreName), weren't
migrated when forEachLoadedCore was added. Turns out they carry the
same bug as the one this ticket already fixed elsewhere:
getCore(name) LOADS the core if it was unloaded between the
getLoadedCoreNames() snapshot and the getCore() call, whereas
forEachLoadedCore's getCoreFromAnyList(name, true) never does.
SolrPackageLoader: a package-update notification or a health check
should never have the side effect of force-loading an unloaded core.
NodeHealth's healthCheckStandaloneMode needed a mutable
AtomicBoolean instead of a plain local, since it's now captured by
the forEachLoadedCore lambda.
Verified: NodeHealthTest, NodeHealthStandaloneTest, TestPackages
green; full compileJava clean.
@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

Fair — pushed a follow-up commit migrating SolrPackageLoader and NodeHealth too. Both had the same footgun this ticket already fixed elsewhere: getCore(coreName) after a getLoadedCoreNames() snapshot reloads the core if it was unloaded in between, which is wrong for a package-update notification or a health check. forEachLoadedCore's getCoreFromAnyList(name, true) never does that. Agree the new method should be the default everywhere this pattern shows up -- these were the only other two call sites left in this PR's diff.

AI-assisted (Claude Sonnet 5)

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

To be fair, the reason cancelCoreRecoveries() was the only one migrated originally is that I was trying to keep the change as small as possible -- didn't weigh that against the correctness gap it left open in the other two spots.

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

Removed the stress test.

@dsmiley

Copy link
Copy Markdown
Contributor

Yesterday I committed changes to use forEachLoadedCore much more. I anticipate merging today.

@dsmileydsmiley added this to the 10.x milestone Aug 24, 2026
dsmileyand others added 2 commits August 24, 2026 22:57
Left dangling by an earlier commit on this branch that replaced its one use
site; failed ecjLint + spotlessJavaCheck in CI.
@dsmiley
dsmiley merged commit 1400dbc into apache:mainAug 25, 2026
6 checks passed
dsmiley added a commit that referenced this pull request Aug 29, 2026
…4764)
semi-replaced with forEachLoadedCore(lambda)
Co-authored-by: David Smiley <dsmiley@apache.org>
(cherry picked from commit 1400dbc)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@serhiy-bzhezytskyy@epugh@dsmiley
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

SOLR-18378: remove CoreContainer.getCores() and SolrCores.getCores(), 26 sites - #4764

Merged
dsmiley merged 9 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18378-remove-corecontainer-getcores
Aug 25, 2026
Merged

SOLR-18378: remove CoreContainer.getCores() and SolrCores.getCores(), 26 sites#4764
dsmiley merged 9 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18378-remove-corecontainer-getcores

Conversation

@serhiy-bzhezytskyy

@serhiy-bzhezytskyyserhiy-bzhezytskyy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/SOLR-18378

Removes CoreContainer.getCores()/SolrCores.getCores() — 26 sites. Not a rename: the removed methods handed out unreserved cores; the replacement (getLoadedCoreNames() + getCore(String)) reserves, so every site now acquires and releases.

Where to look: two shutdown paths (cancelCoreRecoveries, pauseUpdatesAndAwaitInflightRequests) can't use getCore — it can load a core, wrong during shutdown — so they use getCoreFromAnyList(name, true) instead, which never loads. TestTlogReplica's two helpers deliberately return already-released cores, matching the old contract, to avoid an 11-site refactor out of scope here.

Also fixed a dangling {@link #getCores()} — only ecjLintMain catches that, not the compiler or renderJavadoc.

93 tests / 15 classes / 0 failures. Simulated merge order against SOLR-18382 (#4760) and SOLR-18380 (#4762), which also touch shared files — applies cleanly either way.

AI-assisted (Claude Sonnet 5)

… 26 sites
Not a rename. The removed methods handed out cores that were NOT
reference counted - their own javadoc said both "rather dangerous API
because each core is not reserved" and "Don't need to close them". The
replacement, getLoadedCoreNames() plus getCore(String), hands out a
reserved core that the caller must close. So every migrated site now
acquires a refcount and every site has to release it, and a mechanical
replace here would leak one. A leaked refcount does not fail a test;
it hangs a shutdown.
The documented replacement is also subtly wrong in two places, and
reading it was the only way to find out. getCore(name) is not a pure
lookup - its own comment says "Do this in two phases since we don't
want to lock access to the cores over a load", and in standalone mode
it can create a core from its descriptor. The removed method returned
only already-loaded cores. Both CoreContainer.cancelCoreRecoveries and
pauseUpdatesAndAwaitInflightRequests are shutdown paths, where loading
a core would be actively wrong, so those two use
solrCores.getCoreFromAnyList(name, true), which reserves an
already-loaded core and never loads.
Three treatments, chosen per site rather than applied uniformly:
- pure count (3 sites): getLoadedCoreNames().size() - reserves nothing at all
- iterate all cores (5 sites): for (name : getLoadedCoreNames()) try (core = getCore(name)),
with a null guard because the core may be unloaded between the two calls
- the reference escapes the expression (8 sites): restructured so the use sits inside the
try. Where a site only needed the core's name, it now takes the name and opens no core.
getLoadedCoreNames() is unsorted exactly as getCores() was, so sites
picking .get(0) or iterator().next() keep the same arbitrary choice.
Nothing was sorted.
One deliberate exception, stated in the code. TestTlogReplica's two
private helpers return SolrCore references consumed at 11 call sites
combined, one of which uses the cores to stop the jettys hosting them;
propagating reservation there is exactly the hang hazard above. Those
helpers reserve-then-release inside their own try, returning already-
closed instances - the removed API's own contract - with a comment
saying so. The alternative is an 11-site refactor of a test, which is
not this ticket.
The census took four compile rounds - 5 production sites, then 3 in
test-framework/src/java which ships, then 16 in tests, then 2 more in
modules/ltr - because each round's failures stopped the next module
from compiling at all. 26 total.
Also fixed: a dangling {@link #getCores()} on
SolrCores.getNumLoadedPermanentCores, and a commented-out sketch in
TimeAllowedTest that named the removed method. Measured which gate
catches the dangling link, because it is not the obvious one:
compileJava passes, renderJavadoc passes, and ecjLintMain is what
fails.
Verified: compileJava and compileTestJava for the whole build with
zero errors, spotlessCheck, ecjLintMain and ecjLintTest on core,
ecjLintMain on test-framework and ltr, renderJavadoc, and 15 changed
test classes (counting TestTlogReplica's helper subclasses via their
own suites) - 93 tests, 0 failures.
Simulated merge order against SOLR-18382 (open PR, shares
test-framework cloud base classes and TestTlogReplica) and SOLR-18380
(open PR, shares the same test-framework files) - applying this diff
on top of either merged first, and both merged together, succeeds
cleanly.
AI-assisted (Claude Sonnet 5)
@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

@dsmiley you deprecated getCores() on both CoreContainer and SolrCores back in 2023. Worth flagging directly: the javadoc's own suggested replacement (getCore(name)) turns out to load a core if it isn't already loaded -- wrong on 2 shutdown paths, where that's exactly what must not happen. Used getCoreFromAnyList(name, true) instead. Given it's your deprecation and the fix touches a subtlety in the reference-counting contract, a review here would carry real weight.

AI-assisted (Claude Sonnet 5)

@dsmiley
dsmiley self-requested a review August 20, 2026 00:26

@dsmileydsmiley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I wonder if we should have a CoreContainer.forEachCore(Consumer<SolrCore>))

Comment on lines +1388 to +1389
// getCoreFromAnyList, not getCore: never loads, safe during shutdown
try (SolrCore core = solrCores.getCoreFromAnyList(coreName, true)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how could this be correct, using try-with-resources on a getCore that has not been inc-ref'ed, and thus we shouldn't close it.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getCoreFromAnyList(name, true) does inc-ref -- the second param is literally named incRefCount, and its body calls core.open(), whose own javadoc says "expert: increments the core reference count". So try-with-resources's close() here releases exactly that reference, not an un-reserved one. Same pattern is already used elsewhere in this file (SolrCore.java:3447/3482).

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went further and wrote a concurrency stress test for this exact concern (production, not just the single-threaded mechanism) -- 3 threads acquiring/releasing via getCoreFromAnyList(name, true) while a 4th concurrently unloads/reloads the same core. 0 failures across ~730k acquisitions in 4 runs. Added as TestCoreContainer.testGetCoreFromAnyListSafeUnderConcurrentUnload, pushed.

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I misunderstood your other PR comments where I thought you/AI communicated solrCores.getCoreFromAnyList doesn't incref which is why you were using it. Now I actually looked at our code to confirm.


public void cancelCoreRecoveries() {

List<SolrCore> cores = solrCores.getCores();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguably, SolrCores.getCores() shouldn't be deprecated because it's hidden one layer deep on a class only used by CoreContainer.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair -- checked, and SolrCores is package-private and only ever referenced from CoreContainer, so the deprecation was arguably unnecessary ceremony from the start. Not making a case for the original deprecation though -- the ticket scope was to remove both getCores() methods together, so that's what this PR does.

AI-assisted (Claude Sonnet 5)

* _and_ not yet loaded it will _not_ be returned by this call.
* <p>This list is a new copy, it can be modified by the caller (e.g. it can be sorted).
*/
@Deprecated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe shouldn't be deprecated after all (as I say above

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the CoreContainer.java:1386 thread -- fair point, no argument.

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either we bring this back, or we add the method I suggested to CoreContainer forEachLoadedCore(Consumer<SolrCore>))

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added CoreContainer.forEachLoadedCore(Consumer<SolrCore>), migrated cancelCoreRecoveries() to it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice; this is safer as we avoid race condition on a core closing with inspecting its health

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again; added safety :-)

David Smiley's review claimed the core in cancelCoreRecoveries() has
"not been inc-ref'ed", so try-with-resources closing it would be wrong.
Verified false: incRefCount=true calls core.open(), same pattern already
used at SolrCore.java:3447/3482. Proven live on a single-threaded probe
(openCount 1 -> 2 -> 1 across acquire/close).
This test extends that to the actual concern raised -- production
concurrency: 3 threads repeatedly acquire/use/release via
getCoreFromAnyList(name, true) while a 4th thread concurrently
unloads/reloads the same core name. No existing test exercises this;
the one similar-looking loop (SimpleCollectionCreateDeleteTest.java, also
touched by this PR) uses the unrelated CoreContainer.getCore(name),
not SolrCores.getCoreFromAnyList.
Ran 4x before formatting, ~730k total acquisitions, 0 failures.
AI-assisted (Claude Sonnet 5)
@epugh

Copy link
Copy Markdown
Contributor

Just retriggered the CI processes. There is enough change here with ramifications, I'll wait for approved review from someone like @dsmiley before merging.

Comment on lines +1388 to +1389
// getCoreFromAnyList, not getCore: never loads, safe during shutdown
try (SolrCore core = solrCores.getCoreFromAnyList(coreName, true)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I misunderstood your other PR comments where I thought you/AI communicated solrCores.getCoreFromAnyList doesn't incref which is why you were using it. Now I actually looked at our code to confirm.

* _and_ not yet loaded it will _not_ be returned by this call.
* <p>This list is a new copy, it can be modified by the caller (e.g. it can be sorted).
*/
@Deprecated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either we bring this back, or we add the method I suggested to CoreContainer forEachLoadedCore(Consumer<SolrCore>))

@dsmiley

Copy link
Copy Markdown
Contributor

The changelog is debatable but I think the more important (value producing aspect) here is that this reduced risks of SolrCore closing while being used. The methods themselves are internal stuff a reader shouldn't care about.

David's suggestion on SolrCores.java:152 -- migrates
cancelCoreRecoveries() to it, the only site requested in review.
NodeHealth/SolrPackageLoader keep their current getCore()-based loops
unchanged (different semantics: getCore() can lazy-reload on a race,
forEachLoadedCore deliberately never does).

@dsmileydsmiley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the new method but why aren't you calling it nearly everywhere possible?

Comment threadsolr/core/src/java/org/apache/solr/pkg/SolrPackageLoader.java Outdated
Comment threadsolr/core/src/java/org/apache/solr/handler/admin/api/NodeHealth.java Outdated
…EachLoadedCore
dsmiley asked on the PR why these two call sites, which iterate
getLoadedCoreNames() and reopen each via getCore(coreName), weren't
migrated when forEachLoadedCore was added. Turns out they carry the
same bug as the one this ticket already fixed elsewhere:
getCore(name) LOADS the core if it was unloaded between the
getLoadedCoreNames() snapshot and the getCore() call, whereas
forEachLoadedCore's getCoreFromAnyList(name, true) never does.
SolrPackageLoader: a package-update notification or a health check
should never have the side effect of force-loading an unloaded core.
NodeHealth's healthCheckStandaloneMode needed a mutable
AtomicBoolean instead of a plain local, since it's now captured by
the forEachLoadedCore lambda.
Verified: NodeHealthTest, NodeHealthStandaloneTest, TestPackages
green; full compileJava clean.
@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

Fair — pushed a follow-up commit migrating SolrPackageLoader and NodeHealth too. Both had the same footgun this ticket already fixed elsewhere: getCore(coreName) after a getLoadedCoreNames() snapshot reloads the core if it was unloaded in between, which is wrong for a package-update notification or a health check. forEachLoadedCore's getCoreFromAnyList(name, true) never does that. Agree the new method should be the default everywhere this pattern shows up -- these were the only other two call sites left in this PR's diff.

AI-assisted (Claude Sonnet 5)

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

To be fair, the reason cancelCoreRecoveries() was the only one migrated originally is that I was trying to keep the change as small as possible -- didn't weigh that against the correctness gap it left open in the other two spots.

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

Removed the stress test.

@dsmiley

Copy link
Copy Markdown
Contributor

Yesterday I committed changes to use forEachLoadedCore much more. I anticipate merging today.

@dsmileydsmiley added this to the 10.x milestone Aug 24, 2026
dsmileyand others added 2 commits August 24, 2026 22:57
Left dangling by an earlier commit on this branch that replaced its one use
site; failed ecjLint + spotlessJavaCheck in CI.
@dsmiley
dsmiley merged commit 1400dbc into apache:mainAug 25, 2026
6 checks passed
dsmiley added a commit that referenced this pull request Aug 29, 2026
…4764)
semi-replaced with forEachLoadedCore(lambda)
Co-authored-by: David Smiley <dsmiley@apache.org>
(cherry picked from commit 1400dbc)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@serhiy-bzhezytskyy@epugh@dsmiley
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

SOLR-18378: remove CoreContainer.getCores() and SolrCores.getCores(), 26 sites - #4764

Merged
dsmiley merged 9 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18378-remove-corecontainer-getcores
Aug 25, 2026
Merged

SOLR-18378: remove CoreContainer.getCores() and SolrCores.getCores(), 26 sites#4764
dsmiley merged 9 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18378-remove-corecontainer-getcores

Conversation

@serhiy-bzhezytskyy

@serhiy-bzhezytskyyserhiy-bzhezytskyy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/SOLR-18378

Removes CoreContainer.getCores()/SolrCores.getCores() — 26 sites. Not a rename: the removed methods handed out unreserved cores; the replacement (getLoadedCoreNames() + getCore(String)) reserves, so every site now acquires and releases.

Where to look: two shutdown paths (cancelCoreRecoveries, pauseUpdatesAndAwaitInflightRequests) can't use getCore — it can load a core, wrong during shutdown — so they use getCoreFromAnyList(name, true) instead, which never loads. TestTlogReplica's two helpers deliberately return already-released cores, matching the old contract, to avoid an 11-site refactor out of scope here.

Also fixed a dangling {@link #getCores()} — only ecjLintMain catches that, not the compiler or renderJavadoc.

93 tests / 15 classes / 0 failures. Simulated merge order against SOLR-18382 (#4760) and SOLR-18380 (#4762), which also touch shared files — applies cleanly either way.

AI-assisted (Claude Sonnet 5)

… 26 sites
Not a rename. The removed methods handed out cores that were NOT
reference counted - their own javadoc said both "rather dangerous API
because each core is not reserved" and "Don't need to close them". The
replacement, getLoadedCoreNames() plus getCore(String), hands out a
reserved core that the caller must close. So every migrated site now
acquires a refcount and every site has to release it, and a mechanical
replace here would leak one. A leaked refcount does not fail a test;
it hangs a shutdown.
The documented replacement is also subtly wrong in two places, and
reading it was the only way to find out. getCore(name) is not a pure
lookup - its own comment says "Do this in two phases since we don't
want to lock access to the cores over a load", and in standalone mode
it can create a core from its descriptor. The removed method returned
only already-loaded cores. Both CoreContainer.cancelCoreRecoveries and
pauseUpdatesAndAwaitInflightRequests are shutdown paths, where loading
a core would be actively wrong, so those two use
solrCores.getCoreFromAnyList(name, true), which reserves an
already-loaded core and never loads.
Three treatments, chosen per site rather than applied uniformly:
- pure count (3 sites): getLoadedCoreNames().size() - reserves nothing at all
- iterate all cores (5 sites): for (name : getLoadedCoreNames()) try (core = getCore(name)),
with a null guard because the core may be unloaded between the two calls
- the reference escapes the expression (8 sites): restructured so the use sits inside the
try. Where a site only needed the core's name, it now takes the name and opens no core.
getLoadedCoreNames() is unsorted exactly as getCores() was, so sites
picking .get(0) or iterator().next() keep the same arbitrary choice.
Nothing was sorted.
One deliberate exception, stated in the code. TestTlogReplica's two
private helpers return SolrCore references consumed at 11 call sites
combined, one of which uses the cores to stop the jettys hosting them;
propagating reservation there is exactly the hang hazard above. Those
helpers reserve-then-release inside their own try, returning already-
closed instances - the removed API's own contract - with a comment
saying so. The alternative is an 11-site refactor of a test, which is
not this ticket.
The census took four compile rounds - 5 production sites, then 3 in
test-framework/src/java which ships, then 16 in tests, then 2 more in
modules/ltr - because each round's failures stopped the next module
from compiling at all. 26 total.
Also fixed: a dangling {@link #getCores()} on
SolrCores.getNumLoadedPermanentCores, and a commented-out sketch in
TimeAllowedTest that named the removed method. Measured which gate
catches the dangling link, because it is not the obvious one:
compileJava passes, renderJavadoc passes, and ecjLintMain is what
fails.
Verified: compileJava and compileTestJava for the whole build with
zero errors, spotlessCheck, ecjLintMain and ecjLintTest on core,
ecjLintMain on test-framework and ltr, renderJavadoc, and 15 changed
test classes (counting TestTlogReplica's helper subclasses via their
own suites) - 93 tests, 0 failures.
Simulated merge order against SOLR-18382 (open PR, shares
test-framework cloud base classes and TestTlogReplica) and SOLR-18380
(open PR, shares the same test-framework files) - applying this diff
on top of either merged first, and both merged together, succeeds
cleanly.
AI-assisted (Claude Sonnet 5)
@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

@dsmiley you deprecated getCores() on both CoreContainer and SolrCores back in 2023. Worth flagging directly: the javadoc's own suggested replacement (getCore(name)) turns out to load a core if it isn't already loaded -- wrong on 2 shutdown paths, where that's exactly what must not happen. Used getCoreFromAnyList(name, true) instead. Given it's your deprecation and the fix touches a subtlety in the reference-counting contract, a review here would carry real weight.

AI-assisted (Claude Sonnet 5)

@dsmiley
dsmiley self-requested a review August 20, 2026 00:26

@dsmileydsmiley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I wonder if we should have a CoreContainer.forEachCore(Consumer<SolrCore>))

Comment on lines +1388 to +1389
// getCoreFromAnyList, not getCore: never loads, safe during shutdown
try (SolrCore core = solrCores.getCoreFromAnyList(coreName, true)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how could this be correct, using try-with-resources on a getCore that has not been inc-ref'ed, and thus we shouldn't close it.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getCoreFromAnyList(name, true) does inc-ref -- the second param is literally named incRefCount, and its body calls core.open(), whose own javadoc says "expert: increments the core reference count". So try-with-resources's close() here releases exactly that reference, not an un-reserved one. Same pattern is already used elsewhere in this file (SolrCore.java:3447/3482).

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went further and wrote a concurrency stress test for this exact concern (production, not just the single-threaded mechanism) -- 3 threads acquiring/releasing via getCoreFromAnyList(name, true) while a 4th concurrently unloads/reloads the same core. 0 failures across ~730k acquisitions in 4 runs. Added as TestCoreContainer.testGetCoreFromAnyListSafeUnderConcurrentUnload, pushed.

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I misunderstood your other PR comments where I thought you/AI communicated solrCores.getCoreFromAnyList doesn't incref which is why you were using it. Now I actually looked at our code to confirm.


public void cancelCoreRecoveries() {

List<SolrCore> cores = solrCores.getCores();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguably, SolrCores.getCores() shouldn't be deprecated because it's hidden one layer deep on a class only used by CoreContainer.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair -- checked, and SolrCores is package-private and only ever referenced from CoreContainer, so the deprecation was arguably unnecessary ceremony from the start. Not making a case for the original deprecation though -- the ticket scope was to remove both getCores() methods together, so that's what this PR does.

AI-assisted (Claude Sonnet 5)

* _and_ not yet loaded it will _not_ be returned by this call.
* <p>This list is a new copy, it can be modified by the caller (e.g. it can be sorted).
*/
@Deprecated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe shouldn't be deprecated after all (as I say above

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the CoreContainer.java:1386 thread -- fair point, no argument.

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either we bring this back, or we add the method I suggested to CoreContainer forEachLoadedCore(Consumer<SolrCore>))

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added CoreContainer.forEachLoadedCore(Consumer<SolrCore>), migrated cancelCoreRecoveries() to it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice; this is safer as we avoid race condition on a core closing with inspecting its health

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again; added safety :-)

David Smiley's review claimed the core in cancelCoreRecoveries() has
"not been inc-ref'ed", so try-with-resources closing it would be wrong.
Verified false: incRefCount=true calls core.open(), same pattern already
used at SolrCore.java:3447/3482. Proven live on a single-threaded probe
(openCount 1 -> 2 -> 1 across acquire/close).
This test extends that to the actual concern raised -- production
concurrency: 3 threads repeatedly acquire/use/release via
getCoreFromAnyList(name, true) while a 4th thread concurrently
unloads/reloads the same core name. No existing test exercises this;
the one similar-looking loop (SimpleCollectionCreateDeleteTest.java, also
touched by this PR) uses the unrelated CoreContainer.getCore(name),
not SolrCores.getCoreFromAnyList.
Ran 4x before formatting, ~730k total acquisitions, 0 failures.
AI-assisted (Claude Sonnet 5)
@epugh

Copy link
Copy Markdown
Contributor

Just retriggered the CI processes. There is enough change here with ramifications, I'll wait for approved review from someone like @dsmiley before merging.

Comment on lines +1388 to +1389
// getCoreFromAnyList, not getCore: never loads, safe during shutdown
try (SolrCore core = solrCores.getCoreFromAnyList(coreName, true)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I misunderstood your other PR comments where I thought you/AI communicated solrCores.getCoreFromAnyList doesn't incref which is why you were using it. Now I actually looked at our code to confirm.

* _and_ not yet loaded it will _not_ be returned by this call.
* <p>This list is a new copy, it can be modified by the caller (e.g. it can be sorted).
*/
@Deprecated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either we bring this back, or we add the method I suggested to CoreContainer forEachLoadedCore(Consumer<SolrCore>))

@dsmiley

Copy link
Copy Markdown
Contributor

The changelog is debatable but I think the more important (value producing aspect) here is that this reduced risks of SolrCore closing while being used. The methods themselves are internal stuff a reader shouldn't care about.

David's suggestion on SolrCores.java:152 -- migrates
cancelCoreRecoveries() to it, the only site requested in review.
NodeHealth/SolrPackageLoader keep their current getCore()-based loops
unchanged (different semantics: getCore() can lazy-reload on a race,
forEachLoadedCore deliberately never does).

@dsmileydsmiley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the new method but why aren't you calling it nearly everywhere possible?

Comment threadsolr/core/src/java/org/apache/solr/pkg/SolrPackageLoader.java Outdated
Comment threadsolr/core/src/java/org/apache/solr/handler/admin/api/NodeHealth.java Outdated
…EachLoadedCore
dsmiley asked on the PR why these two call sites, which iterate
getLoadedCoreNames() and reopen each via getCore(coreName), weren't
migrated when forEachLoadedCore was added. Turns out they carry the
same bug as the one this ticket already fixed elsewhere:
getCore(name) LOADS the core if it was unloaded between the
getLoadedCoreNames() snapshot and the getCore() call, whereas
forEachLoadedCore's getCoreFromAnyList(name, true) never does.
SolrPackageLoader: a package-update notification or a health check
should never have the side effect of force-loading an unloaded core.
NodeHealth's healthCheckStandaloneMode needed a mutable
AtomicBoolean instead of a plain local, since it's now captured by
the forEachLoadedCore lambda.
Verified: NodeHealthTest, NodeHealthStandaloneTest, TestPackages
green; full compileJava clean.
@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

Fair — pushed a follow-up commit migrating SolrPackageLoader and NodeHealth too. Both had the same footgun this ticket already fixed elsewhere: getCore(coreName) after a getLoadedCoreNames() snapshot reloads the core if it was unloaded in between, which is wrong for a package-update notification or a health check. forEachLoadedCore's getCoreFromAnyList(name, true) never does that. Agree the new method should be the default everywhere this pattern shows up -- these were the only other two call sites left in this PR's diff.

AI-assisted (Claude Sonnet 5)

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

To be fair, the reason cancelCoreRecoveries() was the only one migrated originally is that I was trying to keep the change as small as possible -- didn't weigh that against the correctness gap it left open in the other two spots.

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

Removed the stress test.

@dsmiley

Copy link
Copy Markdown
Contributor

Yesterday I committed changes to use forEachLoadedCore much more. I anticipate merging today.

@dsmileydsmiley added this to the 10.x milestone Aug 24, 2026
dsmileyand others added 2 commits August 24, 2026 22:57
Left dangling by an earlier commit on this branch that replaced its one use
site; failed ecjLint + spotlessJavaCheck in CI.
@dsmiley
dsmiley merged commit 1400dbc into apache:mainAug 25, 2026
6 checks passed
dsmiley added a commit that referenced this pull request Aug 29, 2026
…4764)
semi-replaced with forEachLoadedCore(lambda)
Co-authored-by: David Smiley <dsmiley@apache.org>
(cherry picked from commit 1400dbc)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@serhiy-bzhezytskyy@epugh@dsmiley
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

SOLR-18378: remove CoreContainer.getCores() and SolrCores.getCores(), 26 sites - #4764

Merged
dsmiley merged 9 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18378-remove-corecontainer-getcores
Aug 25, 2026
Merged

SOLR-18378: remove CoreContainer.getCores() and SolrCores.getCores(), 26 sites#4764
dsmiley merged 9 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18378-remove-corecontainer-getcores

Conversation

@serhiy-bzhezytskyy

@serhiy-bzhezytskyyserhiy-bzhezytskyy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/SOLR-18378

Removes CoreContainer.getCores()/SolrCores.getCores() — 26 sites. Not a rename: the removed methods handed out unreserved cores; the replacement (getLoadedCoreNames() + getCore(String)) reserves, so every site now acquires and releases.

Where to look: two shutdown paths (cancelCoreRecoveries, pauseUpdatesAndAwaitInflightRequests) can't use getCore — it can load a core, wrong during shutdown — so they use getCoreFromAnyList(name, true) instead, which never loads. TestTlogReplica's two helpers deliberately return already-released cores, matching the old contract, to avoid an 11-site refactor out of scope here.

Also fixed a dangling {@link #getCores()} — only ecjLintMain catches that, not the compiler or renderJavadoc.

93 tests / 15 classes / 0 failures. Simulated merge order against SOLR-18382 (#4760) and SOLR-18380 (#4762), which also touch shared files — applies cleanly either way.

AI-assisted (Claude Sonnet 5)

… 26 sites
Not a rename. The removed methods handed out cores that were NOT
reference counted - their own javadoc said both "rather dangerous API
because each core is not reserved" and "Don't need to close them". The
replacement, getLoadedCoreNames() plus getCore(String), hands out a
reserved core that the caller must close. So every migrated site now
acquires a refcount and every site has to release it, and a mechanical
replace here would leak one. A leaked refcount does not fail a test;
it hangs a shutdown.
The documented replacement is also subtly wrong in two places, and
reading it was the only way to find out. getCore(name) is not a pure
lookup - its own comment says "Do this in two phases since we don't
want to lock access to the cores over a load", and in standalone mode
it can create a core from its descriptor. The removed method returned
only already-loaded cores. Both CoreContainer.cancelCoreRecoveries and
pauseUpdatesAndAwaitInflightRequests are shutdown paths, where loading
a core would be actively wrong, so those two use
solrCores.getCoreFromAnyList(name, true), which reserves an
already-loaded core and never loads.
Three treatments, chosen per site rather than applied uniformly:
- pure count (3 sites): getLoadedCoreNames().size() - reserves nothing at all
- iterate all cores (5 sites): for (name : getLoadedCoreNames()) try (core = getCore(name)),
with a null guard because the core may be unloaded between the two calls
- the reference escapes the expression (8 sites): restructured so the use sits inside the
try. Where a site only needed the core's name, it now takes the name and opens no core.
getLoadedCoreNames() is unsorted exactly as getCores() was, so sites
picking .get(0) or iterator().next() keep the same arbitrary choice.
Nothing was sorted.
One deliberate exception, stated in the code. TestTlogReplica's two
private helpers return SolrCore references consumed at 11 call sites
combined, one of which uses the cores to stop the jettys hosting them;
propagating reservation there is exactly the hang hazard above. Those
helpers reserve-then-release inside their own try, returning already-
closed instances - the removed API's own contract - with a comment
saying so. The alternative is an 11-site refactor of a test, which is
not this ticket.
The census took four compile rounds - 5 production sites, then 3 in
test-framework/src/java which ships, then 16 in tests, then 2 more in
modules/ltr - because each round's failures stopped the next module
from compiling at all. 26 total.
Also fixed: a dangling {@link #getCores()} on
SolrCores.getNumLoadedPermanentCores, and a commented-out sketch in
TimeAllowedTest that named the removed method. Measured which gate
catches the dangling link, because it is not the obvious one:
compileJava passes, renderJavadoc passes, and ecjLintMain is what
fails.
Verified: compileJava and compileTestJava for the whole build with
zero errors, spotlessCheck, ecjLintMain and ecjLintTest on core,
ecjLintMain on test-framework and ltr, renderJavadoc, and 15 changed
test classes (counting TestTlogReplica's helper subclasses via their
own suites) - 93 tests, 0 failures.
Simulated merge order against SOLR-18382 (open PR, shares
test-framework cloud base classes and TestTlogReplica) and SOLR-18380
(open PR, shares the same test-framework files) - applying this diff
on top of either merged first, and both merged together, succeeds
cleanly.
AI-assisted (Claude Sonnet 5)
@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

@dsmiley you deprecated getCores() on both CoreContainer and SolrCores back in 2023. Worth flagging directly: the javadoc's own suggested replacement (getCore(name)) turns out to load a core if it isn't already loaded -- wrong on 2 shutdown paths, where that's exactly what must not happen. Used getCoreFromAnyList(name, true) instead. Given it's your deprecation and the fix touches a subtlety in the reference-counting contract, a review here would carry real weight.

AI-assisted (Claude Sonnet 5)

@dsmiley
dsmiley self-requested a review August 20, 2026 00:26

@dsmileydsmiley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I wonder if we should have a CoreContainer.forEachCore(Consumer<SolrCore>))

Comment on lines +1388 to +1389
// getCoreFromAnyList, not getCore: never loads, safe during shutdown
try (SolrCore core = solrCores.getCoreFromAnyList(coreName, true)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how could this be correct, using try-with-resources on a getCore that has not been inc-ref'ed, and thus we shouldn't close it.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getCoreFromAnyList(name, true) does inc-ref -- the second param is literally named incRefCount, and its body calls core.open(), whose own javadoc says "expert: increments the core reference count". So try-with-resources's close() here releases exactly that reference, not an un-reserved one. Same pattern is already used elsewhere in this file (SolrCore.java:3447/3482).

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went further and wrote a concurrency stress test for this exact concern (production, not just the single-threaded mechanism) -- 3 threads acquiring/releasing via getCoreFromAnyList(name, true) while a 4th concurrently unloads/reloads the same core. 0 failures across ~730k acquisitions in 4 runs. Added as TestCoreContainer.testGetCoreFromAnyListSafeUnderConcurrentUnload, pushed.

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I misunderstood your other PR comments where I thought you/AI communicated solrCores.getCoreFromAnyList doesn't incref which is why you were using it. Now I actually looked at our code to confirm.


public void cancelCoreRecoveries() {

List<SolrCore> cores = solrCores.getCores();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguably, SolrCores.getCores() shouldn't be deprecated because it's hidden one layer deep on a class only used by CoreContainer.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair -- checked, and SolrCores is package-private and only ever referenced from CoreContainer, so the deprecation was arguably unnecessary ceremony from the start. Not making a case for the original deprecation though -- the ticket scope was to remove both getCores() methods together, so that's what this PR does.

AI-assisted (Claude Sonnet 5)

* _and_ not yet loaded it will _not_ be returned by this call.
* <p>This list is a new copy, it can be modified by the caller (e.g. it can be sorted).
*/
@Deprecated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe shouldn't be deprecated after all (as I say above

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the CoreContainer.java:1386 thread -- fair point, no argument.

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either we bring this back, or we add the method I suggested to CoreContainer forEachLoadedCore(Consumer<SolrCore>))

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added CoreContainer.forEachLoadedCore(Consumer<SolrCore>), migrated cancelCoreRecoveries() to it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice; this is safer as we avoid race condition on a core closing with inspecting its health

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again; added safety :-)

David Smiley's review claimed the core in cancelCoreRecoveries() has
"not been inc-ref'ed", so try-with-resources closing it would be wrong.
Verified false: incRefCount=true calls core.open(), same pattern already
used at SolrCore.java:3447/3482. Proven live on a single-threaded probe
(openCount 1 -> 2 -> 1 across acquire/close).
This test extends that to the actual concern raised -- production
concurrency: 3 threads repeatedly acquire/use/release via
getCoreFromAnyList(name, true) while a 4th thread concurrently
unloads/reloads the same core name. No existing test exercises this;
the one similar-looking loop (SimpleCollectionCreateDeleteTest.java, also
touched by this PR) uses the unrelated CoreContainer.getCore(name),
not SolrCores.getCoreFromAnyList.
Ran 4x before formatting, ~730k total acquisitions, 0 failures.
AI-assisted (Claude Sonnet 5)
@epugh

Copy link
Copy Markdown
Contributor

Just retriggered the CI processes. There is enough change here with ramifications, I'll wait for approved review from someone like @dsmiley before merging.

Comment on lines +1388 to +1389
// getCoreFromAnyList, not getCore: never loads, safe during shutdown
try (SolrCore core = solrCores.getCoreFromAnyList(coreName, true)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I misunderstood your other PR comments where I thought you/AI communicated solrCores.getCoreFromAnyList doesn't incref which is why you were using it. Now I actually looked at our code to confirm.

* _and_ not yet loaded it will _not_ be returned by this call.
* <p>This list is a new copy, it can be modified by the caller (e.g. it can be sorted).
*/
@Deprecated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either we bring this back, or we add the method I suggested to CoreContainer forEachLoadedCore(Consumer<SolrCore>))

@dsmiley

Copy link
Copy Markdown
Contributor

The changelog is debatable but I think the more important (value producing aspect) here is that this reduced risks of SolrCore closing while being used. The methods themselves are internal stuff a reader shouldn't care about.

David's suggestion on SolrCores.java:152 -- migrates
cancelCoreRecoveries() to it, the only site requested in review.
NodeHealth/SolrPackageLoader keep their current getCore()-based loops
unchanged (different semantics: getCore() can lazy-reload on a race,
forEachLoadedCore deliberately never does).

@dsmileydsmiley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the new method but why aren't you calling it nearly everywhere possible?

Comment threadsolr/core/src/java/org/apache/solr/pkg/SolrPackageLoader.java Outdated
Comment threadsolr/core/src/java/org/apache/solr/handler/admin/api/NodeHealth.java Outdated
…EachLoadedCore
dsmiley asked on the PR why these two call sites, which iterate
getLoadedCoreNames() and reopen each via getCore(coreName), weren't
migrated when forEachLoadedCore was added. Turns out they carry the
same bug as the one this ticket already fixed elsewhere:
getCore(name) LOADS the core if it was unloaded between the
getLoadedCoreNames() snapshot and the getCore() call, whereas
forEachLoadedCore's getCoreFromAnyList(name, true) never does.
SolrPackageLoader: a package-update notification or a health check
should never have the side effect of force-loading an unloaded core.
NodeHealth's healthCheckStandaloneMode needed a mutable
AtomicBoolean instead of a plain local, since it's now captured by
the forEachLoadedCore lambda.
Verified: NodeHealthTest, NodeHealthStandaloneTest, TestPackages
green; full compileJava clean.
@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

Fair — pushed a follow-up commit migrating SolrPackageLoader and NodeHealth too. Both had the same footgun this ticket already fixed elsewhere: getCore(coreName) after a getLoadedCoreNames() snapshot reloads the core if it was unloaded in between, which is wrong for a package-update notification or a health check. forEachLoadedCore's getCoreFromAnyList(name, true) never does that. Agree the new method should be the default everywhere this pattern shows up -- these were the only other two call sites left in this PR's diff.

AI-assisted (Claude Sonnet 5)

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

To be fair, the reason cancelCoreRecoveries() was the only one migrated originally is that I was trying to keep the change as small as possible -- didn't weigh that against the correctness gap it left open in the other two spots.

@serhiy-bzhezytskyy

Copy link
Copy Markdown
ContributorAuthor

Removed the stress test.

@dsmiley

Copy link
Copy Markdown
Contributor

Yesterday I committed changes to use forEachLoadedCore much more. I anticipate merging today.

@dsmileydsmiley added this to the 10.x milestone Aug 24, 2026
dsmileyand others added 2 commits August 24, 2026 22:57
Left dangling by an earlier commit on this branch that replaced its one use
site; failed ecjLint + spotlessJavaCheck in CI.
@dsmiley
dsmiley merged commit 1400dbc into apache:mainAug 25, 2026
6 checks passed
dsmiley added a commit that referenced this pull request Aug 29, 2026
…4764)
semi-replaced with forEachLoadedCore(lambda)
Co-authored-by: David Smiley <dsmiley@apache.org>
(cherry picked from commit 1400dbc)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@serhiy-bzhezytskyy@epugh@dsmiley