81: Control Nenjim process construction and ordered startup through NenjimHub.conf

This commit is contained in:
2026-09-03 15:09:57 +02:00
parent 5bff358d73
commit a62bd5df98
19 changed files with 1318 additions and 196 deletions
@@ -0,0 +1,20 @@
package com.r35157.nenjim.hubd.impl.ref;
import com.r35157.nenjim.service.registry.valuetypes.NenjimComponentId;
import java.util.List;
import java.util.Objects;
/** Immutable, validated process-start plan for one Hub startup attempt. */
record NenjimHubConfiguration(
List<NenjimComponentId> autoStartProcessIds) {
NenjimHubConfiguration {
Objects.requireNonNull(
autoStartProcessIds,
"autoStartProcessIds");
autoStartProcessIds.forEach(componentId ->
Objects.requireNonNull(componentId, "autoStartProcessId"));
autoStartProcessIds = List.copyOf(autoStartProcessIds);
}
}
@@ -0,0 +1,383 @@
package com.r35157.nenjim.hubd.impl.ref;
import com.r35157.nenjim.service.registry.valuetypes.NenjimComponentId;
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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Strict parser for the implementation-only Nenjim Hub configuration. */
final class NenjimHubConfigurationParser {
static NenjimHubConfiguration parse(
Path path,
Set<NenjimComponentId> knownProcessIds,
NenjimComponentId registryProcessId,
NenjimComponentId journalProcessId) throws IOException {
Objects.requireNonNull(path, "path");
Objects.requireNonNull(knownProcessIds, "knownProcessIds");
Objects.requireNonNull(registryProcessId, "registryProcessId");
Objects.requireNonNull(journalProcessId, "journalProcessId");
List<String> lines = readConfiguration(path);
List<ParsedProcess> parsedProcesses = parseLines(path, lines);
validateProcessPlan(
path,
parsedProcesses,
Set.copyOf(knownProcessIds),
registryProcessId,
journalProcessId);
return new NenjimHubConfiguration(
parsedProcesses.stream()
.map(ParsedProcess::componentId)
.toList());
}
private static List<String> readConfiguration(Path path)
throws IOException {
if (!Files.exists(path)) {
throw new IOException(
path + ": Nenjim Hub runtime configuration is missing");
}
if (!Files.isRegularFile(path)) {
throw new IOException(
path + ": Nenjim Hub runtime configuration is not a "
+ "regular file");
}
if (!Files.isReadable(path)) {
throw new IOException(
path + ": Nenjim Hub runtime configuration is not "
+ "readable");
}
try {
return Files.readAllLines(path, StandardCharsets.UTF_8);
} catch (IOException exception) {
throw new IOException(
path + ": Failed to read Nenjim Hub runtime "
+ "configuration",
exception);
}
}
private static List<ParsedProcess> parseLines(
Path path,
List<String> lines) {
ParserState state = ParserState.EXPECT_FORMAT_VERSION;
List<ParsedProcess> parsedProcesses = new ArrayList<>();
for (int lineIndex = 0; lineIndex < lines.size(); lineIndex++) {
String sourceLine = lines.get(lineIndex);
String trimmedSourceLine = sourceLine.strip();
boolean blankLine = trimmedSourceLine.isEmpty();
boolean commentOnlyLine = trimmedSourceLine.startsWith("#");
String content = stripTrailingComment(sourceLine).strip();
int lineNumber = lineIndex + 1;
switch (state) {
case EXPECT_FORMAT_VERSION -> {
if (blankLine || commentOnlyLine) {
continue;
}
parseFormatVersion(path, lineNumber, content);
state = ParserState.EXPECT_AUTO_START;
}
case EXPECT_AUTO_START -> {
if (blankLine || commentOnlyLine) {
continue;
}
rejectAdditionalFormatVersion(
path,
lineNumber,
content);
if (!AUTO_START_HEADER.equals(content)) {
throw invalidLine(
path,
lineNumber,
content,
"Expected " + AUTO_START_HEADER
+ " as the first actual entry after "
+ FORMAT_VERSION_DECLARATION);
}
state = ParserState.IN_AUTO_START;
}
case IN_AUTO_START -> {
if (blankLine) {
state = ParserState.AFTER_AUTO_START;
continue;
}
if (commentOnlyLine) {
continue;
}
rejectAdditionalFormatVersion(
path,
lineNumber,
content);
rejectSectionHeader(path, lineNumber, content);
parsedProcesses.add(parseProcessId(
path,
lineNumber,
content));
}
case AFTER_AUTO_START -> {
if (blankLine || commentOnlyLine) {
continue;
}
rejectAdditionalFormatVersion(
path,
lineNumber,
content);
rejectSectionHeader(path, lineNumber, content);
throw invalidLine(
path,
lineNumber,
content,
"Unexpected content after "
+ AUTO_START_HEADER + " terminated");
}
}
}
if (state == ParserState.EXPECT_FORMAT_VERSION) {
throw new IllegalArgumentException(
path + ": Missing " + FORMAT_VERSION_DECLARATION
+ " as the first actual configuration entry");
}
if (state == ParserState.EXPECT_AUTO_START) {
throw new IllegalArgumentException(
path + ": Missing required " + AUTO_START_HEADER
+ " section");
}
return parsedProcesses;
}
private static void parseFormatVersion(
Path path,
int lineNumber,
String content) {
Matcher matcher = FORMAT_VERSION_PATTERN.matcher(content);
if (!matcher.matches()) {
String reason = content.startsWith(FORMAT_VERSION_KEY)
? "Malformed FORMAT_VERSION; expected exactly "
+ FORMAT_VERSION_DECLARATION
: FORMAT_VERSION_DECLARATION
+ " must be the first actual configuration entry";
throw invalidLine(
path,
lineNumber,
content,
reason);
}
int version;
try {
version = Integer.parseInt(matcher.group(1));
} catch (NumberFormatException exception) {
throw invalidLine(
path,
lineNumber,
content,
"Malformed FORMAT_VERSION; expected "
+ FORMAT_VERSION_DECLARATION,
exception);
}
if (version != SUPPORTED_FORMAT_VERSION) {
throw invalidLine(
path,
lineNumber,
content,
"Unsupported FORMAT_VERSION '" + version
+ "'; only version "
+ SUPPORTED_FORMAT_VERSION + " is supported");
}
if (!FORMAT_VERSION_DECLARATION.equals(content)) {
throw invalidLine(
path,
lineNumber,
content,
"Malformed FORMAT_VERSION; expected exactly "
+ FORMAT_VERSION_DECLARATION);
}
}
private static void rejectAdditionalFormatVersion(
Path path,
int lineNumber,
String content) {
if (content.startsWith(FORMAT_VERSION_KEY)) {
throw invalidLine(
path,
lineNumber,
content,
"Duplicate or misplaced FORMAT_VERSION declaration");
}
}
private static void rejectSectionHeader(
Path path,
int lineNumber,
String content) {
if (AUTO_START_HEADER.equals(content)) {
throw invalidLine(
path,
lineNumber,
content,
"Duplicate " + AUTO_START_HEADER + " section");
}
if (content.startsWith("[") || content.endsWith("]")) {
throw invalidLine(
path,
lineNumber,
content,
"Unknown or malformed section; only "
+ AUTO_START_HEADER + " is supported");
}
}
private static ParsedProcess parseProcessId(
Path path,
int lineNumber,
String content) {
try {
return new ParsedProcess(
new NenjimComponentId(content),
lineNumber);
} catch (IllegalArgumentException exception) {
throw invalidLine(
path,
lineNumber,
content,
"Invalid Nenjim process ID: "
+ exception.getMessage(),
exception);
}
}
private static void validateProcessPlan(
Path path,
List<ParsedProcess> parsedProcesses,
Set<NenjimComponentId> knownProcessIds,
NenjimComponentId registryProcessId,
NenjimComponentId journalProcessId) {
Map<NenjimComponentId, Integer> firstLineById = new HashMap<>();
for (ParsedProcess parsedProcess : parsedProcesses) {
NenjimComponentId componentId = parsedProcess.componentId();
Integer firstLine = firstLineById.putIfAbsent(
componentId,
parsedProcess.lineNumber());
if (firstLine != null) {
throw invalidLine(
path,
parsedProcess.lineNumber(),
componentId.value(),
"Duplicate process ID; first declared on line "
+ firstLine);
}
if (!knownProcessIds.contains(componentId)) {
throw invalidLine(
path,
parsedProcess.lineNumber(),
componentId.value(),
"ID does not identify a modern Nenjim process "
+ "constructible by this Hub");
}
}
requireProcessAtPosition(
path,
parsedProcesses,
registryProcessId,
0,
"Registry must be the first AUTO_START process");
requireProcessAtPosition(
path,
parsedProcesses,
journalProcessId,
1,
"Journal must be the second AUTO_START process");
}
private static void requireProcessAtPosition(
Path path,
List<ParsedProcess> parsedProcesses,
NenjimComponentId requiredId,
int requiredIndex,
String reason) {
if (parsedProcesses.size() <= requiredIndex) {
throw new IllegalArgumentException(
path + ": Missing required process ID '"
+ requiredId.value() + "'; " + reason);
}
ParsedProcess actual = parsedProcesses.get(requiredIndex);
if (!requiredId.equals(actual.componentId())) {
throw invalidLine(
path,
actual.lineNumber(),
actual.componentId().value(),
reason + "; found '"
+ actual.componentId().value() + "'");
}
}
private static String stripTrailingComment(String line) {
int commentStart = line.indexOf('#');
return commentStart < 0 ? line : line.substring(0, commentStart);
}
private static IllegalArgumentException invalidLine(
Path path,
int lineNumber,
String value,
String reason) {
return invalidLine(path, lineNumber, value, reason, null);
}
private static IllegalArgumentException invalidLine(
Path path,
int lineNumber,
String value,
String reason,
Exception cause) {
return new IllegalArgumentException(
path + ":" + lineNumber + ": Invalid Nenjim Hub "
+ "configuration value '" + value + "': " + reason,
cause);
}
private record ParsedProcess(
NenjimComponentId componentId,
int lineNumber) {
}
private enum ParserState {
EXPECT_FORMAT_VERSION,
EXPECT_AUTO_START,
IN_AUTO_START,
AFTER_AUTO_START
}
private static final int SUPPORTED_FORMAT_VERSION = 1;
private static final String FORMAT_VERSION_KEY = "FORMAT_VERSION";
private static final String FORMAT_VERSION_DECLARATION =
FORMAT_VERSION_KEY + "=" + SUPPORTED_FORMAT_VERSION;
private static final String AUTO_START_HEADER = "[AUTO_START]";
private static final Pattern FORMAT_VERSION_PATTERN =
Pattern.compile(FORMAT_VERSION_KEY + "=(\\d+)");
private NenjimHubConfigurationParser() {
}
}
@@ -31,7 +31,6 @@ import com.r35157.nenjim.hubd.NenjimHub;
import com.r35157.nenjim.npm.NenjimProcessManager;
import com.r35157.nenjim.npm.impl.ref.NenjimProcessManagerImpl;
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;
@@ -58,7 +57,10 @@ import java.nio.file.Path;
import java.time.Clock;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -74,10 +76,19 @@ public final class NenjimHubImpl implements NenjimHub {
"NenjimHub start has already been attempted");
}
log.info("Initializing Nenjim Registry and components...");
Map<NenjimComponentId, ProcessConstructor> processConstructors =
createProcessConstructorCatalogue();
NenjimHubConfiguration configuration =
NenjimHubConfigurationParser.parse(
HUB_CONFIGURATION_PATH,
selectableProcessIds(processConstructors),
REGISTRY_SERVICE_ID,
JOURNAL_SERVICE_ID);
log.info("Initializing Nenjim Registry...");
NenjimRegistryAssembly registryAssembly =
NenjimRegistryAssembly.create();
NenjimRegistryService registryService =
registryService =
registryAssembly.registryService();
NenjimProcess registryProcess = registryAssembly.registryProcess();
registryServiceManager = registryAssembly.registryServiceManager();
@@ -87,19 +98,19 @@ public final class NenjimHubImpl implements NenjimHub {
REGISTRY_SERVICE_ID,
registryService);
recordStartedProcess(REGISTRY_SERVICE_ID);
initializeAndStartJournal(registryServiceManager);
initializeAndRegisterComponents(
registryService,
registryServiceManager);
log.info("Starting explicitly enabled Nenjim processes...");
startProcess(registryService, TICKER_SERVICE_ID);
startProcess(registryService, EVELYN_SERVICE_PROD_ID);
startProcess(registryService, EVELYN_SERVICE_TEST_ID);
startProcess(registryService, EVELYN_IOU_BURNER_PROD_ID);
startProcess(registryService, JAVAFX_RUNTIME_SERVICE_ID);
startProcess(registryService, EVELYN_MISSION_CONTROL_ID);
startProcess(registryService, PROCESS_MANAGER_ID);
log.info("Initializing passive Nenjim components...");
initializeAndRegisterPassiveComponents(registryServiceManager);
log.info("Constructing and starting configured Nenjim processes...");
for (NenjimComponentId componentId
: configuration.autoStartProcessIds().subList(
1,
configuration.autoStartProcessIds().size())) {
constructRegisterAndStartProcess(
componentId,
processConstructors);
}
System.out.println("Done - Now online!");
@@ -112,29 +123,18 @@ public final class NenjimHubImpl implements NenjimHub {
return List.copyOf(startedProcessIds);
}
private void initializeAndStartJournal(
NenjimRegistryServiceManager registryServiceManager)
throws Exception {
private NenjimProcess constructJournalProcess() {
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);
return journalAssembly.journalProcess();
}
private void initializeAndRegisterComponents(
NenjimRegistryService registryService,
private void initializeAndRegisterPassiveComponents(
NenjimRegistryServiceManager registryServiceManager) {
initializeBuildingBlocks(registryServiceManager);
initializePriceSources(registryServiceManager);
initializeServices(registryService, registryServiceManager);
initializeApplications(registryServiceManager);
initializePassiveServices(registryServiceManager);
}
private void initializeBuildingBlocks(
@@ -199,15 +199,8 @@ public final class NenjimHubImpl implements NenjimHub {
raydiumPoolPriceSource);
}
private void initializeServices(
NenjimRegistryService registryService,
private void initializePassiveServices(
NenjimRegistryServiceManager registryServiceManager) {
tickerService = new TickerServiceImpl(
registryService,
RAYDIUM_POOL_PRICE_SOURCE_ID
);
registryServiceManager.registerComponent(TICKER_SERVICE_ID, tickerService);
jupiterServicePerpsProd = new AnchorIdlJupiterPerpsServiceImpl(
cachedSolanaBlockChain,
solanaWalletEvelynPerpsProd,
@@ -226,21 +219,6 @@ public final class NenjimHubImpl implements NenjimHub {
JUPITER_PERPS_EVELYN_TEST_ID,
jupiterServicePerpsTest);
jupiterPerpsAlarm = new JupiterPerpsAlarmImpl(
solanaWalletEvelynPerpsTest,
jupiterServicePerpsTest,
"--config=conf/alarms.conf"
);
registryServiceManager.registerComponent(
JUPITER_PERPS_ALARM_ID,
jupiterPerpsAlarm);
evelynServiceProd = new EvelynImpl("Production", tickerService, eveUSDT, clockUTC);
registryServiceManager.registerComponent(EVELYN_SERVICE_PROD_ID, evelynServiceProd);
evelynServiceTest = new EvelynImpl("Test", tickerService, eveUSDT, clockUTC);
registryServiceManager.registerComponent(EVELYN_SERVICE_TEST_ID, evelynServiceTest);
jupiterSwapService = new JupiterSwapServiceImpl(solanaBlockChain, solanaWalletEvelynBurnerProd);
registryServiceManager.registerComponent(
JUPITER_SWAP_EVELYN_BURNER_PROD_ID,
@@ -254,16 +232,64 @@ public final class NenjimHubImpl implements NenjimHub {
registryServiceManager.registerComponent(
DISCORD_ASSETAZ_TOKEN_NOTIFICATION_ID,
discordAssetAZTokenNotificationService);
}
private NenjimProcess constructTickerProcess() {
TickerServiceImpl process = new TickerServiceImpl(
registryService,
RAYDIUM_POOL_PRICE_SOURCE_ID);
tickerService = process;
return process;
}
private NenjimProcess constructJupiterPerpsAlarmProcess() {
JupiterPerpsAlarmImpl process = new JupiterPerpsAlarmImpl(
solanaWalletEvelynPerpsTest,
jupiterServicePerpsTest,
"--config=conf/alarms.conf");
jupiterPerpsAlarm = process;
return process;
}
private NenjimProcess constructEvelynProdProcess() {
EvelynImpl process = new EvelynImpl(
"Production",
requireProcessDependency(
tickerService,
EVELYN_SERVICE_PROD_ID,
TICKER_SERVICE_ID),
eveUSDT,
clockUTC);
evelynServiceProd = process;
return process;
}
private NenjimProcess constructEvelynTestProcess() {
EvelynImpl process = new EvelynImpl(
"Test",
requireProcessDependency(
tickerService,
EVELYN_SERVICE_TEST_ID,
TICKER_SERVICE_ID),
eveUSDT,
clockUTC);
evelynServiceTest = process;
return process;
}
private NenjimProcess constructEvelynIOUBurnerProdProcess() {
List<MoneyAmount> burnTokenCandidates = List.of(new MoneyAmount(new ΩAmountΩ("0.1"), cis.resolve(CurrencyTypeIds.USDT_ID)));
ΩUSDCAmountΩ maximumSwapAmountPerIterationInUSD = new ΩUSDCAmountΩ("0.25");
evelynIOUBurnerServiceProd = new EvelynIOUBurnerServiceImpl(
EvelynIOUBurnerServiceImpl process = new EvelynIOUBurnerServiceImpl(
solanaWalletEvelynBurnerProd,
jupiterSwapService,
solanaBlockChain,
tickerService,
requireProcessDependency(
tickerService,
EVELYN_IOU_BURNER_PROD_ID,
TICKER_SERVICE_ID),
cis,
discordAssetAZTokenNotificationService,
burnTokenCandidates,
@@ -273,32 +299,109 @@ public final class NenjimHubImpl implements NenjimHub {
Duration.ofSeconds(10),
Duration.ofHours(1)
);
registryServiceManager.registerComponent(
EVELYN_IOU_BURNER_PROD_ID,
evelynIOUBurnerServiceProd);
evelynIOUBurnerServiceProd = process;
return process;
}
private void initializeApplications(
NenjimRegistryServiceManager registryServiceManager) {
javaFxRuntimeService = new JavaFxRuntimeServiceImpl();
registryServiceManager.registerComponent(
JAVAFX_RUNTIME_SERVICE_ID,
javaFxRuntimeService);
private NenjimProcess constructJavaFxRuntimeProcess() {
JavaFxRuntimeServiceImpl process = new JavaFxRuntimeServiceImpl();
javaFxRuntimeService = process;
return process;
}
missionControl = new EvelynMissionControlImpl(
evelynServiceProd,
evelynServiceTest,
javaFxRuntimeService);
registryServiceManager.registerComponent(
EVELYN_MISSION_CONTROL_ID,
missionControl);
private NenjimProcess constructMissionControlProcess() {
EvelynMissionControlImpl process = new EvelynMissionControlImpl(
requireProcessDependency(
evelynServiceProd,
EVELYN_MISSION_CONTROL_ID,
EVELYN_SERVICE_PROD_ID),
requireProcessDependency(
evelynServiceTest,
EVELYN_MISSION_CONTROL_ID,
EVELYN_SERVICE_TEST_ID),
requireProcessDependency(
javaFxRuntimeService,
EVELYN_MISSION_CONTROL_ID,
JAVAFX_RUNTIME_SERVICE_ID));
missionControl = process;
return process;
}
processManager = new NenjimProcessManagerImpl(
private NenjimProcess constructProcessManagerProcess() {
NenjimProcessManagerImpl process = new NenjimProcessManagerImpl(
this,
javaFxRuntimeService);
registryServiceManager.registerComponent(
PROCESS_MANAGER_ID,
processManager);
requireProcessDependency(
javaFxRuntimeService,
PROCESS_MANAGER_ID,
JAVAFX_RUNTIME_SERVICE_ID));
processManager = process;
return process;
}
private void constructRegisterAndStartProcess(
NenjimComponentId componentId,
Map<NenjimComponentId, ProcessConstructor> processConstructors)
throws Exception {
ProcessConstructor constructor = processConstructors.get(componentId);
if (constructor == null) {
throw new IllegalStateException(
"Validated Nenjim process '" + componentId.value()
+ "' has no constructor");
}
NenjimProcess process = constructor.construct();
registryServiceManager.registerComponent(componentId, process);
startProcess(registryService, componentId);
}
private Map<NenjimComponentId, ProcessConstructor>
createProcessConstructorCatalogue() {
return Map.ofEntries(
Map.entry(JOURNAL_SERVICE_ID, this::constructJournalProcess),
Map.entry(TICKER_SERVICE_ID, this::constructTickerProcess),
Map.entry(
JUPITER_PERPS_ALARM_ID,
this::constructJupiterPerpsAlarmProcess),
Map.entry(
EVELYN_SERVICE_PROD_ID,
this::constructEvelynProdProcess),
Map.entry(
EVELYN_SERVICE_TEST_ID,
this::constructEvelynTestProcess),
Map.entry(
EVELYN_IOU_BURNER_PROD_ID,
this::constructEvelynIOUBurnerProdProcess),
Map.entry(
JAVAFX_RUNTIME_SERVICE_ID,
this::constructJavaFxRuntimeProcess),
Map.entry(
EVELYN_MISSION_CONTROL_ID,
this::constructMissionControlProcess),
Map.entry(
PROCESS_MANAGER_ID,
this::constructProcessManagerProcess));
}
private static Set<NenjimComponentId> selectableProcessIds(
Map<NenjimComponentId, ProcessConstructor> processConstructors) {
Set<NenjimComponentId> processIds =
new HashSet<>(processConstructors.keySet());
processIds.add(REGISTRY_SERVICE_ID);
return Set.copyOf(processIds);
}
private static <T> T requireProcessDependency(
T dependency,
NenjimComponentId processId,
NenjimComponentId dependencyId) {
if (dependency == null) {
throw new IllegalStateException(
"Cannot construct configured Nenjim process '"
+ processId.value() + "': required predecessor '"
+ dependencyId.value()
+ "' has not been constructed");
}
return dependency;
}
private void startProcess(
@@ -328,8 +431,15 @@ public final class NenjimHubImpl implements NenjimHub {
System.out.println("Nenjim is now shut down!");
}
@FunctionalInterface
private interface ProcessConstructor {
NenjimProcess construct() throws Exception;
}
private static final Logger log = LoggerFactory.getLogger(
NenjimHubImpl.class);
private static final Path HUB_CONFIGURATION_PATH =
Path.of("conf/NenjimHub.conf");
private static final NenjimComponentId REGISTRY_SERVICE_ID =
new NenjimComponentId("nenjim.registry.service");
private static final NenjimComponentId JOURNAL_SERVICE_ID =
@@ -386,6 +496,7 @@ public final class NenjimHubImpl implements NenjimHub {
private final CountDownLatch shutdownLatch = new CountDownLatch(1);
private final AtomicBoolean startAttempted = new AtomicBoolean();
private final List<NenjimComponentId> startedProcessIds = new ArrayList<>();
private NenjimRegistryService registryService;
private NenjimRegistryServiceManager registryServiceManager;
private JournalServiceManager journalServiceManager;