5.4 KiB
Context
See proposal.md for motivation. TickerService and TickerServiceImpl already exist as empty shells in AssetAZ-owned packages. AssetPrice wraps ΩPriceΩ and TradingPair; the latter identifies currencies structurally rather than by raw symbol. The repository temporarily hosts AssetAZ code as Cauldron, and NenjimHub's current autorun list directly constructs services.
Goals / Non-Goals
Goals: Establish a small reusable ticker contract, deterministic file-backed startup, atomic persist-before-publish behavior, and temporary autorun wiring while retaining AssetAZ ownership.
Non-Goals: Datasource plugins, real market data, pairs other than EVE_USDC, consumers, subscriptions/callbacks, history queries, retention, compaction, or automated tests.
Decisions
- Define
PriceObservationincom.r35157.assetaz.core.service.tickerexactly as a record ofAssetPrice priceandInstant observedAt. ExtendTickerServicewithstart()andPriceObservation getLatestPrice(TradingPair tradingPair). Use clear exceptions for unsupported/inactive pairs and enabled pairs without a persisted observation;Optionalandnullwere rejected because issue #60 explicitly requires exceptions for unavailable prices. Raw symbol strings were rejected because callers must use the established ValueTypes. - Add stable EVE and USDC currency definitions and an
EVE_USDCwell-knownTradingPair, then construct the sample price withnew ΩPriceΩ("14.85"). This keeps identity consistent with other typed prices instead of parsing pair names at the API boundary. - Keep the public API and
PriceObservationin the AssetAZ API package and implementation details incom.r35157.assetaz.core.service.ticker.impl.ref. Their current repository location is temporary Cauldron placement and does not imply Evelyn or NenjimHub ownership. - Use the fixed path
data/assetaz/ticker/EVE_USDC.prices; do not create its directory or file. Check existence when initializing the pair. Missing means inactive, emits a warning containing the pair and expected path, and does not prevent the rest of Ticker startup; an existing zero-length, blank-only, or comment-only file is active with no latest value. - Store one observation per data line as
<uuuuMMddHHmmssSSS'Z'>:<decimal-price>, using a strict formatter fixed to UTC. Read line by line, remove text from the first#, trim, and skip empty results. Split the remaining data at the required colon and parse it intoInstant,ΩPriceΩ, and the fixedEVE_USDCpair. Wrap any structural, timestamp, or price failure with the filename, one-based physical line number, and identifying invalid content. - Load history synchronously during
start()and select the parsed observation with the greatestobservedAttimestamp rather than assuming the file is chronologically ordered. After loading, generate the first hardcoded observation immediately, then use a single-thread scheduled executor with fixed-delay scheduling: each subsequent attempt begins one minute after the preceding attempt completes. Fixed-rate scheduling was rejected because a long pause can cause rapid catch-up attempts. Do not schedule generation for an inactive pair. The scheduler is an implementation mechanism, not a public lifecycle or subscription API. - Serialize generation and publication, and keep the published latest value behind a thread-safe reference. Validate and encode every generated observation, append all encoded bytes through a
FileChannel, and callforce(true). Only after successful persistence, compare itsobservedAtwith the current latest and replace the in-memory reference only when the new timestamp is later. Thus concurrent callers see the successfully persisted observation with the greatest timestamp across loaded history and newly persisted observations, and a manually inserted future-dated observation remains latest until a later observation is persisted. A buffered-writerflush()alone was rejected because it does not satisfy the issue's explicit flush/force-to-persistence ordering. On open, write, or force failure, log/report the failure and retain the prior latest value; never publish the attempted observation. - Add a temporary
startAssetAZTickerService()entry toNenjimHubImplfollowing its direct autorun pattern. It constructs and startsTickerServiceImpl; no Ticker dependency is passed to Evelyn, EMC, alarms, portfolios, or other services.
Risks / Trade-offs
- [Filesystem or device guarantees can still limit crash durability after
FileChannel.force(true)] → Force each complete appended record before publication and document that the implementation uses the strongest standard Java file-channel guarantee available here. - [An observation attempt can take arbitrarily long] → Fixed-delay scheduling waits until the attempt completes and then waits one minute, preventing overlapping or catch-up attempts.
- [Malformed history prevents the service from starting] → Include exact filename and line number so operators can repair the activation file safely.
- [Hardcoded identifiers and price are temporary] → Isolate them behind well-known ValueTypes and the reference implementation for later source replacement.
Migration Plan
Operators opt in by creating data/assetaz/ticker/EVE_USDC.prices, optionally pre-populated in the documented format. Deploy the service and temporary autorun wiring together. Rollback removes the autorun call and ticker implementation while leaving the append-only data file intact.