Skip to content

Commit 2e7cc5d

Browse files
authored
Merge pull request #6 from worka-ai/feature/caching
feat: implement global namespace with caching and smart S3 routing
2 parents 488203e + 92dc113 commit 2e7cc5d

21 files changed

Lines changed: 600 additions & 227 deletions

Cargo.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 60 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,93 @@
1-
# Anvil: OpenSource Object Storage in Rust
1+
# Anvil: An Open-Source Object Store for AI/ML Research
22

3-
**Anvil** is an open‑source, S3‑compatible object storage server written in Rust. Built by the team behind Worka, Anvil is designed to host large files—such as open‑source model weights—with high performance and reliability. It exposes a familiar S3 HTTP gateway, a high‑performance gRPC API, multi‑tenant isolation, and the ability to scale from a single development node to a multi‑region cluster.
3+
[![Build Status](https://github.com/worka-ai/anvil-enterprise/actions/workflows/ci.yml/badge.svg)](https://github.com/worka-ai/anvil-enterprise/actions/workflows/ci.yml)
4+
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
5+
[![JOSS Submission](https://joss.theoj.org/papers/10.21105/joss.XXXXX/status.svg)](https://joss.theoj.org/papers/10.21105/joss.XXXXX)
46

5-
---
6-
7-
## 🔥 Why Anvil?
8-
9-
- **Written in Rust**: Modern, memory-safe, and highly concurrent.
10-
- **S3-Compatible**: Works out of the box with AWS SDKs, CLI, and third-party tools.
11-
- **gRPC API**: For low-latency, high-throughput access.
12-
- **Multi-Tenant**: Serve different model groups or clients in isolation.
13-
- **Clusterable**: Run standalone or as a horizontally-scalable distributed system.
14-
- **Model Hosting Friendly**: Built to serve billions of tokens efficiently.
7+
**Anvil** is a high-performance, open-source distributed object store built in Rust. It is designed to address the data management and storage challenges inherent in modern computational research, particularly for large-scale Artificial Intelligence (AI) and Machine Learning (ML) workloads. By providing an S3-compatible interface, a native high-throughput gRPC API, and first-class support for content-addressing, Anvil serves as a foundational infrastructure layer for reproducible and efficient research.
158

169
---
1710

18-
## 🚀 Quick Start (Standalone)
11+
## Key Features
1912

20-
```bash
21-
cargo install anvil
22-
anvil server --root ./data --port 9000
23-
```
24-
25-
Now test it:
26-
27-
```bash
28-
aws --endpoint-url http://localhost:9000 s3 ls
29-
```
13+
- **Content-Addressable Storage:** Automatically deduplicates identical data using BLAKE3 hashing, dramatically reducing storage costs for versioned models and datasets.
14+
- **High-Performance gRPC Streaming:** A native gRPC API with bidirectional streaming, ideal for high-throughput ML data loaders that feed GPUs directly from storage.
15+
- **S3-Compatible Gateway:** Provides drop-in compatibility with the vast ecosystem of existing research tools and SDKs that support the S3 API (Boto3, MLflow, Rclone, etc.).
16+
- **Built for the ML Ecosystem:** Includes features like the `anvil hf ingest` command to import model repositories directly from the Hugging Face Hub.
17+
- **Modern, Resilient Architecture:** Built in Rust for memory safety and high concurrency, with a SWIM-like gossip protocol over QUIC for clustering and failure detection.
18+
- **Multi-Tenant by Design:** Provides strong logical isolation between different users, teams, or projects.
3019

3120
---
3221

33-
## 🧪 Example: Upload and Fetch via S3
22+
## 🚀 Quick Start
3423

35-
```bash
36-
# Upload a file
37-
aws --endpoint-url http://localhost:9000 s3 cp weights.gguf s3://mymodels/weights.gguf
24+
The fastest way to get a single-node Anvil instance running is with Docker Compose.
3825

39-
# Fetch the file
40-
curl http://localhost:9000/mymodels/weights.gguf
41-
```
42-
43-
---
44-
45-
## 🏗️ Building From Source
26+
1. **Save the `docker-compose.yml`:**
27+
Save the example `docker-compose.yml` from the [Getting Started Guide](./docs/01-getting-started.md) to a local file.
4628

47-
Anvil uses [Rust](https://www.rust-lang.org/tools/install) and requires at least version 1.72.
29+
2. **Launch Anvil:**
30+
```bash
31+
docker-compose up -d
32+
```
4833

49-
```bash
50-
git clone https://github.com/worka-ai/anvil
51-
cd anvil
52-
cargo build --release
53-
```
54-
55-
---
34+
3. **Create Your First Tenant and App:**
35+
Use the `admin` tool to create a tenant and an app with API credentials.
36+
```bash
37+
# Create a region and a tenant
38+
docker compose exec anvil1 admin region create europe-west-1
39+
docker compose exec anvil1 admin tenant create my-first-tenant
5640
57-
## ⚙️ Running in Cluster Mode
41+
# Create an app and save the credentials
42+
docker compose exec anvil1 admin app create --tenant-name my-first-tenant --app-name my-cli-app
43+
```
5844

59-
Start multiple nodes with a shared cluster config (see [docs](https://worka.ai/docs/anvil/operational-guide/scaling)).
60-
61-
---
62-
63-
## 📡 gRPC API
64-
65-
See full [API reference](https://worka.ai/docs/anvil/user-guide/grpc-api). Example client use:
66-
67-
```bash
68-
anvil grpc-client --list-buckets
69-
```
70-
71-
---
72-
73-
## 🔐 Authentication
74-
75-
Supports API key-based tenant isolation. See [Auth docs](https://worka.ai/docs/anvil/user-guide/auth-permissions).
45+
4. **Configure the Anvil CLI:**
46+
Use the credentials from the previous step to configure your local `anvil` CLI.
47+
```bash
48+
anvil configure --host http://localhost:50051 --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET
49+
```
7650

7751
---
7852

7953
## 📘 Documentation
8054

81-
- [Getting Started](https://worka.ai/docs/anvil/getting-started)
82-
- [Deployment](https://worka.ai/docs/anvil/operational-guide/deployment)
83-
- [S3 Gateway](https://worka.ai/docs/anvil/user-guide/s3-gateway)
84-
- [Cluster Scaling](https://worka.ai/docs/anvil/operational-guide/scaling)
85-
- [Contributing](https://worka.ai/docs/anvil/developer-guide/contributing)
55+
For complete guides on deployment, architecture, and usage, please see the [**Full Documentation**](./docs/index.md).
56+
57+
- [Getting Started](./docs/01-getting-started.md)
58+
- [Authentication & Permissions](./docs/03-user-guide-authentication.md)
59+
- [Using the S3 Gateway](./docs/04-user-guide-s3-gateway.md)
60+
- [Deployment Guide](./docs/06-operational-guide-deployment.md)
8661

8762
---
8863

8964
## 🤝 Contributing
9065

91-
We welcome PRs! Check out [CONTRIBUTING.md](https://worka.ai/docs/anvil/developer-guide/contributing) and start with [good first issues](https://github.com/worka-ai/anvil/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
66+
We welcome contributions of all kinds! Please read our [**Contributing Guide**](./CONTRIBUTING.md) to get started. All participation in the Anvil community is governed by our [**Code of Conduct**](./CODE_OF_CONDUCT.md).
9267

9368
---
9469

95-
## 📣 Community
96-
97-
- [Discord](https://discord.gg/uCWVg5STGh) — Chat with the team
98-
- [Product Hunt](https://www.producthunt.com/products/worka-anvil)
70+
## 📜 Citing Anvil
71+
72+
If you use Anvil in your research, please cite it. Once published in JOSS, a BibTeX entry will be provided here.
73+
74+
```bibtex
75+
@article{Anvil2025,
76+
doi = {10.21105/joss.XXXXX},
77+
url = {https://doi.org/10.21105/joss.XXXXX},
78+
year = {2025},
79+
publisher = {The Open Journal},
80+
volume = {X},
81+
number = {XX},
82+
pages = {XXXXX},
83+
author = {Your Name and Other Authors},
84+
title = {Anvil: An Open-Source Object Store for AI/ML Research},
85+
journal = {Journal of Open Source Software}
86+
}
87+
```
9988

10089
---
10190

10291
## License
10392

104-
Licensed under [Apache 2.0](LICENSE).
93+
Anvil is licensed under the [Apache 2.0 License](./LICENSE).

anvil-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ aes-gcm = "0.10.3"
107107
constant_time_eq = "0.4.2"
108108
http-body-util = "0.1.1"
109109
subtle = "2.6.1"
110+
moka = { version = "0.12.11", features = ["future"] }
110111

111112
[build-dependencies]
112113
tonic-prost-build = { version = "0.14.2" }

anvil-core/src/cache.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use crate::persistence::{Bucket, Tenant};
2+
use moka::future::Cache;
3+
use std::time::Duration;
4+
5+
#[derive(Clone, Debug)]
6+
pub struct MetadataCache {
7+
// (tenant_id, bucket_name) -> Bucket
8+
buckets: Cache<(i64, String), Bucket>,
9+
// bucket_name -> Bucket (for public/S3 lookups without tenant_id context initially)
10+
// This might need to handle conflicts if bucket names aren't globally unique, but
11+
// for S3 compat they should be. Assuming global uniqueness for now.
12+
buckets_by_name: Cache<String, Bucket>,
13+
14+
// api_key -> Tenant
15+
tenants: Cache<String, Tenant>,
16+
17+
// (app_id, resource, action) -> bool (authorized)
18+
// Or perhaps cache the list of policies?
19+
// Let's cache the policies list for an app as that's what `get_policies_for_app` returns.
20+
// app_id -> Vec<String> (policies)
21+
app_policies: Cache<i64, Vec<String>>,
22+
}
23+
24+
impl MetadataCache {
25+
pub fn new(config: &crate::config::Config) -> Self {
26+
let ttl = Duration::from_secs(config.metadata_cache_ttl_secs);
27+
Self {
28+
buckets: Cache::builder()
29+
.max_capacity(10_000)
30+
.time_to_live(ttl)
31+
.build(),
32+
buckets_by_name: Cache::builder()
33+
.max_capacity(10_000)
34+
.time_to_live(ttl)
35+
.build(),
36+
tenants: Cache::builder()
37+
.max_capacity(5_000)
38+
.time_to_live(ttl * 2)
39+
.build(),
40+
app_policies: Cache::builder()
41+
.max_capacity(5_000)
42+
.time_to_live(ttl)
43+
.build(),
44+
}
45+
}
46+
47+
pub async fn get_bucket(&self, tenant_id: i64, name: &str) -> Option<Bucket> {
48+
self.buckets.get(&(tenant_id, name.to_string())).await
49+
}
50+
51+
pub async fn insert_bucket(&self, tenant_id: i64, name: String, bucket: Bucket) {
52+
self.buckets.insert((tenant_id, name.clone()), bucket.clone()).await;
53+
self.buckets_by_name.insert(name, bucket).await;
54+
}
55+
56+
pub async fn invalidate_bucket(&self, tenant_id: i64, name: &str) {
57+
self.buckets.invalidate(&(tenant_id, name.to_string())).await;
58+
self.buckets_by_name.invalidate(name).await;
59+
}
60+
61+
// For when we only know the name (e.g. deleting by name, or cross-tenant lookup if allowed)
62+
pub async fn get_bucket_by_name_only(&self, name: &str) -> Option<Bucket> {
63+
self.buckets_by_name.get(name).await
64+
}
65+
66+
pub async fn invalidate_bucket_by_name(&self, name: &str) {
67+
self.buckets_by_name.invalidate(name).await;
68+
// Note: We can't easily invalidate the (tenant_id, name) key without scanning
69+
// or knowing the tenant_id. This is a trade-off.
70+
// For strict consistency, the caller should provide tenant_id if possible.
71+
// However, P2P events usually contain enough info.
72+
}
73+
74+
pub async fn get_tenant(&self, api_key: &str) -> Option<Tenant> {
75+
self.tenants.get(api_key).await
76+
}
77+
78+
pub async fn insert_tenant(&self, api_key: String, tenant: Tenant) {
79+
self.tenants.insert(api_key, tenant).await;
80+
}
81+
82+
pub async fn invalidate_tenant(&self, api_key: &str) {
83+
self.tenants.invalidate(api_key).await;
84+
}
85+
86+
pub async fn get_app_policies(&self, app_id: i64) -> Option<Vec<String>> {
87+
self.app_policies.get(&app_id).await
88+
}
89+
90+
pub async fn insert_app_policies(&self, app_id: i64, policies: Vec<String>) {
91+
self.app_policies.insert(app_id, policies).await;
92+
}
93+
94+
pub async fn invalidate_app_policies(&self, app_id: i64) {
95+
self.app_policies.invalidate(&app_id).await;
96+
}
97+
}
98+
99+

0 commit comments

Comments
 (0)