Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **Stable shared-memory object headers, payload migrations and persistence classes.** Every
named object in a segment now starts with a 64-byte `lintel::shm::ShmObjectHeader`
(`headerVersion`, `payloadVersion`, type id, persistence class); the payload is a separate
allocation addressed by a relative offset, so a change to the library's bookkeeping no longer
moves the payload. `shm::registerPayloadMigration<T>({ .fromVersion = n, .toVersion = n + 1, .apply = … })`
chains per-type migrations,
and `shm::ShmObjectOptions { payloadVersion, persistence, payloadTypeId }` is accepted by
`shm::SharedMemoryRepository<T>`, the `shm::construct*` helpers and the JSON mirror repositories.
`Volatile`/`Derived` objects may be recreated without operator input, `Persistent` (the default)
aborts instead. The segment layout marker is now version 3. See
`docs/shm-payload-abi-plan.md` and `docs/shm-migration-strategy.md`.
- **Group management over HTTP and in the CLI.** `POST /user/groups`, `DELETE /user/groups/{name}`
and `POST`/`DELETE` `/user/groups/{name}/subgroups/{sub}` (all `Admin-User`) expose the group
operations `GroupRepository` already had; the CLI gains `group_add`, `group_remove`,
Expand Down Expand Up @@ -212,6 +223,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`shm::SharedMemoryRepository<T>` and the containers it stores - is exactly what such a migrator is
for; properties (database or bootstrap XML, the shared memory repository is a mirror that is
rebuilt), users, groups and history live outside the segment and stay untouched.
- **In-segment layout is version 3 (breaking).** Named shared-memory objects are now
`ShmObjectHeader` + separately allocated payload instead of a raw `find_or_construct<T>()`
object. Segments written by an older build have no readable per-object header and are treated
as `Persistent`; they still need the existing migrator/`LINTEL_SHM_RECREATE=1` path. See
`docs/shm-payload-abi-plan.md`.
- **`SharedMemoryRepositoryRegistry.h` moved** from `features/base/services/` to `core/shm/`; the
typed repository header may not depend on a feature header.
- **Logging and property registration use `std::source_location` (breaking).**
Expand Down
97 changes: 83 additions & 14 deletions docs/shm-migration-strategy.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Migrationsstrategie für Shared-Memory-Segmente

Stand: 22. Oktober 2026 · gilt ab Layout-Version 2 (`lintel_segment_layout`)
Stand: 22. Oktober 2026 · gilt ab Layout-Version 3 (`lintel_segment_layout`)

Ein Segment ist eine Datei mit einem Layout, das die Bibliothek definiert. Ändert sich
dieses Layout (in 0.2 zum Beispiel: `ShmSegmentAccessor` von einem Zeiger auf 40 Byte,
Expand Down Expand Up @@ -29,10 +29,12 @@ folgende Reihenfolge (`SharedMemoryService.cpp`, `create()`):

1. **Migrator registriert** (`lintel::shm::SegmentLayoutPolicy::setMigrator`) → er
entscheidet. `true` = Inhalte dürfen weg, `false` = Start abbrechen.
2. **Policy `Recreate`** (`set(LayoutMismatchPolicy::Recreate)`) oder
2. **Alle Objekte `Volatile`/`Derived`** (und ihre Header lesbar) → das Segment wird
ohne Operator-Eingriff neu angelegt.
3. **Policy `Recreate`** (`set(LayoutMismatchPolicy::Recreate)`) oder
`LINTEL_SHM_RECREATE=1` → Inhalt wird verworfen.
3. Sonst **`Fail`** (Default): `std::runtime_error` mit Segmentname und Dateipfad, die
Datei bleibt unangetastet.
4. Sonst **`Fail`** (Default): `std::runtime_error` mit Segmentname, Dateipfad und den
blockierenden Objekten; die Datei bleibt unangetastet.

Der Migrator läuft, **bevor** irgendetwas gelöscht wird, und bekommt den Pfad der
aktuellen Datei sowie die gefundene und die erwartete Layout-Version.
Expand Down Expand Up @@ -69,21 +71,88 @@ Regeln für den Migrator:

## 4. Grenze der aktuellen Umsetzung

Die Entscheidung gilt **pro Segment, nicht pro Objekt**: die Objekte im Segment tragen
keine eigene Version, also kann der Migrator nur „alles retten" oder „alles aufgeben".
Für die typisierten Repositories ist das ausreichend, weil ihr Payload ohnehin über
`serialize()`/`deserialize()` der DTOs geht.
Die segmentweite Entscheidung bleibt der letzte Fallback. Ab Layout-Version 3
trägt jedes benannte Objekt einen eigenen `ShmObjectHeader`; die Bibliothek kann
damit **pro Objekt** entscheiden statt nur pro Segment:

- `Volatile` und `Derived` dürfen ohne Migrator neu angelegt werden.
- `Persistent` verlangt eine registrierte Migration oder einen Migrator.
- Ein Objekt ohne lesbaren Header (Segment aus einem älteren Build) gilt
weiterhin als `Persistent`, weil seine Persistenzklasse unbekannt ist.

Der Header liegt am Anfang des benannten Objekts; die Nutzdaten selbst sind eine
separate Allokation, die über `payloadOffset` adressiert wird. Änderungen an der
internen Buchhaltung (Accessor, Header, Record) verschieben die Nutzdaten daher
nicht mehr.

## 5. Nächste Stufen (Backlog)

| Stufe | Inhalt | Nutzen |
| Stufe | Inhalt | Status |
|-------|--------|--------|
| **2** | `ShmObjectHeader { magic, version, payloadBytes }` vor jedem benannten Objekt; pro Typ eine Migrationsfunktion `v(n) → v(n+1)` | echte Teil-Migration, ohne Export/Import |
| **3** | Persistenzklasse pro Objekt beim Registrieren: `Volatile` (darf weg), `Derived` (aus DB/XML rekonstruierbar), `Persistent` (muss migriert werden, sonst Startabbruch) | die Bibliothek weiß selbst, was sie verwerfen darf |
| **4** | stabile Payload-ABI: Nutzdaten als versionierte Blobs, interne Buchhaltung (Accessor, Header) versioniert getrennt | Layout-Änderungen der Bibliothek invalidieren Nutzdaten nie wieder |
| **2** | `ShmObjectHeader { magic, headerVersion, payloadVersion, payloadBytes, … }` vor jedem benannten Objekt; pro Typ eine Migrationsfunktion `v(n) → v(n+1)` | **umgesetzt** (Layout-Version 3) |
| **3** | Persistenzklasse pro Objekt beim Registrieren: `Volatile`, `Derived`, `Persistent` | **umgesetzt** |
| **4** | stabile Payload-ABI: Nutzdaten als versionierte, separat allokierte Blobs; interne Buchhaltung (Accessor, Header) versioniert getrennt | **umgesetzt** (Header v1, Payload v1) |

Die Umsetzung ist in `docs/shm-payload-abi-plan.md` in Phasen und
Akzeptanzkriterien zerlegt.

### 5.1 Objekt registrieren

```cpp
#include <lintel/core/shm/ShmObjectHeader.h>

lintel::shm::ShmObjectOptions options{
.payloadVersion = 2,
.persistence = lintel::shm::ShmPersistence::Persistent,
};

lintel::shm::SharedMemoryRepository<MyDto> repository(
service, segment, "my-dto", options, MyDto{});
```

Ohne explizite Angabe bleibt `payloadVersion = 1` und `Persistent` der Default.
Das ist die sichere Variante: ein bestehender Aufrufer verliert nie
stillschweigend Daten. Der Property-Spiegel registriert sich als `Derived`,
EventBus und MessageQueue als `Volatile`.

### 5.2 Payload migrieren

```cpp
lintel::shm::registerPayloadMigration<MyDto>({
.fromVersion = 1,
.toVersion = 2,
.apply = [](const void *oldBytes, std::size_t oldSize, void *newPayload) {
// alten Stand lesen und den neuen Typ an newPayload konstruieren
},
});
```

Die Registry verkettet `1 → 2 → 3` automatisch. Der alte Payload bleibt
unangetastet, bis die Migration vollständig durchgelaufen ist; schlägt sie fehl,
öffnet sich das Objekt beim nächsten Versuch erneut mit dem alten Stand.

### 5.3 Segmentweite Entscheidung

`SharedMemoryService` prüft bei einem Layout-Konflikt zuerst den Migrator, dann
die Objekt-Header:

1. Migrator registriert → er entscheidet.
2. Alle Objekte sind `Volatile`/`Derived` und lesbar → Segment wird neu angelegt.
3. Policy `Recreate` bzw. `LINTEL_SHM_RECREATE=1` → Inhalt wird verworfen.
4. Sonst → `std::runtime_error` mit Segmentname, Pfad und den blockierenden
Objekten; die Datei bleibt unangetastet.

### 5.4 Grenzen

Stufe 2 ist die Voraussetzung für 3 und 4; ohne sie bleibt es beim Segment als
Ganzes.
- Eine Payload-Migration kann den Destruktor des alten Typs nicht aufrufen, weil
dessen Layout im neuen Build nicht mehr existiert. Verschachtelte Allokationen
des alten Stands werden erst beim nächsten Neuanlegen des Segments frei.
- Die Typ-ID ist ein FNV-1a-Hash des vollqualifizierten Typnamens. Bei einem
Compiler-/ABI-Wechsel kann sie sich ändern; dann ist `payloadTypeId` explizit
zu setzen.
- `Derived` heißt „die Anwendung kann den Inhalt rekonstruieren“. Die Bibliothek
ruft keinen Rebuild-Hook auf; das bleibt Aufgabe der besitzenden
Service-Schicht (z. B. `PropertyService`).

## 6. Betrieb

Expand Down
133 changes: 133 additions & 0 deletions docs/shm-payload-abi-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Shared-Memory Payload-ABI — Umsetzungsplan für Stufe 2–4

Stand: 2026-10-22
Basis: `docs/shm-migration-strategy.md` §5 (Backlog Stufe 2, 3, 4)
Branch: aktueller Arbeitsstand

Ziel: Der Inhalt eines Segments wird nicht mehr als Ganzes verworfen, nur weil
sich die Buchhaltung der Bibliothek ändert. Jedes benannte Objekt bekommt eine
eigene, versionierte Hülle; Nutzdaten und interne Struktur (Accessor, Header,
Record) werden getrennt versioniert. Die Bibliothek kann dadurch selbst
entscheiden, welche Objekte sie verwerfen darf.

## Leitentscheidungen

1. **Ein stabiler Header pro benanntem Objekt.** `ShmObjectHeader` ist ein
`trivially copyable` POD mit fester Größe (64 Byte) und steht am Anfang des
benannten Objekts. Seine Felder `headerVersion` und `payloadVersion` sind
getrennt: Änderungen an der Hülle invalidieren die Nutzdaten nicht.
2. **Nutzdaten liegen in einer separaten Allokation.** Der Header speichert
`payloadOffset`, `payloadBytes` und `payloadAlignment` relativ zu sich
selbst. Ein Layout-Wechsel der Bibliothek verschiebt damit nie die Nutzdaten.
3. **Persistenzklasse pro Objekt.** `Volatile`, `Derived`, `Persistent` werden
beim Registrieren mitgegeben und im Header gespeichert. `Persistent` ist der
sichere Default; nur `Volatile`/`Derived` dürfen ohne Migrator verworfen
werden.
4. **Migration pro Typ und Version.** `ShmPayloadMigrations` registriert
`v(n) → v(n+1)`-Schritte. Die Bibliothek verkettet sie beim Öffnen eines
Objekts, bevor der erste Zugriff auf die Nutzdaten erfolgt.
5. **Segmentweite Entscheidung bleibt der letzte Fallback.** Der Migrator aus
`SegmentLayoutPolicy` hat weiterhin Vorrang. Ohne Migrator darf ein Segment
automatisch neu angelegt werden, wenn **alle** enthaltenen Objekte
`Volatile` oder `Derived` sind und ihre Header lesbar sind.

## Phasen und Tasks

### Phase 1 — Hülle und Migrationskern (Stufe 2 + 4)

| ID | Task | Dateien | Tests | Akzeptanzkriterien |
|----|------|---------|-------|---------------------|
| **ABI-1** | `ShmObjectHeader` + `ShmPersistence` + `ShmObjectOptions` | `src/include/lintel/core/shm/ShmObjectHeader.h` (neu) | `tests/features/ShmObjectMigrationTest.cpp` | 64 Byte, `trivially copyable`, `headerVersion`/`payloadVersion` getrennt, Magic- und State-Prüfung. |
| **ABI-2** | Typ-ID und Migrationsregistry `v(n) → v(n+1)` | `ShmObjectHeader.h`, `src/features/base/services/ShmObject.cpp` (neu) | `ShmObjectMigrationTest.cpp` | Typ-ID stabil innerhalb eines Builds, Registrierung pro Typ/Version, Verkettung mehrerer Schritte, `reset()` für Tests. |
| **ABI-3** | `ShmObject` als typ-erased Hülle (bisher `ShmCellRecord`) | `src/include/lintel/core/shm/ShmObject.h` (neu), `src/features/base/services/ShmObject.cpp` | `ShmObjectMigrationTest.cpp`, `SharedMemoryRepositoryTest.cpp` | Header + separate Payload-Allokation, `resolve()` migriert oder legt neu an, `isStale()`/`generation()` bleiben. |
| **ABI-4** | `ShmCell`/`shm::SharedMemoryRepository<T>` auf `ShmObject` umstellen | `src/include/lintel/core/shm/SharedMemoryRepository.h` | `SharedMemoryRepositoryTest.cpp` | Konstruktor und `of()` akzeptieren `ShmObjectOptions`; `header()`, `persistence()`, `payloadVersion()` verfügbar. Bestehende Aufrufe bleiben quellkompatibel. |
| **ABI-5** | Segment-Layout auf 3 anheben | `src/features/base/services/SharedMemoryService.cpp` | `SharedMemoryServiceTest.cpp` | Marker wird auf 3 gestempelt; Tests prüfen 3 statt 2. |

**Phase-1-DoD:** Ein Objekt mit alter `payloadVersion` wird über eine
registrierte Kette migriert; ohne Migration und mit `Persistent` bricht der
Start ab. Nutzdaten liegen physisch getrennt vom Header.

### Phase 2 — Persistenzklassen und segmentweite Entscheidung (Stufe 3)

| ID | Task | Dateien | Tests | Akzeptanzkriterien |
|----|------|---------|-------|---------------------|
| **ABI-6** | Persistenzklasse in alle Registrierungspfade | `ShmObjectOptions`, `ShmConstructors.h`, `features/base/repositories/SharedMemoryRepository.h`, `SharedMemoryPropertyRepository.h` | `SharedMemoryRepositoryTest.cpp`, `SharedMemoryServiceTest.cpp` | Stack A und B können `Volatile`/`Derived`/`Persistent` setzen; Property-Spiegel = `Derived`; Default = `Persistent`. |
| **ABI-7** | Segment-Inspektion vor dem Verwerfen | `SharedMemoryService.cpp`, `ShmObjectHeader.h` | `SharedMemoryServiceTest.cpp` | Iteration über benannte Objekte, Header lesen, `allDiscardable` nur wenn alle Header gültig und nicht `Persistent`. |
| **ABI-8** | Auto-Recreate für rein flüchtige/abgeleitete Segmente | `SharedMemoryService.cpp` | `SharedMemoryServiceTest.cpp` | Alt-Segment mit nur `Volatile`/`Derived` wird ohne Env/Migrator neu angelegt; ein `Persistent`-Objekt verhindert das und liefert Pfad + Anleitung. |
| **ABI-9** | Objekt-Migration bei Versionskonflikt | `ShmObject.cpp` | `ShmObjectMigrationTest.cpp` | `Persistent` ohne Migration → `runtime_error`; `Volatile`/`Derived` ohne Migration → Objekt neu; mit Migration → Daten übernommen. |

**Phase-2-DoD:** Die Bibliothek unterscheidet selbst zwischen „darf weg" und
„muss migriert werden"; der Operator-Eingriff ist nur noch bei `Persistent`
nötig.

### Phase 3 — Bestehende Objekttypen integrieren (Stufe 2/3 flächendeckend)

| ID | Task | Dateien | Tests | Akzeptanzkriterien |
|----|------|---------|-------|---------------------|
| **ABI-10** | `ShmConstructors`-Helfer auf Header umstellen | `ShmConstructors.h` | `SharedMemoryRepositoryTest.cpp`, `ShmViewTest.cpp`, `SharedMemoryCleanupServiceTest.cpp` | `constructArray/Map/Vector/Object/ObjectWith` liefern weiter `T&`, legen aber Header + Payload an; `constructString` bleibt Wert-Allokation. |
| **ABI-11** | EventBus als `Volatile` registrieren | `EventBusService.cpp`, `SharedMemoryBootstrapPlugin.cpp` | `EventBusServiceTest.cpp`, `SharedMemoryBootstrapPluginTest.cpp` | Bus-Objekt hat Header; Bootstrap liest die Payload über den Header; alte Segmente werden nicht als gültiger Bus interpretiert. |
| **ABI-12** | MessageQueueShmCore als `Volatile` registrieren | `ShmSegmentAccessor.h`, `MessageQueueShmCore.cpp` | `MessageQueueShmCoreTest.cpp` | Queue-Header liegt in einer versionierten Hülle; ein Versionskonflikt verwirft die flüchtige Queue, ohne das Segment zu löschen. |
| **ABI-13** | `ShmView` und Cleanup header-aware machen | `ShmView.cpp`, `SharedMemoryCleanupService.cpp` | `ShmViewTest.cpp`, `SharedMemoryCleanupServiceTest.cpp` | Views zeigen auf die Payload, nicht auf den Header; Cleanup gibt Header und Payload frei, ohne fremde Objekte zu treffen. |

**Phase-3-DoD:** Jedes benannte Objekt im Segment trägt einen Header; die
bestehenden APIs bleiben kompatibel.

### Phase 4 — Dokumentation und Abnahme (Stufe 4)

| ID | Task | Dateien | Tests | Akzeptanzkriterien |
|----|------|---------|-------|---------------------|
| **ABI-14** | Migrationsstrategie fortschreiben | `docs/shm-migration-strategy.md` | — | Stufe 2–4 als umgesetzt beschrieben, neue API und Grenzen dokumentiert. |
| **ABI-15** | Changelog | `CHANGELOG.md` | — | `Added`/`Changed`-Einträge mit Verweis auf Plan und Strategie. |
| **ABI-16** | Abnahme | Build + CTest | alle SHM-Tests | Release-Build, `ctest -L unit`, Format- und Namespace-Check grün. |

## API-Skizze

```cpp
#include <lintel/core/shm/ShmObjectHeader.h>

lintel::shm::ShmObjectOptions options{
.payloadVersion = 2,
.persistence = lintel::shm::ShmPersistence::Persistent,
};

lintel::shm::registerPayloadMigration<MyDto>({
.fromVersion = 1,
.toVersion = 2,
.apply = [](const void *oldBytes, std::size_t oldSize, void *newPayload) {
// alten Stand lesen, neuen Stand an newPayload konstruieren
},
});

lintel::shm::SharedMemoryRepository<MyDto> repository(
service, segment, "my-dto", options, MyDto{});
```

## Risiken und Grenzen

- **Kein Destruktor für alte Nutzdaten.** Eine Migration liest die alten Bytes
und konstruiert den neuen Stand; verschachtelte Allokationen des alten Typs
werden nicht freigegeben. Das ist bei einem Layout-Wechsel unvermeidbar, weil
der alte Typ im neuen Build nicht mehr existiert. Der Speicher wird erst beim
nächsten Neuanlegen des Segments frei.
- **Header-Änderungen.** Der Header ist ab Version 1 eingefroren. Neue Felder
dürfen nur in `reserved` wandern; eine echte Header-Migration ist ein eigener
Schritt und muss `headerVersion` erhöhen.
- **Typ-ID.** Die ID wird aus dem vollqualifizierten Typnamen gehasht. Bei einem
Compiler-/ABI-Wechsel kann sie sich ändern; dann ist eine explizite ID in
`ShmObjectOptions` zu setzen.
- **`Derived`** bedeutet „die Anwendung kann den Inhalt rekonstruieren". Die
Bibliothek ruft keinen Rebuild-Hook auf; das bleibt Aufgabe der besitzenden
Service-Schicht (z. B. `PropertyService`).
- **`Persistent`** ist der Default. Bestehende Aufrufer ohne explizite Angabe
verlieren also nie stillschweigend Daten.

## Fortschritt

| Phase | Status |
|-------|--------|
| 1 — Hülle und Migrationskern | ☑ |
| 2 — Persistenzklassen | ☑ |
| 3 — Integration bestehender Typen | ☑ |
| 4 — Doku und Abnahme | ☑ |

Letztes Update: 2026-10-22
4 changes: 3 additions & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ set(headers
include/lintel/core/shm/ShmSegmentAccessor.h
include/lintel/core/shm/SegmentLayoutPolicy.h
include/lintel/core/shm/ShmView.h
include/lintel/core/shm/ShmObject.h
include/lintel/core/shm/ShmObjectHeader.h
include/lintel/core/shm/SharedMemoryRepository.h
include/lintel/core/shm/SharedMemoryRepositoryRegistry.h
include/lintel/core/shm/containers/ArrayShm.h
Expand Down Expand Up @@ -397,7 +399,7 @@ set(sources
core/services/ReadinessRegistry.cpp
features/base/services/ProcessService.cpp
features/base/services/SharedMemoryService.cpp
features/base/services/SharedMemoryRepository.cpp
features/base/services/ShmObject.cpp
features/base/services/SharedMemoryRepositoryRegistry.cpp
core/services/StopWatchService.cpp
# core - utils
Expand Down
Loading
Loading