# PlasmaVMC ChainFire Key Schema **Date:** 2025-12-08 **Task:** T013 S1 **Status:** Design Complete ## Key Layout ### VM Metadata ``` Key: /plasmavmc/vms/{org_id}/{project_id}/{vm_id} Value: JSON-serialized VirtualMachine (plasmavmc_types::VirtualMachine) ``` ### VM Handle ``` Key: /plasmavmc/handles/{org_id}/{project_id}/{vm_id} Value: JSON-serialized VmHandle (plasmavmc_types::VmHandle) ``` ### Lock Key (for atomic operations) ``` Key: /plasmavmc/locks/{org_id}/{project_id}/{vm_id} Value: JSON-serialized LockInfo { timestamp: u64, node_id: String } TTL: 30 seconds (via ChainFire lease) ``` ## Key Structure Rationale 1. **Prefix-based organization**: `/plasmavmc/` namespace isolates PlasmaVMC data 2. **Tenant scoping**: `{org_id}/{project_id}` ensures multi-tenancy 3. **Resource separation**: Separate keys for VM metadata and handles enable independent updates 4. **Lock mechanism**: Uses ChainFire lease TTL for distributed locking without manual cleanup ## Serialization - **Format**: JSON (via `serde_json`) - **Rationale**: Human-readable, debuggable, compatible with existing `PersistedState` structure - **Alternative considered**: bincode (rejected for debuggability) ## Atomic Write Strategy ### Option 1: Transaction-based (Preferred) Use ChainFire transactions to atomically update VM + handle: ```rust // Pseudo-code let txn = TxnRequest { compare: vec![Compare { key: lock_key, result: CompareResult::Equal, target: CompareTarget::Version(0), // Lock doesn't exist }], success: vec![ RequestOp { request: Some(Request::Put(vm_put)) }, RequestOp { request: Some(Request::Put(handle_put)) }, RequestOp { request: Some(Request::Put(lock_put)) }, ], failure: vec![], }; ``` ### Option 2: Lease-based Locking (Fallback) 1. Acquire lease (30s TTL) 2. Put lock key with lease_id 3. Update VM + handle 4. Release lease (or let expire) ## Fallback Behavior ### File Fallback Mode - **Trigger**: `PLASMAVMC_STORAGE_BACKEND=file` or `PLASMAVMC_CHAINFIRE_ENDPOINT` unset - **Behavior**: Use existing file-based persistence (`PLASMAVMC_STATE_PATH`) - **Locking**: File-based lockfile (`{state_path}.lock`) with `flock()` or atomic rename ### Migration Path 1. On startup, if ChainFire unavailable and file exists, load from file 2. If ChainFire available, prefer ChainFire; migrate file → ChainFire on first write 3. File fallback remains for development/testing without ChainFire cluster ## Configuration ### Environment Variables - `PLASMAVMC_STORAGE_BACKEND`: `chainfire` (default) | `file` - `PLASMAVMC_CHAINFIRE_ENDPOINT`: ChainFire gRPC endpoint (e.g., `http://127.0.0.1:50051`) - `PLASMAVMC_STATE_PATH`: File fallback path (default: `/var/run/plasmavmc/state.json`) - `PLASMAVMC_LOCK_TTL_SECONDS`: Lock TTL (default: 30) ### Config File (Future) ```toml [storage] backend = "chainfire" # or "file" chainfire_endpoint = "http://127.0.0.1:50051" state_path = "/var/run/plasmavmc/state.json" lock_ttl_seconds = 30 ``` ## Operations ### Create VM 1. Generate `vm_id` (UUID) 2. Acquire lock (transaction or lease) 3. Put VM metadata key 4. Put VM handle key 5. Release lock ### Update VM 1. Acquire lock 2. Get current VM (verify exists) 3. Put updated VM metadata 4. Put updated handle (if changed) 5. Release lock ### Delete VM 1. Acquire lock 2. Delete VM metadata key 3. Delete VM handle key 4. Release lock ### Load on Startup 1. Scan prefix `/plasmavmc/vms/{org_id}/{project_id}/` 2. For each VM key, extract `vm_id` 3. Load VM metadata 4. Load corresponding handle 5. Populate in-memory DashMap ## Error Handling - **ChainFire unavailable**: Fall back to file mode (if configured) - **Lock contention**: Retry with exponential backoff (max 3 retries) - **Serialization error**: Log and return error (should not happen) - **Partial write**: Transaction rollback ensures atomicity ## Testing Considerations - Unit tests: Mock ChainFire client - Integration tests: Real ChainFire server (env-gated) - Fallback tests: Disable ChainFire, verify file mode works - Lock tests: Concurrent operations, verify atomicity