Repository navigation
perf: serialize write requests outside the write stream lock - #435
Merged
Merged
Conversation
ManagedWriteStream.send() held the per-shard stream lock while onNext() serialized the request in the generated gRPC marshaller. handleResponse() takes the same lock on the event loop, so a response waited behind another batch's serialization. send() now serializes the request with the generated marshaller before taking the lock, and the write stream call sends that stream through a pass-through marshaller. Under the lock, send() only appends the in-flight entry and calls onNext(), which copies the bytes into gRPC's buffer. Allocations don't change: the marshaller still reuses its per-thread spare array. A serialized stream can be read once, so replays serialize again under the lock, as before; the channels don't retry calls. handlesResponseWhileAnotherWriteIsSerialized blocks a put's value in the middle of its serialization and checks that the response to an earlier write still completes. Without this change it times out. Fixes oxia-db#397 Signed-off-by: Matteo Merli <mmerli@apache.org>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #397.
Problem
ManagedWriteStream.send()takes the per-shard stream lock and then callsonNext(), where the generated gRPC marshaller serializes theWriteRequest.handleResponse()takes the same lock for every response, on the gRPC event loop (the channels usedirectExecutor()). So a response can wait behind another batch's serialization, and so can everything else on that event loop. The batcher thread can also park on the lock while the event loop holds it: when a shard's dispatch window is full,DispatchWindow.release()sends the next batch from the event loop.#433 already moved building the request out of the lock:
WriteBatchcallstoProto()beforesend(). Serializing it is what's left.Example
The work done under the lock in
onNext()for a full batch (128 KB by default), from a throwaway JMH benchmark (Apple M1 Max, JDK 26):Change
send()serializes the request before taking the lock, with the generated marshaller, and the write stream call takes the serialized stream through a pass-through marshaller:Under the lock,
send()now only appends the in-flight entry and callsonNext(), which copies the bytes into gRPC's buffer and queues the frame.byte[]instead would add about 140 KB of allocation per 128 KB batch.WriteRequesteach entry keeps, under the lock, as before. gRPC never streams a message twice here, as the channels are built withdisableRetry().send()serializes it before it's in the in-flight queue, and replays only serialize queued requests, under the lock.RpcProvider.writeStream()returns aStreamObserver<InputStream>.Testing
ManagedWriteStreamTest.handlesResponseWhileAnotherWriteIsSerialized: a put whose value blocks in the middle of its serialization is sent from another thread, and the server then answers an earlier write. Before the change, that response doesn't complete the earlier write within 5 s (TimeoutException: the event loop waits on the lock); after it, it does, and the blocked write then reaches the server with its value.RpcProvider.writeStream()takeInputStreamrequests, and parse them where they check the keys../gradlew :client:test: all 406 tests pass locally, including the Docker integration tests (OxiaClientIT,ClientReconnectIT, ...). The new test passes 10 out of 10 repeated runs.