From ddbf6f047db5cce89229b0efee4d857cfaa4bd19 Mon Sep 17 00:00:00 2001 From: Minimons Date: Thu, 3 Sep 2026 20:34:49 +0200 Subject: [PATCH] 82: Remove lifecycle operations from the TickerService API --- docs/Nenjim-public.html | 5 +++ docs/Nenjim.md | 5 +++ docs/Terminologi.md | 1 + .../.openspec.yaml | 2 + .../design.md | 43 ++++++++++++++++++ .../proposal.md | 23 ++++++++++ .../specs/assetaz-ticker-service/spec.md | 44 +++++++++++++++++++ .../tasks.md | 15 +++++++ openspec/specs/assetaz-ticker-service/spec.md | 12 +++-- .../services/ticker/TickerService.tjava | 16 ++----- .../ticker/impl/ref/TickerServiceImpl.tjava | 23 +++------- 11 files changed, 155 insertions(+), 34 deletions(-) create mode 100644 openspec/changes/archive/2026-09-03-82-remove-ticker-service-lifecycle-operations/.openspec.yaml create mode 100644 openspec/changes/archive/2026-09-03-82-remove-ticker-service-lifecycle-operations/design.md create mode 100644 openspec/changes/archive/2026-09-03-82-remove-ticker-service-lifecycle-operations/proposal.md create mode 100644 openspec/changes/archive/2026-09-03-82-remove-ticker-service-lifecycle-operations/specs/assetaz-ticker-service/spec.md create mode 100644 openspec/changes/archive/2026-09-03-82-remove-ticker-service-lifecycle-operations/tasks.md diff --git a/docs/Nenjim-public.html b/docs/Nenjim-public.html index ea60d0b..312df94 100644 --- a/docs/Nenjim-public.html +++ b/docs/Nenjim-public.html @@ -181,6 +181,11 @@ nenjim.journal.service applications. Construction, registration, and startup remain fail-fast without rollback. Earlier successful snapshot entries remain truthful after a later failure, and the online point plus blocking wait are reached only after every configured process succeeds.

+

Ordinary consumers receive the lifecycle-free TickerService view, which exposes only the + latest-price query. The same TickerServiceImpl object separately implements + NenjimProcess so the Hub owns startup. The implementation starts its configured active + PriceSource instances internally and stops those already started if Ticker startup fails; it + exposes no general Ticker stop operation.

The Ticker still explicitly selects the Raydium EVE/USDT source ID while the hardcoded source remains a passive, registered component. Process Manager continues to poll immutable ordered snapshots of actual successful startup milestones rather than displaying configured intent or claiming current health.

diff --git a/docs/Nenjim.md b/docs/Nenjim.md index a63db14..574dd7e 100644 --- a/docs/Nenjim.md +++ b/docs/Nenjim.md @@ -199,6 +199,11 @@ kan starte og tracke, implementerer desuden den canonical `com.r35157.nenjim.pro `start()` og ikke en fælles stop-, restart- eller health-operation. En service- eller applikationsimplementation kan derfor også være en process; passive componenter er det ikke. +Ticker følger denne opdeling: almindelige consumers modtager `TickerService` med kun latest-price query, mens det +samme `TickerServiceImpl`-objekt separat er en `NenjimProcess`, som Hub'en starter. Tickerens source-lifecycle er +implementation-owned; `PriceSource`-instanser startes internt, og en fejlet Ticker-start stopper de sources, som nåede +at starte. Der findes ingen almindelig Ticker-serviceoperation til start eller stop. + En `NenjimComponentId` er registration metadata i ét Registry og er ikke en egenskab ved objektet. ID-formatet er lower-case ASCII med dot-separated segments og valgfrie interne bindestreger, f.eks. `nenjim.registry.service` og `assetaz.price-source.raydium-pool.eve-usdt`. Det samme objekt må registreres under flere forskellige ID'er, og én diff --git a/docs/Terminologi.md b/docs/Terminologi.md index 63446c0..5b76372 100644 --- a/docs/Terminologi.md +++ b/docs/Terminologi.md @@ -46,6 +46,7 @@ | 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. | | JavaFX runtime | Det ene JVM-wide JavaFX toolkit, som udfører UI-arbejde på JavaFX Application Thread. Når processen er valgt, ejer Hub'en dets eneste startup-sti gennem `JavaFxRuntimeServiceImpl` og holder det aktivt efter sidste lukkede vindue med `Platform.setImplicitExit(false)`. En headless konfiguration kan udelade runtime og alle JavaFX-applikationer helt. | | JavaFxRuntimeService | Registrerbart service-view med kun non-null `runLater(Runnable)`. Samme referenceimplementation er en Hub-styret `NenjimProcess`; service-consumers får den gennem constructor injection, men får ingen lifecycle-, status-, configuration-, window-, event-, stop- eller restart-operationer. | +| TickerService | Lifecycle-frit domain/query-view med kun latest-price lookup. Samme `TickerServiceImpl`-objekt er separat en Hub-styret `NenjimProcess`; det ejer `PriceSource`-startup internt og stopper allerede startede sources som rollback, hvis Ticker-start fejler, men eksponerer ingen almindelig start- eller stop-operation. | | 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. | diff --git a/openspec/changes/archive/2026-09-03-82-remove-ticker-service-lifecycle-operations/.openspec.yaml b/openspec/changes/archive/2026-09-03-82-remove-ticker-service-lifecycle-operations/.openspec.yaml new file mode 100644 index 0000000..9696e00 --- /dev/null +++ b/openspec/changes/archive/2026-09-03-82-remove-ticker-service-lifecycle-operations/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-03 diff --git a/openspec/changes/archive/2026-09-03-82-remove-ticker-service-lifecycle-operations/design.md b/openspec/changes/archive/2026-09-03-82-remove-ticker-service-lifecycle-operations/design.md new file mode 100644 index 0000000..fe6de7c --- /dev/null +++ b/openspec/changes/archive/2026-09-03-82-remove-ticker-service-lifecycle-operations/design.md @@ -0,0 +1,43 @@ +## Context + +The Ticker implementation already combines two roles on one object: `TickerService` supplies domain queries and `NenjimProcess` supplies Hub-managed startup. The service interface redundantly declares startup and also declares stop even though the canonical process API exposes no stop operation. Internally, Ticker must still stop sources that were started before a later source makes startup fail. + +## Goals / Non-Goals + +**Goals:** + +- Make the public Ticker service view lifecycle-free. +- Preserve the identical implementation object as both `TickerService` and `NenjimProcess`. +- Preserve complete reverse-order cleanup of sources started during a failed startup attempt, including suppressed stop failures. +- Preserve Hub startup, source activation, persistence, callbacks, and latest-price semantics. + +**Non-Goals:** + +- No stop or restart addition to `NenjimProcess`. +- No Hub shutdown or process-control design. +- No change to `PriceSource.start(PriceSink)` or `PriceSource.stop()`. +- No cleanup of unrelated service interfaces and no new automated tests. + +## Decisions + +### Keep startup only on the process view + +Delete both lifecycle declarations from `TickerService` and retain `TickerServiceImpl.start()` solely as its `NenjimProcess.start()` implementation. `NenjimHubImpl` already returns the constructed implementation as `NenjimProcess`, registers the same object, resolves that process view, and invokes startup; no Hub wiring change is needed. + +### Keep source stopping as private failed-start rollback + +Delete the public `TickerServiceImpl.stop()` method but retain its existing private rollback path. On startup failure the implementation enters its internal stopping state, drains accepted callbacks, stops every source marked started in reverse registration order, resets its transient state, and reports stop failures as suppressed exceptions on the startup failure. This preserves resource safety without offering ordinary shutdown control. + +The internal stopped state remains available after rollback so the existing failed-start retry behavior is not changed. A successful start has no newly introduced stop or restart path. + +## Risks / Trade-offs + +- [A successful Ticker has no public shutdown path] → This is intentional until common Hub/process stop semantics are designed. +- [Private state names still describe stopping] → They model rollback of `PriceSource` resources, not an exposed Ticker lifecycle operation. +- [Downstream source callers may have compiled against the removed methods] → Treat the service API removal as the explicit breaking change and retain the unchanged process startup route for the Hub. + +## Migration Plan + +1. Recompile consumers against the lifecycle-free `TickerService` contract. +2. Remove direct service-level startup or stop calls; runtime startup remains configured and performed by the Hub. +3. Roll back the software version if a consumer cannot yet migrate; no data or configuration migration is required. diff --git a/openspec/changes/archive/2026-09-03-82-remove-ticker-service-lifecycle-operations/proposal.md b/openspec/changes/archive/2026-09-03-82-remove-ticker-service-lifecycle-operations/proposal.md new file mode 100644 index 0000000..51ff951 --- /dev/null +++ b/openspec/changes/archive/2026-09-03-82-remove-ticker-service-lifecycle-operations/proposal.md @@ -0,0 +1,23 @@ +## Why + +`TickerService` exposes `start()` and `stop()` to ordinary consumers even though the current Nenjim architecture makes the Hub the lifecycle owner and models active implementations separately through `NenjimProcess`. This leaks process control through a domain service API and promises a stop lifecycle that the canonical process contract does not support. + +## What Changes + +- **BREAKING**: Remove `start()` and `stop()` from the public `TickerService` contract. +- Keep `TickerServiceImpl` as both the lifecycle-free service view and the Hub-started `NenjimProcess` view. +- Remove the implementation's public general stop operation while retaining private rollback that stops every `PriceSource` started during a failed startup attempt. +- Keep `PriceSource` lifecycle contracts, latest-price behavior, persistence, Registry resolution, and Hub startup wiring unchanged. +- Update the canonical Ticker specification, JavaDoc, and Nenjim documentation to describe Hub-owned lifecycle. + +## Capabilities + +### Modified Capabilities + +- `assetaz-ticker-service`: Separates the ordinary latest-price service API from Hub-owned process startup and limits source stopping to failed-start cleanup. + +## Impact + +- Changes the public `com.r35157.assetaz.services.ticker.TickerService` source contract and the public methods of `TickerServiceImpl`. +- Existing callers must stop invoking Ticker lifecycle through the service view; the Hub continues to invoke startup through `NenjimProcess`. +- Adds no common stop/restart support, shutdown orchestration, source lifecycle changes, dependencies, or automated tests. diff --git a/openspec/changes/archive/2026-09-03-82-remove-ticker-service-lifecycle-operations/specs/assetaz-ticker-service/spec.md b/openspec/changes/archive/2026-09-03-82-remove-ticker-service-lifecycle-operations/specs/assetaz-ticker-service/spec.md new file mode 100644 index 0000000..8849fa8 --- /dev/null +++ b/openspec/changes/archive/2026-09-03-82-remove-ticker-service-lifecycle-operations/specs/assetaz-ticker-service/spec.md @@ -0,0 +1,44 @@ +## MODIFIED Requirements + +### Requirement: Price sources are discovered and lifecycle-managed +Nenjim SHALL be able to construct and register each price source independently in an initialized but unstarted state without supplying a ticker or sink. The ticker SHALL receive the public Nenjim Registry through constructor injection and resolve its explicitly configured `PriceSource` component IDs, while ordinary ticker clients SHALL receive only the latest-price domain API and SHALL NOT receive public source-registration, start, stop, restart, or other lifecycle operations. The same reference implementation object SHALL implement both `TickerService` and `NenjimProcess`; the Hub SHALL start it only through its `NenjimProcess` view. The ticker SHALL fail clearly when a configured ID is missing or names a component that does not implement `PriceSource`; it SHALL NOT silently select the first source listed for the interface. The ticker SHALL identify each resolved source by its `TradingPair` and stable source name, reject duplicate identities clearly, and start each active source by passing itself as `PriceSink`. If startup fails, the ticker SHALL stop every source already started during that attempt before reporting the failure, but SHALL expose no general Ticker stop operation. Source names SHALL be non-empty safe directory components and SHALL reject `/`, `\`, `..`, and control characters without rewriting them. A source SHALL announce typed prices and timestamps only through the sink received at start and SHALL remain unaware of its persistence path. A repeated source start SHALL reject the call before replacing its existing sink. + +#### Scenario: Sources are constructed independently +- **WHEN** the NenjimHub constructs and registers a price source before constructing or starting the ticker +- **THEN** source construction requires neither a `PriceSink` nor a ticker reference and leaves the source unstarted + +#### Scenario: Ticker resolves configured source components +- **WHEN** the ticker initializes its source set from explicit component IDs and an injected public Registry +- **THEN** it resolves each ID as `PriceSource`, not `PriceSink`, and manages only the resolved configured sources internally + +#### Scenario: Missing configured source fails clearly +- **WHEN** the ticker is configured with an absent component ID or an ID whose object is not a `PriceSource` +- **THEN** construction fails with a diagnostic identifying the ID and required interface + +#### Scenario: Ordinary clients cannot manage sources +- **WHEN** a caller uses the public `TickerService` contract +- **THEN** the contract exposes only latest-price queries and no source-registration, start, stop, restart, or other lifecycle operation + +#### Scenario: Hub starts Ticker through its process view +- **WHEN** the configured Ticker process is reached in the Hub startup plan +- **THEN** the same object registered as `TickerService` and `NenjimProcess` is started through `NenjimProcess` + +#### Scenario: Distinct configured sources are accepted +- **WHEN** the ticker resolves two configured sources with different source names or trading pairs +- **THEN** it accepts both and manages each source independently + +#### Scenario: Duplicate persistent identity is rejected +- **WHEN** the ticker resolves sources with the same trading pair and source name +- **THEN** initialization fails with an error identifying the duplicate source history + +#### Scenario: Unsafe source name is rejected +- **WHEN** a source name is empty or contains a slash, backslash, `..`, or a control character +- **THEN** ticker initialization or startup fails clearly without rewriting the source name or creating filesystem content + +#### Scenario: Active source lifecycle is managed +- **WHEN** one active source starts successfully and a later active source makes Ticker startup fail +- **THEN** the ticker stops every source started during that attempt and reports the startup failure without exposing a general stop operation + +#### Scenario: Repeated source start preserves existing sink +- **WHEN** an already-started source receives another start call with a different sink +- **THEN** it rejects the call before replacing the sink from the successful start diff --git a/openspec/changes/archive/2026-09-03-82-remove-ticker-service-lifecycle-operations/tasks.md b/openspec/changes/archive/2026-09-03-82-remove-ticker-service-lifecycle-operations/tasks.md new file mode 100644 index 0000000..319c2fa --- /dev/null +++ b/openspec/changes/archive/2026-09-03-82-remove-ticker-service-lifecycle-operations/tasks.md @@ -0,0 +1,15 @@ +## 1. Ticker API and implementation + +- [x] 1.1 Remove `start()` and `stop()` from `TickerService` and document the lifecycle-free consumer contract. +- [x] 1.2 Keep `TickerServiceImpl.start()` as `NenjimProcess.start()`, remove its public normal stop operation, and preserve private failed-start source cleanup. +- [x] 1.3 Confirm Hub construction, Registry resolution, process startup, and latest-price behavior remain unchanged. + +## 2. Specification and documentation + +- [x] 2.1 Update the canonical `assetaz-ticker-service` capability to distinguish service queries, Hub-owned startup, and failed-start rollback. +- [x] 2.2 Update Nenjim architecture, public, terminology, and relevant JavaDoc descriptions consistently. + +## 3. Verification + +- [x] 3.1 Run strict OpenSpec validation and archive the completed change into the canonical specifications. +- [x] 3.2 Run the complete Gradle build and inspect the final diff for lifecycle leaks or unintended behavior changes. diff --git a/openspec/specs/assetaz-ticker-service/spec.md b/openspec/specs/assetaz-ticker-service/spec.md index 7650fdb..7657abd 100644 --- a/openspec/specs/assetaz-ticker-service/spec.md +++ b/openspec/specs/assetaz-ticker-service/spec.md @@ -5,7 +5,7 @@ Provide AssetAZ callers with typed latest prices backed by explicitly enabled, durable observation histories. ## Requirements ### Requirement: Price sources are discovered and lifecycle-managed -Nenjim SHALL be able to construct and register each price source independently in an initialized but unstarted state without supplying a ticker or sink. The ticker SHALL receive the public Nenjim Registry through constructor injection and resolve its explicitly configured `PriceSource` component IDs, while ordinary ticker clients SHALL NOT receive a public operation for adding or managing sources. The ticker SHALL fail clearly when a configured ID is missing or names a component that does not implement `PriceSource`; it SHALL NOT silently select the first source listed for the interface. The ticker SHALL identify each resolved source by its `TradingPair` and stable source name, reject duplicate identities clearly, start each active source by passing itself as `PriceSink`, and stop every source it started. Source names SHALL be non-empty safe directory components and SHALL reject `/`, `\`, `..`, and control characters without rewriting them. A source SHALL announce typed prices and timestamps only through the sink received at start and SHALL remain unaware of its persistence path. A repeated source start SHALL reject the call before replacing its existing sink. +Nenjim SHALL be able to construct and register each price source independently in an initialized but unstarted state without supplying a ticker or sink. The ticker SHALL receive the public Nenjim Registry through constructor injection and resolve its explicitly configured `PriceSource` component IDs, while ordinary ticker clients SHALL receive only the latest-price domain API and SHALL NOT receive public source-registration, start, stop, restart, or other lifecycle operations. The same reference implementation object SHALL implement both `TickerService` and `NenjimProcess`; the Hub SHALL start it only through its `NenjimProcess` view. The ticker SHALL fail clearly when a configured ID is missing or names a component that does not implement `PriceSource`; it SHALL NOT silently select the first source listed for the interface. The ticker SHALL identify each resolved source by its `TradingPair` and stable source name, reject duplicate identities clearly, and start each active source by passing itself as `PriceSink`. If startup fails, the ticker SHALL stop every source already started during that attempt before reporting the failure, but SHALL expose no general Ticker stop operation. Source names SHALL be non-empty safe directory components and SHALL reject `/`, `\`, `..`, and control characters without rewriting them. A source SHALL announce typed prices and timestamps only through the sink received at start and SHALL remain unaware of its persistence path. A repeated source start SHALL reject the call before replacing its existing sink. #### Scenario: Sources are constructed independently - **WHEN** the NenjimHub constructs and registers a price source before constructing or starting the ticker @@ -21,7 +21,11 @@ Nenjim SHALL be able to construct and register each price source independently i #### Scenario: Ordinary clients cannot manage sources - **WHEN** a caller uses the public `TickerService` contract -- **THEN** the contract exposes ticker lifecycle and latest-price queries but no source-registration operation +- **THEN** the contract exposes only latest-price queries and no source-registration, start, stop, restart, or other lifecycle operation + +#### Scenario: Hub starts Ticker through its process view +- **WHEN** the configured Ticker process is reached in the Hub startup plan +- **THEN** the same object registered as `TickerService` and `NenjimProcess` is started through `NenjimProcess` #### Scenario: Distinct configured sources are accepted - **WHEN** the ticker resolves two configured sources with different source names or trading pairs @@ -36,8 +40,8 @@ Nenjim SHALL be able to construct and register each price source independently i - **THEN** ticker initialization or startup fails clearly without rewriting the source name or creating filesystem content #### Scenario: Active source lifecycle is managed -- **WHEN** the ticker starts and later stops with an active Registry-resolved source -- **THEN** it calls `start` with itself as `PriceSink` and subsequently stops the source without leaving source-owned resources running +- **WHEN** one active source starts successfully and a later active source makes Ticker startup fail +- **THEN** the ticker stops every source started during that attempt and reports the startup failure without exposing a general stop operation #### Scenario: Repeated source start preserves existing sink - **WHEN** an already-started source receives another start call with a different sink diff --git a/src/main/tjava/com/r35157/assetaz/services/ticker/TickerService.tjava b/src/main/tjava/com/r35157/assetaz/services/ticker/TickerService.tjava index 85a4ede..1db1339 100644 --- a/src/main/tjava/com/r35157/assetaz/services/ticker/TickerService.tjava +++ b/src/main/tjava/com/r35157/assetaz/services/ticker/TickerService.tjava @@ -6,21 +6,13 @@ import org.jetbrains.annotations.NotNull; /** * Provides price observations for trading pairs. + * + *

The service contract contains only operations for ordinary consumers. + * The Hub owns startup through the implementation's separate + * {@code NenjimProcess} view.

*/ public interface TickerService extends NenjimComponent { - /** - * Starts the ticker service. - * - * @throws IllegalStateException if the service cannot be started - */ - void start(); - - /** - * Stops every source started by this service. - */ - void stop(); - /** * Returns the latest available price observation for a trading pair. * diff --git a/src/main/tjava/com/r35157/assetaz/services/ticker/impl/ref/TickerServiceImpl.tjava b/src/main/tjava/com/r35157/assetaz/services/ticker/impl/ref/TickerServiceImpl.tjava index 6d94864..78a68e6 100644 --- a/src/main/tjava/com/r35157/assetaz/services/ticker/impl/ref/TickerServiceImpl.tjava +++ b/src/main/tjava/com/r35157/assetaz/services/ticker/impl/ref/TickerServiceImpl.tjava @@ -39,6 +39,11 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +/** + * Reference Ticker implementation whose domain API is exposed through + * {@link TickerService} and whose startup is owned by the Hub through + * {@link NenjimProcess}. + */ public final class TickerServiceImpl implements TickerService, PriceSink, @@ -88,24 +93,6 @@ public final class TickerServiceImpl implements } } - @Override - public synchronized void stop() { - if (state == LifecycleState.NEW || state == LifecycleState.STOPPED) { - return; - } - if (state != LifecycleState.STARTED) { - throw new IllegalStateException("Ticker service cannot stop while in state " + state); - } - - state = LifecycleState.STOPPING; - RuntimeException failure = stopStartedSources(); - resetStoppedState(); - - if (failure != null) { - throw failure; - } - } - @Override public synchronized @NotNull PriceObservation getLatestPrice( @NotNull TradingPair tradingPair