feat(codec): add compression API, standard algorithms, and global reg…#2648
feat(codec): add compression API, standard algorithms, and global reg…#2648sauravzg wants to merge 1 commit into
Conversation
gRPC-Go only implements gzip. Do any other languages offer these other ones out of the box? |
dfawley
left a comment
There was a problem hiding this comment.
Please make sure all files include the copyright header.
| use crate::codec::compression::Decompressor; | ||
|
|
||
| /// A read-only interface to query supported compression algorithms. | ||
| pub trait CompressionResolver: Send + Sync + 'static { |
There was a problem hiding this comment.
What problem is solved by putting traits around the registry?
There was a problem hiding this comment.
The traits serve two main purposes:
Testing: Global singletons are painful to test with Rust's parallel test runner. An interface allows for easy mocks or fakes in unit tests.
Interface Segregation/data hiding: Interceptors only need 'get' APIs, so I needed a trait for read-only things. I could've probably achieved this via making the write methods &mut self , but it seems like for global registries interior mutability was more common (tracing lib).
Although, I must admit it's mostly the former, global singletons are painful during testing.
There was a problem hiding this comment.
But this is just a glorified map -- surely we don't need a mock implementation of the map, do we? Whether we have a global instance of it or one that gets passed around is independent from whether we have a trait for it. The trait here feels like overkill.
There was a problem hiding this comment.
It being a glorified global map is implementation detail.
Using global instance might be painful in unit tests, because rust runs them parallely.
The trait helps abstract the implementation detail, while allowing polymorphism. Injecting a map might leak too much.
There was a problem hiding this comment.
It being a glorified global map is implementation detail.
Not really. The API is a map-like API. Global vs. not has nothing to do with whether we use a concrete type or a trait to implement it. Adding a trait adds complexity for no benefit if we don't intend for a very different implementation of it (which I can't see us needing).
There was a problem hiding this comment.
Removed the trait.
Replaced with a builder pattern for mutation and the built instance to be immutable.
The global immutable instance will default to identity+gzip.
The idea would be that provide the ability to inject and propagate immutable registries into the relevant locations, as applicable.
PTAL
2e65c81 to
c4d2db5
Compare
| use crate::codec::compression::Decompressor; | ||
|
|
||
| /// A read-only interface to query supported compression algorithms. | ||
| pub trait CompressionResolver: Send + Sync + 'static { |
There was a problem hiding this comment.
But this is just a glorified map -- surely we don't need a mock implementation of the map, do we? Whether we have a global instance of it or one that gets passed around is independent from whether we have a trait for it. The trait here feels like overkill.
| /// Returns an `io::Error` if compression fails. Implementations should gracefully | ||
| /// handle constrained `destination` buffers by returning an error rather than panicking | ||
| /// (e.g., by verifying `destination.remaining_mut()` is sufficient before writing). | ||
| fn compress(&self, source: &mut dyn Buf, destination: &mut dyn BufMut) -> Result<(), String>; |
There was a problem hiding this comment.
SendMessage has the return type of Result<Box<dyn Buf + Send + Sync>, String>. I believe these two traits should match (one way or the other). Is there any advantage to having gRPC allocate the destination BufMut as opposed to letting the codec produce the Buf? The latter seems more flexible than the former depending on the implementation of the codec itself -- it seems like if the compression library's API isn't as we expect, then you could end up having to do a byte-wise copy to get it into the BufMut we allocate (put<T:Buf> does a copy).
I believe if we return a Buf that we could have gzip allocate a BytesMut and use that with the gzip library (it impls BufMut) and return it (it also impls Buf).
There was a problem hiding this comment.
The current sendMessage API forces an allocation per message which may not be optimal. I believe we may want to benchmark before taking a call.
So, having non allocating APIs can help amortize allocation costs. We allocate once per rpc and then keep chopping of slices for each message as needed.
There was a problem hiding this comment.
The current sendMessage API forces an allocation per message which may not be optimal. I believe we may want to benchmark before taking a call.
How are we avoiding an allocation otherwise? gRPC would have to allocate something to pass into the encode method, wouldn't it? We could probably devise some kind of scheme to reuse them, but that would probably be more trouble than it's worth. I'm more worried about BufMut::put() performing a full copy than that one extra allocation (that we probably have in our library anyway).
I think we should keep these consistent. If we want to do benchmarking and it shows better performance with BufMut then we can change all 3 of them together.
There was a problem hiding this comment.
For unary, likely doesn't matter.
For a streaming RPC, we allocate a large-ish block. We treat the large-ish block as a queue , make the message write to this pre-allocated buffer, cut from beginning and expand from end as needed. This is a common patter that h2 and tonic use.
This reduces the number of allocations from O(messages) to O(max(rpcs, stream size)).
The compression API doesn't use BufMut::put() , so that's a non issue here.
| /// Returns an `io::Error` if decompression fails. Implementations should gracefully | ||
| /// handle constrained `destination` buffers by returning an error rather than panicking | ||
| /// (e.g., by verifying `destination.remaining_mut()` is sufficient before writing). | ||
| fn decompress(&self, source: &mut dyn Buf, destination: &mut dyn BufMut) -> Result<(), String>; |
There was a problem hiding this comment.
(The same decision would apply here I believe.)
…istry This change introduces the foundational compression abstractions and implementations for the grpc server component. - **Compression API (`grpc/src/codec/compression.rs`)**: Defines the `Compressor` and `Decompressor` traits for handling the compression and decompression of gRPC payloads via byte buffers. - **Simplified Design Philosophy**: The API is intentionally kept simple. It deliberately avoids handling concerns like limiting input and output buffer sizes, instead delegating these safety boundaries to the higher-level gRPC business logic. - **Performance Improvements**: By relying on direct buffer manipulation, this implementation avoids the hidden memory copying overhead present in the `tonic` crate (which utilizes 8KB intermediate buffers for I/O). - **Standard Algorithms**: Implements `gzip`, `deflate`, and `zstd` compression logic in their respective modules. These are conditionally compiled based on feature flags. - **Global Registry (`registry.rs`)**: Introduces a thread-safe `GlobalCompressionRegistry` (backed by `RwLock` and `Arc`) to manage registered compressors and decompressors. It handles safe concurrent access and automatically updates the broadcasted `grpc-accept-encoding` headers. - **Module Integration**: Exposes the new `codec` and `compression` modules to the library's root structure.
c4d2db5 to
09eb70c
Compare
…istry
This change introduces the foundational compression abstractions and implementations for the grpc server component.
grpc/src/codec/compression.rs): Defines theCompressorandDecompressortraits for handling the compression and decompression of gRPC payloads via byte buffers.toniccrate (which utilizes 8KB intermediate buffers for I/O).gzip,deflate, andzstdcompression logic in their respective modules. These are conditionally compiled based on feature flags.registry.rs): Introduces a thread-safeGlobalCompressionRegistry(backed byRwLockandArc) to manage registered compressors and decompressors. It handles safe concurrent access and automatically updates the broadcastedgrpc-accept-encodingheaders.codecandcompressionmodules to the library's root structure.