Commit 17555178 authored by Dimitrios Gogos's avatar Dimitrios Gogos
Browse files

feat: Implement ADR-0036 to define SRM's ownership of async network capability...

feat: Implement ADR-0036 to define SRM's ownership of async network capability expiry and update related documentation
parent a9f60fa3
Loading
Loading
Loading
Loading
+118 −0
Changes for decisions/0036-srm-owns-network-capability-expiry.md: 118 added lines, 0 removed lines.
Original line number Diff line number Diff line
---
type: Decision Record
title: "ADR-0036: SRM owns expiry of async network capabilities and reports it as a backend-driven status change"
description: "A QoD session's requested duration is enforced by SRM, not by an OEG timer. SRM detects expiry and emits event.srm.operation.status carrying the reason in metadata; OEG maps it to the CAMARA DURATION_EXPIRED notification."
tags: [adr]
timestamp: 2026-08-05T00:00:00Z
status: "accepted"
---

# ADR-0036: SRM owns expiry of async network capabilities and reports it as a backend-driven status change

**Status:** accepted
**Date:** 2026-08-05
**Affects:** [srm/interface-contract.md](/srm/interface-contract.md), [srm/architecture.md](/srm/architecture.md), [oeg/persistence-model.md](/oeg/persistence-model.md), [oeg/interface-contract.md](/oeg/interface-contract.md)

## Context

A CAMARA QoD session carries a requested `duration`, and OEG returns `expiresAt` to the caller in
every `SessionInfo`. The CAMARA QoD spec makes expiry the **primary** way a session ends normally:
when the duration elapses the API consumer receives a `QOS_STATUS_CHANGED` event with
`qosStatus: UNAVAILABLE` and `statusInfo: DURATION_EXPIRED`.

Neither component implemented it. `DURATION_EXPIRED` appeared in OEG's type signatures and was
emitted from nowhere, and no component watched the clock — so `duration` and `expiresAt` were
promises the platform returned to callers and never kept. Sessions simply ran until the network
dropped them or the caller deleted them.

The choice was between OEG running a timer over `qod_sessions` and publishing a deactivate when
one lapses, or SRM owning it. Three things point at SRM:

- **The backend often already enforces it.** A NEF `AsSessionWithQoSSubscription` created with a
  duration expires on its own. An OEG timer would race the backend and, on redelivery or clock
  skew, issue a deactivate for a capability the network had already released.
- **SRM owns the realization lifecycle.** `capability_instance` state transitions
  (`active → terminated`) are SRM's to make. An OEG timer would drive a state change by
  publishing a command, leaving SRM's inventory correct only after a round trip.
- **SRM already runs periodic work.** Reconciliation (REQ-SRM-11) is a scheduled sweep over
  inventory; expiry is the same shape and needs no new machinery on the OEG side.

OEG owning a timer would also duplicate the authority: two components independently deciding a
session is over, with no tie-break.

## Decision

**SRM owns expiry of async network capabilities.** OEG runs no timer and holds no scheduled work
for `qod_sessions` or `traffic_influences`.

**Reporting contract.** SRM reports expiry through the existing backend-driven status-change path
(§C.3): it emits `event.srm.operation.status` reusing the **original activate `operation_id`**,
with the operational `state` and the reason in `metadata`:

```json
{
  "schema_version": "1.0",
  "operation_id": "<the activate operation_id>",
  "state": "completed",
  "capability": "qod_session",
  "metadata": { "qos_status": "UNAVAILABLE", "qos_status_info": "duration_expired" },
  "correlation_id": "…",
  "emitted_at": "2026-06-19T13:05:00Z"
}
```

`qos_status_info` is a new, optional metadata key distinguishing **why** a capability became
unavailable. Defined values are `duration_expired` and `network_terminated`. It is metadata, never
`state` — backend and API-family vocabulary does not enter SRM's operational enum. When absent,
consumers treat the change as `network_terminated`, which preserves today's behaviour.

**OEG's mapping.** OEG maps `metadata.qos_status_info` to CAMARA's `statusInfo` on the
`QOS_STATUS_CHANGED` notification: `duration_expired` → `DURATION_EXPIRED`,
`network_terminated` → `NETWORK_TERMINATED`. The third CAMARA value, `DELETE_REQUESTED`, is
OEG-generated on the delete path and never arrives from SRM (ADR-0034).

**Mechanism is SRM's choice.** This decision fixes the *event*, not how SRM arrives at it —
a scheduled sweep, a backend expiry notification, or reconciliation detecting a released
capability are all conforming implementations, and the right one differs per backend. OEG's
contract is only that the event arrives.

**Terminal state.** On expiry SRM terminates the `capability_instance` and, per the last-live-child
rule (§B.7), the session-holder `service_instance` with it. OEG moves its record to `UNAVAILABLE`.
The record is not deleted — `DELETE` remains the caller's action, and a lapsed session reads as
`UNAVAILABLE` until the caller removes it or it is aged out.

## Consequences

What becomes easier:

- `duration` and `expiresAt` become promises the platform keeps, and CAMARA's primary
  end-of-session notification is delivered.
- One authority decides a session is over, so there is no OEG-versus-backend race and no
  compensating logic for a deactivate that arrives after the network already released the session.
- OEG stays stateless with respect to time: no scheduler, no leader election across replicas, no
  clock-skew handling for a table it merely reflects.
- The reason is carried on a path that already exists — no new subject, no new event type, and
  consumers that ignore the new metadata key keep working.

What becomes harder:

- SRM must track the requested duration per realization. The value is already persisted in
  `capability_instance.parameters_snapshot` (`parameters.duration_seconds`), so no schema change
  is needed, but SRM must act on it rather than only record it.
- Expiry accuracy is bounded by whatever mechanism SRM chooses. A periodic sweep means the
  notification lands within one sweep interval of `expiresAt`, not exactly on it. CAMARA sets no
  precision requirement, but the interval should be small relative to the shortest supported
  duration.
- A backend that expires a session silently, without notifying SRM, is only detected on the next
  reconciliation pass. That is the same detection latency the drift model already accepts.

Deferred or excluded:

- `POST /sessions/{sessionId}/extend` is still unimplemented. When it lands, an accepted extension
  updates `parameters.duration_seconds` on the realization and moves the expiry point with it.
- No expiry-warning or grace-period notification ahead of the terminal event; CAMARA defines none.

Revisit trigger:

- A CAMARA release that specifies expiry precision, or an API family whose expiry semantics differ
  from "terminate and report" — for example one requiring auto-renewal.
+1 −0
Changes for decisions/index.md: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -38,3 +38,4 @@ the reasoning lives here. Never renumber; supersede instead (see the record-deci
* [ADR-0033](0033-async-network-capability-caller-id.md) - async network-capability caller-facing ids (QoD sessionId, trafficInfluenceID) are OEG-minted and adopted as service_instance.id; external_ref reverts to the realization ref
* [ADR-0034](0034-network-capability-delete-is-async.md) - network-capability delete is async via command.srm.network.capability.deactivate, and its completion is consumed to a terminal record state
* [ADR-0035](0035-qod-session-service-specification.md) - a QoD session references one platform-seeded service specification held in OEG config; profile names stay unvalidated, and QoS profiles are a catalog-layer concern (resolves F2)
* [ADR-0036](0036-srm-owns-network-capability-expiry.md) - SRM owns expiry of async network capabilities and reports it via event.srm.operation.status metadata (qos_status_info); OEG runs no timer and maps it to CAMARA DURATION_EXPIRED
+1 −2
Changes for oeg/interface-contract.md: 1 added line, 2 removed lines.
Original line number Diff line number Diff line
@@ -236,7 +236,7 @@ This section describes the async messages OEG sends and receives.
| Subject | Action |
|---|---|
| `event.srm.operation.completed` | Read `instances[]`; update `operations` and the pre-created row(s) — `app_instances` (single for `/appinstances`, array for `/deployments`, ADR-0005) or `qod_sessions` / `traffic_influences` (single, ADR-0033). For a **deactivate** operation the target record is resolved from the operation's own `metadata`, and the record moves to a terminal state (ADR-0034). Trigger callback(s) |
| `event.srm.operation.status` | Update in-flight/backend-driven state; map backend capability state changes to CAMARA notifications. Events carrying no state this family recognizes are ignored without warning — the subject is shared platform-wide |
| `event.srm.operation.status` | Update in-flight/backend-driven state; map backend capability state changes to CAMARA notifications. For async network capabilities, `metadata.qos_status` gives the new condition and `metadata.qos_status_info` the reason — `duration_expired` → CAMARA `DURATION_EXPIRED`, `network_terminated` (or absent) → `NETWORK_TERMINATED` (ADR-0036). Expiry is SRM-owned; OEG runs no timer. Events carrying no state this family recognizes are ignored without warning — the subject is shared platform-wide |
| `event.srm.catalog.updated` | Invalidate `catalog_cache_entries` or Redis keys |
| `event.srm.inventory.drift_detected` / `event.srm.inventory.drift_resolved` | Log and emit metrics; optional Portal notification later |

@@ -522,4 +522,3 @@ relying solely on the callback arriving.
| Version | Date | Summary |
|---------|------|---------|
| 2.0 | 2026-07-07 | OOP Release 2.0. |
 No newline at end of file
| 2.0 | 2026-08-05 | ADR-0033/ADR-0034: caller-facing ids for async network capabilities are OEG-minted and adopted as `service_instance_id`; capability reads via `GET /internal/network-capabilities`; §L.2 completion handling covers deactivate operations and the shared-subject ignore rule. |
+1 −1
Changes for oeg/persistence-model.md: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -69,7 +69,7 @@ A metering record for either synchronous or asynchronous API usage. Stored in `a
The retry and delivery history for outbound webhook notifications. Stored in `callback_deliveries`.

**QodSession**  
The OEG-side record of a CAMARA QoD session (`POST /sessions`). `session_id` is **OEG-minted at request time and is the value SRM adopts as the session-holder `service_instance.id`** (ADR-0033) — the same mint-and-adopt relationship `AppInstance` has with `appInstanceId` (ADR-0005), so the CAMARA `sessionId`, OEG's primary key, and SRM's runtime root id are one value. The row is written eagerly (`state=REQUESTED`) alongside the `command.srm.network.capability.activate` publish (`capability_type=qod_session`). `state` transitions `REQUESTED` → `AVAILABLE`|`UNAVAILABLE` when that operation's `event.srm.operation.completed` is consumed, and again on backend-driven `event.srm.operation.status` (§L.2). `DELETE /sessions/{id}` publishes a second, distinct `command.srm.network.capability.deactivate` operation keyed on the same id, moves the row to `DELETION_REQUESTED`, and drives it to `DELETED`/`ERROR` when that operation completes (ADR-0034). Not a duplicate of SRM inventory: the row carries identity, ownership and cached status; capability detail (canonical parameters and result) is read live from `GET /internal/network-capabilities/{service_instance_id}`, the same live-read split `AppInstance` uses. Stored in `qod_sessions`.
The OEG-side record of a CAMARA QoD session (`POST /sessions`). `session_id` is **OEG-minted at request time and is the value SRM adopts as the session-holder `service_instance.id`** (ADR-0033) — the same mint-and-adopt relationship `AppInstance` has with `appInstanceId` (ADR-0005), so the CAMARA `sessionId`, OEG's primary key, and SRM's runtime root id are one value. The row is written eagerly (`state=REQUESTED`) alongside the `command.srm.network.capability.activate` publish (`capability_type=qod_session`). `state` transitions `REQUESTED` → `AVAILABLE`|`UNAVAILABLE` when that operation's `event.srm.operation.completed` is consumed, and again on backend-driven `event.srm.operation.status` (§L.2) — including **expiry**, which SRM owns and reports with `metadata.qos_status_info=duration_expired` (ADR-0036). OEG runs no timer over this table: `duration_seconds` is stored for the request record and for serving `expiresAt`, never to schedule work. `DELETE /sessions/{id}` publishes a second, distinct `command.srm.network.capability.deactivate` operation keyed on the same id, moves the row to `DELETION_REQUESTED`, and drives it to `DELETED`/`ERROR` when that operation completes (ADR-0034). Not a duplicate of SRM inventory: the row carries identity, ownership and cached status; capability detail (canonical parameters and result) is read live from `GET /internal/network-capabilities/{service_instance_id}`, the same live-read split `AppInstance` uses. Stored in `qod_sessions`.

**TrafficInfluence**  
The OEG-side record of a CAMARA Traffic Influence policy (`POST /traffic-influences`). Structurally identical to `QodSession`: `traffic_influence_id` is OEG-minted, adopted by SRM as `service_instance.id` (ADR-0033), written eagerly (`state=ordered`) alongside the activate publish (`capability_type=traffic_influence`), advanced by the completion and status events, and torn down by a second deactivate operation (ADR-0034). It differs in one respect — its `service_specification_id` is **resolved, not minted**: a Traffic Influence request always targets an already-registered `appId`, which OEG resolves to the owning `app_registration_id` (= `service_specification_id`, ADR-0011 — note `app_id` itself is **not** the specification id). Stored in `traffic_influences`.
+2 −1
Changes for srm/architecture.md: 2 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -91,7 +91,8 @@ clean boundaries.
| JSON Schema validation (REQ-SRM-12) | Validate all `command.srm.*` inbound and `event.srm.operation.*` outbound against published schemas |
| Reconciliation (REQ-SRM-11) | Periodic; emit `event.srm.inventory.drift_detected` on divergence |
| Scale / terminate | Same async `command.srm.service.scale` / `.terminate` path as deploy |
| Network capability | `command.srm.network.capability.*` for async lifecycle/session (QoD, Traffic Influence) |
| Network capability | `command.srm.network.capability.*` for async lifecycle/session (QoD, Traffic Influence) |d
| Capability expiry | SRM enforces a realization's requested duration and reports lapse as a backend-driven `event.srm.operation.status` (`metadata.qos_status_info=duration_expired`); OEG runs no timer (ADR-0036). Mechanism is an implementation choice — scheduled sweep, backend notification, or reconciliation |
| Sync query/data | Internal HTTP for Device Location/Status; no inventory |
| QoS / capability profiles | Referenced by `profile_ref` in canonical params — unvalidated today; validation, when added, is a catalog-layer specification per offering (ADR-0035) |
| Public APIs | Owned by OEG/FM, not SRM |
Loading