Skip to main content

v0.12.0 Release Notes

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

Metricv0.11.0 (single tenant)v0.12.0 (32 tenants)Description
Tenants per cluster11-256Hard isolation
Inter-tenant latency interferenceN/A< 2%CPU isolation
Resource utilization22%78%Shared improvement
Tenant creation timeN/A120msHot path
Quota validation latencyN/A8μsKernel-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

LevelIsolation ObjectImplementation MechanismStrength
L1 NamespaceTopology/Agent/TaskPrefix routingLogical isolation
L2 ResourceCPU/Memory/Storagecgroup + quotasHard isolation
L3 PermissionAPI/OperationsRBAC + ABACMandatory access
L4 DataTime series/Snapshots/LogsRow-level encryption + viewsEncrypted 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 PrefixTenantResource CountRouting Latency
gpagrid-province-a482301.2μs
vppvirtual-pp-112400.8μs
indindustrial-user-75600.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

ResourceStandard TenantLarge TenantSmall Tenant
CPU cores8322
Memory16GB64GB4GB
Agent count645128
Time series writes100,000 points/sec1,000,000 points/sec10,000 points/sec
Storage space128GB2TB16GB

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 TypeIsolation MethodEncryptionKey Management
Time series dataTenant prefix + row-levelAES-256-GCMIndependent key per tenant
Topology snapshotsNamespaceAES-256-GCMIndependent key per tenant
Audit logsTenant prefixAES-256-GCMIndependent key per tenant
Agent stateNamespaceOptional encryptionTenant 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-metrics submodule, exposing metrics by tenant dimension
  • Quota Alerts: Supports configuring quota usage thresholds, auto-alerting when thresholds are reached

Bug Fixes

  • Fixed eneros-agent context residue during tenant switching (#1208)
  • Fixed eneros-timeseries cross-tenant query leaking data when index misses (#1215)
  • Fixed eneros-topology not reporting errors on namespace prefix conflicts (#1221)
  • Fixed eneros-sched quota validation occasional escape during rapid task creation (#1227)

Breaking Changes

  • Agent::new signature change: Added tenant: TenantId parameter; original signature moved to Agent::new_standalone
  • Topology::create_bus: First parameter changed to Namespace, original BusId parameter order adjusted
  • Timeseries::query: Defaults to current tenant scope; cross-tenant queries require query_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