Skip to content

feat(codec): add compression API, standard algorithms, and global reg…#2648

Open
sauravzg wants to merge 1 commit into
masterfrom
sauravz/compression-api
Open

feat(codec): add compression API, standard algorithms, and global reg…#2648
sauravzg wants to merge 1 commit into
masterfrom
sauravz/compression-api

Conversation

@sauravzg

Copy link
Copy Markdown
Contributor

…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.

@sauravzg sauravzg requested a review from dfawley May 19, 2026 14:20
@dfawley

dfawley commented May 21, 2026

Copy link
Copy Markdown
Member

Standard Algorithms: Implements gzip, deflate, and zstd compression logic in their respective modules. These are conditionally compiled based on feature flags.

gRPC-Go only implements gzip. Do any other languages offer these other ones out of the box?

@dfawley dfawley left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please make sure all files include the copyright header.

Comment thread grpc/src/codec/compression/gzip.rs
Comment thread grpc/src/codec/compression/registry.rs Outdated
use crate::codec::compression::Decompressor;

/// A read-only interface to query supported compression algorithms.
pub trait CompressionResolver: Send + Sync + 'static {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What problem is solved by putting traits around the registry?

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.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

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 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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).

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.

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

Comment thread grpc/src/lib.rs Outdated
Comment thread grpc/src/codec/compression.rs Outdated
@sauravzg sauravzg force-pushed the sauravz/compression-api branch 2 times, most recently from 2e65c81 to c4d2db5 Compare May 22, 2026 11:39
@sauravzg sauravzg requested a review from dfawley May 22, 2026 11:52
Comment thread grpc/src/codec/compression/registry.rs Outdated
use crate::codec::compression::Decompressor;

/// A read-only interface to query supported compression algorithms.
pub trait CompressionResolver: Send + Sync + 'static {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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>;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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).

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.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

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.

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>;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(The same decision would apply here I believe.)

Comment thread grpc/src/codec/compression.rs Outdated
…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.
@sauravzg sauravzg force-pushed the sauravz/compression-api branch from c4d2db5 to 09eb70c Compare July 14, 2026 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants