ADR-020: Session Entity Model Redesign¶
| Attribute | Value |
|---|---|
| Status | Accepted |
| Date | 2026-02-18 |
| Deciders | Architecture Team |
| Related ADRs | ADR-018 (LDS Integration), ADR-019 (LabRecord β partially superseded), ADR-021 (Child Entities), ADR-003 (CloudEvents) |
| Supersedes | ADR-019 Β§LabletLabBinding (binding model only) |
| Knowledge Refs | AD-38, AD-39, AD-42, AD-43, AD-44 |
Context¶
The original domain model had several entities whose responsibilities overlapped:
- LabletInstance: Represented a CML lab reservation + running session, but the name implied infrastructure rather than user-facing session semantics.
- LabletRecordRun: A join entity tracking the operational relationship between a LabletInstance and a LabRecord, with fields like
allocated_ports,started_at,ended_at, andduration_seconds. - LabletLabBinding: A join entity managing the many-to-many relationship between LabletInstance and LabRecord over time.
This three-entity model introduced:
- Naming confusion: "Instance" implied infrastructure; the actual concept is a user-facing session combining CML lab + LDS session + grading.
- Unnecessary indirection: LabletLabBinding managed many-to-many bindings, but the actual relationship is 1:1 at any point in time.
- Split state: Operational fields (ports, timing) lived in LabletRecordRun, forcing cross-entity lookups for common operations.
- Lifecycle fragmentation: LabletInstance and LabletRecordRun had separate status enums that had to be kept in sync.
With the introduction of LDS sessions, GradingEngine integration, and the Sessions UI, the model needed simplification.
Decision¶
1. Rename LabletInstance β LabletSession (AD-38)¶
LabletInstance is renamed to LabletSession. The session IS the top-level aggregate β there is no separate "Session" entity wrapping it. LabletSession represents the complete user experience: CML lab + LDS session + grading, all managed through a single lifecycle.
2. Eliminate LabletRecordRun and LabletLabBinding (AD-39, AD-42)¶
Both entities are eliminated entirely:
| Eliminated Entity | Absorbed By | Fields Moved |
|---|---|---|
| LabletRecordRun | LabletSession | allocated_ports, started_at, ended_at, duration_seconds |
| LabletLabBinding | LabletSession | lab_record_id (direct field) |
The lablet_lab_bindings MongoDB collection is dropped. The lablet_record_runs collection is dropped.
3. LabletSession-to-LabRecord: 1:1 Active Binding (AD-43)¶
LabletSession has a direct lab_record_id field β no join entity needed:
- A LabletSession references exactly one LabRecord at a time (1:1)
- Multiple LabletSessions may reference the same LabRecord over time (reuse via wipe-for-reuse pattern, ADR-024)
- The binding is set during SCHEDULING and is immutable for the session lifetime
- LabRecord remains an independent AggregateRoot (ADR-019) with its own lifecycle
@dataclass
class LabletSessionState:
# ... other fields
lab_record_id: str | None = None # Direct binding (was LabletLabBinding)
allocated_ports: dict[str, int] = field(default_factory=dict) # Absorbed from LabletRecordRun
started_at: datetime | None = None # Absorbed from LabletRecordRun
ended_at: datetime | None = None # Absorbed from LabletRecordRun
duration_seconds: float | None = None # Absorbed from LabletRecordRun
4. Consolidated Lifecycle (AD-44)¶
The LabletSession lifecycle merges the previous LabletInstanceStatus and LabletRecordRunStatus into a single enum:
PENDING β SCHEDULED β INSTANTIATING β READY β RUNNING β COLLECTING β GRADING β STOPPING β STOPPED β ARCHIVED
β
TERMINATED β (from any state, on explicit termination or error)
| Status | Description | Trigger |
|---|---|---|
PENDING |
Created, awaiting scheduling | User request |
SCHEDULED |
Worker assigned, ports allocated | resource-scheduler |
INSTANTIATING |
CML lab importing + LDS provisioning | lablet-controller |
READY |
Infrastructure ready, awaiting user login | lablet-controller |
RUNNING |
User actively using the lab session | LDS CloudEvent (lds.session.started) |
COLLECTING |
Assessment data collection in progress | Collect command or LDS CloudEvent (lds.session.ended) |
GRADING |
GradingEngine scoring in progress | lablet-controller β GradingSPI |
STOPPING |
Lab stopping + LDS archiving | lablet-controller |
STOPPED |
Lab stopped, resources partially released | lablet-controller |
ARCHIVED |
Session archived, score report finalized | lablet-controller |
TERMINATED |
Emergency/manual termination from any state | Admin action or error |
Key change: The READY state (between INSTANTIATING and RUNNING) explicitly tracks when infrastructure is fully provisioned but the user has not yet logged in. This enables user engagement metrics and no-show detection.
Rationale¶
Why rename to "Session"?¶
- The concept represents a user session (lab time + LDS + grading), not just an infrastructure instance.
- Aligns with LDS terminology (LabSession) and user-facing UI ("Sessions" page).
- Eliminates the confusion between "instance" (AWS EC2) and "instance" (lab reservation).
Why eliminate join entities?¶
- The actual relationship is 1:1 at runtime β many-to-many was over-engineered.
- Operational fields (ports, timing) belong on the session aggregate, not a satellite entity.
- Reduces the number of MongoDB collections from 5 to 3 (lablet_sessions, lab_records, lablet_definitions).
- Simplifies query patterns β no cross-collection joins needed for common operations.
Why a consolidated lifecycle?¶
- Users see a single session with one status, not separate infrastructure and operational states.
- Reduces state synchronization bugs between formerly separate entities.
- The READY state enables explicit tracking of the "infrastructure ready, waiting for user" window.
Consequences¶
Positive¶
- Simpler domain model: 1 aggregate instead of 3 entities for the core session concept
- Fewer collections: 2 collections dropped (
lablet_lab_bindings,lablet_record_runs) - Unified lifecycle: Single status enum replaces 2 separate status enums
- Better naming: "Session" aligns with user-facing semantics and LDS terminology
- Simpler queries: No cross-collection joins for session + ports + timing data
Negative¶
- Migration required: Existing data must be migrated from 3 collections to 1
- Breaking API changes: All
/api/lablet-instancesendpoints rename to/api/v1/sessions - CloudEvent type changes:
ccm.lablet.instance.*βccm.lablet.session.* - etcd key changes:
/lcm/instances/β/lcm/sessions/
Risks¶
- Migration script complexity for production data
- External systems consuming old CloudEvent types must be updated
Implementation Notes¶
Migration Checklist¶
- Create
lablet_sessionscollection with merged schema - Migrate data: LabletInstance + LabletRecordRun + LabletLabBinding β LabletSession
- Update etcd key prefixes:
/lcm/instances/β/lcm/sessions/ - Update all API endpoints
- Update CloudEvent type prefixes
- Drop old collections after verification
- Update UI to use new API endpoints
Entity Hierarchy (Post-Redesign)¶
LabletSession (AggregateRoot) β renamed from LabletInstance
βββ lab_record_id β absorbed from LabletLabBinding
βββ allocated_ports β absorbed from LabletRecordRun
βββ started_at, ended_at β absorbed from LabletRecordRun
βββ user_session_id β UserSession β see ADR-021
βββ grading_session_id β GradingSession β see ADR-021
βββ score_report_id β ScoreReport β see ADR-021
LabRecord (AggregateRoot) β unchanged (ADR-019)
LabletDefinition (AggregateRoot) β unchanged
Related Documents¶
- Lablet Resource Manager Architecture Β§3.3
- LabletSession Lifecycle Flow
- Architecture Overview β Data Flow diagram