SOLR-3284: let ConcurrentUpdateSolrClient report which docs failed to send#4632
SOLR-3284: let ConcurrentUpdateSolrClient report which docs failed to send#4632serhiy-bzhezytskyy wants to merge 1 commit into
Conversation
| ConcurrentUpdateSolrClient can now report which documents failed to reach the server. | ||
| Register a handler via the Builder's withErrorHandler(...) to recover the documents of a | ||
| failed batch, for example to route them to a retry queue or dead-letter topic. | ||
| type: added |
There was a problem hiding this comment.
That's the best changelog entry I've seen in a long time; thank you!
I think our project's embrace of the 3rd party "changelog" with it's choice of wording of "title" has led to a deterioration of changelog informative quality compared to before. You are bucking that trend. Thanks again. Our project seriously needs to find a way to use a different label here like "summary". CC @janhoy
There was a problem hiding this comment.
Thanks — that means a lot. Agree the "title" label nudges people toward terse one-liners; "summary" would invite more.
| this.basePath = builder.baseSolrUrl; | ||
| this.defaultCollection = builder.defaultCollection; | ||
| this.pollQueueTimeMillis = builder.pollQueueTimeMillis; | ||
| this.errorHandler = builder.errorHandler; |
There was a problem hiding this comment.
IMO we probably want to make this new errorHandler the preferred mechanism... maybe even eventually mandatory in 11.
If we agree on this direction for 11 (I'm +1), then here we can use DeprecationLog to warn that a user forgot to pass an erroHandler, that it'll eventually be mandatory.
There was a problem hiding this comment.
+1 on making the handler the preferred mechanism, and mandatory in 11. One wrinkle: DeprecationLog lives in solr-core (org.apache.solr.logging), so solrj can't reach it — wrong direction in the module graph. I can do the equivalent with a log.warn here when a client is built without a handler (and hasn't overridden handleError), worded as "will be required in a future release". Want me to go that way, or is there a solrj-side deprecation helper you'd prefer?
There was a problem hiding this comment.
Confirmed there's no solrj-side deprecation helper — solrj only has @deprecated + ad-hoc log.warn, and DeprecationLog is core-only as noted. The one thing DeprecationLog adds over a bare log.warn is log-once dedup + the org.apache.solr.DEPRECATED.* logger prefix. To avoid warning on every client construction I'd add a small log-once guard here in the client. If the project later wants solrj to share core's deprecation convention, a tiny solrj-side helper would be a clean follow-up — but I'd keep this PR scoped to the guard. Sound good?
|
Documented the handler's threading contract, with a test pinning it: the handler is invoked on runner threads, possibly concurrently — implementations must be thread-safe and should return quickly (a slow handler slows update throughput). An exception thrown by the handler is logged and does not stop the client — the runner's existing safety net already contains it; the test locks that promise in. No ordering guarantees across failures. No behavior change in this commit. |
5d9bf45 to
1f652f6
Compare
|
Moving the tests into The fix: |
| // the updates coalesced into this stream, so a failure can report every affected one | ||
| private final List<ConcurrentUpdateBaseSolrClient.Update> updates = new ArrayList<>(); |
There was a problem hiding this comment.
hmm; I'm concerned that this error tracking feature is going to hurt a benefit of CUSC -- memory efficiency (streaming a huge volume of updates).
What if we only track a designated ID field? The error handler could have a customizable overrideable method, defaulting to return the "id" field.
…ed to send Add a Builder-configurable error handler so callers can recover the documents of an update batch that failed to reach the server. handleError(Throwable) alone only gave the exception, not which documents were lost, so callers could not route the failed docs to a retry queue or dead-letter topic. - New UpdateErrorHandler: onError(Throwable, List<String> failedIds, String collection) reports the ids of the failed documents. Ids are obtained via an overridable extractId(SolrInputDocument) that defaults to the "id" field, so callers whose uniqueKey differs can override it. Registered via Builder.withErrorHandler(...). With no handler the client keeps its existing log-only behavior and never reads the documents, so this is backward compatible and preserves the client's streaming memory efficiency (only ids are retained, not the documents). - doSendUpdateStream returns SentStream(response, ids, collection). The Jetty client coalesces several queued updates into one stream, so it collects the ids of all of them -- the handler receives every affected id on failure, not just the first. The JDK client sends one request per update. - An exception thrown by the handler is logged and does not stop the runner. Tests live in ConcurrentUpdateSolrClientTestBase so they run for both the JDK and Jetty clients.
1f652f6 to
7392d54
Compare
|
Switched it to report the failed document ids instead of the requests — The id comes from an overridable On the Jetty side the ids are collected as each doc is streamed, so a coalesced batch still reports every affected id on failure. |
https://issues.apache.org/jira/browse/SOLR-3284
Description
ConcurrentUpdateSolrClientsends updates asynchronously on background threads. When a batch fails, the only signal a caller gets ishandleError(Throwable)— the exception, but not which documents didn't make it to the server. So a caller can't route the failed documents anywhere (retry queue, dead-letter topic); it only knows that something in the batch failed.This is the pain SOLR-3284 has tracked since 2012, and it's still present on
main(ConcurrentUpdateBaseSolrClient). We hit it in production indexing into Solr via the Builder, which is what prompted this.The 2016 discussion on the JIRA (David Smiley, Mark Miller) converged on a Builder-configurable error handler, ideally a lambda. This implements that.
Changes
UpdateErrorHandlerfunctional interface:onError(Throwable ex, UpdateRequest request, String collection). It exposes the publicUpdateRequest(and thus its documents), not theprotected Updaterecord, so it's usable by external callers. The caller reads whatever field is theiruniqueKey— the client doesn't assume one (no hardcodedid).Builder.withErrorHandler(...). The defaulthandleError(Throwable, Update)invokes the handler if one is registered, otherwise falls back tohandleError(Throwable). With no handler the behavior is unchanged (logs), so this is backward compatible; existinghandleError(Throwable)overrides keep working.Updateis in scope and every failure path — HTTP error status and a thrown exception — reports it to the handler.Usage:
Two decisions I'd flag for review
Default behavior. I kept it optional + log-by-default for backward compatibility. The 2016 discussion leaned toward surfacing errors by default (throw when no handler is set, like
SafeConcurrentUpdateSolrClient). I went with compatibility, but I'm happy to switch to throw-by-default if that's preferred — it's a small change.Runner-loop behavior. To pass the failing request to the handler I catch failures per-update inside the runner loop. A side effect is that the runner now continues to the next batch instead of exiting the loop on error. This seems better (one bad batch no longer stalls the runner), but it is a behavior change and I want it visible rather than buried.
Side finding (not fixed here)
While testing I hit a pre-existing NPE:
RemoteSolrException(host, code, remoteError)throws whenremoteErrorisnull, because the constructor doesMap.of("remoteError", remoteError)andMap.ofrejects null values. It's triggered when the server returns an error body that doesn't parse. Unrelated to this change and probably deserves its own issue — flagging it rather than expanding scope here.Testing
ConcurrentUpdateJdkSolrClientTest#testFailedDocsAreRecoverableViaErrorHandler: registers a handler via the Builder, sends 5 docs to a server returning 500, asserts all 5 doc ids are recovered../gradlew :solr:solrj:check :solr:solrj-jetty:checkpass (solrj-jetty shares the base class).