kvcache-ai / kvcache-ai/Mooncake

[RFC]: Consolidate Master Metadata Routing

Open
#2,706 2 comments 0 reactions 0 assignees View on GitHub
RFC
Dominant language
C++
Stars
6.6k
Forks
1.2k
Avg merge
3d 5h
Merged PRs (30d)
312

Description

### Changes proposed

# RFC: Consolidate Master Metadata Routing

## Summary

`MasterService` should keep the current tenant-isolated metadata layout:

```text
metadata_shards_[shard].tenants[tenant_id].metadata[user_key]
```

This preserves the strong tenant boundary: each tenant owns an independent `TenantState`, and duplicate `user_key` values across tenants are naturally isolated.

The proposed change is not to flatten tenant storage. Instead, it is to consolidate routing and per-object access behind a canonical object identity:

```text
ObjectIdentity{tenant_id, user_key}
-> MetadataRoute{shard, tenant_state, object_state}
-> ObjectState{metadata, processing state, tasks, group state}
```

The goal is to get most of the simplicity of a flat metadata model without weakening tenant isolation. Object operations should use one routing/accessor path and then read or mutate object-scoped state from one colocated object entry.

## Motivation

The current lookup path is `tenant_id + user_key -> shard -> TenantState -> metadata[user_key]`. This tenant layer is useful and should remain, but object-scoped state is spread across several tenant-local maps such as `metadata`, `processing_keys`, `replication_tasks`, `offloading_tasks`, `promotion_tasks`, and `group_members`.

That means a single object operation can repeat the same `user_key` lookup across multiple maps after it has already found the shard and tenant. It also makes routing logic easy to duplicate across call sites.

The master already has `ObjectIdentity` and `MetadataAccessorRO/RW`. This RFC proposes making that route/accessor layer the single way to reach object metadata and related state, instead of open-coding the route at individual call sites.

## Goals

- Preserve `MetadataShard::tenants[tenant_id]` as the isolation boundary.
- Use `ObjectIdentity{tenant_id, user_key}` as the canonical input to routing and accessors.
- Consolidate shard selection, tenant lookup, tenant creation, and per-object lookup in one route/accessor path.
- Colocate object-scoped state inside a per-object `ObjectState` so common operations avoid several tenant-local map lookups.
- Keep public APIs compatible: callers may continue passing `tenant_id` and `user_key` separately.
- Preserve single-tenant mode by normalizing requests to the `default` tenant when multi-tenant mode is disabled.
- Keep snapshot compatibility with the existing tenant-nested shape.

## Non-Goals

- Do not flatten `MetadataShard::tenants`.
- Do not remove tenant semantics from Mooncake Store.
- Do not require globally unique user keys.
- Do not redesign tenant quota policy storage.
- Do not introduce auth or access control.

## Proposed Model

Keep the shard and tenant structure:

```cpp
struct MetadataShard {
mutable SharedMutex mutex;
std::unordered_map tenants GUARDED_BY(mutex);
long disk_object_count GUARDED_BY(mutex) = 0;
};
```

Use `ObjectIdentity` as the canonical routing key:

```cpp
struct ObjectIdentity {
std::string tenant_id;
std::string user_key;
};
```

Normalize tenant IDs at the API boundary:

```cpp
ObjectIdentity MasterService::MakeObjectIdentityForRequest(
const std::string& user_key,
const std::string& tenant_id) const {
return {NormalizeRequestTenantId(tenant_id), user_key};
}
```

Inside each `TenantState`, aggregate per-object state by `user_key`:

```cpp
struct ObjectState {
std::optional metadata;
bool processing = false;
std::optional replication_task;
std::optional offloading_task;
std::optional promotion_task;
std::optional group_id;
};

struct TenantState {
std::unordered_map objects;
std::unordered_map>
group_members; // group_id -> set of user keys in this tenant
};
```

This keeps tenant isolation explicit while avoiding parallel lookups in `metadata`, `processing_keys`, `replication_tasks`, `offloading_tasks`, and `promotion_tasks`.

## Routing and Access

Object operations should use one route:

```text
request tenant_id + user_key
-> NormalizeRequestTenantId()
-> ObjectIdentity{tenant_id, user_key}
-> getMetadataShardIndex(object_id)
-> shard.tenants[object_id.tenant_id]
-> tenant_state.objects[object_id.user_key]
-> ObjectState
```

`MetadataAccessorRO/RW` should own this route. Call sites should not manually repeat shard lookup, tenant lookup, and object-state lookup unless they are doing bulk iteration.

For compatibility, the `default` tenant may keep the current `hash(user_key)` routing. Other tenants should combine tenant and key when choosing the shard.

## Tenant-Aware Behavior

Tenant-aware subsystems keep using tenant ID explicitly:

- Quota remains keyed by tenant ID. Create/delete paths take the tenant from `object_id.tenant_id`.
- Metrics continue aggregating by tenant.
- Tenant admin operations can still find and remove an entire `TenantState` per shard.
- Snapshot/restore can keep the existing tenant-nested shape.

## Migration Plan

1. Keep `MetadataShard::tenants` and `TenantState` as the storage boundary.
2. Introduce `ObjectState` inside `TenantState`.
3. Update `MetadataAccessorRO/RW` to route from `ObjectIdentity` to one `ObjectState`.
4. Move `metadata`, `processing_keys`, `replication_tasks`, `offloading_tasks`, and `promotion_tasks` into `ObjectState` fields.
5. Keep `group_members` tenant-local, but store the per-object group link in `ObjectState::group_id`.
6. Update iteration paths: quota reconciliation, eviction, snapshot, admin remove/list, and cache accounting.
7. Remove the old parallel tenant-local maps after all call sites use `ObjectState`.

## Testing Plan

- Same `user_key` under two tenants maps to two isolated `TenantState` entries.
- Single-tenant mode still normalizes to `default`.
- Object create/delete quota accounting remains correct.
- Tenant remove/list operations still work without scanning a global object map.
- Snapshot/restore preserves tenant-nested metadata.
- Group IDs are isolated per tenant.
- Replication/offloading/promotion state is found through the same `ObjectState` as metadata.
- Eviction and disk-object accounting remain correct after object deletion.

## Risks

- A partial migration could create a hybrid model where metadata is in `ObjectState` but tasks still live in parallel maps, losing the lookup reduction.
- `ObjectState` must not blur tenant boundaries; it should always live under one `TenantState`.
- Existing snapshot code may need a compatibility layer if `TenantState` serialization changes from parallel maps to `objects[user_key] -> ObjectState`.

## Conclusion

Keep the tenant-nested metadata layout for strong isolation, but make `ObjectIdentity` and `MetadataAccessorRO/RW` the only normal route into object state. Within each `TenantState`, consolidate per-object metadata, processing flags, and tasks into `ObjectState` so common object operations get a flat-access experience without giving up tenant isolation.

### Before submitting a new issue...

- [x] Make sure you already searched for relevant issues and read the [documentation](https://kvcache-ai.github.io/Mooncake/)

Contributor guide

Open the contributing guide

Research direction

Start by locating MasterService and MetadataAccessorRO/RW, then review the proposed ObjectIdentity, ObjectState, and TenantState relationships. Compare the migration and testing plans against the current routing, iteration, snapshot, quota, group, and task paths; done means tenant isolation, default-tenant normalization, compatibility, accounting, and state access all remain correct.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
backend, databases, distributed-systems
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.