78: Make JournalService a Hub-managed Nenjim process and integrate it into startup
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user