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

feat: Update persistence model and standards view for async network capabilities

- Revised `service_instance` and `capability_instance` definitions to clarify caller-facing IDs are OEG-minted and adopted by SRM.
- Enhanced documentation in `persistence-cross-standards-view.md` to reflect changes in identity handling for async network capabilities.
- Introduced ADR-0033 to formalize the adoption of caller-facing IDs for async network capabilities.
- Added ADR-0034 to establish that network capability deletions are asynchronous, detailing the process for handling deletions and their completion.
- Updated SQL schema to ensure clarity on primary keys and identity handling across service instances and capability instances.
parent f2b8de9c
Loading
Loading
Loading
Loading
+3 −2
Changes for decisions/0028-topology-ref-fields-internal-only.md: 3 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -38,10 +38,11 @@ Options considered:
ADR-0013's rule generalizes to the rest of the topology chain: `domain.ref`, `capability.ref`,
`control_path_binding.ref`, and `control_path_ref` are internal-only, exactly like `zone.ref`.
None of them are returned to external NBI/EWBI consumers. Callers see UUIDs (`appInstanceId`,
`edgeCloudZoneId`), caller-facing `external_ref`, and client-supplied names (e.g.
`sessionId`, `trafficInfluenceID`, `edgeCloudZoneId`) and client-supplied names (e.g.
`service_instance.ref` when it is the CAMARA instance `name`) — never the internal slugs. OEG and
FM are responsible for stripping these fields before returning any response to an external
consumer.
consumer. `capability_instance.external_ref` is the backend realization ref and is stripped on
the same rule (ADR-0033).

## Consequences

+136 −0
Changes for decisions/0033-async-network-capability-caller-id.md: 136 added lines, 0 removed lines.
Original line number Diff line number Diff line
---
type: Decision Record
title: "ADR-0033: Async network-capability caller-facing ids are OEG-minted and adopted as `service_instance.id`"
description: "QoD sessionId and trafficInfluenceID are minted by OEG, carried on command.srm.network.capability.activate, and adopted by SRM as the session-holder service_instance.id — the same pattern ADR-0005 established for appInstanceId; external_ref reverts to holding the realization ref only."
tags: [adr]
timestamp: 2026-08-05T00:00:00Z
status: "accepted"
---

# ADR-0033: Async network-capability caller-facing ids are OEG-minted and adopted as `service_instance.id`

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

## Context

REQ-SRM-17 and [srm/persistence-model.md](/srm/persistence-model.md) §B.4 addressed caller-facing
**capability-scoped** resources (QoD `sessionId`, traffic-influence id) by
`capability_instance.external_ref`, with app instances called out as the sole exception addressed
by `service_instance_id` (ADR-0005). That rule assumed the **backend names the resource**: SRM
receives an id from the adapter, stores it in `external_ref`, and passes it upward — which is why
§B.4 instructed adapters to "populate `external_ref` even when it equals `external_id`".

That assumption does not survive the async decision already taken for these API families. CAMARA
requires the caller-facing id in the creation response (`sessionId` in QoD's `201`,
`trafficInfluenceID` in Traffic Influence's response), and OEG returns that response **before**
SRM has consumed the command, let alone before the backend has been called. The caller-facing id
therefore **cannot** originate from the backend; OEG must mint it up front.

`SrmNetworkCapabilityActivateV1` had no field to carry that minted id, so SRM continued to mint
`external_ref` independently. The consequences were structural, not incidental:

- The caller-facing id existed **only** in `oeg_db`. It appeared nowhere in `srm_db`, SRM logs, or
  SRM audit trails, so no SRM-side lookup or investigation could start from the id the customer
  holds.
- OEG learned `external_ref` only when `event.srm.operation.completed` arrived, so `DELETE`
  issued before completion had no key to send and returned `404` — on a resource that
  demonstrably existed, contradicting CAMARA's "id does not exist" semantics for that status.
- §B.4 mandated a read resolving on `external_ref`, but [srm/interface-contract.md](/srm/interface-contract.md)
  §E.1 exposed no endpoint capable of it. Two independent implementations each invented a
  different undocumented SRM path to work around the same hole — evidence that the gap was in the
  contract rather than in either implementation.

Options considered:

- **Option A:** keep SRM minting `external_ref`, and add a by-`external_ref` read endpoint. Fixes
  the read, leaves the identity split — `DELETE`-before-completion and SRM-side investigation stay
  broken.
- **Option B:** OEG mints; SRM adopts the value as `capability_instance.external_ref`. Fixes the
  identity split and keeps §B.4's wording, but still requires a new read endpoint, and leaves two
  addressing schemes in the platform.
- **Option C:** OEG mints; SRM adopts the value as the session-holder `service_instance.id`,
  exactly as ADR-0005 established for `app_instance_id`. Reuses the existing
  `GET /service-instances/{id}` read path and the existing `service_instance_id` identification
  arm of update/deactivate.

## Decision

**Option C.** For every async network capability
(`command.srm.network.capability.activate` — `capability_type` `qod_session`,
`traffic_influence`, and any future async lifecycle/session capability):

- `SrmNetworkCapabilityActivateV1` carries **`service_instance_id`** (UUID, required) — minted by
  OEG/FM and equal to the caller-facing CAMARA id (`sessionId`, `trafficInfluenceID`). SRM adopts
  it as the session-holder `service_instance.id`. This mirrors `DeployTargetV1.app_instance_id`
  (ADR-0005).
- **`capability_instance.external_ref` reverts to its edge-app meaning** — the realization ref of
  the backend-side object — and is no longer the caller-facing identity for these families.
  `external_id` continues to hold the backend-native id the adapter acts through. This makes the
  rule uniform: *the caller-facing identity always lives on `service_instance`; `capability_instance`
  holds realization detail.*
- The provisional `ref = si-{service_instance.id}` write-then-rename step disappears. SRM knows the
  final identity at INSERT.
- **Update and deactivate** for these families identify the realization by `service_instance_id`.
  The `external_ref` arm of §B.6/§B.7 remains valid for callers that hold one, but is no longer the
  path OEG uses.
- `GET`/`DELETE /sessions/{id}` and `/traffic-influences/{id}` resolve on `service_instance_id`.
  A dedicated capability projection is added at `GET /internal/network-capabilities`
  (see [srm/interface-contract.md](/srm/interface-contract.md) §E.1) because the caller needs the
  capability's parameters and result, not only the instance projection.

REQ-SRM-17 is rewritten accordingly: caller-facing resources are addressed by
`service_instance_id` across **all** API families; there is no longer an app-instance exception.

**Scope condition.** This decision depends on the session-holder rule
([srm/interface-contract.md](/srm/interface-contract.md) §B.5): an activate always creates a new
`service_instance`, and there is no v1 field attaching a network capability under an existing
instance, so a network-capability holder has **exactly one** live capability child. The holder's
id can therefore name the capability unambiguously.

## Consequences

What becomes easier:

- One identity pattern platform-wide — OEG mints, SRM adopts, both sides share the id. Logs,
  audit, and support investigations start from the id the customer holds, on either side.
- `DELETE` and update work from the moment of creation; no waiting for a completion event to learn
  a key, and no `404` on a resource that exists.
- Reads use an endpoint that already exists and is already exercised by the `/appinstances` path.
- SRM does less work per activate: no provisional ref, no rename transaction.
- Each new async capability family inherits the rule instead of re-deriving it.

What becomes harder:

- `service_instance.id` is a primary key, and SRM retains terminated rows for audit, so a
  caller-facing id can never be reused once issued — the hazard ADR-0011 documents for
  `service_specification.id`. (`capability_instance.external_ref`, by contrast, is unique only
  among live rows — `WHERE state NOT IN ('terminated','failed')` — so values there *are*
  reusable. That reusability is what this decision trades away.)

  Mitigated because these ids are server-generated in both API families and OEG mints a fresh
  UUID per resource. CAMARA QoD has no request-body slot for `sessionId` at all — it appears only
  in responses and as a path parameter. CAMARA Traffic Influence does carry `trafficInfluenceID`
  on the resource object, but `PostTrafficInfluence` marks it `readOnly` and the spec states any
  supplied value "will be ignored by the API Producer. It is automatically generated by the
  system." **Implementation obligation:** OEG must ignore a client-supplied `trafficInfluenceID`
  rather than honour it — honouring it would let a client pick an id, and a client that reused a
  previously-deleted one would hit a primary-key conflict that surfaces as a `500` rather than a
  meaningful CAMARA error.
- Callers holding only an `external_ref` cannot address these resources through the OEG surface.
  Nothing on the CAMARA surface exposes `external_ref`, so this is internal-only.

Deferred or excluded:

- The QoD `service_specification_id` question is **not** resolved here. A QoD session has no
  CAMARA registration step and therefore no onboarded specification to reference; see
  [oeg/persistence-model.md](/oeg/persistence-model.md) §J.1 open question OQ-QOD-01. Traffic
  Influence is unaffected — it resolves its specification from the registered app
  (`app_registration_id`, ADR-0011).

Revisit trigger:

- If §B.5's session-holder rule is relaxed so one holder may carry more than one live capability,
  the holder id stops naming a single capability and this decision must be revisited — most likely
  reverting to Option B for the multi-capability case.
+97 −0
Changes for decisions/0034-network-capability-delete-is-async.md: 97 added lines, 0 removed lines.
Original line number Diff line number Diff line
---
type: Decision Record
title: "ADR-0034: Network-capability delete is async via command.srm.network.capability.deactivate, and its completion is consumed to a terminal record state"
description: "DELETE /sessions/{id} and DELETE /traffic-influences/{id} publish the deactivate command over the DataBus and return without waiting; OEG consumes the deactivate operation's completion event and drives its own record to a terminal state, making repeat deletes idempotent."
tags: [adr]
timestamp: 2026-08-05T00:00:00Z
status: "accepted"
---

# ADR-0034: Network-capability delete is async, and its completion is consumed to a terminal record state

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

## Context

Teardown of an async network capability (QoD session, traffic-influence policy) had two open
questions: whether `DELETE` is synchronous or async, and what happens to OEG's own record
afterwards.

**Transport.** SRM's contract models `command.srm.network.capability.deactivate` as an async
fire-and-forget mutation with the same shape as `.activate`
([srm/interface-contract.md](/srm/interface-contract.md) §B.7). A synchronous OEG-to-SRM `DELETE`
would require either a second, undocumented SRM HTTP mutation endpoint or a blocking wait on
`event.srm.operation.completed` inside the request/response cycle — neither of which the contract
provides, and both of which regress the async pattern already accepted for creation.

**Record lifecycle.** The cheaper option is to leave OEG's record untouched after a delete — not
consuming the deactivate operation's completion, so the row keeps its last activate-derived state
with no terminal value. That option is rejected here, because it produces three defects:

- The deactivate `operations` row never left `PENDING`, contradicting the operations lifecycle in
  [oeg/persistence-model.md](/oeg/persistence-model.md) §J.1, where every accepted async request
  reaches a terminal status on completion.
- Because the record stayed at its live state, a **repeat `DELETE` re-published the deactivate
  command and re-sent the CAMARA notification**. The QoD spec forbids the second notification
  outright: there is no `QOS_STATUS_CHANGED` event if the status was already `UNAVAILABLE`.
- Nothing distinguished "teardown requested" from "teardown confirmed", so neither the API surface
  nor operations tooling could tell whether resources had actually been released.

The Traffic Influence implementation had already solved this by carrying the record id in the
deactivate operation's `metadata` and resolving it back when the completion event arrived, driving
its record through `deletion in progress` to `deleted`/`error`. That approach generalizes.

## Decision

`DELETE` on an async network capability is **asynchronous**, and its completion **is** consumed.

1. OEG mints a new `operation_id` for the teardown (a delete is its own operation, not a
   continuation of the activate), writes a `PENDING` `operations` row carrying the target record's
   id in `metadata`, and publishes `command.srm.network.capability.deactivate`.
2. The command identifies the realization by **`service_instance_id`** — the caller-facing id
   (ADR-0033). No lookup of a previously-learned `external_ref` is required, so a delete issued
   before the activate has completed is valid and is **not** a `404`.
3. OEG moves its record to a **non-terminal deleting state** and returns `204` without waiting.
4. On the deactivate operation's `event.srm.operation.completed`, OEG resolves the record from the
   operation's `metadata`, sets the `operations` row to its terminal status, and moves the record
   to a **terminal state** (`deleted`, or an error state when SRM reports failure).

**Notification rule.** The CAMARA notification for user-requested teardown fires **only** on the
transition out of the live state. Because step 3 moves the record off that state synchronously, a
repeat `DELETE` is naturally idempotent — it neither re-publishes nor re-notifies. For QoD this
satisfies the spec rule that no event is sent when the status was already `UNAVAILABLE`.

**Read behaviour after deletion.** CAMARA has no representation for a deleted session or policy,
so reads of a terminal record return `404`. The row is retained for audit and for the
`operations` history; it is not removed.

## Consequences

What becomes easier:

- Creation and teardown follow one async pattern: publish, mint an `operations` row, do not block.
- Repeat deletes are safe by construction, with no separate guard and no duplicate customer
  notification.
- `GET /platform/v1/operations/{operationId}` reports a truthful terminal status for teardowns.
- Operations tooling can distinguish requested from confirmed teardown.
- No new SRM HTTP mutation endpoint is required; §B.7 is reused as written.

What becomes harder:

- The CAMARA caller receives `204` before SRM has finished releasing resources. A consumer that
  assumes synchronous deletion — for instance immediately recreating a session for the same device
  and flow — is not supported by this flow. The same tradeoff was already accepted for creation.
- OEG must carry the record id on the deactivate operation's `metadata`, since the deactivate
  operation is distinct from the activate operation that owns the record.

Deferred or excluded:

- `grace_period_seconds` is always sent as `0`. No CAMARA surface exposes a grace period for these
  families; the field stays available for non-CAMARA callers.

Revisit trigger:

- If SRM adds a synchronous HTTP mutation path for network capabilities, re-evaluate whether the
  async DataBus route is still preferred for teardown.
+2 −0
Changes for decisions/index.md: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -35,3 +35,5 @@ the reasoning lives here. Never renumber; supersede instead (see the record-deci
* [ADR-0030](0030-oeg-error-model-is-camara-errorinfo-not-rfc7807.md) - OEG's northbound error model is CAMARA's ErrorInfo {status,code,message}, not RFC 7807 problem+json
* [ADR-0031](0031-internal-field-names-mirror-srm-vocabulary.md) - internal contract field names mirror SRM entity vocabulary (zone_id/domain_id); kind-qualified prose never becomes a field name (domain-only pins return failed_before_start)
* [ADR-0032](0032-databus-validation-failure-split-by-answerability.md) - DataBus validation failures split by answerability: dead-letter only unattributable messages; every readable-envelope failure returns failed_before_start
* [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
+9 −2
Changes for oeg/interface-contract.md: 9 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -140,6 +140,8 @@ This is the simplest read path: OEG answers from its own operation record when t
| POST | `http://srm:8081/internal/catalog/service-specifications` | Synchronous catalog write for app registration (`POST /apps`) — the one synchronous internal-HTTP mutation, see [oeg/architecture.md](/oeg/architecture.md) |
| GET | `http://srm:8081/internal/zones` | Zone list for deployment UX and operator tooling |
| GET | `http://srm:8081/internal/service-instances/{id}` | Live instance snapshot read (used by `GET`/`DELETE /appinstances`) |
| GET | `http://srm:8081/internal/network-capabilities/{service_instance_id}` | Live capability detail for one async network capability — used by `GET /sessions/{id}` and `GET /traffic-influences/{id}`. The path key is the caller-facing CAMARA id (ADR-0033) |
| GET | `http://srm:8081/internal/network-capabilities` | Collection form, filtered by `capability_type` / `service_specification_id` — used by `GET /traffic-influences?appId=...` |
| POST | `http://srm:8081/internal/network-queries/location` | Sync CAMARA Device Location query |
| POST | `http://srm:8081/internal/network-queries/device-status` | Sync CAMARA Device Status query |

@@ -171,6 +173,10 @@ OEG publishes `command.srm.service.deploy` with body fields matching `CommandEnv

OEG also publishes `command.srm.network.capability.activate` / `.update` / `.deactivate` with `CommandEnvelopeV1` plus, respectively, `SrmNetworkCapabilityActivateV1` / `SrmNetworkCapabilityUpdateV1` / `SrmNetworkCapabilityDeactivateV1` (§L.1) for async lifecycle/session capabilities (e.g. QoD sessions, Traffic Influence).

**Caller-facing ids on these families.** OEG mints the CAMARA id (`sessionId`, `trafficInfluenceID`), returns it in the creation response, and carries it on the activate command as `service_instance_id`; SRM adopts it as the session-holder `service_instance.id` (ADR-0033). This is the same mint-and-adopt relationship as `appInstanceId` (ADR-0005), so **every** caller-facing resource is addressed by `service_instance_id` on both sides — there is no per-family exception. Update and deactivate key on the same value, so follow-ups are valid immediately and never wait for the activate's completion event. `capability_instance.external_ref` is the backend realization ref and is never surfaced to CAMARA callers.

Reads for these families use `GET /internal/network-capabilities/{service_instance_id}` and its collection form ([srm/interface-contract.md](/srm/interface-contract.md) §E.1) for capability detail; OEG resolves identity, ownership and cached status from `qod_sessions` / `traffic_influences` first, exactly as `/appinstances` resolves from `app_instances` before reading instance detail live.

This alignment matters because OEG and SRM must share the same interpretation of `command.srm.*` messages.

TMF921 (TM Forum Intent Management API) intent-driven work and an `intent`/`intent_id` provenance value are **not** part of the v1 contract (see [srm/architecture.md](/srm/architecture.md) §D and ADR-0001).
@@ -229,8 +235,8 @@ This section describes the async messages OEG sends and receives.

| Subject | Action |
|---|---|
| `event.srm.operation.completed` | Read `instances[]`; update `operations` and the pre-created `app_instances` rows, trigger callback(s) — single for `/appinstances`, array for `/deployments` (ADR-0005) |
| `event.srm.operation.status` | Update in-flight/backend-driven state; map backend capability state changes to CAMARA notifications |
| `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.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 |

@@ -516,3 +522,4 @@ relying solely on the callback arriving.
| Version | Date | Summary |
|---------|------|---------|
| 2.0 | 2026-07-07 | OOP Release 2.0. |
| 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. |
Loading