drpc: add compression#64
Conversation
bab59b3 to
bd6926a
Compare
shubhamdhama
left a comment
There was a problem hiding this comment.
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.
|
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? |
|
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 ( From @cthumuluru-crdb's suggestion, now compression and decompression happen entirely within the |
|
TFTR! @shubhamdhama |
| 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 { |
There was a problem hiding this comment.
How about implementing no-op compressor and make rest of the code free from checking the compression type?
| var compressor drpcwire.Compression | ||
| if pkt.metadata != nil { | ||
| if name, ok := pkt.metadata[drpcwire.CompressorMetadataKey]; ok { | ||
| comp, found := drpcwire.CompressionFromName(name) |
There was a problem hiding this comment.
CompressionFromName -> CompressorFromName ?
| if name, ok := pkt.metadata[drpcwire.CompressorMetadataKey]; ok { | ||
| comp, found := drpcwire.CompressionFromName(name) | ||
| if !found { | ||
| m.pdone.Send() |
There was a problem hiding this comment.
nit: separately, we should consider removing this hand-off. I don't see value in it except for the overhead and code complexity.
There was a problem hiding this comment.
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.
| wr *drpcwire.MuxWriter | ||
| recvQueue ringBuffer | ||
| wbuf []byte | ||
| cbuf []byte // compression scratch buffer |
There was a problem hiding this comment.
I think we can avoid this shared state altogether. Can you check?
There was a problem hiding this comment.
cbuf/dbuf provide buffer reuse across messages hence avoiding per-message allocations for snappy so i would keep it in.
| defer s.write.Unlock() | ||
|
|
||
| if kind == drpcwire.KindMessage { | ||
| data = s.compressLocked(data) |
There was a problem hiding this comment.
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?
| } | ||
| defer s.recvQueue.Done() | ||
|
|
||
| b, err = s.decompressLocked(b) |
There was a problem hiding this comment.
I would suggest the same here too. Inline the code and use a local buffer?
| ctx := drpctest.NewTracker(t) | ||
| defer ctx.Close() | ||
|
|
||
| cli, close := createCompressedConnection(t, standardImpl) |
There was a problem hiding this comment.
nit: createCompressedConnection -> createConnectionWithCompression or let the createConnection take an optional compression type?
@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.
|
|
||
| // CompressorMetadataKey is the metadata key used to signal the compression | ||
| // algorithm from client to server during stream invocation. | ||
| const CompressorMetadataKey = "drpc-compressor" |
There was a problem hiding this comment.
We should make this "drpc-compression".
Let's avoid using the word "compressor". In most of the context, we can just use compression.
| // 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 { |
There was a problem hiding this comment.
WithCompression is enough unless there is an overloading.
| 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() |
There was a problem hiding this comment.
Resolve this once for the whole connection.
| 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) |
There was a problem hiding this comment.
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.
|
I think you are yet to update the |
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
CompressionFromNameatstream creation time, returning a
ProtocolErrorimmediately if thealgorithm is unknown.
Compression and decompression are performed in
MsgSend/MsgRecvandRawWrite/RawRecvvia symmetriccompressLocked/decompressLockedhelpers, keeping
handlePacketandrawWriteLockedas pure transportwith zero compression awareness. Scratch buffers are cleanly owned
by their respective lock sides (
cbufunder write,dbufunder 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/RawRecvcallrather 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
KindMessageframesare compressed; control frames flow through unmodified.
Streams without compression are completely untouched so no new
wire framing, no per-message overhead, full backward compatibility.