Skip to content

Commit 88bfd31

Browse files
committed
Introduces the core serialization and compression infrastructure via the codec module.
Supports Protobuf and Prost, alongside Gzip, Deflate, and Zstd compression via a global lock-free registry. The serialization API is designed to allow supporting zero copy improvements using grpc's Buf and BufMut extensions to model chain of non contiguos buffers.
1 parent 1bdcc41 commit 88bfd31

12 files changed

Lines changed: 601 additions & 1 deletion

File tree

grpc/Cargo.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ allowed_external_types = [
1515
]
1616

1717
[features]
18-
default = ["dns", "_runtime-tokio","tls-rustls", "protobuf"]
18+
default = ["dns", "_runtime-tokio","tls-rustls", "protobuf", "gzip", "deflate", "zstd"]
1919
protobuf = ["dep:protobuf"]
2020
prost = ["dep:prost"]
21+
gzip = ["dep:flate2"]
22+
deflate = ["dep:flate2"]
23+
zstd = ["dep:zstd"]
2124
unsafe-downcast = []
2225
dns = ["dep:hickory-resolver", "_runtime-tokio"]
2326
# The following feature is used to ensure all modules use the runtime
@@ -82,6 +85,10 @@ tower = { version = "0.5.2", features = [
8285
tower-service = "0.3.3"
8386
trait-variant = "0.1.2"
8487
url = "2.5.0"
88+
# TODO(sauravzg): Vendor this in instead of adding a dependency
89+
arc-swap = "1.7"
90+
flate2 = { version = "1.0", optional = true }
91+
zstd = { version = "0.13", optional = true }
8592

8693
[dev-dependencies]
8794
async-stream = "0.3.6"

grpc/src/codec.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ use tonic::codec::Decoder;
3131
use tonic::codec::EncodeBuf;
3232
use tonic::codec::Encoder;
3333

34+
pub mod compression;
35+
pub mod serialization;
36+
3437
/// An adapter for sending and receiving messages as bytes using tonic.
3538
/// Coding/decoding is handled within gRPC.
3639
/// TODO: Remove this when tonic allows access to bytes without requiring a

grpc/src/codec/compression.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use bytes::{Buf, BufMut};
2+
use std::io;
3+
4+
#[cfg(feature = "deflate")]
5+
pub mod deflate;
6+
#[cfg(feature = "gzip")]
7+
pub mod gzip;
8+
#[cfg(feature = "zstd")]
9+
pub mod zstd;
10+
11+
pub mod registry;
12+
13+
pub use self::registry::get_codec;
14+
15+
/// A trait for identifying the encoding of a compression algorithm.
16+
pub trait Encoding {
17+
/// The name of the compression algorithm, e.g., "gzip".
18+
const NAME: &'static str;
19+
}
20+
21+
/// A trait for compressing and decompressing data.
22+
pub trait Compressor: Send + Sync + 'static {
23+
/// Compress data from `source` into `destination`.
24+
fn compress(&self, source: &mut dyn Buf, destination: &mut dyn BufMut)
25+
-> Result<(), io::Error>;
26+
27+
/// Decompress data from `source` into `destination`.
28+
fn decompress(
29+
&self,
30+
source: &mut dyn Buf,
31+
destination: &mut dyn BufMut,
32+
) -> Result<(), io::Error>;
33+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
use crate::codec::compression::{Compressor, Encoding};
2+
use bytes::{Buf, BufMut};
3+
use flate2::{
4+
bufread::{ZlibDecoder, ZlibEncoder},
5+
Compression as FlateCompression,
6+
};
7+
use std::io;
8+
9+
/// A deflate compression implementation.
10+
#[derive(Debug, Clone, Copy)]
11+
pub struct Deflate {
12+
level: FlateCompression,
13+
}
14+
15+
impl Deflate {
16+
/// Creates a new deflate compression implementation.
17+
pub fn new() -> Self {
18+
Self {
19+
level: FlateCompression::new(6),
20+
}
21+
}
22+
}
23+
24+
impl Default for Deflate {
25+
fn default() -> Self {
26+
Self::new()
27+
}
28+
}
29+
30+
impl Compressor for Deflate {
31+
fn compress(
32+
&self,
33+
source: &mut dyn Buf,
34+
destination: &mut dyn BufMut,
35+
) -> Result<(), io::Error> {
36+
let mut encoder = ZlibEncoder::new(source.reader(), self.level);
37+
io::copy(&mut encoder, &mut destination.writer())?;
38+
Ok(())
39+
}
40+
41+
fn decompress(
42+
&self,
43+
source: &mut dyn Buf,
44+
destination: &mut dyn BufMut,
45+
) -> Result<(), io::Error> {
46+
let mut decoder = ZlibDecoder::new(source.reader());
47+
io::copy(&mut decoder, &mut destination.writer())?;
48+
Ok(())
49+
}
50+
}
51+
52+
impl Encoding for Deflate {
53+
const NAME: &'static str = "deflate";
54+
}
55+
56+
#[cfg(test)]
57+
mod tests {
58+
use super::*;
59+
use bytes::Bytes;
60+
61+
#[test]
62+
fn deflate_compress_decompress() {
63+
let compressor = Deflate::new();
64+
let data = Bytes::from_static(b"hello world");
65+
let mut compressed = Vec::new();
66+
compressor
67+
.compress(&mut data.clone(), &mut compressed)
68+
.unwrap();
69+
let mut decompressed = Vec::new();
70+
compressor
71+
.decompress(&mut compressed.as_slice(), &mut decompressed)
72+
.unwrap();
73+
assert_eq!(data, decompressed.as_slice());
74+
}
75+
}

grpc/src/codec/compression/gzip.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
use crate::codec::compression::{Compressor, Encoding};
2+
use bytes::{Buf, BufMut};
3+
use flate2::{
4+
bufread::{GzDecoder, GzEncoder},
5+
Compression as FlateCompression,
6+
};
7+
use std::io;
8+
9+
/// A gzip compression implementation.
10+
#[derive(Debug, Clone, Copy)]
11+
pub struct Gzip {
12+
level: FlateCompression,
13+
}
14+
15+
impl Gzip {
16+
/// Creates a new gzip compression implementation.
17+
pub fn new() -> Self {
18+
Self {
19+
level: FlateCompression::new(6),
20+
}
21+
}
22+
}
23+
24+
impl Default for Gzip {
25+
fn default() -> Self {
26+
Self::new()
27+
}
28+
}
29+
30+
impl Compressor for Gzip {
31+
fn compress(
32+
&self,
33+
source: &mut dyn Buf,
34+
destination: &mut dyn BufMut,
35+
) -> Result<(), io::Error> {
36+
let mut encoder = GzEncoder::new(source.reader(), self.level);
37+
io::copy(&mut encoder, &mut destination.writer())?;
38+
Ok(())
39+
}
40+
41+
fn decompress(
42+
&self,
43+
source: &mut dyn Buf,
44+
destination: &mut dyn BufMut,
45+
) -> Result<(), io::Error> {
46+
let mut decoder = GzDecoder::new(source.reader());
47+
io::copy(&mut decoder, &mut destination.writer())?;
48+
Ok(())
49+
}
50+
}
51+
52+
impl Encoding for Gzip {
53+
const NAME: &'static str = "gzip";
54+
}
55+
56+
#[cfg(test)]
57+
mod tests {
58+
use super::*;
59+
use bytes::Bytes;
60+
61+
#[test]
62+
fn gzip_compress_decompress() {
63+
let compressor = Gzip::new();
64+
let data = Bytes::from_static(b"hello world");
65+
let mut compressed = Vec::new();
66+
compressor
67+
.compress(&mut data.clone(), &mut compressed)
68+
.unwrap();
69+
let mut decompressed = Vec::new();
70+
compressor
71+
.decompress(&mut compressed.as_slice(), &mut decompressed)
72+
.unwrap();
73+
assert_eq!(data, decompressed.as_slice());
74+
}
75+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use super::{Compressor, Encoding};
2+
use arc_swap::ArcSwap;
3+
use std::collections::HashMap;
4+
use std::sync::{Arc, LazyLock};
5+
6+
/// A registry of compression implementations.
7+
#[derive(Default, Clone)]
8+
pub struct CompressionRegistry {
9+
codecs: HashMap<&'static str, Arc<dyn Compressor>>,
10+
}
11+
12+
// The global registry using ArcSwap for lock-free reads to implement something
13+
// closer to the RCU pattern.
14+
static GLOBAL_REGISTRY: LazyLock<ArcSwap<CompressionRegistry>> =
15+
LazyLock::new(|| ArcSwap::from(Arc::new(CompressionRegistry::new())));
16+
17+
/// Get a codec from the global registry.
18+
/// This operation is extremely fast and lock-free.
19+
pub fn get_codec(name: &str) -> Option<Arc<dyn Compressor>> {
20+
GLOBAL_REGISTRY.load().get(name)
21+
}
22+
23+
/// Add a new codec to the global registry using a copy-on-write strategy.
24+
pub fn add_codec(name: &'static str, codec: Arc<dyn Compressor>) {
25+
let current_registry = GLOBAL_REGISTRY.load();
26+
let new_registry = current_registry.with_codec(name, codec);
27+
GLOBAL_REGISTRY.store(Arc::new(new_registry));
28+
}
29+
30+
impl CompressionRegistry {
31+
/// Creates a new compression registry with default codecs enabled by features.
32+
pub fn new() -> Self {
33+
let mut codecs: HashMap<&'static str, Arc<dyn Compressor>> = HashMap::new();
34+
35+
#[cfg(feature = "gzip")]
36+
{
37+
let gzip = Arc::new(super::gzip::Gzip::new());
38+
codecs.insert(<super::gzip::Gzip as Encoding>::NAME, gzip);
39+
}
40+
41+
#[cfg(feature = "deflate")]
42+
{
43+
let deflate = Arc::new(super::deflate::Deflate::new());
44+
codecs.insert(<super::deflate::Deflate as Encoding>::NAME, deflate);
45+
}
46+
47+
#[cfg(feature = "zstd")]
48+
{
49+
let zstd = Arc::new(super::zstd::Zstd::new());
50+
codecs.insert(<super::zstd::Zstd as Encoding>::NAME, zstd);
51+
}
52+
53+
Self { codecs }
54+
}
55+
56+
/// Get a codec from this specific registry instance.
57+
pub fn get(&self, name: &str) -> Option<Arc<dyn Compressor>> {
58+
self.codecs.get(name).cloned()
59+
}
60+
61+
/// Creates a new registry from an existing one, adding a new codec.
62+
pub fn with_codec(&self, name: &'static str, codec: Arc<dyn Compressor>) -> Self {
63+
// Clone the existing map of codecs
64+
let mut new_codecs = self.codecs.clone();
65+
// Add the new one
66+
new_codecs.insert(name, codec);
67+
// Return a new CompressionRegistry instance
68+
Self { codecs: new_codecs }
69+
}
70+
}
71+
72+
#[cfg(test)]
73+
mod tests {
74+
use super::*;
75+
use crate::codec::compression::{Compressor, Encoding};
76+
use bytes::{Buf, BufMut};
77+
use std::io;
78+
79+
#[derive(Debug, Clone, Copy)]
80+
struct MockCompression;
81+
82+
impl Compressor for MockCompression {
83+
fn compress(
84+
&self,
85+
_source: &mut dyn Buf,
86+
_destination: &mut dyn BufMut,
87+
) -> Result<(), io::Error> {
88+
Ok(())
89+
}
90+
91+
fn decompress(
92+
&self,
93+
_source: &mut dyn Buf,
94+
_destination: &mut dyn BufMut,
95+
) -> Result<(), io::Error> {
96+
Ok(())
97+
}
98+
}
99+
100+
impl Encoding for MockCompression {
101+
const NAME: &'static str = "mock";
102+
}
103+
104+
#[test]
105+
fn registry_get_with() {
106+
let registry = CompressionRegistry::new();
107+
let registry = registry.with_codec("mock", Arc::new(MockCompression));
108+
assert!(registry.get("mock").is_some());
109+
}
110+
111+
#[test]
112+
fn global_registry() {
113+
#[cfg(feature = "gzip")]
114+
assert!(get_codec("gzip").is_some());
115+
#[cfg(feature = "deflate")]
116+
assert!(get_codec("deflate").is_some());
117+
#[cfg(feature = "zstd")]
118+
assert!(get_codec("zstd").is_some());
119+
120+
add_codec("mock", Arc::new(MockCompression));
121+
assert!(get_codec("mock").is_some());
122+
}
123+
}

0 commit comments

Comments
 (0)