EnerOS v0.12.0
Release Date: December 1, 2024 Codename: Tenant Git Tag: v0.12.0 Support Status: Stable Total Crates: 26 (4 new) Test Cases: 3580+ (480 new)
Version Overview
EnerOS v0.12.0 “Tenant” is a key version for EnerOS targeting “multi-entity sharing” scenarios. The core goal of this version is to introduce Multi-Tenant Isolation capabilities, enabling a single EnerOS cluster to safely host workloads from multiple independent power entities (such as provincial grid companies, electricity sales companies, virtual power plant operators, and large industrial users), with complete isolation between tenants across four layers: namespace, resource quotas, permission model, and data access.
In the context of electricity market reform and new power system construction, more and more entities need to share the same scheduling and operations infrastructure — for example, a provincial power trading center may simultaneously provide declaration services for 30+ electricity sales companies, and virtual power plant operators need to aggregate hundreds of distributed resources. The traditional approach is to deploy independent clusters for each entity, which has problems such as high cost, low resource utilization, and difficulty in cross-entity collaboration. v0.12.0 provides kernel-level multi-tenant support, enabling a single cluster to safely host tens to hundreds of tenants, improving resource utilization by 4-6x.
This version introduces four new crates: eneros-tenant (tenant management), eneros-tenant-quota (resource quotas), eneros-tenant-rbac (tenant-level RBAC), and eneros-tenant-isolation (data isolation). The design philosophy is “power responsibility boundary is tenant boundary” — tenant division strictly aligns with the power system’s safety responsibility areas, dispatch jurisdiction, and asset ownership, ensuring cross-tenant operations cannot bypass power safety regulations.
Key Data
| Metric | v0.11.0 (single tenant) | v0.12.0 (32 tenants) | Description |
|---|---|---|---|
| Tenants per cluster | 1 | 1-256 | Hard isolation |
| Inter-tenant latency interference | N/A | < 2% | CPU isolation |
| Resource utilization | 22% | 78% | Shared improvement |
| Tenant creation time | N/A | 120ms | Hot path |
| Quota validation latency | N/A | 8μs | Kernel-level |
New Features
1. Multi-Tenant Isolation Architecture
Introduces the eneros-tenant crate, providing complete tenant lifecycle management. Each tenant has an independent namespace, resource quotas, permission policies, and data view; tenants are completely isolated by default and can only collaborate through explicit authorization.
Tenant Model
use eneros_tenant::{Tenant, TenantId, TenantConfig};
// Create a provincial grid tenant
let tenant = Tenant::new(TenantId::from("grid-province-a"))
.config(TenantConfig {
display_name: "Province A Power Company".into(),
namespace_prefix: "gpa".into(),
max_agents: 256,
max_topology_nodes: 50000,
max_timeseries_throughput: 1_000_000, // points/sec
storage_quota: 512 * 1024 * 1024 * 1024, // 512GB
isolation_level: IsolationLevel::Strict,
})?;
// Activate tenant
tenant.activate().await?;
Isolation Levels
| Level | Isolation Object | Implementation Mechanism | Strength |
|---|---|---|---|
| L1 Namespace | Topology/Agent/Task | Prefix routing | Logical isolation |
| L2 Resource | CPU/Memory/Storage | cgroup + quotas | Hard isolation |
| L3 Permission | API/Operations | RBAC + ABAC | Mandatory access |
| L4 Data | Time series/Snapshots/Logs | Row-level encryption + views | Encrypted isolation |
2. Namespace Isolation
Each tenant has an independent namespace; all resources (topology nodes, Agents, tasks, time series data) belong to a specific namespace, and cross-namespace access requires explicit authorization. Namespaces are implemented through prefix routing, avoiding performance overhead from global lookups.
Namespace Operations
use eneros_tenant::Namespace;
// Create topology under tenant namespace
let bus = topology.create_bus(
Namespace::of("grid-province-a"),
BusId::from(1),
BusConfig { voltage_level: 110, base_kv: 110.0 },
)?;
// Cross-namespace access requires authorization
let cross = topology.access(
Namespace::of("grid-province-a"),
BusId::from(1),
AccessScope::Read,
).authorize(&admin_token)?;
// Query automatically limited to current tenant namespace
let buses = topology.list_buses()
.in_namespace(Namespace::of("grid-province-a"))
.execute()?;
Namespace Routing Table
| Namespace Prefix | Tenant | Resource Count | Routing Latency |
|---|---|---|---|
| gpa | grid-province-a | 48230 | 1.2μs |
| vpp | virtual-pp-1 | 1240 | 0.8μs |
| ind | industrial-user-7 | 560 | 0.6μs |
3. Resource Quota Management
Introduces the eneros-tenant-quota crate for fine-grained quota management of each tenant’s CPU, memory, storage, Agent count, time series write throughput, and other resources. Quota validation is completed on the kernel hot path with latency below 10μs.
Quota Configuration
use eneros_tenant_quota::{Quota, QuotaConfig, Resource};
let quota = Quota::for_tenant(TenantId::from("vpp-1"))
.limit(Resource::Cpu, QuotaConfig::cores(8))
.limit(Resource::Memory, QuotaConfig::bytes(16 * 1024 * 1024 * 1024))
.limit(Resource::Agents, QuotaConfig::count(64))
.limit(Resource::TimeseriesWrite, QuotaConfig::per_second(100_000))
.limit(Resource::Storage, QuotaConfig::bytes(128 * 1024 * 1024 * 1024))
.burst_allowance(0.2) // Allow 20% burst
.enforce_mode(EnforceMode::Hard);
Quota Validation
// Validate quota when creating Agent
match quota.check_and_reserve(Resource::Agents, 1) {
Ok(reservation) => {
let agent = Agent::new(...).spawn()?;
reservation.commit();
}
Err(QuotaError::Exceeded { current: 64, limit: 64 }) => {
return Err("Tenant Agent count has reached the limit");
}
}
Default Resource Quotas
| Resource | Standard Tenant | Large Tenant | Small Tenant |
|---|---|---|---|
| CPU cores | 8 | 32 | 2 |
| Memory | 16GB | 64GB | 4GB |
| Agent count | 64 | 512 | 8 |
| Time series writes | 100,000 points/sec | 1,000,000 points/sec | 10,000 points/sec |
| Storage space | 128GB | 2TB | 16GB |
4. Tenant-level RBAC
Introduces the eneros-tenant-rbac crate, maintaining independent role and permission models for each tenant. Permission validation considers both tenant boundaries and role boundaries, preventing cross-tenant unauthorized access.
Role Definition
use eneros_tenant_rbac::{Role, Permission, Scope};
let dispatcher_role = Role::for_tenant(TenantId::from("grid-province-a"))
.name("dispatcher")
.permission(Permission::new("topology:read"))
.permission(Permission::new("dispatch:command"))
.permission(Permission::new("agent:manage"))
.scope(Scope::TenantOnly); // Limited to this tenant only
let observer_role = Role::for_tenant(TenantId::from("grid-province-a"))
.name("observer")
.permission(Permission::new("topology:read"))
.permission(Permission::new("timeseries:read"))
.scope(Scope::TenantOnly);
Permission Validation
// Validate user permissions under a specific tenant
let ctx = TenantContext::new(user_id, TenantId::from("grid-province-a"));
if ctx.has_permission("dispatch:command")? {
ctx.syscall(DispatchCommand { ... }).await?;
} else {
return Err(PermissionDenied);
}
// Cross-tenant operations require explicit cross-tenant authorization
ctx.cross_tenant(TenantId::from("vpp-1"))
.require_permission("topology:read")?;
5. Data Isolation
Introduces the eneros-tenant-isolation crate for row-level isolation of time series data, topology snapshots, and audit logs. Each tenant’s data can physically share storage while being completely logically isolated, and supports independent per-tenant encryption.
Time Series Data Isolation
use eneros_tenant_isolation::TenantTimeseries;
// Automatically tag tenant on write
let ts = TenantTimeseries::for_tenant(TenantId::from("grid-province-a"));
ts.write(Point::new("bus.1.voltage", 1.024).at(now())).await?;
// Automatically filter on query
let result = ts.query()
.metric("bus.1.voltage")
.range(now() - Duration::minutes(30), now())
.execute()?; // Returns only this tenant's data
Data Encryption Strategy
| Data Type | Isolation Method | Encryption | Key Management |
|---|---|---|---|
| Time series data | Tenant prefix + row-level | AES-256-GCM | Independent key per tenant |
| Topology snapshots | Namespace | AES-256-GCM | Independent key per tenant |
| Audit logs | Tenant prefix | AES-256-GCM | Independent key per tenant |
| Agent state | Namespace | Optional encryption | Tenant self-managed |
Improvements
- API Gateway: All REST/GraphQL APIs automatically inject tenant context, no application-layer handling needed
- Audit Logs: All operations automatically record tenant identifiers, supporting per-tenant retrieval
- Observability: Added
eneros-tenant-metricssubmodule, exposing metrics by tenant dimension - Quota Alerts: Supports configuring quota usage thresholds, auto-alerting when thresholds are reached
Bug Fixes
- Fixed
eneros-agentcontext residue during tenant switching (#1208) - Fixed
eneros-timeseriescross-tenant query leaking data when index misses (#1215) - Fixed
eneros-topologynot reporting errors on namespace prefix conflicts (#1221) - Fixed
eneros-schedquota validation occasional escape during rapid task creation (#1227)
Breaking Changes
Agent::newsignature change: Addedtenant: TenantIdparameter; original signature moved toAgent::new_standaloneTopology::create_bus: First parameter changed toNamespace, originalBusIdparameter order adjustedTimeseries::query: Defaults to current tenant scope; cross-tenant queries requirequery_cross_tenant
Performance Improvements
- Tenant creation time reduced from 800ms to 120ms (6.7x improvement)
- Quota validation latency reduced from 45μs to 8μs (5.6x improvement)
- Namespace routing latency stable within 1.2μs
- Single cluster tenant capacity increased from 8 to 256
Contributors
This version was completed by 21 contributors with 358 commits. Special thanks to:
- @tenant-core: Tenant model and lifecycle management
- @quota-master: Kernel-level quota validation engine
- @rbac-arch: Tenant-level RBAC permission model
- @crypto-isolation: Row-level data isolation and encryption scheme
Upgrade Guide
Upgrading from v0.11.0
1. Update Dependencies
# Cargo.toml
[dependencies]
eneros-tenant = { version = "0.12" }
eneros-tenant-quota = { version = "0.12" }
eneros-tenant-rbac = { version = "0.12" }
eneros-tenant-isolation = { version = "0.12", features = ["encryption"] }
2. Initialize Default Tenant
After upgrade, create a default tenant and migrate existing resources:
use eneros_tenant::migrate;
// Automatically migrate all v0.11.0 resources to the "default" tenant
migrate::from_single_tenant(TenantId::from("default")).await?;
3. Adapt to New API
// v0.11.0 old style
let agent = Agent::new("my-agent").spawn()?;
let bus = topology.create_bus(1, BusConfig { ... })?;
// v0.12.0 new style
let agent = Agent::new("my-agent", TenantId::from("default")).spawn()?;
let bus = topology.create_bus(
Namespace::of("default"),
BusId::from(1),
BusConfig { ... },
)?;
4. Configure Quotas
Configure reasonable quotas for the default tenant to prevent a single tenant from exhausting cluster resources:
# eneros.toml
[tenant.default]
cpu_cores = 32
memory = "64GB"
max_agents = 512
timeseries_write_per_second = 1000000
storage = "2TB"
5. Data Encryption (Optional)
To enable tenant-level data encryption, configure a key management service:
[tenant.encryption]
enabled = true
algorithm = "aes-256-gcm"
kms_endpoint = "https://kms.internal:8200"
key_rotation_days = 90