Repository navigation
Conversation
rzo1
left a comment
There was a problem hiding this comment.
Thanks, looks good to me.
A few small things:
- In
StatusUpdaterBolt,waitAck.addTuple()runs beforeconnection.addToProcessor(op). Now thatadd()can throw on a serialization error, the tuple gets failed but its entry stays in the cache. A later bulk for the same URL would then ack a tuple that's already been failed, or the cache expiry would fail it a second time.IndexerBoltalready callswaitAck.invalidate(docID)in its catch block. Could you do the same here? - The README says items rejected with 429 are resent. If OpenSearch rejects the whole request with a 429, it isn't retried, same as in the legacy module. Maybe say "items rejected with 429 in the bulk response", so nobody expects more than that.
- If a retry hits a connection error, items that already got a 200 are failed as well. The legacy module does the same, and your test covers it, but one line in the README would help.
- In
duplicateIdIsAckedWhenOneWriteSucceeds, both tuples get acked even though the second write never went through. That's how it already worked since #832, so it's fine, but a short comment in the test saying so would save the next reader some head scratching.
|
@rzo1 I was going to ping you after the weekend tests, but you beat me to it :) pushed the fixes, thanks! StatusUpdaterBolt now drops the cache entry when add() throws, like IndexerBolt does, plus a test for it. README now also covers whole-request rejections and retries that fail as a whole, and the duplicate test has its comment. |
|
thanks @dpol1, 2 observations: Every operation is now serialized twice. The second time happens on the bolt’s thread, for size counting. That’s cheap for status and delete operations, but IndexerBolt sends full page text, so it may cost noticeable CPU. The legacy module estimated the size instead of serializing. DeletionBolt doesn’t handle the new exception from add(). At DeletionBolt.java:112-114 it calls waitAck.addTuple and then addToProcessor with no try/catch. A serialization error now escapes execute(), which in Storm kills the worker, and leaves a stale entry in WaitAckCache. Serializing a delete operation is unlikely to fail, but for consistency it should do what IndexerBolt and now StatusUpdaterBolt do: invalidate the entry, then fail the tuple. |
|
@jnioche thanks, DeletionBolt now invalidates the entry and fails the tuple, with a test. On the double serialization: in a microbenchmark on my laptop the size count costs about 1.5 µs per KB of JSON, so 16 µs for a page with 10 KB of text and 160 µs for 100 KB. That's roughly 70% of what the transport spends serializing the same operation. I'd keep the exact count so the 5 MiB limit matches the bulk body. If you prefer an estimate, as the legacy module did, I can do it in a follow-up. |
AsyncBulkProcessor passed the items that OpenSearch rejected with 429 straight to the listener, so the bolts failed their tuples at once. The legacy BulkProcessor resends them with its default exponential backoff when every failed item of the response has status 429. AsyncBulkProcessor now resends the 429 items after the same delays (50, 60, 80, 150, 280, 580, 1250 and 2740 ms), also when other items failed with another status, for example a 409 from the create of a DISCOVERED URL that is already known. Each retry carries only the items still rejected, and the bulk keeps its concurrency permit until the listener has run. afterBulk is called once per bulk, with the items of all attempts in request order. If a retry request fails as a whole, the whole bulk fails, as on the first attempt. Retries run on their own thread, because the flush thread can be blocked waiting for the permit that a retrying bulk holds. awaitClose() waits for pending retries within its timeout and then drops the ones still waiting, as the legacy BulkProcessor does. Signed-off-by: Davide Polato <dpol1@apache.org>
The legacy BulkProcessor sends a bulk once its estimated size reaches 5 MiB. AsyncBulkProcessor flushed by count and time only, so large documents could produce very large requests. The README claimed size-based flushing. When an operation is added, AsyncBulkProcessor now computes the bytes that the transport writes for it: each NDJSON line serialized with the transport's mapper, before compression, plus a newline. This serializes each operation one more time, on the caller's thread. The bulk is sent once the buffered operations reach 5 MiB, including the operation that crosses the threshold. The README now lists what AsyncBulkProcessor does. Signed-off-by: Davide Polato <dpol1@apache.org>
AsyncBulkProcessor.add() serializes the operation and can throw. StatusUpdaterBolt put the tuple in WaitAckCache before calling it, so when add() threw, AbstractStatusUpdaterBolt failed the tuple while its entry stayed in the cache. A later bulk for the same document ID then acked the failed tuple, or the cache expiry failed it a second time. StatusUpdaterBolt now invalidates the entry when add() throws, as IndexerBolt already does. Signed-off-by: Davide Polato <dpol1@apache.org>
The README now says that only the items rejected with 429 in the bulk response are resent, and that a retry that fails as a whole, for example on a connection error, makes IndexerBolt, DeletionBolt and StatusUpdaterBolt fail the tuples of the whole bulk. The duplicate ID test says why both tuples are acked. Signed-off-by: Davide Polato <dpol1@apache.org>
AsyncBulkProcessor.add() serializes the operation and can throw. DeletionBolt called it without a try/catch, so the exception escaped execute(), which kills the Storm worker, and the tuple's entry stayed in WaitAckCache. DeletionBolt now logs the error, invalidates the entry and fails the tuple, as IndexerBolt and StatusUpdaterBolt do. Signed-off-by: Davide Polato <dpol1@apache.org>
2ed5475 to
026ae44
Compare
@rzo1 asked for 429 retries and flushing by size before we move
opensearch-javaintoexternal/opensearch(#2212). This PR adds both toAsyncBulkProcessor. The move will follow in a second PR on top of this one.Today an item rejected with 429 goes straight to the listener, so
StatusUpdaterBolt,IndexerBoltandDeletionBoltfail its tuple, and bulks are flushed by count or time only. With this change:BulkProcessordefault backoff, 8 retries from 50 ms up to 2.7 s. Each retry carries only those items.afterBulkis called once, with the items of all attempts in request order.There is one difference with the legacy module. It retries only when every failed item of the response is a 429, so a single 409, e.g. a
createfor a DISCOVERED URL that already exists, stops the retry of the whole bulk. Here the 429 items are resent whatever happened to the others.I didn't use
BulkIngester. It releases its in-flight slot before the listener has run and holds none while items wait for a retry, the opposite of what #2225 did forAsyncBulkProcessor. Its listener is also called once per attempt. In a local test with two writes for the same URL in one bulk (#832), a 200 then a 429,WaitAckCacheacked the tuples before the second write was resent.A few things worth a look:
add(), where on main it failed the whole bulk.awaitClose()takes a permit with no timeout, as in the legacy module, so while bulks are retrying it can wait about 5 s of delays plus the round trips. Retries still pending whenawaitClose()times out are dropped, also as in the legacy module. The bolts that track acks have already failed those tuples incleanup().The new tests don't need Docker and take about 5 s, and the module suite passes locally with Docker. I haven't yet run a crawl against an OpenSearch loaded enough to return 429s, nor
StatusUpdaterBenchmarkagainst main.