78: Make JournalService a Hub-managed Nenjim process and integrate it into startup
This commit is contained in:
+31
-20
@@ -20,8 +20,9 @@
|
||||
<main>
|
||||
<h1>Nenjim</h1>
|
||||
<p class="status"><strong>Implementation status:</strong> Journal format version 1, the immutable Java model,
|
||||
internal strict parser, read-only <code>JournalService</code>, explicitly refreshed lifecycle
|
||||
<code>JournalServiceManager</code>, and the first central component Registry are implemented. Contexts,
|
||||
internal strict parser, read-only <code>JournalService</code> with a Hub-managed process implementation,
|
||||
passive explicitly refreshed <code>JournalServiceManager</code>, and the first central component Registry
|
||||
are implemented. Contexts,
|
||||
resolution, classloading, discovery, runtime loading/removal, downloading, publishing, synchronization,
|
||||
watching, signatures, permissions, and license filtering remain future work.</p>
|
||||
|
||||
@@ -96,20 +97,26 @@ PREFERRED=nenjim:com_r35157_nenjim-hubd-api=1.0.3:'Built together'</code></pre>
|
||||
semantically inconsistent content fails with source name, one-based line number, and a reason. Validation is
|
||||
complete before service state changes. Returned objects and collections are immutable and defensively copied.</p>
|
||||
|
||||
<h2>Service, ServiceManager, packages, and disk layout</h2>
|
||||
<h2>Service, process, ServiceManager, packages, and disk layout</h2>
|
||||
<p>A <code>Service</code> is the public domain API used by applications and other services during ordinary
|
||||
operation. <code>JournalService</code> is therefore filesystem-independent and read-only: it exposes current
|
||||
and historical Journal queries without lifecycle, paths, refresh, parser, or mutation operations.</p>
|
||||
<p>A <code>ServiceManager</code> is the lifecycle and administrative API owned by a composition/lifecycle owner.
|
||||
<code>JournalServiceManager</code> starts and stops the Journal service, reports its normalized data root,
|
||||
performs explicit refresh, and exposes <code>JournalService</code> only after complete successful initial
|
||||
loading. Its reference implementation reads UTF-8 runtime files chronologically and validates directory and
|
||||
filename placement. It creates, overwrites, and deletes nothing.</p>
|
||||
<p><code>JournalServiceImpl</code> keeps exact-text SHA-256 identity, chain state, idempotency, conflicts, forks,
|
||||
and atomic ingestion internal. The public package contains only the two central interfaces; composed models,
|
||||
value types, and exceptions live under <code>.model</code>, <code>.valuetypes</code>, and
|
||||
<code>.exception</code>, while parser, ingestion, chain state, and filesystem loading live under
|
||||
<code>.impl.ref</code> without leaking through public signatures.</p>
|
||||
operation. <code>JournalService</code> is a registrable component but remains filesystem-independent and
|
||||
read-only: it exposes current and historical Journal queries without lifecycle, paths, refresh, parser, or
|
||||
mutation operations. Queries become available only after successful Journal process startup.</p>
|
||||
<p>The Hub constructs one Journal object exposed as both <code>JournalService</code> and
|
||||
<code>NenjimProcess</code>, plus one distinct passive <code>JournalServiceManager</code>. The manager reports
|
||||
the normalized data root and performs explicit refresh; it has no start, stop, service accessor, component
|
||||
identity, or process lifecycle. It delegates through a package-private administration view that ordinary
|
||||
service consumers cannot name or derive.</p>
|
||||
<p>The Journal process reads UTF-8 runtime files chronologically and validates directory and filename placement.
|
||||
Its complete initial worldview is published only after every runtime file succeeds; failed startup leaves
|
||||
queries unavailable and cannot be retried on the same instance. Later refresh remains explicit, idempotent,
|
||||
and atomic per ingestion. Neither startup nor refresh creates, overwrites, or deletes Journal files.</p>
|
||||
<p>The package-private <code>JournalServiceImpl</code> keeps exact-text SHA-256 identity, chain state,
|
||||
idempotency, conflicts, forks, and ingestion internal. The public package contains only the two central
|
||||
interfaces; composed models, value types, and exceptions live under <code>.model</code>,
|
||||
<code>.valuetypes</code>, and <code>.exception</code>, while parser, ingestion, chain state, filesystem
|
||||
loading, and the admin boundary live under <code>.impl.ref</code> without leaking through public
|
||||
signatures.</p>
|
||||
<pre><code>data/nenjim/journals/
|
||||
└── <artifact-coordinate>/
|
||||
├── <journal-version>.journal
|
||||
@@ -135,11 +142,15 @@ PREFERRED=nenjim:com_r35157_nenjim-hubd-api=1.0.3:'Built together'</code></pre>
|
||||
registered, and cannot start anything. The internal administration interface must remain package-private so
|
||||
an ordinary service consumer cannot cast the Registry object to a mutation view.</p>
|
||||
<p><code>NenjimHubImpl</code> is the global composition and runtime owner. It constructs the Registry assembly,
|
||||
starts and registers the Registry first, records its successful process ID, and then constructs and registers
|
||||
the hardcoded component graph in dependency order. The Ticker explicitly selects the Raydium EVE/USDT source
|
||||
ID while the hardcoded source remains registered but inactive. The Hub activates only the existing explicit
|
||||
five-process sequence after the Registry; it does not start every registered process. Successful process IDs
|
||||
are exposed as immutable ordered point-in-time snapshots, without implying continued health.</p>
|
||||
starts and registers Registry first, and records its successful process ID. It then constructs the Journal
|
||||
assembly for <code>data/nenjim/journals</code>, registers <code>nenjim.journal.service</code>, starts Journal
|
||||
second, and records it only after complete successful loading. Only then does the Hub construct the hardcoded
|
||||
business component graph in dependency order. A Journal failure therefore stops startup before Ticker,
|
||||
Evelyn, Burner, Mission Control, the online point, and the blocking wait.</p>
|
||||
<p>The Ticker explicitly selects the Raydium EVE/USDT source ID while the hardcoded source remains registered but
|
||||
inactive. After Registry and Journal, the Hub activates only the existing explicit five-business-process
|
||||
sequence; it does not start every registered process. Successful process IDs are exposed as immutable ordered
|
||||
point-in-time snapshots, without implying continued health.</p>
|
||||
<p><code>com.r35157.nenjim.hubd.Main</code> remains only the outer Java bootstrap. It constructs the reference Hub
|
||||
through the public <code>NenjimHub</code> interface and calls <code>start()</code> once. The Hub is neither a
|
||||
component nor a process and is not registered inside its own Registry.</p>
|
||||
|
||||
+37
-24
@@ -1,9 +1,9 @@
|
||||
#<center><H1>Nenjim<br>Documentation</H1></center>
|
||||
|
||||
> **Dokumentstatus:** Journal-format version 1, den immutable Java-model, den interne tekstparser, det read-only
|
||||
> `JournalService`, den eksplicit refresh-baserede `JournalServiceManager` og første version af det centrale
|
||||
> component Registry er implementeret. Contexts, resolution, discovery, runtime loading og removal er fortsat
|
||||
> fremtidig funktionalitet.
|
||||
> `JournalService` med en Hub-styret procesimplementation, den passive eksplicit refresh-baserede
|
||||
> `JournalServiceManager` og første version af det centrale component Registry er implementeret. Contexts,
|
||||
> resolution, discovery, runtime loading og removal er fortsat fremtidig funktionalitet.
|
||||
|
||||
**Indholdsfortegnelse:**
|
||||
<!-- TOC -->
|
||||
@@ -125,16 +125,23 @@ state ændres. Alle publicerede modeller og collections er immutable og defensiv
|
||||
|
||||
### 2.8 JournalService, JournalServiceManager og disk-layout
|
||||
|
||||
En `Service` er det domæne-API, som applikationer og andre services bruger i daglig drift. `JournalService` er derfor
|
||||
filesystem-uafhængigt og read-only: det kan vise coordinates, kronologiske revisionslister, latest, exact og
|
||||
newest-at-or-before snapshots samt en eksakt release fra et valgt snapshot. Det eksponerer ikke start/stop,
|
||||
filesystem paths, refresh, parser eller mutation.
|
||||
En `Service` er det domæne-API, som applikationer og andre services bruger i daglig drift. `JournalService` er en
|
||||
registrerbar Nenjim component, men API'et er fortsat filesystem-uafhængigt og read-only: det kan vise coordinates,
|
||||
kronologiske revisionslister, latest, exact og newest-at-or-before snapshots samt en eksakt release fra et valgt
|
||||
snapshot. Det eksponerer ikke lifecycle, filesystem paths, refresh, parser eller mutation. Queries er først
|
||||
tilgængelige, når Journal-processens initiale loading er lykkedes.
|
||||
|
||||
En `ServiceManager` ejes af en composition/lifecycle owner og håndterer start, stop, configuration/data location,
|
||||
refresh og adgang til den tilhørende Service. `JournalServiceManager` ejer den normaliserede Journal-root. Ved start
|
||||
læser referenceimplementationen komplette UTF-8-filer kronologisk, validerer placement/navne og gør først sit
|
||||
`JournalService` tilgængeligt, når hele initial loading er lykkedes. Refresh er explicit og idempotent; der er ingen
|
||||
watcher eller background polling, og stop ændrer ikke Journal-filer.
|
||||
En `ServiceManager` er et separat administrationshandle, som en composition/lifecycle owner kan beholde uden at
|
||||
give almindelige service-consumers administrative rettigheder. `JournalServiceManager` er passiv og eksponerer kun
|
||||
den normaliserede Journal-root og explicit refresh; den har ingen `start()`, `stop()` eller service-accessor og er
|
||||
hverken component eller process. Hub'en konstruerer ét Journal-objekt, som både er `JournalService` og
|
||||
`NenjimProcess`, samt en separat manager bundet til objektets package-private admin-view.
|
||||
|
||||
Når Hub'en starter Journal-processen, læser referenceimplementationen komplette UTF-8-filer kronologisk og
|
||||
validerer placement/navne. Hele den initiale worldview publiceres først, når alle runtime-filer er accepteret; en
|
||||
fejl efterlader servicen utilgængelig uden partial state og samme instans kan ikke startes igen. Refresh efter
|
||||
vellykket startup er fortsat explicit, idempotent og per-ingestion atomisk; der er ingen watcher eller background
|
||||
polling.
|
||||
|
||||
`JournalServiceImpl` vedligeholder internt kæderne og udfører atomisk ingestion med exact-text SHA-256. Identisk tekst
|
||||
er idempotent; anderledes tekst med samme coordinate og Journal-version er en conflict. Parser, ingestion og mutable
|
||||
@@ -156,7 +163,7 @@ data/nenjim/journals/
|
||||
```
|
||||
|
||||
Kun `.journal` er runtime-input. `.journal.example` dokumenterer formatet og indlæses ikke. Roden skal allerede
|
||||
eksistere og være læsbar; service-manageren opretter, overskriver eller sletter ikke Journal-filer.
|
||||
eksistere og være læsbar; Journal-processen og dens manager opretter, overskriver eller sletter ikke Journal-filer.
|
||||
|
||||
## 3. Opdateringer og Sikkerhed
|
||||
Det implementerede Journal-lag kan repræsentere nye sikkerhedsoplysninger som en nyere immutable revision, for
|
||||
@@ -167,9 +174,9 @@ upgrade/downgrade er fremtidigt arbejde.
|
||||
Betaling og AssetAZ-integration er langsigtet vision og er ikke en del af Journal-formatet eller denne implementation.
|
||||
|
||||
## 5. Backing Store og Fleksibilitet
|
||||
Den nuværende `JournalServiceManagerImpl` læser kun det dokumenterede lokale disk-layout. Artifact downloading, IPFS retrieval,
|
||||
remote Journals og andre backing stores er ikke implementeret. En `ipfs:`-værdi i `INFORMATION_URI` er metadata,
|
||||
ikke en retrieval-instruks.
|
||||
Den nuværende Journal-proces læser kun det dokumenterede lokale disk-layout gennem en intern filesystem-loader.
|
||||
Artifact downloading, IPFS retrieval, remote Journals og andre backing stores er ikke implementeret. En
|
||||
`ipfs:`-værdi i `INFORMATION_URI` er metadata, ikke en retrieval-instruks.
|
||||
|
||||
## 6. Standardiseret og Automatiseret Pakkehåndtering
|
||||
Journalen modellerer dependency-krav og indholdsidentiteter, men downloader ikke bytes og verificerer ikke downloadet
|
||||
@@ -201,12 +208,17 @@ Det kan returnere et immutable registration-order snapshot af ID'er for et compo
|
||||
gennem et forventet interface. Et manglende ID giver `null`; et eksisterende ID med forkert interface er en caller
|
||||
programming error. Public API'et kan ikke registrere eller fjerne componenter.
|
||||
|
||||
`NenjimHubImpl` repræsenterer én kørende Nenjim runtime. Hub'en konstruerer Registry-subsystemet og hele den
|
||||
nuværende hardcodede component graph, ejer dependency wiring og vælger de processer, som skal startes. Et
|
||||
`NenjimHubImpl` repræsenterer én kørende Nenjim runtime. Hub'en konstruerer Registry- og Journal-subsystemerne samt
|
||||
hele den nuværende hardcodede component graph, ejer dependency wiring og vælger de processer, som skal startes. Et
|
||||
implementation-only Registry assembly udleverer read-only service-view, process-view af det samme Registry-objekt
|
||||
og en separat `NenjimRegistryServiceManager`. Manageren er kun Hub'ens passive registrationshandle: den er hverken
|
||||
component eller process, har ingen `start()` eller service-accessor, registreres ikke og starter aldrig en component.
|
||||
|
||||
Et tilsvarende implementation-only Journal assembly udleverer read-only service-view, process-view af det samme
|
||||
Journal-objekt og en separat passiv `JournalServiceManager`. Journal-manageren registreres ikke og giver kun adgang
|
||||
til root og refresh gennem et package-private admin-view; almindelige `JournalService`-consumers kan ikke udlede
|
||||
administrationsadgangen.
|
||||
|
||||
Den package-private `NenjimRegistryServiceAdmin` er managerens interne mutationsadgang. Den må ikke gøres public,
|
||||
fordi almindelige consumers ellers kunne caste deres Registry-service til mutationsinterfacet og bryde det
|
||||
read-only service contract. Registration starter ikke et objekt, og Registry'et forbliver internt åbent for senere
|
||||
@@ -214,12 +226,13 @@ registration efter startup.
|
||||
|
||||
Applikationen ejer sine konkrete valg og sin activation. Tickerens initiale configuration vælger eksplicit
|
||||
`assetaz.price-source.raydium-pool.eve-usdt` gennem Registry'et; den hardcodede price source er registreret, men
|
||||
inaktiv. Hub'en starter Registry-processen først og registrerer den som `nenjim.registry.service`. Derefter starter
|
||||
Hub'en kun den nuværende eksplicitte, dependency-safe liste: Ticker, Production Evelyn, Test Evelyn, Production IOU
|
||||
Burner og Mission Control. Alarmen og øvrige tidligere udkommenterede applikationer bliver ikke automatisk
|
||||
aktiveret. Efter hvert succesfuldt `start()` registrerer Hub'en process-ID'et i et immutable, ordered snapshot; et
|
||||
fejlet start bliver ikke registreret, og listen lover ikke fortsat health eller liveness. Startup er fail-fast og kan
|
||||
kun forsøges én gang uden generic rollback.
|
||||
inaktiv. Hub'en starter Registry-processen først og registrerer den som `nenjim.registry.service`. Journal-servicen
|
||||
registreres dernæst som `nenjim.journal.service`, startes som den anden proces og skal være fuldt initialiseret, før
|
||||
business graph'en fortsætter. Derefter starter Hub'en kun den nuværende eksplicitte, dependency-safe liste: Ticker,
|
||||
Production Evelyn, Test Evelyn, Production IOU Burner og Mission Control. Alarmen og øvrige tidligere
|
||||
udkommenterede applikationer bliver ikke automatisk aktiveret. Efter hvert succesfuldt `start()` registrerer Hub'en
|
||||
process-ID'et i et immutable, ordered snapshot; et fejlet start bliver ikke registreret, og listen lover ikke fortsat
|
||||
health eller liveness. Startup er fail-fast og kan kun forsøges én gang uden generic rollback.
|
||||
|
||||
`com.r35157.nenjim.hubd.Main` er kun det yderste Java-bootstrap. `Main.main(...)` konstruerer `NenjimHubImpl` gennem
|
||||
det globale `NenjimHub`-interface og kalder `start()` præcis én gang. Hub'en ejer også det eksisterende online point
|
||||
|
||||
+6
-5
@@ -1,14 +1,15 @@
|
||||
# Tekniske termer
|
||||
|
||||
> **Dokumentstatus:** Journal-format version 1 og de tilhørende Java-typer, den interne parser, query-service,
|
||||
> lifecycle-manager og første version af det centrale component Registry er implementeret. Resolver-, Context-,
|
||||
> Hub-styret Journal-proces, passive service-managers og første version af det centrale component Registry er
|
||||
> implementeret. Resolver-, Context-,
|
||||
> classloader-, discovery-, runtime-loading-, retrieval- og publishing-termer beskriver kun fremtidig vision, når
|
||||
> det er angivet.
|
||||
|
||||
| Term | Beskrivelse |
|
||||
|------|-------------|
|
||||
| Service | Det offentlige domæne-API, som applikationer og andre services bruger i daglig drift. Det eksponerer almindelige domæneoperationer, men ikke lifecycle, configuration, data-source administration, refresh, parser eller mutation af intern viden. En konkret serviceimplementation kan samtidig være en `NenjimProcess`, uden at lifecycle føjes til service-interfacet. |
|
||||
| ServiceManager | Det offentlige lifecycle- eller administrations-API, som en composition/lifecycle owner bruger. De konkrete operationer følger serviceområdet: JournalServiceManager har start, stop, data location, refresh og adgang til JournalService, mens Registry service manageren kun er et passivt registrationshandle. Det er ikke det daglige domain/query-API for almindelige consumers. |
|
||||
| ServiceManager | Et separat, passivt administrations-API, som en composition/lifecycle owner beholder uden at give almindelige service-consumers administrative rettigheder. Journal-manageren eksponerer root og refresh, mens Registry-manageren eksponerer registration. En manager er ikke det daglige domain/query-API, er hverken component eller process og ejer ikke startup. |
|
||||
| Nenjim component | En færdigkonstrueret Nenjim-byggeklods, som er klar til brug. `NenjimComponent` er en ren marker uden ID eller lifecycle state. Service, applikation, algoritme og adapter beskriver roller, mens process beskriver en uafhængig lifecycle-klassifikation. |
|
||||
| Applikation | En user-facing eller orchestration-rolle. Rollen gør ikke i sig selv en component til en process; en aktiv applikationsimplementation kan separat implementere `NenjimProcess`. |
|
||||
| NenjimProcess | Det canonical `com.r35157.nenjim.process.NenjimProcess` component interface for noget, som Hub'en kan starte og tracke. Det eksponerer kun `start()` og lover ikke stop, restart, health, Context eller dependency lookup. |
|
||||
@@ -37,9 +38,9 @@
|
||||
| BLACKLIST | En hård artifact-policy: matchende releases må ikke vælges. |
|
||||
| DISCOURAGED | Et negativt artifact-policy-hint: matchende releases bør undgås, men er fortsat selectable. |
|
||||
| RECOMMENDED | Et positivt hint til én eksakt release, der findes i samme snapshot. Højeste version er kun en fremtidig fallback, ikke en ubetinget selection-regel. |
|
||||
| JournalService | Filesystem-uafhængigt, read-only Service-API for current og historical Journal-queries. Det eksponerer ingen lifecycle-, path-, refresh-, parser- eller mutationsoperationer. |
|
||||
| JournalServiceManager | Lifecycle- og administrations-API for JournalService. Det ejer den normaliserede data root, udfører komplet initial loading ved start, stopper servicen, udfører explicit refresh og udleverer JournalService efter vellykket initialisering. |
|
||||
| Journal referenceimplementation | `JournalServiceImpl`, `JournalServiceManagerImpl`, `JournalTextParser`, intern ingestion, chain state og filesystem-loading under `com.r35157.nenjim.service.journal.impl.ref`; implementation details lækker ikke gennem public signatures. |
|
||||
| JournalService | Registrerbart, filesystem-uafhængigt og read-only Service-API for current og historical Journal-queries. Det eksponerer ingen lifecycle-, path-, refresh-, parser- eller mutationsoperationer, og queries er gated indtil vellykket proces-startup. |
|
||||
| JournalServiceManager | Passivt, uregistreret administrationshandle for én Journal-service med kun normalized data root og explicit refresh. Hub'en beholder det; det har ingen start, stop, service-accessor eller query-operationer. |
|
||||
| Journal referenceimplementation | Ét package-private `JournalServiceImpl`-objekt er både `JournalService` og `NenjimProcess`; en separat `JournalServiceManagerImpl`, intern admin-view, parser, ingestion, chain state og filesystem-loader ligger under `com.r35157.nenjim.service.journal.impl.ref` uden at lække gennem public signatures. |
|
||||
| Context | Fremtidig beskrivelse af ønskede/resolverede artifactvalg og isoleret runtime; ikke implementeret af Journal-laget eller første Registry-version. |
|
||||
| Resolver | Fremtidig komponent for dependency graph og release-selection; Journal-laget repræsenterer reglerne, men vælger ikke en graf. |
|
||||
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-02
|
||||
@@ -0,0 +1,123 @@
|
||||
## Context
|
||||
|
||||
Issue #75 implemented a strict filesystem-backed Journal and put construction,
|
||||
initial loading, service access, refresh and stop on `JournalServiceManager`.
|
||||
Issue #77 subsequently established a common runtime architecture: the Hub
|
||||
constructs and starts active `NenjimProcess` objects, domain service interfaces
|
||||
stay lifecycle-free, and service managers are separate passive administrative
|
||||
handles retained by the Hub.
|
||||
|
||||
The Journal format, parser, exact-text digest, chain validation, immutable
|
||||
queries and explicit refresh behavior are already accepted and must remain
|
||||
unchanged. Only ownership, lifecycle placement and Hub integration change.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Preserve one read-only registrable `JournalService` domain interface.
|
||||
- Make the same Journal object available as the canonical `NenjimProcess`.
|
||||
- Publish no queryable initial state until the complete filesystem load
|
||||
succeeds.
|
||||
- Keep refresh available only through a distinct passive manager.
|
||||
- Start and record Journal second, after Registry and before business
|
||||
composition/startup.
|
||||
- Keep runtime files read-only and `.journal.example` files ignored.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Journal format, model, parser, chain or policy redesign.
|
||||
- Background watching, polling or remote synchronization.
|
||||
- Generic stop, restart, health, rollback or recovery.
|
||||
- Context, resolution, classloading, artifact transfer, configurable autorun or
|
||||
Process Manager integration.
|
||||
- New general unit tests or production test hooks.
|
||||
|
||||
## Decisions
|
||||
|
||||
### One hidden service implementation provides service, process and admin views
|
||||
|
||||
`JournalServiceImpl` remains under `.impl.ref` and becomes package-private. It
|
||||
implements public `JournalService`, public `NenjimProcess`, and a new
|
||||
package-private `JournalServiceAdmin`. Keeping the concrete type and admin view
|
||||
non-public prevents ordinary service consumers from naming and casting to the
|
||||
filesystem administration contract.
|
||||
|
||||
`JournalServiceAssembly.create(Path)` constructs one service object, exposes it
|
||||
through identity-preserving service and process views, and constructs one
|
||||
distinct `JournalServiceManagerImpl` against its admin view. The assembly owns
|
||||
no lifecycle and exposes no reverse path from an active service reference to
|
||||
its manager.
|
||||
|
||||
### The service process owns a single startup attempt
|
||||
|
||||
The configured root is normalized once during service construction. An atomic
|
||||
start-attempt guard is consumed before filesystem work, so every second
|
||||
`start()` call fails with `IllegalStateException`, including after a failed
|
||||
first attempt. Queries check a volatile availability gate and fail with
|
||||
`IllegalStateException` until startup has succeeded.
|
||||
|
||||
### Initial loading is all-or-nothing
|
||||
|
||||
Startup loads every runtime file into a private candidate Journal service using
|
||||
the existing parser and ingestion rules. Only after the loader has completed
|
||||
does the active object copy the candidate's immutable chains and digest index
|
||||
under its state lock and open the availability gate. A root, layout, parse or
|
||||
chain failure therefore propagates unchanged and leaves the active service with
|
||||
no queryable partial worldview.
|
||||
|
||||
Extracting a separate new mutable chain-store abstraction was rejected because
|
||||
it would broaden a lifecycle refactor into a Journal-domain redesign. The
|
||||
candidate uses the already accepted ingestion implementation and is never
|
||||
published.
|
||||
|
||||
### Filesystem loading belongs behind the service admin boundary
|
||||
|
||||
The existing strict loader logic moves from the manager into a package-private
|
||||
`JournalFilesystemLoader`. The Journal process invokes it for initial loading;
|
||||
the manager delegates refresh through `JournalServiceAdmin`. The loader keeps
|
||||
the exact root/layout/readability checks, deterministic ordering, placement
|
||||
validation, `.journal.example` exclusion and no-write behavior.
|
||||
|
||||
Post-start refresh loads into the active service and deliberately preserves the
|
||||
existing per-ingestion atomicity and idempotency. The issue does not strengthen
|
||||
refresh into whole-filesystem transactional replacement.
|
||||
|
||||
### Hub starts Journal as infrastructure before the business graph
|
||||
|
||||
After Registry start, registration and successful-history recording,
|
||||
`NenjimHubImpl` creates the Journal assembly for `data/nenjim/journals`, retains
|
||||
its manager, registers the service as `nenjim.journal.service`, starts its
|
||||
process view, and records the ID only after `start()` returns. Business graph
|
||||
construction and the existing enabled startup sequence occur only afterwards.
|
||||
|
||||
Consequently, a Journal failure propagates from the Hub while the Registry is
|
||||
the sole recorded process. The Hub neither constructs/starts later business
|
||||
processes nor reaches its online point or blocking wait. No rollback is added
|
||||
because the common process contract has no stop operation.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [The hidden implementation was formerly public] → No accepted public API
|
||||
signature returns it; the assembly exposes only public interfaces and keeps
|
||||
the documented implementation boundary intact.
|
||||
- [Loading through a candidate duplicates temporary in-memory state] → Startup
|
||||
happens once and Journal data is expected to be modest; the temporary cost
|
||||
buys clear all-or-nothing publication without changing domain semantics.
|
||||
- [Refresh can accept an earlier new revision before a later file fails] → This
|
||||
is the already specified per-ingestion atomic behavior and is intentionally
|
||||
preserved.
|
||||
- [The Hub's successful path blocks indefinitely] → Verify Journal lifecycle,
|
||||
identity, boundaries and sample-root loading with temporary probes rather
|
||||
than invoking production `main()`.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Change Journal contracts and add the internal admin, loader and assembly
|
||||
boundary.
|
||||
2. Move startup into the service process and reduce the manager to passive
|
||||
delegation.
|
||||
3. Register/start Journal in the Hub before business composition and startup.
|
||||
4. Update OpenSpec and directly affected documentation.
|
||||
5. Run strict OpenSpec validation, the complete Gradle build and safe temporary
|
||||
boundary/lifecycle probes, then leave this change active for review.
|
||||
@@ -0,0 +1,40 @@
|
||||
## Why
|
||||
|
||||
Issue #75 left the Journal subsystem on a manager-owned lifecycle, while issue
|
||||
#77 established `NenjimHub` as the runtime owner and `NenjimProcess` as the
|
||||
canonical active lifecycle. Journal must use that common model before later
|
||||
runtime work can depend on it consistently.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Make `JournalService` a registrable Nenjim component while keeping its query
|
||||
API read-only and filesystem-independent.
|
||||
- Make the reference Journal service object the single-use active
|
||||
`NenjimProcess`, with atomic publication after a complete successful initial
|
||||
filesystem load.
|
||||
- Reduce `JournalServiceManager` to the passive `journalRoot()` and `refresh()`
|
||||
administrative capability, bound through a non-public admin view.
|
||||
- Add an implementation-only Journal assembly that provides identical service
|
||||
and process views plus a distinct manager.
|
||||
- Have `NenjimHubImpl` register and start `nenjim.journal.service` immediately
|
||||
after the Registry and before every business process.
|
||||
- Preserve Journal format, parsing, chain, query, refresh and filesystem
|
||||
semantics while updating directly affected documentation.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `nenjim-journal-management`: move lifecycle and initial loading from the
|
||||
manager to the service process, add startup query gating and the safe
|
||||
construction boundary, and preserve explicit refresh.
|
||||
- `nenjim-component-registry`: extend Hub-owned composition, startup order,
|
||||
started-process history and fail-fast behavior with the Journal process.
|
||||
|
||||
## Impact
|
||||
|
||||
Affected code is limited to the Journal public contracts and reference
|
||||
implementation, the Hub composition root, directly affected architecture
|
||||
documentation, and this active OpenSpec change. No Journal data format,
|
||||
business-service behavior, configurable autorun, stop lifecycle, Context,
|
||||
resolver, classloader, watcher or general test suite is added.
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: NenjimHub owns composition and runtime startup
|
||||
`NenjimHub` SHALL be the global runtime owner and SHALL expose `start() throws Exception` plus an immutable ordered snapshot of successfully started process IDs. It SHALL be neither a component nor a process and SHALL not register itself. Its only accepted startup attempt SHALL construct the Registry subsystem, start the Registry process first, register it as `nenjim.registry.service`, construct and register the Journal subsystem as `nenjim.journal.service`, start Journal second, and continue to the complete hardcoded business component set from issue #76 only after Journal startup succeeds. The Hub SHALL then resolve only the explicitly enabled business IDs through `NenjimProcess`, start them in dependency-safe order, announce the existing online point, and preserve the existing blocking wait. Registration SHALL never imply startup, and both the Registry and Journal managers SHALL remain distinct passive, unregistered handles retained privately by the Hub.
|
||||
|
||||
#### Scenario: Registry starts before graph construction
|
||||
- **WHEN** the Hub performs its first startup attempt
|
||||
- **THEN** it starts, registers and records Registry first, before constructing Journal or components in the business graph
|
||||
|
||||
#### Scenario: Journal starts before business composition
|
||||
- **WHEN** Registry startup and self-registration have succeeded
|
||||
- **THEN** the Hub registers, starts and records Journal before constructing components in the business graph
|
||||
|
||||
#### Scenario: Registry is process-discoverable
|
||||
- **WHEN** the Registry registration has completed
|
||||
- **THEN** the same object is retrievable as both `NenjimRegistryService` and `NenjimProcess`
|
||||
|
||||
#### Scenario: Journal is process-discoverable
|
||||
- **WHEN** the Journal registration has completed
|
||||
- **THEN** the same object is retrievable as both `JournalService` and `NenjimProcess` under `nenjim.journal.service`, while its manager is not registered
|
||||
|
||||
#### Scenario: Required hardcoded bindings are registered
|
||||
- **WHEN** Hub composition completes
|
||||
- **THEN** Journal and all component IDs and implementation choices enumerated by issue #76 are present exactly as configured, including intentionally inactive components and excluding both service managers
|
||||
|
||||
#### Scenario: Explicit startup subset preserves order
|
||||
- **WHEN** Hub startup succeeds through the business-process phase
|
||||
- **THEN** successful process order is `nenjim.registry.service`, `nenjim.journal.service`, `assetaz.ticker.default`, `evelyn.service.prod`, `evelyn.service.test`, `evelyn.iou-burner.prod`, and `evelyn.mission-control.default`
|
||||
|
||||
#### Scenario: Deliberately inactive processes remain inactive
|
||||
- **WHEN** Hub startup reaches its online point
|
||||
- **THEN** `jupiter-perps-alarm.default` and the dormant Composer, Process Manager, Test Tool, Soda Task Manager, and Suwimo Client paths have not been started
|
||||
|
||||
#### Scenario: Journal failure is fail-fast
|
||||
- **WHEN** Journal process startup throws
|
||||
- **THEN** Registry remains the only recorded process, no business component is constructed or started, the exception propagates, and the Hub neither announces online nor enters its final wait
|
||||
|
||||
#### Scenario: Hub start is not retried
|
||||
- **WHEN** any first Hub startup attempt has begun and `start()` is requested again
|
||||
- **THEN** the later request fails with `IllegalStateException` whether the first attempt succeeded or failed
|
||||
|
||||
#### Scenario: Startup failure is fail-fast
|
||||
- **WHEN** any process startup throws
|
||||
- **THEN** the exception propagates, no later process starts, the Hub neither announces online nor enters its final wait, and no generic rollback or retry is attempted
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Journal construction preserves process identity and administration boundaries
|
||||
The reference Journal construction boundary SHALL create one Journal object configured with a normalized absolute data root and exposed through both `JournalService` and `NenjimProcess`, plus one distinct `JournalServiceManager` bound to that object through a package-private administration view. The service and process views SHALL preserve object identity. The construction helper SHALL own no lifecycle, SHALL be neither a component nor a process, SHALL not be registered, and SHALL expose neither parser, ingestion, chain state, filesystem administration nor a way to derive the active Journal's manager from an ordinary service reference.
|
||||
|
||||
#### Scenario: Journal views preserve identity
|
||||
- **WHEN** the Hub obtains a newly constructed Journal assembly
|
||||
- **THEN** its service and process views reference the identical Journal object while its manager is a different object
|
||||
|
||||
#### Scenario: Ordinary service reference remains read-only
|
||||
- **WHEN** an ordinary consumer receives the public Journal service view
|
||||
- **THEN** the consumer cannot name or derive the package-private administration view or obtain the manager bound to that active Journal
|
||||
|
||||
### Requirement: Journal process startup is single-use and publishes atomically
|
||||
The reference Journal service object SHALL implement `NenjimProcess`. Its first `start()` call SHALL be the only accepted attempt, SHALL load only its construction-configured root, and SHALL make query state available only after the complete initial filesystem load succeeds. A later start SHALL fail with `IllegalStateException`, including after a failed first attempt. Queries before successful startup or after failed startup SHALL fail with `IllegalStateException`, and no failed initial load SHALL expose a partial Journal worldview.
|
||||
|
||||
#### Scenario: Successful initial process start
|
||||
- **WHEN** the configured root and every runtime Journal entry are valid
|
||||
- **THEN** the process returns only after the complete chronological load is queryable
|
||||
|
||||
#### Scenario: Example-only root starts empty
|
||||
- **WHEN** the configured root exists and contains only valid artifact directories with `.journal.example` files
|
||||
- **THEN** process startup succeeds and the service exposes an empty immutable coordinate set
|
||||
|
||||
#### Scenario: Query before successful startup
|
||||
- **WHEN** any Journal query is called before startup succeeds or after startup fails
|
||||
- **THEN** it fails with `IllegalStateException` and exposes no candidate state
|
||||
|
||||
#### Scenario: Failed startup is not retried
|
||||
- **WHEN** the first Journal process start fails and `start()` is called again
|
||||
- **THEN** the second call fails with `IllegalStateException` without another loading attempt
|
||||
|
||||
#### Scenario: Failed initial load publishes nothing
|
||||
- **WHEN** an earlier runtime entry is valid but any later entry fails root, layout, parsing, placement or chain validation
|
||||
- **THEN** the original meaningful failure propagates and no accepted candidate revision becomes queryable
|
||||
|
||||
### Requirement: Passive Journal service manager
|
||||
`JournalServiceManager` SHALL expose exactly the normalized configured `journalRoot()` and explicit `refresh()` administration operations. It SHALL be neither a Nenjim component nor a Nenjim process, SHALL be retained privately by the Hub, SHALL bind to exactly one Journal object through non-public administration, and SHALL expose no start, stop, service accessor, query, parser, ingestion or chain-state operation. Refresh before successful Journal process startup SHALL fail with `IllegalStateException`.
|
||||
|
||||
#### Scenario: Manager contract is passive
|
||||
- **WHEN** the public manager interface and reference manager are inspected
|
||||
- **THEN** only root reporting and refresh are available and the manager has no lifecycle or component identity
|
||||
|
||||
#### Scenario: Refresh follows process startup
|
||||
- **WHEN** refresh is requested before its Journal process has started successfully
|
||||
- **THEN** the manager rejects it with `IllegalStateException`
|
||||
|
||||
#### Scenario: Manager delegates after startup
|
||||
- **WHEN** refresh is requested after successful Journal startup
|
||||
- **THEN** the manager delegates through the non-public admin view to the same active Journal object
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Filesystem-independent read-only Journal service
|
||||
`JournalService` SHALL extend `NenjimComponent` as the ordinary registrable, application-facing, filesystem-independent, read-only API. It SHALL list known artifact coordinates and chronological revisions; obtain latest, exact, and newest-at-or-before revisions; obtain an exact artifact release from a selected revision; return immutable results without `null`; and expose no lifecycle, path, refresh, parser, configuration, administration or mutation operation. Its queries SHALL be available only after the Journal process has completed successful startup.
|
||||
|
||||
#### Scenario: Chronological and latest queries
|
||||
- **WHEN** an artifact has several accepted revisions after successful startup
|
||||
- **THEN** the service lists all complete snapshots in ascending Journal-version order and returns the chain head as latest
|
||||
|
||||
#### Scenario: Exact and as-of historical queries
|
||||
- **WHEN** a caller requests an exact revision or the newest revision at or before a Journal version
|
||||
- **THEN** the service returns the complete worldview from that chain position rather than filtering a newer snapshot by release publication time
|
||||
|
||||
#### Scenario: Release query from selected worldview
|
||||
- **WHEN** a caller requests an exact artifact version from a selected revision
|
||||
- **THEN** the release, including license, content, dependencies, and policy context in that snapshot, is returned only if that revision knows it
|
||||
|
||||
#### Scenario: Missing query result
|
||||
- **WHEN** a coordinate, revision, as-of revision, or artifact release is unknown after successful startup
|
||||
- **THEN** the service returns an empty optional or immutable empty collection rather than `null`
|
||||
|
||||
#### Scenario: Read-only boundary
|
||||
- **WHEN** an ordinary consumer receives `JournalService`
|
||||
- **THEN** the interface is registrable as a component but provides no start, stop, filesystem root, refresh, parsing, update, ingestion, administration or other mutation operation
|
||||
|
||||
### Requirement: Filesystem-backed lifecycle loading
|
||||
The reference Journal service process SHALL own a configured existing readable root and SHALL use non-public filesystem loading to discover runtime files only at `<root>/<artifact-coordinate>/<journal-version>.journal`, ignore `.journal.example`, validate directory and filename placement against parsed content, sort each artifact's files chronologically, read complete UTF-8 text, and supply it to the existing internal ingestion operation. Initial loading SHALL publish only after the complete root succeeds. Later refresh SHALL target the active service through its non-public administration view. Neither path SHALL create, overwrite, delete, publish, download, synchronize or watch files.
|
||||
|
||||
#### Scenario: Valid filesystem load
|
||||
- **WHEN** an artifact directory contains chronologically named valid genesis and successor `.journal` files whose content identity matches their placement
|
||||
- **THEN** initial process startup or a later explicit refresh ingests both complete UTF-8 texts in chronological order and exposes their chain through the service
|
||||
|
||||
#### Scenario: Example files are ignored
|
||||
- **WHEN** a directory contains `<journal-version>.journal.example`
|
||||
- **THEN** loading does not ingest it as a runtime Journal
|
||||
|
||||
#### Scenario: Invalid directory placement
|
||||
- **WHEN** a runtime Journal's metadata-derived coordinate differs from its parent directory name
|
||||
- **THEN** loading fails clearly
|
||||
|
||||
#### Scenario: Invalid filename placement
|
||||
- **WHEN** a runtime filename does not use the strict timestamp syntax or differs from the parsed `JOURNAL_VERSION`
|
||||
- **THEN** loading fails clearly
|
||||
|
||||
#### Scenario: Invalid root
|
||||
- **WHEN** the configured Journal root is missing, not a directory, or unreadable
|
||||
- **THEN** startup or refresh fails clearly without creating it
|
||||
|
||||
#### Scenario: Unexpected filesystem entry
|
||||
- **WHEN** the configured root contains a runtime entry outside the artifact-directory/revision-file structure
|
||||
- **THEN** loading fails rather than silently treating it as a Journal
|
||||
|
||||
### Requirement: Explicit idempotent refresh
|
||||
The passive service manager SHALL support explicit refresh only after successful Journal process startup. Repeating refresh for unchanged files SHALL be idempotent, and a later refresh SHALL discover valid newly added successors while preserving existing historical revisions and the existing per-ingestion atomicity.
|
||||
|
||||
#### Scenario: Unchanged refresh
|
||||
- **WHEN** refresh runs twice after startup without filesystem changes
|
||||
- **THEN** the second run leaves all chains unchanged and succeeds through ingestion idempotency
|
||||
|
||||
#### Scenario: New revision discovered
|
||||
- **WHEN** a valid successor file is added after successful startup or an earlier refresh
|
||||
- **THEN** the next refresh ingests it and the read-only service exposes it as the new head
|
||||
|
||||
#### Scenario: Refresh is not a service operation
|
||||
- **WHEN** ordinary consumers inspect `JournalService` and `NenjimProcess`
|
||||
- **THEN** neither interface exposes refresh
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Journal service lifecycle manager
|
||||
**Reason**: Lifecycle ownership belongs to the Journal service object as a Hub-managed `NenjimProcess`; the manager is only a passive administrative capability.
|
||||
|
||||
**Migration**: The Hub starts the Journal through its process view and retains a distinct manager exposing only `journalRoot()` and `refresh()`.
|
||||
@@ -0,0 +1,24 @@
|
||||
## 1. Journal contracts and construction boundary
|
||||
|
||||
- [x] 1.1 Make `JournalService` a registrable component without adding lifecycle or administration to its query API.
|
||||
- [x] 1.2 Reduce `JournalServiceManager` to `journalRoot()` and `refresh()` and remove lifecycle/service-access compatibility methods.
|
||||
- [x] 1.3 Add the package-private Journal admin and filesystem-loader boundary plus an implementation-only assembly with identity-preserving service/process views.
|
||||
|
||||
## 2. Journal lifecycle and Hub integration
|
||||
|
||||
- [x] 2.1 Make the hidden reference service a single-attempt `NenjimProcess` with query gating and all-or-nothing initial-state publication.
|
||||
- [x] 2.2 Preserve strict read-only filesystem discovery, chronological ingestion, `.journal.example` exclusion and explicit idempotent refresh.
|
||||
- [x] 2.3 Construct, register, start and record `nenjim.journal.service` after Registry and before the existing business graph while retaining the passive manager privately.
|
||||
|
||||
## 3. OpenSpec and documentation
|
||||
|
||||
- [x] 3.1 Add Journal-management deltas for the process lifecycle, passive manager, construction boundary, startup availability and preserved refresh semantics.
|
||||
- [x] 3.2 Extend Hub runtime requirements with Journal registration, second-process ordering and fail-fast behavior.
|
||||
- [x] 3.3 Update directly affected Markdown, terminology, public HTML and JavaDoc to describe the common service/process/manager model.
|
||||
|
||||
## 4. Verification
|
||||
|
||||
- [x] 4.1 Strictly validate this active change and all repository specifications without synchronizing or archiving it.
|
||||
- [x] 4.2 Run the complete Gradle clean build without invoking production `main()`.
|
||||
- [x] 4.3 Use removable probes to verify view identity, pre-start/failed-start query gating, one-shot startup, example-only empty startup, refresh and non-public administration.
|
||||
- [x] 4.4 Audit the final diff, generated-source references, acceptance criteria and out-of-scope boundaries without committing or pushing.
|
||||
@@ -186,34 +186,46 @@ The reference Registry construction boundary SHALL create one Registry object ex
|
||||
- **THEN** no component ID identifies the manager and no lookup returns it as a component or process
|
||||
|
||||
### Requirement: NenjimHub owns composition and runtime startup
|
||||
`NenjimHub` SHALL be the global runtime owner and SHALL expose `start() throws Exception` plus an immutable ordered snapshot of successfully started process IDs. It SHALL be neither a component nor a process and SHALL not register itself. Its only accepted startup attempt SHALL construct the Registry subsystem, start the Registry process first, register it as `nenjim.registry.service`, construct and register the complete hardcoded component set from issue #76 in dependency order, resolve only the explicitly enabled IDs through `NenjimProcess`, start them in dependency-safe order, announce the existing online point, and preserve the existing blocking wait. Registration SHALL never imply startup.
|
||||
`NenjimHub` SHALL be the global runtime owner and SHALL expose `start() throws Exception` plus an immutable ordered snapshot of successfully started process IDs. It SHALL be neither a component nor a process and SHALL not register itself. Its only accepted startup attempt SHALL construct the Registry subsystem, start the Registry process first, register it as `nenjim.registry.service`, construct and register the Journal subsystem as `nenjim.journal.service`, start Journal second, and continue to the complete hardcoded business component set from issue #76 only after Journal startup succeeds. The Hub SHALL then resolve only the explicitly enabled business IDs through `NenjimProcess`, start them in dependency-safe order, announce the existing online point, and preserve the existing blocking wait. Registration SHALL never imply startup, and both the Registry and Journal managers SHALL remain distinct passive, unregistered handles retained privately by the Hub.
|
||||
|
||||
#### Scenario: Registry starts before graph construction
|
||||
- **WHEN** the Hub performs its first startup attempt
|
||||
- **THEN** it starts the Registry, registers it as `nenjim.registry.service`, and records that ID before constructing components whose constructors may query the Registry
|
||||
- **THEN** it starts, registers and records Registry first, before constructing Journal or components in the business graph
|
||||
|
||||
#### Scenario: Journal starts before business composition
|
||||
- **WHEN** Registry startup and self-registration have succeeded
|
||||
- **THEN** the Hub registers, starts and records Journal before constructing components in the business graph
|
||||
|
||||
#### Scenario: Registry is process-discoverable
|
||||
- **WHEN** the Registry registration has completed
|
||||
- **THEN** the same object is retrievable as both `NenjimRegistryService` and `NenjimProcess`
|
||||
|
||||
#### Scenario: Journal is process-discoverable
|
||||
- **WHEN** the Journal registration has completed
|
||||
- **THEN** the same object is retrievable as both `JournalService` and `NenjimProcess` under `nenjim.journal.service`, while its manager is not registered
|
||||
|
||||
#### Scenario: Required hardcoded bindings are registered
|
||||
- **WHEN** Hub composition completes
|
||||
- **THEN** all component IDs and implementation choices enumerated by issue #76 are present exactly as configured, including intentionally inactive components and excluding the former Registry-manager ID
|
||||
- **THEN** Journal and all component IDs and implementation choices enumerated by issue #76 are present exactly as configured, including intentionally inactive components and excluding both service managers
|
||||
|
||||
#### Scenario: Explicit startup subset preserves order
|
||||
- **WHEN** the Hub starts processes after the Registry
|
||||
- **THEN** it starts `assetaz.ticker.default`, `evelyn.service.prod`, `evelyn.service.test`, `evelyn.iou-burner.prod`, and `evelyn.mission-control.default` in that order
|
||||
- **WHEN** Hub startup succeeds through the business-process phase
|
||||
- **THEN** successful process order is `nenjim.registry.service`, `nenjim.journal.service`, `assetaz.ticker.default`, `evelyn.service.prod`, `evelyn.service.test`, `evelyn.iou-burner.prod`, and `evelyn.mission-control.default`
|
||||
|
||||
#### Scenario: Deliberately inactive processes remain inactive
|
||||
- **WHEN** Hub startup reaches its online point
|
||||
- **THEN** `jupiter-perps-alarm.default` and the dormant Composer, Process Manager, Test Tool, Soda Task Manager, and Suwimo Client paths have not been started
|
||||
|
||||
#### Scenario: Journal failure is fail-fast
|
||||
- **WHEN** Journal process startup throws
|
||||
- **THEN** Registry remains the only recorded process, no business component is constructed or started, the exception propagates, and the Hub neither announces online nor enters its final wait
|
||||
|
||||
#### Scenario: Hub start is not retried
|
||||
- **WHEN** any first Hub startup attempt has begun and `start()` is requested again
|
||||
- **THEN** the later request fails with `IllegalStateException` whether the first attempt succeeded or failed
|
||||
|
||||
#### Scenario: Startup failure is fail-fast
|
||||
- **WHEN** a process startup throws
|
||||
- **WHEN** any process startup throws
|
||||
- **THEN** the exception propagates, no later process starts, the Hub neither announces online nor enters its final wait, and no generic rollback or retry is attempted
|
||||
|
||||
### Requirement: Started-process snapshots are ordered immutable and truthful
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
## Purpose
|
||||
|
||||
Defines the public Journal query service, its lifecycle manager, internal atomic chain ingestion, historical query behavior, and explicit filesystem loading of immutable Journal revisions.
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Strict Journal API package boundary
|
||||
The public Journal API SHALL begin at `com.r35157.nenjim.service.journal`, where the only directly contained Java types SHALL be the public interfaces `JournalService` and `JournalServiceManager`. Public composed domain objects SHALL be under `.model`, public value types SHALL be under `.valuetypes`, public Journal-specific exceptions SHALL be under `.exception`, and the reference implementation and its parser, ingestion, chain-state, and filesystem helpers SHALL be under `.impl.ref` or its subpackages without leaking implementation types through public signatures.
|
||||
|
||||
@@ -23,10 +20,10 @@ The public Journal API SHALL begin at `com.r35157.nenjim.service.journal`, where
|
||||
- **THEN** no public `JournalManager`, deprecated alias, forwarding wrapper, or compatibility type remains under `com.r35157.nenjim.journal` or another superseded package
|
||||
|
||||
### Requirement: Filesystem-independent read-only Journal service
|
||||
`JournalService` SHALL be the ordinary application-facing, filesystem-independent, read-only API. It SHALL list known artifact coordinates and chronological revisions; obtain latest, exact, and newest-at-or-before revisions; obtain an exact artifact release from a selected revision; return immutable results without `null`; and expose no lifecycle, path, refresh, parser, configuration, or mutation operation.
|
||||
`JournalService` SHALL extend `NenjimComponent` as the ordinary registrable, application-facing, filesystem-independent, read-only API. It SHALL list known artifact coordinates and chronological revisions; obtain latest, exact, and newest-at-or-before revisions; obtain an exact artifact release from a selected revision; return immutable results without `null`; and expose no lifecycle, path, refresh, parser, configuration, administration or mutation operation. Its queries SHALL be available only after the Journal process has completed successful startup.
|
||||
|
||||
#### Scenario: Chronological and latest queries
|
||||
- **WHEN** an artifact has several accepted revisions
|
||||
- **WHEN** an artifact has several accepted revisions after successful startup
|
||||
- **THEN** the service lists all complete snapshots in ascending Journal-version order and returns the chain head as latest
|
||||
|
||||
#### Scenario: Exact and as-of historical queries
|
||||
@@ -38,12 +35,12 @@ The public Journal API SHALL begin at `com.r35157.nenjim.service.journal`, where
|
||||
- **THEN** the release, including license, content, dependencies, and policy context in that snapshot, is returned only if that revision knows it
|
||||
|
||||
#### Scenario: Missing query result
|
||||
- **WHEN** a coordinate, revision, as-of revision, or artifact release is unknown
|
||||
- **WHEN** a coordinate, revision, as-of revision, or artifact release is unknown after successful startup
|
||||
- **THEN** the service returns an empty optional or immutable empty collection rather than `null`
|
||||
|
||||
#### Scenario: Read-only boundary
|
||||
- **WHEN** an ordinary consumer receives `JournalService`
|
||||
- **THEN** the interface provides no start, stop, filesystem root, refresh, parsing, update, ingestion, or other mutation operation
|
||||
- **THEN** the interface is registrable as a component but provides no start, stop, filesystem root, refresh, parsing, update, ingestion, administration or other mutation operation
|
||||
|
||||
### Requirement: Internal atomic text ingestion and chain identity
|
||||
The reference service implementation SHALL keep mutable ingestion non-public and filesystem-independent. One ingestion SHALL reject null, empty, or blank input; parse and validate the complete text; compute SHA-256 over its exact UTF-8 bytes including whitespace and line endings; validate its chain relationship; and atomically expose the immutable Journal through `JournalService`. A failed ingestion SHALL leave every previously accepted revision unchanged for that operation.
|
||||
@@ -90,35 +87,12 @@ Supplying the same coordinate, Journal version, and exact complete text more tha
|
||||
- **WHEN** different text is supplied for an already stored coordinate and Journal version
|
||||
- **THEN** ingestion reports a conflict and does not mutate state
|
||||
|
||||
### Requirement: Journal service lifecycle manager
|
||||
`JournalServiceManager` SHALL be the lifecycle and administrative API. It SHALL start and stop the service, report the normalized configured Journal data root, perform explicit refresh, and expose the corresponding `JournalService` only after successful initialization. It SHALL not duplicate the Journal query API.
|
||||
|
||||
#### Scenario: Successful initial start
|
||||
- **WHEN** the configured root and all runtime Journal data are valid
|
||||
- **THEN** start performs the complete initial chronological load before the manager reports success and exposes its read-only service
|
||||
|
||||
#### Scenario: Failed initial start
|
||||
- **WHEN** the root, placement, filename, text, or chain is invalid during initial loading
|
||||
- **THEN** start fails clearly and the manager does not report a started service or expose a partially loaded service
|
||||
|
||||
#### Scenario: Service access follows lifecycle
|
||||
- **WHEN** service access or refresh is requested before successful start or after stop
|
||||
- **THEN** the manager rejects the operation as unavailable in the current lifecycle state
|
||||
|
||||
#### Scenario: Stop is non-destructive
|
||||
- **WHEN** the manager is stopped
|
||||
- **THEN** service-owned maintenance stops cleanly without creating, rewriting, or deleting Journal files
|
||||
|
||||
#### Scenario: Administrative API does not duplicate queries
|
||||
- **WHEN** `JournalServiceManager` is inspected
|
||||
- **THEN** artifact and revision query operations are available only through its associated `JournalService`
|
||||
|
||||
### Requirement: Filesystem-backed lifecycle loading
|
||||
The reference service manager SHALL own a configured existing readable root, discover runtime files only at `<root>/<artifact-coordinate>/<journal-version>.journal`, ignore `.journal.example`, validate directory and filename placement against parsed content, sort each artifact's files chronologically, read complete UTF-8 text, and supply it to the internal ingestion operation. It SHALL not create, overwrite, delete, publish, download, synchronize, or watch files.
|
||||
The reference Journal service process SHALL own a configured existing readable root and SHALL use non-public filesystem loading to discover runtime files only at `<root>/<artifact-coordinate>/<journal-version>.journal`, ignore `.journal.example`, validate directory and filename placement against parsed content, sort each artifact's files chronologically, read complete UTF-8 text, and supply it to the existing internal ingestion operation. Initial loading SHALL publish only after the complete root succeeds. Later refresh SHALL target the active service through its non-public administration view. Neither path SHALL create, overwrite, delete, publish, download, synchronize or watch files.
|
||||
|
||||
#### Scenario: Valid filesystem load
|
||||
- **WHEN** an artifact directory contains chronologically named valid genesis and successor `.journal` files whose content identity matches their placement
|
||||
- **THEN** initial start or refresh ingests both complete UTF-8 texts in chronological order and exposes their chain through the service
|
||||
- **THEN** initial process startup or a later explicit refresh ingests both complete UTF-8 texts in chronological order and exposes their chain through the service
|
||||
|
||||
#### Scenario: Example files are ignored
|
||||
- **WHEN** a directory contains `<journal-version>.journal.example`
|
||||
@@ -134,23 +108,27 @@ The reference service manager SHALL own a configured existing readable root, dis
|
||||
|
||||
#### Scenario: Invalid root
|
||||
- **WHEN** the configured Journal root is missing, not a directory, or unreadable
|
||||
- **THEN** start or refresh fails clearly without creating it
|
||||
- **THEN** startup or refresh fails clearly without creating it
|
||||
|
||||
#### Scenario: Unexpected filesystem entry
|
||||
- **WHEN** the configured root contains a runtime entry outside the artifact-directory/revision-file structure
|
||||
- **THEN** loading fails rather than silently treating it as a Journal
|
||||
|
||||
### Requirement: Explicit idempotent refresh
|
||||
The service manager SHALL support explicit refresh. Repeating refresh for unchanged files SHALL be idempotent, and a later refresh SHALL discover valid newly added successors while preserving existing historical revisions.
|
||||
The passive service manager SHALL support explicit refresh only after successful Journal process startup. Repeating refresh for unchanged files SHALL be idempotent, and a later refresh SHALL discover valid newly added successors while preserving existing historical revisions and the existing per-ingestion atomicity.
|
||||
|
||||
#### Scenario: Unchanged refresh
|
||||
- **WHEN** refresh runs twice without filesystem changes
|
||||
- **WHEN** refresh runs twice after startup without filesystem changes
|
||||
- **THEN** the second run leaves all chains unchanged and succeeds through ingestion idempotency
|
||||
|
||||
#### Scenario: New revision discovered
|
||||
- **WHEN** a valid successor file is added after an earlier successful refresh
|
||||
- **WHEN** a valid successor file is added after successful startup or an earlier refresh
|
||||
- **THEN** the next refresh ingests it and the read-only service exposes it as the new head
|
||||
|
||||
#### Scenario: Refresh is not a service operation
|
||||
- **WHEN** ordinary consumers inspect `JournalService` and `NenjimProcess`
|
||||
- **THEN** neither interface exposes refresh
|
||||
|
||||
### Requirement: Version-controlled examples and migration
|
||||
Version-controlled examples SHALL use `<journal-version>.journal.example` beneath an artifact-coordinate directory and collectively demonstrate quoted description, a scheme-independent information URI, per-release license, dependencies, exclusions, preferences, policy, exact/range syntax, and optional descriptions. Any multi-revision example chain SHALL use the predecessor file's real exact-text SHA-256 digest.
|
||||
|
||||
@@ -161,3 +139,52 @@ Version-controlled examples SHALL use `<journal-version>.journal.example` beneat
|
||||
#### Scenario: Example chain integrity
|
||||
- **WHEN** examples include a successor revision
|
||||
- **THEN** its predecessor digest equals the SHA-256 of the predecessor example file's exact UTF-8 bytes
|
||||
|
||||
### Requirement: Journal construction preserves process identity and administration boundaries
|
||||
The reference Journal construction boundary SHALL create one Journal object configured with a normalized absolute data root and exposed through both `JournalService` and `NenjimProcess`, plus one distinct `JournalServiceManager` bound to that object through a package-private administration view. The service and process views SHALL preserve object identity. The construction helper SHALL own no lifecycle, SHALL be neither a component nor a process, SHALL not be registered, and SHALL expose neither parser, ingestion, chain state, filesystem administration nor a way to derive the active Journal's manager from an ordinary service reference.
|
||||
|
||||
#### Scenario: Journal views preserve identity
|
||||
- **WHEN** the Hub obtains a newly constructed Journal assembly
|
||||
- **THEN** its service and process views reference the identical Journal object while its manager is a different object
|
||||
|
||||
#### Scenario: Ordinary service reference remains read-only
|
||||
- **WHEN** an ordinary consumer receives the public Journal service view
|
||||
- **THEN** the consumer cannot name or derive the package-private administration view or obtain the manager bound to that active Journal
|
||||
|
||||
### Requirement: Journal process startup is single-use and publishes atomically
|
||||
The reference Journal service object SHALL implement `NenjimProcess`. Its first `start()` call SHALL be the only accepted attempt, SHALL load only its construction-configured root, and SHALL make query state available only after the complete initial filesystem load succeeds. A later start SHALL fail with `IllegalStateException`, including after a failed first attempt. Queries before successful startup or after failed startup SHALL fail with `IllegalStateException`, and no failed initial load SHALL expose a partial Journal worldview.
|
||||
|
||||
#### Scenario: Successful initial process start
|
||||
- **WHEN** the configured root and every runtime Journal entry are valid
|
||||
- **THEN** the process returns only after the complete chronological load is queryable
|
||||
|
||||
#### Scenario: Example-only root starts empty
|
||||
- **WHEN** the configured root exists and contains only valid artifact directories with `.journal.example` files
|
||||
- **THEN** process startup succeeds and the service exposes an empty immutable coordinate set
|
||||
|
||||
#### Scenario: Query before successful startup
|
||||
- **WHEN** any Journal query is called before startup succeeds or after startup fails
|
||||
- **THEN** it fails with `IllegalStateException` and exposes no candidate state
|
||||
|
||||
#### Scenario: Failed startup is not retried
|
||||
- **WHEN** the first Journal process start fails and `start()` is called again
|
||||
- **THEN** the second call fails with `IllegalStateException` without another loading attempt
|
||||
|
||||
#### Scenario: Failed initial load publishes nothing
|
||||
- **WHEN** an earlier runtime entry is valid but any later entry fails root, layout, parsing, placement or chain validation
|
||||
- **THEN** the original meaningful failure propagates and no accepted candidate revision becomes queryable
|
||||
|
||||
### Requirement: Passive Journal service manager
|
||||
`JournalServiceManager` SHALL expose exactly the normalized configured `journalRoot()` and explicit `refresh()` administration operations. It SHALL be neither a Nenjim component nor a Nenjim process, SHALL be retained privately by the Hub, SHALL bind to exactly one Journal object through non-public administration, and SHALL expose no start, stop, service accessor, query, parser, ingestion or chain-state operation. Refresh before successful Journal process startup SHALL fail with `IllegalStateException`.
|
||||
|
||||
#### Scenario: Manager contract is passive
|
||||
- **WHEN** the public manager interface and reference manager are inspected
|
||||
- **THEN** only root reporting and refresh are available and the manager has no lifecycle or component identity
|
||||
|
||||
#### Scenario: Refresh follows process startup
|
||||
- **WHEN** refresh is requested before its Journal process has started successfully
|
||||
- **THEN** the manager rejects it with `IllegalStateException`
|
||||
|
||||
#### Scenario: Manager delegates after startup
|
||||
- **WHEN** refresh is requested after successful Journal startup
|
||||
- **THEN** the manager delegates through the non-public admin view to the same active Journal object
|
||||
|
||||
@@ -29,6 +29,9 @@ import com.r35157.libs.solana.impl.cached.CachedSolanaBlockChain;
|
||||
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||
import com.r35157.nenjim.hubd.NenjimHub;
|
||||
import com.r35157.nenjim.process.NenjimProcess;
|
||||
import com.r35157.nenjim.service.journal.JournalService;
|
||||
import com.r35157.nenjim.service.journal.JournalServiceManager;
|
||||
import com.r35157.nenjim.service.journal.impl.ref.JournalServiceAssembly;
|
||||
import com.r35157.nenjim.service.registry.NenjimRegistryService;
|
||||
import com.r35157.nenjim.service.registry.NenjimRegistryServiceManager;
|
||||
import com.r35157.nenjim.service.registry.impl.ref.NenjimRegistryAssembly;
|
||||
@@ -47,6 +50,7 @@ import org.slf4j.LoggerFactory;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
@@ -79,6 +83,7 @@ public final class NenjimHubImpl implements NenjimHub {
|
||||
REGISTRY_SERVICE_ID,
|
||||
registryService);
|
||||
recordStartedProcess(REGISTRY_SERVICE_ID);
|
||||
initializeAndStartJournal(registryServiceManager);
|
||||
initializeAndRegisterComponents(
|
||||
registryService,
|
||||
registryServiceManager);
|
||||
@@ -101,6 +106,22 @@ public final class NenjimHubImpl implements NenjimHub {
|
||||
return List.copyOf(startedProcessIds);
|
||||
}
|
||||
|
||||
private void initializeAndStartJournal(
|
||||
NenjimRegistryServiceManager registryServiceManager)
|
||||
throws Exception {
|
||||
JournalServiceAssembly journalAssembly =
|
||||
JournalServiceAssembly.create(JOURNAL_ROOT);
|
||||
JournalService journalService = journalAssembly.journalService();
|
||||
NenjimProcess journalProcess = journalAssembly.journalProcess();
|
||||
journalServiceManager = journalAssembly.journalServiceManager();
|
||||
|
||||
registryServiceManager.registerComponent(
|
||||
JOURNAL_SERVICE_ID,
|
||||
journalService);
|
||||
journalProcess.start();
|
||||
recordStartedProcess(JOURNAL_SERVICE_ID);
|
||||
}
|
||||
|
||||
private void initializeAndRegisterComponents(
|
||||
NenjimRegistryService registryService,
|
||||
NenjimRegistryServiceManager registryServiceManager) {
|
||||
@@ -290,6 +311,10 @@ public final class NenjimHubImpl implements NenjimHub {
|
||||
NenjimHubImpl.class);
|
||||
private static final NenjimComponentId REGISTRY_SERVICE_ID =
|
||||
new NenjimComponentId("nenjim.registry.service");
|
||||
private static final NenjimComponentId JOURNAL_SERVICE_ID =
|
||||
new NenjimComponentId("nenjim.journal.service");
|
||||
private static final Path JOURNAL_ROOT =
|
||||
Path.of("data/nenjim/journals");
|
||||
private static final NenjimComponentId OBJECT_CACHE_ID =
|
||||
new NenjimComponentId("nenjim.object-cache.default");
|
||||
private static final NenjimComponentId CURRENCY_IDENTITY_SERVICE_ID =
|
||||
@@ -337,6 +362,7 @@ public final class NenjimHubImpl implements NenjimHub {
|
||||
private final AtomicBoolean startAttempted = new AtomicBoolean();
|
||||
private final List<NenjimComponentId> startedProcessIds = new ArrayList<>();
|
||||
private NenjimRegistryServiceManager registryServiceManager;
|
||||
private JournalServiceManager journalServiceManager;
|
||||
|
||||
// Dependency components
|
||||
private ObjectCache objectCache;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.r35157.nenjim.service.journal;
|
||||
|
||||
import com.r35157.libs.valuetypes.basic.SemanticVersion;
|
||||
import com.r35157.nenjim.component.NenjimComponent;
|
||||
import com.r35157.nenjim.service.journal.model.ArtifactRelease;
|
||||
import com.r35157.nenjim.service.journal.model.Journal;
|
||||
import com.r35157.nenjim.service.journal.valuetypes.ArtifactCoordinate;
|
||||
@@ -10,8 +11,15 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/** Filesystem-independent, read-only access to current and historical Journal knowledge. */
|
||||
public interface JournalService {
|
||||
/**
|
||||
* Filesystem-independent, read-only access to current and historical Journal
|
||||
* knowledge.
|
||||
*
|
||||
* <p>The service is a registrable Nenjim component, but exposes neither its
|
||||
* process lifecycle nor its filesystem administration to ordinary
|
||||
* consumers.</p>
|
||||
*/
|
||||
public interface JournalService extends NenjimComponent {
|
||||
/** Lists known artifact coordinates in stable lexical order. */
|
||||
Set<ArtifactCoordinate> artifactCoordinates();
|
||||
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
package com.r35157.nenjim.service.journal;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/** Lifecycle and administrative access for one filesystem-backed Journal service. */
|
||||
/** Passive filesystem administration for one Hub-owned Journal service. */
|
||||
public interface JournalServiceManager {
|
||||
/** Starts the service and completes its initial Journal loading before returning. */
|
||||
void start() throws IOException;
|
||||
|
||||
/** Stops service-owned maintenance without changing Journal files. */
|
||||
void stop();
|
||||
|
||||
/** Returns the normalized configured Journal data root. */
|
||||
@NotNull
|
||||
Path journalRoot();
|
||||
|
||||
/** Returns the read-only service after successful initialization. */
|
||||
JournalService journalService();
|
||||
|
||||
/** Explicitly reloads the configured runtime Journal files. */
|
||||
/**
|
||||
* Explicitly reloads the configured runtime Journal files after the
|
||||
* Journal process has started successfully.
|
||||
*/
|
||||
void refresh() throws IOException;
|
||||
}
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
package com.r35157.nenjim.service.journal.impl.ref;
|
||||
|
||||
import com.r35157.nenjim.service.journal.model.Journal;
|
||||
import com.r35157.nenjim.service.journal.valuetypes.ArtifactCoordinate;
|
||||
import com.r35157.nenjim.service.journal.valuetypes.JournalVersion;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/** Strict, read-only filesystem loading for one Journal service. */
|
||||
final class JournalFilesystemLoader {
|
||||
private static final String JOURNAL_SUFFIX = ".journal";
|
||||
private static final String EXAMPLE_SUFFIX = ".journal.example";
|
||||
|
||||
private JournalFilesystemLoader() {
|
||||
}
|
||||
|
||||
static void loadAll(
|
||||
Path journalRoot,
|
||||
JournalServiceImpl service) throws IOException {
|
||||
validateRoot(journalRoot);
|
||||
for (Path artifactDirectory : listEntries(journalRoot)) {
|
||||
if (!Files.isDirectory(artifactDirectory)) {
|
||||
throw new IOException(
|
||||
"Unexpected entry in Journal root; expected an artifact directory: "
|
||||
+ artifactDirectory);
|
||||
}
|
||||
loadArtifactDirectory(service, artifactDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateRoot(Path journalRoot) throws IOException {
|
||||
if (!Files.exists(journalRoot)) {
|
||||
throw new IOException(
|
||||
"Configured Journal root does not exist: " + journalRoot);
|
||||
}
|
||||
if (!Files.isDirectory(journalRoot)) {
|
||||
throw new IOException(
|
||||
"Configured Journal root is not a directory: " + journalRoot);
|
||||
}
|
||||
if (!Files.isReadable(journalRoot)) {
|
||||
throw new IOException(
|
||||
"Configured Journal root is not readable: " + journalRoot);
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadArtifactDirectory(
|
||||
JournalServiceImpl service,
|
||||
Path directory) throws IOException {
|
||||
String directoryName = directory.getFileName().toString();
|
||||
ArtifactCoordinate expectedCoordinate;
|
||||
try {
|
||||
expectedCoordinate = ArtifactCoordinate.parse(directoryName);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new IOException(
|
||||
"Invalid artifact-coordinate directory name: " + directory,
|
||||
exception);
|
||||
}
|
||||
if (!Files.isReadable(directory)) {
|
||||
throw new IOException(
|
||||
"Artifact Journal directory is not readable: " + directory);
|
||||
}
|
||||
|
||||
ArrayList<JournalFile> runtimeFiles = new ArrayList<>();
|
||||
for (Path entry : listEntries(directory)) {
|
||||
if (!Files.isRegularFile(entry)) {
|
||||
throw new IOException(
|
||||
"Unexpected entry in artifact Journal directory: " + entry);
|
||||
}
|
||||
String filename = entry.getFileName().toString();
|
||||
if (filename.endsWith(EXAMPLE_SUFFIX)) {
|
||||
continue;
|
||||
}
|
||||
if (!filename.endsWith(JOURNAL_SUFFIX)) {
|
||||
throw new IOException(
|
||||
"Unexpected file in artifact Journal directory: " + entry);
|
||||
}
|
||||
if (!Files.isReadable(entry)) {
|
||||
throw new IOException("Journal file is not readable: " + entry);
|
||||
}
|
||||
|
||||
String versionText = filename.substring(
|
||||
0,
|
||||
filename.length() - JOURNAL_SUFFIX.length());
|
||||
JournalVersion expectedVersion;
|
||||
try {
|
||||
expectedVersion = JournalVersion.parse(versionText);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new IOException(
|
||||
"Invalid Journal filename; expected <journal-version>.journal: "
|
||||
+ entry,
|
||||
exception);
|
||||
}
|
||||
runtimeFiles.add(new JournalFile(entry, expectedVersion));
|
||||
}
|
||||
|
||||
runtimeFiles.sort(Comparator.comparing(JournalFile::journalVersion));
|
||||
for (JournalFile runtimeFile : runtimeFiles) {
|
||||
String text = Files.readString(
|
||||
runtimeFile.path,
|
||||
StandardCharsets.UTF_8);
|
||||
String source = runtimeFile.path.toString();
|
||||
Journal parsed = service.parse(text, source);
|
||||
if (!parsed.metadata().coordinate().equals(expectedCoordinate)) {
|
||||
throw new IOException(
|
||||
"Journal metadata coordinate "
|
||||
+ parsed.metadata().coordinate()
|
||||
+ " does not match directory "
|
||||
+ expectedCoordinate + ": " + runtimeFile.path);
|
||||
}
|
||||
if (!parsed.journalVersion().equals(runtimeFile.journalVersion)) {
|
||||
throw new IOException(
|
||||
"JOURNAL_VERSION " + parsed.journalVersion()
|
||||
+ " does not match filename version "
|
||||
+ runtimeFile.journalVersion + ": "
|
||||
+ runtimeFile.path);
|
||||
}
|
||||
service.ingest(text, source);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Path> listEntries(Path directory) throws IOException {
|
||||
try (Stream<Path> entries = Files.list(directory)) {
|
||||
return entries.sorted(Comparator.comparing(
|
||||
path -> path.getFileName().toString())).toList();
|
||||
} catch (SecurityException exception) {
|
||||
throw new IOException(
|
||||
"Cannot read Journal directory: " + directory,
|
||||
exception);
|
||||
}
|
||||
}
|
||||
|
||||
private record JournalFile(Path path, JournalVersion journalVersion) {
|
||||
private JournalFile {
|
||||
Objects.requireNonNull(path, "path");
|
||||
Objects.requireNonNull(journalVersion, "journalVersion");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.r35157.nenjim.service.journal.impl.ref;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* Internal administrative view of the active Journal service.
|
||||
*
|
||||
* <p>The same object is exposed publicly through the read-only
|
||||
* {@code JournalService} interface. This interface must remain package-private:
|
||||
* making it public would let ordinary consumers cast that service object to
|
||||
* filesystem administration and would break the read-only service contract.
|
||||
* It must never be exposed outside this reference implementation package.</p>
|
||||
*/
|
||||
interface JournalServiceAdmin {
|
||||
@NotNull
|
||||
Path journalRoot();
|
||||
|
||||
void refresh() throws IOException;
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package com.r35157.nenjim.service.journal.impl.ref;
|
||||
|
||||
import com.r35157.nenjim.process.NenjimProcess;
|
||||
import com.r35157.nenjim.service.journal.JournalService;
|
||||
import com.r35157.nenjim.service.journal.JournalServiceManager;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Implementation-only construction boundary for one Journal subsystem.
|
||||
*
|
||||
* <p>This helper owns no lifecycle and exposes neither parser, ingestion,
|
||||
* chain state nor filesystem administration through the ordinary service
|
||||
* view. A consumer holding only {@link JournalService} cannot use that
|
||||
* reference to derive the manager bound to the active Journal service.</p>
|
||||
*/
|
||||
public final class JournalServiceAssembly {
|
||||
private JournalServiceAssembly(@NotNull JournalServiceImpl service) {
|
||||
journalService = service;
|
||||
journalProcess = service;
|
||||
journalServiceManager = new JournalServiceManagerImpl(service);
|
||||
}
|
||||
|
||||
/** Creates mutually consistent public views for one configured Journal. */
|
||||
public static @NotNull JournalServiceAssembly create(
|
||||
@NotNull Path journalRoot) {
|
||||
return new JournalServiceAssembly(new JournalServiceImpl(
|
||||
Objects.requireNonNull(journalRoot, "journalRoot")));
|
||||
}
|
||||
|
||||
/** Returns the ordinary read-only service view. */
|
||||
public @NotNull JournalService journalService() {
|
||||
return journalService;
|
||||
}
|
||||
|
||||
/** Returns the process view of the same object as the service view. */
|
||||
public @NotNull NenjimProcess journalProcess() {
|
||||
return journalProcess;
|
||||
}
|
||||
|
||||
/** Returns the distinct passive manager bound to this Journal service. */
|
||||
public @NotNull JournalServiceManager journalServiceManager() {
|
||||
return journalServiceManager;
|
||||
}
|
||||
|
||||
private final JournalService journalService;
|
||||
private final NenjimProcess journalProcess;
|
||||
private final JournalServiceManager journalServiceManager;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.r35157.nenjim.service.journal.impl.ref;
|
||||
|
||||
import com.r35157.libs.valuetypes.basic.SemanticVersion;
|
||||
import com.r35157.nenjim.process.NenjimProcess;
|
||||
import com.r35157.nenjim.service.journal.JournalService;
|
||||
import com.r35157.nenjim.service.journal.exception.InvalidJournalException;
|
||||
import com.r35157.nenjim.service.journal.model.ArtifactRelease;
|
||||
@@ -9,7 +10,9 @@ import com.r35157.nenjim.service.journal.valuetypes.ArtifactCoordinate;
|
||||
import com.r35157.nenjim.service.journal.valuetypes.ContentDigest;
|
||||
import com.r35157.nenjim.service.journal.valuetypes.JournalVersion;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
@@ -25,26 +28,68 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Filesystem-independent, thread-safe reference service for immutable per-artifact Journal chains.
|
||||
* Thread-safe reference service and Hub-managed process for immutable
|
||||
* per-artifact Journal chains.
|
||||
*/
|
||||
public final class JournalServiceImpl implements JournalService {
|
||||
final class JournalServiceImpl implements
|
||||
JournalService,
|
||||
NenjimProcess,
|
||||
JournalServiceAdmin {
|
||||
private static final String DEFAULT_SOURCE = "<journal>";
|
||||
|
||||
private final Object stateLock = new Object();
|
||||
private final Path journalRoot;
|
||||
private final JournalTextParser parser;
|
||||
private final Map<ArtifactCoordinate, NavigableMap<JournalVersion, StoredRevision>> chains = new HashMap<>();
|
||||
private final Map<String, ArtifactCoordinate> digestOwners = new HashMap<>();
|
||||
private final AtomicBoolean startAttempted = new AtomicBoolean();
|
||||
private volatile boolean available;
|
||||
|
||||
JournalServiceImpl() {
|
||||
this(new JournalTextParser());
|
||||
JournalServiceImpl(Path journalRoot) {
|
||||
this(journalRoot, new JournalTextParser());
|
||||
}
|
||||
|
||||
JournalServiceImpl(JournalTextParser parser) {
|
||||
JournalServiceImpl(Path journalRoot, JournalTextParser parser) {
|
||||
this.journalRoot = Objects.requireNonNull(
|
||||
journalRoot,
|
||||
"journalRoot").toAbsolutePath().normalize();
|
||||
this.parser = Objects.requireNonNull(parser, "parser");
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the complete initial filesystem worldview and publishes it only
|
||||
* after every runtime Journal has been accepted.
|
||||
*/
|
||||
@Override
|
||||
public void start() throws IOException {
|
||||
if (!startAttempted.compareAndSet(false, true)) {
|
||||
throw new IllegalStateException(
|
||||
"Journal service start has already been attempted");
|
||||
}
|
||||
|
||||
JournalServiceImpl candidate = new JournalServiceImpl(
|
||||
journalRoot,
|
||||
parser);
|
||||
JournalFilesystemLoader.loadAll(journalRoot, candidate);
|
||||
publishInitialState(candidate);
|
||||
}
|
||||
|
||||
/** Returns the normalized configured Journal data root. */
|
||||
@Override
|
||||
public Path journalRoot() {
|
||||
return journalRoot;
|
||||
}
|
||||
|
||||
/** Explicitly reloads runtime Journal files after successful startup. */
|
||||
@Override
|
||||
public void refresh() throws IOException {
|
||||
requireAvailable();
|
||||
JournalFilesystemLoader.loadAll(journalRoot, this);
|
||||
}
|
||||
|
||||
/** Parses and validates a complete snapshot without changing service state. */
|
||||
Journal parse(String completeJournalText) {
|
||||
return parse(completeJournalText, DEFAULT_SOURCE);
|
||||
@@ -114,6 +159,7 @@ public final class JournalServiceImpl implements JournalService {
|
||||
/** Lists known artifact coordinates in stable lexical order. */
|
||||
@Override
|
||||
public Set<ArtifactCoordinate> artifactCoordinates() {
|
||||
requireAvailable();
|
||||
synchronized (stateLock) {
|
||||
TreeSet<ArtifactCoordinate> coordinates = new TreeSet<>(Comparator.comparing(ArtifactCoordinate::toString));
|
||||
coordinates.addAll(chains.keySet());
|
||||
@@ -124,6 +170,7 @@ public final class JournalServiceImpl implements JournalService {
|
||||
/** Lists complete revisions in ascending Journal-version order. */
|
||||
@Override
|
||||
public List<Journal> revisions(ArtifactCoordinate coordinate) {
|
||||
requireAvailable();
|
||||
Objects.requireNonNull(coordinate, "coordinate");
|
||||
synchronized (stateLock) {
|
||||
NavigableMap<JournalVersion, StoredRevision> chain = chains.get(coordinate);
|
||||
@@ -137,6 +184,7 @@ public final class JournalServiceImpl implements JournalService {
|
||||
/** Returns the current complete worldview for an artifact. */
|
||||
@Override
|
||||
public Optional<Journal> latest(ArtifactCoordinate coordinate) {
|
||||
requireAvailable();
|
||||
Objects.requireNonNull(coordinate, "coordinate");
|
||||
synchronized (stateLock) {
|
||||
NavigableMap<JournalVersion, StoredRevision> chain = chains.get(coordinate);
|
||||
@@ -147,6 +195,7 @@ public final class JournalServiceImpl implements JournalService {
|
||||
/** Returns one exact historical complete worldview. */
|
||||
@Override
|
||||
public Optional<Journal> revision(ArtifactCoordinate coordinate, JournalVersion journalVersion) {
|
||||
requireAvailable();
|
||||
Objects.requireNonNull(coordinate, "coordinate");
|
||||
Objects.requireNonNull(journalVersion, "journalVersion");
|
||||
synchronized (stateLock) {
|
||||
@@ -162,6 +211,7 @@ public final class JournalServiceImpl implements JournalService {
|
||||
public Optional<Journal> revisionAtOrBefore(
|
||||
ArtifactCoordinate coordinate,
|
||||
JournalVersion journalVersion) {
|
||||
requireAvailable();
|
||||
Objects.requireNonNull(coordinate, "coordinate");
|
||||
Objects.requireNonNull(journalVersion, "journalVersion");
|
||||
synchronized (stateLock) {
|
||||
@@ -178,6 +228,7 @@ public final class JournalServiceImpl implements JournalService {
|
||||
ArtifactCoordinate coordinate,
|
||||
JournalVersion journalVersion,
|
||||
SemanticVersion releaseVersion) {
|
||||
requireAvailable();
|
||||
Objects.requireNonNull(releaseVersion, "releaseVersion");
|
||||
return revision(coordinate, journalVersion).flatMap(journal -> journal.release(releaseVersion));
|
||||
}
|
||||
@@ -196,6 +247,23 @@ public final class JournalServiceImpl implements JournalService {
|
||||
}
|
||||
}
|
||||
|
||||
private void publishInitialState(JournalServiceImpl candidate) {
|
||||
synchronized (candidate.stateLock) {
|
||||
synchronized (stateLock) {
|
||||
chains.putAll(candidate.chains);
|
||||
digestOwners.putAll(candidate.digestOwners);
|
||||
available = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void requireAvailable() {
|
||||
if (!available) {
|
||||
throw new IllegalStateException(
|
||||
"Journal service is unavailable until startup succeeds");
|
||||
}
|
||||
}
|
||||
|
||||
private InvalidJournalException invalidPredecessor(
|
||||
String source,
|
||||
ArtifactCoordinate candidateCoordinate,
|
||||
|
||||
+10
-145
@@ -1,164 +1,29 @@
|
||||
package com.r35157.nenjim.service.journal.impl.ref;
|
||||
|
||||
import com.r35157.nenjim.service.journal.JournalService;
|
||||
import com.r35157.nenjim.service.journal.JournalServiceManager;
|
||||
import com.r35157.nenjim.service.journal.model.Journal;
|
||||
import com.r35157.nenjim.service.journal.valuetypes.ArtifactCoordinate;
|
||||
import com.r35157.nenjim.service.journal.valuetypes.JournalVersion;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/** Filesystem-backed reference lifecycle manager for a read-only Journal service. */
|
||||
/** Passive reference manager for one Hub-owned Journal service. */
|
||||
public final class JournalServiceManagerImpl implements JournalServiceManager {
|
||||
private static final String JOURNAL_SUFFIX = ".journal";
|
||||
private static final String EXAMPLE_SUFFIX = ".journal.example";
|
||||
|
||||
private final Path journalRoot;
|
||||
private JournalServiceImpl activeService;
|
||||
|
||||
public JournalServiceManagerImpl(Path journalRoot) {
|
||||
this.journalRoot = Objects.requireNonNull(journalRoot, "journalRoot").toAbsolutePath().normalize();
|
||||
JournalServiceManagerImpl(JournalServiceAdmin journalAdmin) {
|
||||
this.journalAdmin = Objects.requireNonNull(
|
||||
journalAdmin,
|
||||
"journalAdmin");
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void start() throws IOException {
|
||||
if (activeService != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
JournalServiceImpl candidate = new JournalServiceImpl();
|
||||
loadAll(candidate);
|
||||
activeService = candidate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void stop() {
|
||||
activeService = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path journalRoot() {
|
||||
return journalRoot;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized JournalService journalService() {
|
||||
return requireActiveService();
|
||||
public @NotNull Path journalRoot() {
|
||||
return journalAdmin.journalRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void refresh() throws IOException {
|
||||
loadAll(requireActiveService());
|
||||
journalAdmin.refresh();
|
||||
}
|
||||
|
||||
private JournalServiceImpl requireActiveService() {
|
||||
if (activeService == null) {
|
||||
throw new IllegalStateException("Journal service manager has not been started");
|
||||
}
|
||||
return activeService;
|
||||
}
|
||||
|
||||
private void loadAll(JournalServiceImpl service) throws IOException {
|
||||
validateRoot();
|
||||
for (Path artifactDirectory : listEntries(journalRoot)) {
|
||||
if (!Files.isDirectory(artifactDirectory)) {
|
||||
throw new IOException("Unexpected entry in Journal root; expected an artifact directory: "
|
||||
+ artifactDirectory);
|
||||
}
|
||||
loadArtifactDirectory(service, artifactDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateRoot() throws IOException {
|
||||
if (!Files.exists(journalRoot)) {
|
||||
throw new IOException("Configured Journal root does not exist: " + journalRoot);
|
||||
}
|
||||
if (!Files.isDirectory(journalRoot)) {
|
||||
throw new IOException("Configured Journal root is not a directory: " + journalRoot);
|
||||
}
|
||||
if (!Files.isReadable(journalRoot)) {
|
||||
throw new IOException("Configured Journal root is not readable: " + journalRoot);
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadArtifactDirectory(
|
||||
JournalServiceImpl service,
|
||||
Path directory) throws IOException {
|
||||
String directoryName = directory.getFileName().toString();
|
||||
ArtifactCoordinate expectedCoordinate;
|
||||
try {
|
||||
expectedCoordinate = ArtifactCoordinate.parse(directoryName);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new IOException("Invalid artifact-coordinate directory name: " + directory, exception);
|
||||
}
|
||||
if (!Files.isReadable(directory)) {
|
||||
throw new IOException("Artifact Journal directory is not readable: " + directory);
|
||||
}
|
||||
|
||||
ArrayList<JournalFile> runtimeFiles = new ArrayList<>();
|
||||
for (Path entry : listEntries(directory)) {
|
||||
if (!Files.isRegularFile(entry)) {
|
||||
throw new IOException("Unexpected entry in artifact Journal directory: " + entry);
|
||||
}
|
||||
String filename = entry.getFileName().toString();
|
||||
if (filename.endsWith(EXAMPLE_SUFFIX)) {
|
||||
continue;
|
||||
}
|
||||
if (!filename.endsWith(JOURNAL_SUFFIX)) {
|
||||
throw new IOException("Unexpected file in artifact Journal directory: " + entry);
|
||||
}
|
||||
if (!Files.isReadable(entry)) {
|
||||
throw new IOException("Journal file is not readable: " + entry);
|
||||
}
|
||||
|
||||
String versionText = filename.substring(0, filename.length() - JOURNAL_SUFFIX.length());
|
||||
JournalVersion expectedVersion;
|
||||
try {
|
||||
expectedVersion = JournalVersion.parse(versionText);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new IOException("Invalid Journal filename; expected <journal-version>.journal: " + entry,
|
||||
exception);
|
||||
}
|
||||
runtimeFiles.add(new JournalFile(entry, expectedVersion));
|
||||
}
|
||||
|
||||
runtimeFiles.sort(Comparator.comparing(JournalFile::journalVersion));
|
||||
for (JournalFile runtimeFile : runtimeFiles) {
|
||||
String text = Files.readString(runtimeFile.path, StandardCharsets.UTF_8);
|
||||
String source = runtimeFile.path.toString();
|
||||
Journal parsed = service.parse(text, source);
|
||||
if (!parsed.metadata().coordinate().equals(expectedCoordinate)) {
|
||||
throw new IOException("Journal metadata coordinate " + parsed.metadata().coordinate()
|
||||
+ " does not match directory " + expectedCoordinate + ": " + runtimeFile.path);
|
||||
}
|
||||
if (!parsed.journalVersion().equals(runtimeFile.journalVersion)) {
|
||||
throw new IOException("JOURNAL_VERSION " + parsed.journalVersion()
|
||||
+ " does not match filename version " + runtimeFile.journalVersion + ": " + runtimeFile.path);
|
||||
}
|
||||
service.ingest(text, source);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Path> listEntries(Path directory) throws IOException {
|
||||
try (Stream<Path> entries = Files.list(directory)) {
|
||||
return entries.sorted(Comparator.comparing(path -> path.getFileName().toString())).toList();
|
||||
} catch (SecurityException exception) {
|
||||
throw new IOException("Cannot read Journal directory: " + directory, exception);
|
||||
}
|
||||
}
|
||||
|
||||
private record JournalFile(Path path, JournalVersion journalVersion) {
|
||||
private JournalFile {
|
||||
Objects.requireNonNull(path, "path");
|
||||
Objects.requireNonNull(journalVersion, "journalVersion");
|
||||
}
|
||||
}
|
||||
private final JournalServiceAdmin journalAdmin;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user