Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b4722bf
fix: replace unwrap with proper error on empty gRPC response
dahankzter Feb 18, 2026
9576c6f
fix: add default max capacity to main_cache to prevent unbounded growth
dahankzter Feb 18, 2026
24ad8f0
fix: store SocketAddr directly in VNode to eliminate parse panics
dahankzter Feb 18, 2026
886253a
fix: eliminate TOCTOU race in add_peer and remove_peer
dahankzter Feb 18, 2026
5250e1f
fix: log remote load errors instead of silently swallowing them
dahankzter Feb 18, 2026
b0825bb
fix: make GroupcacheError a public enum for pattern matching
dahankzter Feb 18, 2026
07c8121
fix: clean up build.rs and remove tonic-prost-build from runtime deps
dahankzter Feb 18, 2026
003279e
perf: use Arc<str> instead of String for cache keys
dahankzter Feb 18, 2026
331d51b
fix: abort service discovery task on shutdown
dahankzter Feb 18, 2026
b0dbf0b
fix: don't hold Arc across sleep in service discovery loop
dahankzter Feb 18, 2026
d0badba
perf: use rmp_serde::from_slice instead of from_read
dahankzter Feb 18, 2026
d220e15
feat: implement Display for GroupcachePeer
dahankzter Feb 18, 2026
18315d9
build: add Makefile with build, test, lint, and coverage targets
dahankzter Feb 18, 2026
6003e7d
test: add comprehensive tests to reach ~98% line coverage
dahankzter Feb 19, 2026
7beb093
feat: add streaming invalidation with near-realtime cache coherence
dahankzter Mar 29, 2026
3c63cbf
feat: add Maelstrom convergence test harness
dahankzter Mar 29, 2026
290a918
perf: lock-free routing with ArcSwap, bincode feature, benchmarks
dahankzter Mar 29, 2026
707beb4
feat: feature-gated Kubernetes service discovery
dahankzter Mar 29, 2026
8793948
feat: add DNS and Consul service discovery
dahankzter Mar 29, 2026
28ddc26
feat: require CancellationToken for graceful shutdown
dahankzter Mar 29, 2026
ef21ccd
feat: add status() for health reporting
dahankzter Mar 29, 2026
33e1e1f
feat: make metric names public with documentation
dahankzter Mar 29, 2026
f18dbd6
feat: configurable gRPC timeout for peer requests
dahankzter Mar 30, 2026
8432374
chore: rename to groupcache-ng for crates.io publication
dahankzter Mar 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ Cargo.lock
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

# Coverage output
coverage/

# Ignore IDE specific files
.idea/*
.vscode/*
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ members= [
"examples/simple",
"examples/simple-multiple-instances"
]
exclude = [
"maelstrom-tests"
]


[workspace.dependencies.cargo-husky]
Expand Down
118 changes: 118 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# groupcache — Makefile
# Run `make help` to see all available targets.

.PHONY: help build check test test-all clippy fmt lint bench doc clean \
coverage coverage-html coverage-lcov coverage-summary \
maelstrom-test maelstrom-baseline maelstrom-partitions

# ── Build & Check ────────────────────────────────────────────────────

help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-22s\033[0m %s\n", $$1, $$2}'

build: ## Build all workspace crates
cargo build --workspace

build-release: ## Build optimized release
cargo build --workspace --release

check: ## Type-check without building
cargo check --workspace --all-targets

# ── Testing ──────────────────────────────────────────────────────────

test: ## Run all tests
cargo test --workspace

test-lib: ## Run only library unit + integration tests
cargo test -p groupcache

test-integration: ## Run only integration tests
cargo test -p groupcache --test integration_test

test-metrics: ## Run only metrics tests
cargo test -p groupcache --test metrics_test

test-bincode: ## Run tests with bincode feature enabled
cargo test -p groupcache --features bincode --lib --tests

test-all: test test-bincode ## Run all tests including feature variants

# ── Code Quality ─────────────────────────────────────────────────────

clippy: ## Run clippy with warnings as errors
cargo clippy --workspace --all-targets -- -D warnings

fmt: ## Format code
cargo fmt --all

fmt-check: ## Check formatting without modifying
cargo fmt --all -- --check

lint: clippy fmt-check ## Run all lints (clippy + format check)

# ── Benchmarks ───────────────────────────────────────────────────────

bench: ## Run all benchmarks
cargo bench -p groupcache

bench-codec: ## Run serialization benchmarks (msgpack vs bincode)
cargo bench -p groupcache --bench codec_bench

bench-routing: ## Run routing contention benchmarks (RwLock vs ArcSwap)
cargo bench -p groupcache --bench routing_contention

# ── Coverage ─────────────────────────────────────────────────────────

coverage: coverage-summary ## Show coverage summary (alias)

coverage-summary: ## Show per-file coverage summary (merged default + bincode runs)
cargo llvm-cov clean --workspace
cargo llvm-cov --no-report --workspace
cargo llvm-cov --no-report -p groupcache --features bincode --lib --tests
cargo llvm-cov report --summary-only

coverage-text: ## Show line-by-line coverage in terminal
cargo llvm-cov clean --workspace
cargo llvm-cov --no-report --workspace
cargo llvm-cov --no-report -p groupcache --features bincode --lib --tests
cargo llvm-cov report --text

coverage-html: ## Generate HTML coverage report and open in browser
cargo llvm-cov clean --workspace
cargo llvm-cov --no-report --workspace
cargo llvm-cov --no-report -p groupcache --features bincode --lib --tests
cargo llvm-cov report --html --open

coverage-lcov: ## Generate LCOV report (for CI integration)
cargo llvm-cov clean --workspace
cargo llvm-cov --no-report --workspace
cargo llvm-cov --no-report -p groupcache --features bincode --lib --tests
cargo llvm-cov report --lcov --output-path lcov.info
@echo "Written to lcov.info"

# ── Documentation ────────────────────────────────────────────────────

doc: ## Build and open documentation
cargo doc --workspace --no-deps --open

# ── Maelstrom Convergence Tests ──────────────────────────────────────

maelstrom-test: ## Run all Maelstrom convergence test scenarios
$(MAKE) -C maelstrom-tests test-maelstrom

maelstrom-baseline: ## Run Maelstrom baseline (no faults)
$(MAKE) -C maelstrom-tests test-baseline

maelstrom-partitions: ## Run Maelstrom partition test
$(MAKE) -C maelstrom-tests test-partitions

maelstrom-results: ## View Maelstrom results in browser
$(MAKE) -C maelstrom-tests serve-results

# ── Cleanup ──────────────────────────────────────────────────────────

clean: ## Remove build artifacts
cargo clean
rm -f lcov.info
6 changes: 3 additions & 3 deletions examples/kubernetes-service-discovery/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ publish = false
[dependencies]
# This pulls version from main branch so that docker build works (docker was confused by paths)
#groupcache = { git = "https://github.com/Petroniuss/groupcache.git" }
groupcache = { path = "../../groupcache" }
groupcache = { path = "../../groupcache", package = "groupcache-ng", features = ["kubernetes"] }
tonic = "0.14.2"
axum = "0.8.0"

Expand All @@ -25,5 +25,5 @@ axum-prometheus = "0.9.0"
anyhow = "1"
async-trait = "0.1"

kube = { version = "2.0.1", features = ["runtime", "derive"] }
k8s-openapi = { version = "0.26.0", features = ["latest"] }
# kube and k8s-openapi come through groupcache's "kubernetes" feature
kube = { version = "3", features = ["runtime"] }
63 changes: 2 additions & 61 deletions examples/kubernetes-service-discovery/src/k8s.rs
Original file line number Diff line number Diff line change
@@ -1,61 +1,2 @@
use async_trait::async_trait;
use groupcache::{GroupcachePeer, ServiceDiscovery};
use k8s_openapi::api::core::v1::Pod;
use kube::api::ListParams;
use kube::{Api, Client};
use std::collections::HashSet;
use std::error::Error;
use std::net::SocketAddr;

pub struct Kubernetes {
api: Api<Pod>,
}

pub struct KubernetesBuilder {
client: Option<Client>,
}

impl KubernetesBuilder {
pub fn build(self) -> Kubernetes {
Kubernetes {
api: Api::default_namespaced(self.client.unwrap()),
}
}
pub fn client(mut self, client: Client) -> Self {
self.client = Some(client);
self
}
}

impl Kubernetes {
pub fn builder() -> KubernetesBuilder {
KubernetesBuilder { client: None }
}
}

#[async_trait]
impl ServiceDiscovery for Kubernetes {
async fn pull_instances(
&self,
) -> Result<HashSet<GroupcachePeer>, Box<dyn Error + Send + Sync + 'static>> {
let pods_with_label_query = ListParams::default().labels("app=groupcache-powered-backend");
Ok(self
.api
.list(&pods_with_label_query)
.await
.unwrap()
.into_iter()
.filter_map(|pod| {
let status = pod.status?;
let pod_ip = status.pod_ip?;

let Ok(ip) = pod_ip.parse() else {
return None;
};

let addr = SocketAddr::new(ip, 3000);
Some(GroupcachePeer::from_socket(addr))
})
.collect())
}
}
// Re-export from the library's built-in kubernetes discovery.
pub use groupcache::discovery::kubernetes::KubernetesDiscovery;
13 changes: 10 additions & 3 deletions examples/kubernetes-service-discovery/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ mod cache;
mod k8s;

use crate::cache::CachedValue;
use crate::k8s::Kubernetes;
use crate::k8s::KubernetesDiscovery;
use anyhow::Context;
use anyhow::Result;
use axum::extract::{Path, State};
Expand Down Expand Up @@ -59,8 +59,15 @@ async fn main() -> Result<()> {
let client = Client::try_default().await?;

// Configuring groupcache to use Kubernetes API server for peer auto-discovery.
let groupcache = Groupcache::builder(addr.into(), loader)
.service_discovery(Kubernetes::builder().client(client).build())
let discovery = KubernetesDiscovery::builder()
.client(client)
.label_selector("app=groupcache-powered-backend")
.port(pod_port.parse()?)
.build()
.map_err(|e| anyhow::anyhow!("{}", e))?;

let groupcache = Groupcache::builder(addr.into(), loader, groupcache::CancellationToken::new())
.service_discovery(discovery)
.build();

// Example axum app with endpoint to retrieve value from groupcache.
Expand Down
2 changes: 1 addition & 1 deletion examples/simple-multiple-instances/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ edition = "2021"
publish = false

[dependencies]
groupcache = { path = "../../groupcache" }
groupcache = { path = "../../groupcache", package = "groupcache-ng" }

tonic = "0.14.2"
tokio = { version = "1.34", features = ["full"] }
Expand Down
2 changes: 1 addition & 1 deletion examples/simple-multiple-instances/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ pub async fn spawn_groupcache_instance_on_addr(addr: SocketAddr) -> Result<Examp
addr: addr.to_string(),
};

let groupcache = Groupcache::builder(addr.into(), loader)
let groupcache = Groupcache::builder(addr.into(), loader, groupcache::CancellationToken::new())
.hot_cache(
CacheBuilder::default()
.time_to_live(Duration::from_secs(10))
Expand Down
2 changes: 1 addition & 1 deletion examples/simple/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ edition = "2021"
publish = false

[dependencies]
groupcache = { path = "../../groupcache" }
groupcache = { path = "../../groupcache", package = "groupcache-ng" }

tokio = { version = "1.34", features = ["full"] }
serde = { version = "1", features = ["derive", "rc"] }
Expand Down
2 changes: 1 addition & 1 deletion examples/simple/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ async fn main() -> Result<()> {
let loader = ComputeProtectedValue;

// we crate groupcache with only a single peer - this process
let groupcache = Groupcache::builder(addr.into(), loader).build();
let groupcache = Groupcache::builder(addr.into(), loader, groupcache::CancellationToken::new()).build();

// we make 3 concurrent requests for hot key
let key = "some-hot-requested-key";
Expand Down
13 changes: 7 additions & 6 deletions groupcache-pb/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
[package]
name = "groupcache-pb"
version = "0.3.0"
name = "groupcache-ng-pb"
version = "0.4.0"
edition = "2021"
authors = ["Patryk Wojtyczek"]
authors = ["Patryk Wojtyczek", "Henrik Johansson <dahankzter@gmail.com>"]
categories = ["caching", "web-programming", "concurrency", "asynchronous"]
description = "groupcache protocol buffers - internal crate"
homepage = "https://github.com/Petroniuss/groupcache"
description = "Protocol buffer definitions for groupcache-ng (internal crate)"
homepage = "https://github.com/dahankzter/groupcache"
keywords = ["distributed", "cache", "shard", "memcached", "gRPC"]
license = "MIT"
readme = "../readme.md"
repository = "https://github.com/Petroniuss/groupcache"
repository = "https://github.com/dahankzter/groupcache"

[dependencies]
prost = "0.14.1"
tonic = "0.14.2"
tonic-prost = "0.14.2"
tonic-prost-build = { version = "0.14.2" }
tokio-stream = "0.1"

[build-dependencies]
tonic-prost-build = { version = "0.14.2" }
22 changes: 7 additions & 15 deletions groupcache-pb/build.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
// skip codegen if there hasn't been any updates.
if true {
return Ok(());
// Generated code is checked in so that users don't need protoc installed.
// To regenerate after updating the .proto or bumping tonic/prost versions,
// change `false` to `true` below and run `cargo build -p groupcache-pb`.
if false {
tonic_prost_build::configure()
.out_dir("src/")
.compile_protos(&["protos/groupcache.proto"], &["protos/"])?;
}

let current_dir = std::env::current_dir()?;
if !current_dir.ends_with("groupcache-pb") {
return Err(format!(
"must be run from the root of the crate, instead was {:#?}",
current_dir
)
.into());
}

tonic_prost_build::configure()
.out_dir("src/")
.compile_protos(&["protos/groupcache.proto"], &["protos/"])?;
Ok(())
}
7 changes: 7 additions & 0 deletions groupcache-pb/protos/groupcache.proto
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ message RemoveRequest {

message RemoveResponse {}

message WatchRequest {}

message InvalidationEvent {
string key = 1;
}

service Groupcache {
rpc Get(GetRequest) returns (GetResponse) {};
rpc Remove(RemoveRequest) returns (RemoveResponse) {};
rpc WatchInvalidations(WatchRequest) returns (stream InvalidationEvent) {};
}
Loading