Skip to content

drpc: add compression#64

Open
Nukitt wants to merge 1 commit into
cockroachdb:mainfrom
Nukitt:add-compression
Open

drpc: add compression#64
Nukitt wants to merge 1 commit into
cockroachdb:mainfrom
Nukitt:add-compression

Conversation

@Nukitt

@Nukitt Nukitt commented Jun 24, 2026

Copy link
Copy Markdown

Introduce per-message compression for drpc streams. A Compression
enum type (CompressionNone, CompressionSnappy) in drpcwire provides
Compress/Decompress methods with built-in Snappy block-format support,
including buffer-reuse.

Compression is negotiated per-stream via the "drpc-compressor"
metadata key: the client injects the algorithm name into outgoing
metadata, and the server resolves it via CompressionFromName at
stream creation time, returning a ProtocolError immediately if the
algorithm is unknown.

Compression and decompression are performed in MsgSend/MsgRecv and
RawWrite/RawRecv via symmetric compressLocked/decompressLocked
helpers, keeping handlePacket and rawWriteLocked as pure transport
with zero compression awareness. Scratch buffers are cleanly owned
by their respective lock sides (cbuf under write, dbuf under read).
This also means the ring buffer enqueue copies smaller compressed
bytes rather than larger decompressed bytes, and decompression
errors are scoped to the affected stream's MsgRecv/RawRecv call
rather than tearing down all streams on the connection via the
manager's terminate path.

The compression metadata key is stripped from application-visible
metadata before populating the context. Only KindMessage frames
are compressed; control frames flow through unmodified.

Streams without compression are completely untouched so no new
wire framing, no per-message overhead, full backward compatibility.

@Nukitt Nukitt force-pushed the add-compression branch 3 times, most recently from bab59b3 to bd6926a Compare June 24, 2026 06:52

@shubhamdhama shubhamdhama left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Now that I see https://github.com/cockroachlabs/cockroach/pull/1150, it seems we are not re-using the compressors. If that's the case then I'm strongly not in favor of Compressor interface and this whole registration/init business. Instead of having WithCompressor, just have WithCompression where you pass the compression algorithm, Snappy, Zstd. That is, cockroach library should be able to use compression provided by DRPC library using just an enum from its perspective.

The reason gRPC has done it that way because it's a general purpose library. Here we own DRPC. We can add new compressors as we like in this repo.

Also, the code would be much less.

Comment thread drpcstream/stream.go Outdated
Comment thread drpcconn/conn.go Outdated
Comment thread drpcmanager/manager.go Outdated
@cthumuluru-crdb

Copy link
Copy Markdown

Any reason the whole compression cannot be pushed to MsgSend/MsgReceive APIs? The sender compresses the buffer and replaces it as message data. Received receives compressed buffer, knows its compressed, uncompresses it and decodes it?

@Nukitt Nukitt force-pushed the add-compression branch from bd6926a to 89f6464 Compare June 30, 2026 06:29
@Nukitt

Nukitt commented Jun 30, 2026

Copy link
Copy Markdown
Author

So some major changes I've incorporated after the initial round of reviews.

Starting with @shubhamdhama's suggestion of moving registration into drpc itself, now on cockroach a single dial option (WithCompression) is all that's needed and drpc automatically injects the compressor name into stream metadata, and the server extracts, validates, and applies it at stream creation time. This indeed keeps the boundary clean as drpc owns compression end-to-end and consumers opt in with a constant.

From @cthumuluru-crdb's suggestion, now compression and decompression happen entirely within the MsgSend/MsgRecv and RawWrite/RawRecv methods via symmetric compressLocked/decompressLocked helpers, keeping it out of the manager's read-loop hot path where handlePacket now just enqueues raw bytes without touching compression at all. This also means the enqueue copy is over smaller compressed bytes, rather than the earlier larger decompressed bytes. handlePacket and rawWriteLocked are back to pure transport with zero compression awareness.
The old compressAndWriteLocked helper was mixing compression with writing which is now been split so compressLocked and decompressLocked are both pure data transforms, with the scratch buffers cleanly owned by their respective lock sides (cbuf under write, dbuf under read). Elaborating further on moving decompression out of handlePacket fixes a blast-radius issue: a previous decompression error was returned from handlePacket, which caused the manager to call terminate() and tear down all streams on the connection. Now decompression errors surface as ProtocolError in the individual MsgRecv/RawRecv call, scoped to the affected stream. Also, only KindMessage frames are compressed and decompressed. Control frames (KindError, KindCancel, KindClose, KindCloseSend) flow uncompressed through handlePacket and are handled as protocol signals.

@Nukitt Nukitt requested a review from shubhamdhama June 30, 2026 10:13

@shubhamdhama shubhamdhama left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM!

Please update the description and before i click approve I want to see it working.

Comment thread drpcstream/stream.go Outdated
@Nukitt Nukitt force-pushed the add-compression branch from 89f6464 to f4be0fb Compare July 1, 2026 10:01
@Nukitt

Nukitt commented Jul 1, 2026

Copy link
Copy Markdown
Author

TFTR! @shubhamdhama
Updated desc.

shubhamdhama
shubhamdhama previously approved these changes Jul 1, 2026
Comment thread drpcmanager/manager.go Outdated
func (m *Manager) newStream(ctx context.Context, sid uint64, kind drpc.StreamKind, rpc string) (*drpcstream.Stream, error) {
func (m *Manager) newStream(ctx context.Context, sid uint64, kind drpc.StreamKind, rpc string, compressor drpcwire.Compression) (*drpcstream.Stream, error) {
opts := m.opts.Stream
if compressor != drpcwire.CompressionNone {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

How about implementing no-op compressor and make rest of the code free from checking the compression type?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done.

Comment thread drpcmanager/manager.go Outdated
var compressor drpcwire.Compression
if pkt.metadata != nil {
if name, ok := pkt.metadata[drpcwire.CompressorMetadataKey]; ok {
comp, found := drpcwire.CompressionFromName(name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CompressionFromName -> CompressorFromName ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done.

Comment thread drpcmanager/manager.go
if name, ok := pkt.metadata[drpcwire.CompressorMetadataKey]; ok {
comp, found := drpcwire.CompressionFromName(name)
if !found {
m.pdone.Send()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: separately, we should consider removing this hand-off. I don't see value in it except for the overhead and code complexity.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I'll keep it in for now and track it separately since this handoff ensures manageReader waits until the stream is registered before reading the next frame.

Comment thread drpcstream/stream.go
wr *drpcwire.MuxWriter
recvQueue ringBuffer
wbuf []byte
cbuf []byte // compression scratch buffer

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think we can avoid this shared state altogether. Can you check?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

cbuf/dbuf provide buffer reuse across messages hence avoiding per-message allocations for snappy so i would keep it in.

Comment thread drpcstream/stream.go Outdated
defer s.write.Unlock()

if kind == drpcwire.KindMessage {
data = s.compressLocked(data)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

How about making this call inline and use the buffer to compress, write and let go of the buffer? That way you can avoid the buffer escaping to heap?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done.

Comment thread drpcstream/stream.go Outdated
}
defer s.recvQueue.Done()

b, err = s.decompressLocked(b)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I would suggest the same here too. Inline the code and use a local buffer?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

yup done.

Comment thread internal/integration/compress_test.go Outdated
ctx := drpctest.NewTracker(t)
defer ctx.Close()

cli, close := createCompressedConnection(t, standardImpl)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: createCompressedConnection -> createConnectionWithCompression or let the createConnection take an optional compression type?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done.

@cthumuluru-crdb

Copy link
Copy Markdown

So some major changes I've incorporated after the initial round of reviews.

Starting with @shubhamdhama's suggestion of moving registration into drpc itself, now on cockroach a single dial option (WithCompression) is all that's needed and drpc automatically injects the compressor name into stream metadata, and the server extracts, validates, and applies it at stream creation time. This indeed keeps the boundary clean as drpc owns compression end-to-end and consumers opt in with a constant.

From @cthumuluru-crdb's suggestion, now compression and decompression happen entirely within the MsgSend/MsgRecv and RawWrite/RawRecv methods via symmetric compressLocked/decompressLocked helpers, keeping it out of the manager's read-loop hot path where handlePacket now just enqueues raw bytes without touching compression at all. This also means the enqueue copy is over smaller compressed bytes, rather than the earlier larger decompressed bytes. handlePacket and rawWriteLocked are back to pure transport with zero compression awareness. The old compressAndWriteLocked helper was mixing compression with writing which is now been split so compressLocked and decompressLocked are both pure data transforms, with the scratch buffers cleanly owned by their respective lock sides (cbuf under write, dbuf under read). Elaborating further on moving decompression out of handlePacket fixes a blast-radius issue: a previous decompression error was returned from handlePacket, which caused the manager to call terminate() and tear down all streams on the connection. Now decompression errors surface as ProtocolError in the individual MsgRecv/RawRecv call, scoped to the affected stream. Also, only KindMessage frames are compressed and decompressed. Control frames (KindError, KindCancel, KindClose, KindCloseSend) flow uncompressed through handlePacket and are handled as protocol signals.

@Nukitt, can you also details on how compression can be turned off or on. Do we have a cluster setting using which can turn compression on/off at runtime?

Introduce per-message compression for drpc streams. A Compression
enum type (CompressionNone, CompressionSnappy) in drpcwire provides
Compress/Decompress methods with built-in Snappy block-format support,
including buffer-reuse via scratch buffers owned by each lock side
(cbuf under write, dbuf under read).

Compression is negotiated per-stream via the "drpc-compressor"
metadata key: the client injects the algorithm name into outgoing
metadata, and the server resolves it via CompressorFromName at
stream creation time, returning a ProtocolError immediately if the
algorithm is unknown. A CompressorFunc callback on the manager
options allows dynamic per-stream compression selection (e.g. after
a version gate activates) without recycling the connection.

Compression and decompression are performed in `MsgSend`/`MsgRecv` and
`RawWrite`/`RawRecv` via symmetric `compressLocked`/`decompressLocked`
helpers, keeping `handlePacket` and `rawWriteLocked` as pure transport
with zero compression awareness. Scratch buffers are cleanly owned
by their respective lock sides (`cbuf` under write, `dbuf` under read).
This also means the ring buffer enqueue copies smaller compressed
bytes rather than larger decompressed bytes, and decompression
errors are scoped to the affected stream's `MsgRecv`/`RawRecv` call
rather than tearing down all streams on the connection via the
manager's terminate path.

The compression metadata key is stripped from application-visible
metadata before populating the context. Only `KindMessage` frames
are compressed; control frames flow through unmodified.

Streams without compression are completely untouched so no new
wire framing, no per-message overhead, full backward compatibility.
@Nukitt Nukitt force-pushed the add-compression branch from f4be0fb to ebb6054 Compare July 3, 2026 08:07
@shubhamdhama shubhamdhama self-requested a review July 6, 2026 12:17
Comment thread drpcwire/compress.go

// CompressorMetadataKey is the metadata key used to signal the compression
// algorithm from client to server during stream invocation.
const CompressorMetadataKey = "drpc-compressor"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We should make this "drpc-compression".

Let's avoid using the word "compressor". In most of the context, we can just use compression.

Comment thread drpcclient/dialoptions.go
// per-stream to determine the compression algorithm. This allows
// compression to be enabled dynamically (e.g. after a version gate
// activates) without recycling the connection.
func WithCompressorFunc(f func() drpcwire.Compression) DialOption {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WithCompression is enough unless there is an overloading.

Comment thread drpcconn/conn.go
func (c *Conn) NewStream(ctx context.Context, rpc string, enc drpc.Encoding) (_ drpc.Stream, err error) {
defer func() { err = drpc.ToRPCErr(err) }()

comp := c.resolveCompressor()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Resolve this once for the whole connection.

Comment thread drpcwire/compress.go
Comment on lines +40 to +47
maxLen := snappy.MaxEncodedLen(len(src))
if maxLen < 0 {
panic("drpcwire: snappy source too large for encoding")
}
if cap(dst) >= maxLen {
return snappy.Encode(dst[:cap(dst)], src)
}
return snappy.Encode(nil, src)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This whole block is redundant as it's present in snappy.Encode. The only line we need to keep is return snappy.Encode(dst[:cap(dst)], src) explaining why we are resetting the length to the capacity for destination.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same for Decompress

@shubhamdhama shubhamdhama dismissed their stale review July 6, 2026 12:56

More updates needed.

@shubhamdhama

Copy link
Copy Markdown

I think you are yet to update the Stream.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants