Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog/unreleased/SOLR-18142.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
title: CloudSolrClient- fixed state refresh race; didn't refresh. Regression from 9.10.1/10.0.
type: fixed
authors:
- name: David Smiley
links:
- name: SOLR-18142
url: https://issues.apache.org/jira/browse/SOLR-18142
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public abstract class CloudSolrClient extends SolrClient {
private final boolean directUpdatesToLeadersOnly;
private final RequestReplicaListTransformerGenerator requestRLTGenerator;
private final boolean parallelUpdates;
private ExecutorService threadPool =
private final ExecutorService threadPool =

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

not critical but it's needless that this was non-final.

ExecutorUtil.newMDCAwareCachedThreadPool(
new SolrNamedThreadFactory("CloudSolrClient ThreadPool"));

Expand Down Expand Up @@ -642,9 +642,8 @@ protected boolean wasCommError(Throwable t) {
public void close() {
closed = true;
collectionRefreshes.clear();
if (this.threadPool != null && !ExecutorUtil.isShutdown(this.threadPool)) {
if (!ExecutorUtil.isShutdown(this.threadPool)) {
ExecutorUtil.shutdownAndAwaitTermination(this.threadPool);
this.threadPool = null;
}
}

Expand Down Expand Up @@ -1658,41 +1657,34 @@ protected DocCollection getDocCollection(String collection, Integer expectedVers
}

private CompletableFuture<DocCollection> triggerCollectionRefresh(String collection) {
if (closed) {
ExpiringCachedDocCollection cacheEntry = collectionStateCache.peek(collection);
DocCollection cached = cacheEntry == null ? null : cacheEntry.cached;
return CompletableFuture.completedFuture(cached);
}
Comment on lines -1661 to -1665

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I found it confusing for this method to have two code paths for the closed scenario. So I centralized it to one spot within the map.compute call.

return collectionRefreshes.computeIfAbsent(
return collectionRefreshes.compute(
collection,
key -> {
ExecutorService executor = threadPool;
CompletableFuture<DocCollection> future;
if (executor == null || ExecutorUtil.isShutdown(executor)) {
future = new CompletableFuture<>();
try {
future.complete(loadDocCollection(key));
} catch (Throwable t) {
future.completeExceptionally(t);
}
(key, existingFuture) -> {
// A refresh is still in progress; return it.
if (existingFuture != null && !existingFuture.isDone()) {
return existingFuture;
}
Comment on lines +1662 to +1666

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the essence of the fix. Everything else in the PR is an improvement but non-critical.

// No refresh is in-progress, so trigger it.

if (ExecutorUtil.isShutdown(threadPool)) {
assert closed; // see close() for the sequence
ExpiringCachedDocCollection cacheEntry = collectionStateCache.peek(key);
DocCollection cached = cacheEntry == null ? null : cacheEntry.cached;
return CompletableFuture.completedFuture(cached);
} else {
future =
CompletableFuture.supplyAsync(
() -> {
stateRefreshSemaphore.acquireUninterruptibly();
try {
return loadDocCollection(key);
} finally {
stateRefreshSemaphore.release();
}
},
executor);
return CompletableFuture.supplyAsync(
() -> {
stateRefreshSemaphore.acquireUninterruptibly();
try {
return loadDocCollection(key);
} finally {
stateRefreshSemaphore.release();
// Remove the entry in case of many collections
collectionRefreshes.remove(key);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

it should always remove the same future, by the way.

}
},
threadPool);
}
future.whenCompleteAsync(
(result, error) -> {
collectionRefreshes.remove(key, future);
});
return future;
Comment on lines -1691 to -1695

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's much lighter weight & simpler to read to incorporate this into the single lambda callback to occur after the collection is loaded.

I spent a fair amount of time previously trying to assure myself on the nuances of whenComplete vs whenCompleteAsync, and on returning the result of this future vs not, or having the outer method actually do this. Played with a debugger to inspect threads; putting sleep in places and running tests. It was educational but I concluded we can do something much simpler.

});
}

Expand Down