86: Add AssetAZ Journal Service with durable PDE event journal and replayable consumers
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
package com.r35157.assetaz.events;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/** A fact/state change, never a command. Sequence, not semantic time, orders facts.
|
||||
* Replay rebuilds state without repeating external side effects. Inseparable changes
|
||||
* belong in one event; correlation groups independent facts, without atomicity.
|
||||
*/
|
||||
public interface AssetAZEvent {
|
||||
@NotNull Instant occurredAt();
|
||||
@Nullable String note();
|
||||
@Nullable UUID correlationId();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.r35157.assetaz.events;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
public record AssetOwnerRegistered(
|
||||
@NotNull Instant occurredAt,
|
||||
@NotNull UUID assetOwnerId,
|
||||
@NotNull AssetOwnerType assetOwnerType,
|
||||
@NotNull ΩNameΩ name,
|
||||
@Nullable String note,
|
||||
@Nullable UUID correlationId
|
||||
) implements AssetAZEvent { }
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.r35157.assetaz.events;
|
||||
|
||||
/** Juridical owner category. Persistent values are textual, never enum ordinals. */
|
||||
public enum AssetOwnerType { NATURAL_PERSON, LEGAL_PERSON }
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.r35157.assetaz.events;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Ordinary chained fact; target format takes effect for subsequent records. */
|
||||
public record JournalFormatVersionChanged(
|
||||
@NotNull Instant occurredAt,
|
||||
int journalFormatVersion,
|
||||
@Nullable String note,
|
||||
@Nullable UUID correlationId
|
||||
) implements AssetAZEvent { }
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.r35157.assetaz.services.journal;
|
||||
|
||||
import com.r35157.assetaz.events.AssetAZEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Sequential callbacks on one subscription worker, identical for replay and live facts.
|
||||
* Each event must update the externally visible model atomically, or leave it unchanged
|
||||
* on failure. Rebuild state only during catch-up; never replay historical side effects.
|
||||
*/
|
||||
public interface AssetAZJournalConsumer {
|
||||
void onEvent(long sequence, @NotNull AssetAZEvent event) throws Exception;
|
||||
/** Successful ONLINE return establishes readiness to act. This callback should
|
||||
* establish readiness without itself performing irreversible domain actions. */
|
||||
void requestState(@NotNull AssetAZJournalConsumerState state) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.r35157.assetaz.services.journal;
|
||||
|
||||
public enum AssetAZJournalConsumerState { CATCHING_UP, ONLINE, DEGRADED }
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.r35157.assetaz.services.journal;
|
||||
|
||||
import com.r35157.assetaz.events.AssetAZEvent;
|
||||
import com.r35157.nenjim.component.NenjimComponent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import java.util.Set;
|
||||
|
||||
/** Authoritative append-only fact journal. Lifecycle belongs to the Nenjim process view. */
|
||||
public interface AssetAZJournalService extends NenjimComponent {
|
||||
/** Returns the committed global sequence only after durable persistence. */
|
||||
long append(@NotNull AssetAZEvent event);
|
||||
/** Returns without waiting for replay. Scan starts inclusively at replayFromSequence,
|
||||
* in [1, registration head + 1]. Empty filters mean all; nonempty filters match
|
||||
* exact runtime classes. The same consumer object cannot register twice. */
|
||||
@NotNull AssetAZJournalSubscription registerConsumer(
|
||||
long replayFromSequence,
|
||||
@NotNull Set<Class<? extends AssetAZEvent>> eventTypes,
|
||||
@NotNull AssetAZJournalConsumer consumer);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.r35157.assetaz.services.journal;
|
||||
|
||||
import com.r35157.assetaz.services.journal.exception.DegradedException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/** A permanent registration; v1 has no unregister or degraded recovery. */
|
||||
public interface AssetAZJournalSubscription {
|
||||
/** All records through this cursor were handled or examined as irrelevant. */
|
||||
long getProcessedThroughSequence();
|
||||
@NotNull AssetAZJournalConsumerState getState();
|
||||
/** Minimum freshness, not a historical snapshot. Succeeds even when degraded
|
||||
* if the safe cursor already meets the minimum; otherwise degradation throws. */
|
||||
void awaitProcessedThrough(long requiredSequence) throws DegradedException, InterruptedException;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.r35157.assetaz.services.journal.codec;
|
||||
|
||||
import com.r35157.assetaz.events.AssetAZEvent;
|
||||
import com.r35157.libs.pde.PDEEncoder;
|
||||
import com.r35157.libs.pde.PDEDecoder;
|
||||
import com.r35157.nenjim.component.NenjimComponent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/** One persistent schema mapped to a current domain type, obtained through Registry.
|
||||
* Implementations must be thread-safe. Only one codec per domain type is a writer;
|
||||
* historical read-only versions may coexist. No reflection discovery is involved.
|
||||
*/
|
||||
public interface AssetAZEventCodec extends NenjimComponent {
|
||||
@NotNull String eventType();
|
||||
int eventVersion();
|
||||
@NotNull Class<? extends AssetAZEvent> eventClass();
|
||||
boolean supportsEncoding();
|
||||
void encode(@NotNull AssetAZEvent event, @NotNull PDEEncoder payload);
|
||||
@NotNull AssetAZEvent decode(@NotNull PDEDecoder payload);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.r35157.assetaz.services.journal.codec;
|
||||
|
||||
/** Domain schema rejection, distinct from PDE validity and record integrity. */
|
||||
public final class EventSchemaException extends IllegalArgumentException {
|
||||
public EventSchemaException(String message) { super("event/schema validation failure: " + message); }
|
||||
public EventSchemaException(String message, Throwable cause) { super("event/schema validation failure: " + message, cause); }
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.r35157.assetaz.services.journal.codec.impl.ref;
|
||||
|
||||
import com.r35157.assetaz.events.AssetAZEvent;
|
||||
import com.r35157.assetaz.events.AssetOwnerRegistered;
|
||||
import com.r35157.assetaz.events.AssetOwnerType;
|
||||
import com.r35157.assetaz.services.journal.codec.AssetAZEventCodec;
|
||||
import com.r35157.assetaz.services.journal.codec.EventSchemaException;
|
||||
import com.r35157.libs.pde.PDEDecoder;
|
||||
import com.r35157.libs.pde.PDEEncoder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Persistent AssetOwnerRegistered schema 1, with exactly seven positional values. */
|
||||
public final class AssetOwnerRegisteredCodec implements AssetAZEventCodec {
|
||||
@Override public @NotNull String eventType() { return "AssetOwnerRegistered"; }
|
||||
@Override public int eventVersion() { return 1; }
|
||||
@Override public @NotNull Class<AssetOwnerRegistered> eventClass() { return AssetOwnerRegistered.class; }
|
||||
@Override public boolean supportsEncoding() { return true; }
|
||||
@Override public void encode(@NotNull AssetAZEvent event, @NotNull PDEEncoder out) {
|
||||
if (!(event instanceof AssetOwnerRegistered owner)) throw new EventSchemaException("expected AssetOwnerRegistered");
|
||||
EventFields.writeCommon(owner, out);
|
||||
out.writeBytes(EventFields.uuid(EventFields.required(owner.assetOwnerId(), "assetOwnerId")));
|
||||
out.writeUtf8(EventFields.required(owner.assetOwnerType(), "assetOwnerType").name());
|
||||
out.writeUtf8(EventFields.required(owner.name(), "name"));
|
||||
}
|
||||
@Override public @NotNull AssetOwnerRegistered decode(@NotNull PDEDecoder in) {
|
||||
Instant occurredAt = EventFields.instant(in);
|
||||
String note = in.readNullableUtf8();
|
||||
UUID correlationId = EventFields.uuid(in.readNullableBytes());
|
||||
UUID ownerId = EventFields.required(EventFields.uuid(in.readNullableBytes()), "assetOwnerId");
|
||||
String type = EventFields.required(in.readNullableUtf8(), "assetOwnerType");
|
||||
AssetOwnerType ownerType;
|
||||
try { ownerType = AssetOwnerType.valueOf(type); }
|
||||
catch (IllegalArgumentException failure) { throw new EventSchemaException("unknown AssetOwnerType " + type, failure); }
|
||||
ΩNameΩ name = EventFields.required(in.readNullableUtf8(), "name");
|
||||
EventFields.end(in);
|
||||
return new AssetOwnerRegistered(occurredAt, ownerId, ownerType, name, note, correlationId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.r35157.assetaz.services.journal.codec.impl.ref;
|
||||
|
||||
import com.r35157.assetaz.events.AssetAZEvent;
|
||||
import com.r35157.assetaz.services.journal.codec.EventSchemaException;
|
||||
import com.r35157.libs.pde.PDEDecoder;
|
||||
import com.r35157.libs.pde.PDEEncoder;
|
||||
import com.r35157.libs.pde.PDEException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.time.DateTimeException;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Codec-level domain conversions. PDE itself has no UUID or Instant semantics. */
|
||||
final class EventFields {
|
||||
private EventFields() { }
|
||||
static void writeCommon(AssetAZEvent event, PDEEncoder out) {
|
||||
required(event.occurredAt(), "occurredAt");
|
||||
out.writeInteger(event.occurredAt().getEpochSecond());
|
||||
out.writeInteger(event.occurredAt().getNano());
|
||||
out.writeNullableUtf8(event.note());
|
||||
out.writeNullableBytes(uuid(event.correlationId()));
|
||||
}
|
||||
static Instant instant(PDEDecoder in) {
|
||||
long seconds = in.readLong();
|
||||
int nanos = in.readInt();
|
||||
if (nanos < 0 || nanos > 999999999) throw new EventSchemaException("invalid nano-of-second");
|
||||
try { return Instant.ofEpochSecond(seconds, nanos); }
|
||||
catch (DateTimeException failure) { throw new EventSchemaException("invalid occurredAt", failure); }
|
||||
}
|
||||
static byte[] uuid(UUID value) {
|
||||
return value == null ? null : ByteBuffer.allocate(16)
|
||||
.putLong(value.getMostSignificantBits()).putLong(value.getLeastSignificantBits()).array();
|
||||
}
|
||||
static UUID uuid(byte[] bytes) {
|
||||
if (bytes == null) return null;
|
||||
if (bytes.length != 16) throw new EventSchemaException("UUID must contain exactly 16 bytes");
|
||||
ByteBuffer buffer = ByteBuffer.wrap(bytes);
|
||||
return new UUID(buffer.getLong(), buffer.getLong());
|
||||
}
|
||||
static <T> T required(T value, String name) {
|
||||
if (value == null) throw new EventSchemaException(name + " is required");
|
||||
return value;
|
||||
}
|
||||
static void end(PDEDecoder in) {
|
||||
try { in.requireEnd(); }
|
||||
catch (PDEException failure) { throw new EventSchemaException("unexpected trailing payload values", failure); }
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.r35157.assetaz.services.journal.codec.impl.ref;
|
||||
|
||||
import com.r35157.assetaz.events.AssetAZEvent;
|
||||
import com.r35157.assetaz.events.JournalFormatVersionChanged;
|
||||
import com.r35157.assetaz.services.journal.codec.AssetAZEventCodec;
|
||||
import com.r35157.assetaz.services.journal.codec.EventSchemaException;
|
||||
import com.r35157.libs.pde.PDEDecoder;
|
||||
import com.r35157.libs.pde.PDEEncoder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Common event fields followed by the positive target journal format. */
|
||||
public final class JournalFormatVersionChangedCodec implements AssetAZEventCodec {
|
||||
@Override public @NotNull String eventType() { return "JournalFormatVersionChanged"; }
|
||||
@Override public int eventVersion() { return 1; }
|
||||
@Override public @NotNull Class<JournalFormatVersionChanged> eventClass() { return JournalFormatVersionChanged.class; }
|
||||
@Override public boolean supportsEncoding() { return true; }
|
||||
@Override public void encode(@NotNull AssetAZEvent event, @NotNull PDEEncoder out) {
|
||||
if (!(event instanceof JournalFormatVersionChanged changed)) throw new EventSchemaException("expected JournalFormatVersionChanged");
|
||||
EventFields.writeCommon(changed, out);
|
||||
out.writeInteger(positive(changed.journalFormatVersion()));
|
||||
}
|
||||
@Override public @NotNull JournalFormatVersionChanged decode(@NotNull PDEDecoder in) {
|
||||
Instant occurredAt = EventFields.instant(in);
|
||||
String note = in.readNullableUtf8();
|
||||
UUID correlation = EventFields.uuid(in.readNullableBytes());
|
||||
int version = positive(in.readInt());
|
||||
EventFields.end(in);
|
||||
return new JournalFormatVersionChanged(occurredAt, version, note, correlation);
|
||||
}
|
||||
private static int positive(int version) {
|
||||
if (version < 1) throw new EventSchemaException("journal format must be positive");
|
||||
return version;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.r35157.assetaz.services.journal.exception;
|
||||
|
||||
/** Requested freshness exceeds this consumer's last safely processed sequence. */
|
||||
public final class DegradedException extends Exception {
|
||||
public DegradedException(String message, Throwable cause) { super(message, cause); }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.r35157.assetaz.services.journal.exception;
|
||||
|
||||
/** Authoritative history could not be verified or durably appended. */
|
||||
public final class JournalException extends IllegalStateException {
|
||||
public JournalException(String message) { super(message); }
|
||||
public JournalException(String message, Throwable cause) { super(message, cause); }
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package com.r35157.assetaz.services.journal.impl.ref;
|
||||
|
||||
import com.r35157.assetaz.events.AssetAZEvent;
|
||||
import com.r35157.assetaz.services.journal.AssetAZJournalConsumer;
|
||||
import com.r35157.assetaz.services.journal.AssetAZJournalService;
|
||||
import com.r35157.assetaz.services.journal.AssetAZJournalSubscription;
|
||||
import com.r35157.assetaz.services.journal.exception.JournalException;
|
||||
import com.r35157.nenjim.process.NenjimProcess;
|
||||
import com.r35157.nenjim.service.registry.NenjimRegistryService;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/** Hub-managed lifetime process; public API has no restart/recovery operations. */
|
||||
public final class AssetAZJournalServiceImpl implements AssetAZJournalService, NenjimProcess {
|
||||
private final NenjimRegistryService registry;
|
||||
private final Path path;
|
||||
private final IdentityHashMap<AssetAZJournalConsumer, JournalSubscription> subscriptions = new IdentityHashMap<>();
|
||||
private boolean startAttempted;
|
||||
private boolean started;
|
||||
private Throwable writeFailure;
|
||||
private FileChannel channel;
|
||||
private FileLock fileLock;
|
||||
private EventCodecs codecs;
|
||||
private long head;
|
||||
private byte[] checksum;
|
||||
|
||||
public AssetAZJournalServiceImpl(@NotNull NenjimRegistryService registry, @NotNull Path path) {
|
||||
this.registry = Objects.requireNonNull(registry, "registry");
|
||||
this.path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
|
||||
}
|
||||
@Override public synchronized void start() throws IOException {
|
||||
if (startAttempted) throw new IllegalStateException("AssetAZ journal start already attempted");
|
||||
startAttempted = true;
|
||||
codecs = new EventCodecs(registry);
|
||||
Files.createDirectories(path.getParent());
|
||||
FileChannel opened = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE);
|
||||
try {
|
||||
fileLock = opened.tryLock();
|
||||
if (fileLock == null) throw new JournalException("AssetAZ journal is already owned by another process");
|
||||
try (JournalRecords.Reader reader = new JournalRecords.Reader(path, codecs)) {
|
||||
while (reader.next() != null) { /* Complete authoritative validation. */ }
|
||||
head = reader.sequence();
|
||||
checksum = reader.checksum();
|
||||
}
|
||||
opened.position(opened.size());
|
||||
channel = opened;
|
||||
started = true;
|
||||
} catch (IOException | RuntimeException | Error failure) {
|
||||
try { opened.close(); } catch (IOException closeFailure) { failure.addSuppressed(closeFailure); }
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
@Override public synchronized long append(@NotNull AssetAZEvent event) {
|
||||
requireStarted();
|
||||
if (writeFailure != null) throw new JournalException("AssetAZ journal failed closed after uncertain write", writeFailure);
|
||||
Objects.requireNonNull(event, "event");
|
||||
if (head == Long.MAX_VALUE) throw new JournalException("Journal sequence exhausted");
|
||||
long candidate = head + 1;
|
||||
JournalRecords.Encoded encoded = JournalRecords.encode(candidate, checksum, event, codecs);
|
||||
try {
|
||||
ByteBuffer bytes = ByteBuffer.wrap(encoded.bytes());
|
||||
while (bytes.hasRemaining()) channel.write(bytes);
|
||||
channel.force(true);
|
||||
} catch (IOException | RuntimeException | Error failure) {
|
||||
// Retain the file lock and failed state for this JVM. Never retry/truncate.
|
||||
writeFailure = failure;
|
||||
throw new JournalException("Uncertain append at candidate sequence " + candidate + "; journal failed closed", failure);
|
||||
}
|
||||
head = candidate;
|
||||
checksum = encoded.checksum();
|
||||
JournalRecords.Record record = new JournalRecords.Record(candidate, event, checksum);
|
||||
for (JournalSubscription subscription : subscriptions.values()) subscription.publish(record);
|
||||
return candidate;
|
||||
}
|
||||
@Override public synchronized @NotNull AssetAZJournalSubscription registerConsumer(
|
||||
long replayFromSequence, @NotNull Set<Class<? extends AssetAZEvent>> eventTypes,
|
||||
@NotNull AssetAZJournalConsumer consumer) {
|
||||
requireStarted();
|
||||
Objects.requireNonNull(consumer, "consumer");
|
||||
Set<Class<? extends AssetAZEvent>> types = Set.copyOf(Objects.requireNonNull(eventTypes, "eventTypes"));
|
||||
if (replayFromSequence < 1 || (replayFromSequence > head && replayFromSequence - 1 != head)) {
|
||||
throw new IllegalArgumentException("replayFromSequence must be in [1, registration head + 1]");
|
||||
}
|
||||
if (subscriptions.containsKey(consumer)) throw new IllegalArgumentException("Consumer instance already registered");
|
||||
JournalSubscription subscription = new JournalSubscription(path, codecs, replayFromSequence, head, types, consumer);
|
||||
subscriptions.put(consumer, subscription);
|
||||
subscription.start();
|
||||
return subscription;
|
||||
}
|
||||
private void requireStarted() {
|
||||
if (!started) throw new IllegalStateException("AssetAZ journal has not successfully started");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.r35157.assetaz.services.journal.impl.ref;
|
||||
|
||||
import com.r35157.assetaz.events.AssetAZEvent;
|
||||
import com.r35157.assetaz.services.journal.codec.AssetAZEventCodec;
|
||||
import com.r35157.assetaz.services.journal.exception.JournalException;
|
||||
import com.r35157.nenjim.service.registry.NenjimRegistryService;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/** Immutable catalogue snapshot; no concrete codec construction or reflection. */
|
||||
final class EventCodecs {
|
||||
private record Key(String type, int version) { }
|
||||
private final Map<Key, AssetAZEventCodec> readers = new HashMap<>();
|
||||
private final Map<Class<? extends AssetAZEvent>, AssetAZEventCodec> writers = new HashMap<>();
|
||||
EventCodecs(NenjimRegistryService registry) {
|
||||
for (var id : registry.getComponentIds(AssetAZEventCodec.class)) {
|
||||
AssetAZEventCodec codec = Objects.requireNonNull(registry.getComponent(id, AssetAZEventCodec.class), "registered codec");
|
||||
String type = Objects.requireNonNull(codec.eventType(), "eventType");
|
||||
if (!type.matches("[A-Za-z][A-Za-z0-9_]*") || codec.eventVersion() < 1) {
|
||||
throw new JournalException("Invalid stable codec identity: " + id);
|
||||
}
|
||||
Objects.requireNonNull(codec.eventClass(), "eventClass");
|
||||
if (readers.putIfAbsent(new Key(type, codec.eventVersion()), codec) != null) {
|
||||
throw new JournalException("Duplicate event codec: " + type + "/" + codec.eventVersion());
|
||||
}
|
||||
if (codec.supportsEncoding() && writers.putIfAbsent(codec.eventClass(), codec) != null) {
|
||||
throw new JournalException("Duplicate current encoder: " + codec.eventClass().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
AssetAZEventCodec reader(String type, int version) {
|
||||
AssetAZEventCodec codec = readers.get(new Key(type, version));
|
||||
if (codec == null) throw new JournalException("unknown event codec: " + type + "/" + version);
|
||||
return codec;
|
||||
}
|
||||
AssetAZEventCodec writer(AssetAZEvent event) {
|
||||
AssetAZEventCodec codec = writers.get(event.getClass());
|
||||
if (codec == null) throw new JournalException("unknown event codec for append: " + event.getClass().getName());
|
||||
return codec;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.r35157.assetaz.services.journal.impl.ref;
|
||||
|
||||
import com.r35157.assetaz.events.AssetAZEvent;
|
||||
import com.r35157.assetaz.events.JournalFormatVersionChanged;
|
||||
import com.r35157.assetaz.services.journal.codec.AssetAZEventCodec;
|
||||
import com.r35157.assetaz.services.journal.codec.EventSchemaException;
|
||||
import com.r35157.assetaz.services.journal.exception.JournalException;
|
||||
import com.r35157.libs.pde.impl.ref.PDEDecoderImpl;
|
||||
import com.r35157.libs.pde.impl.ref.PDEEncoderImpl;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
/** Record integrity owns hashes, chain, sequence and file boundaries, never schemas. */
|
||||
final class JournalRecords {
|
||||
private static final int MAX_RECORD_SIZE_BYTES = 1024;
|
||||
private JournalRecords() { }
|
||||
record Record(long sequence, AssetAZEvent event, byte[] checksum) { }
|
||||
record Encoded(byte[] bytes, byte[] checksum) { }
|
||||
|
||||
static byte[] initialChecksum() { return digest(new byte[0], 0, 0); }
|
||||
static Encoded encode(long sequence, byte[] previous, AssetAZEvent event, EventCodecs codecs) {
|
||||
requireSupportedFormat(event);
|
||||
AssetAZEventCodec codec = codecs.writer(event);
|
||||
PDEEncoderImpl body = new PDEEncoderImpl();
|
||||
body.writeBytes(previous);
|
||||
body.writeInteger(sequence);
|
||||
body.writeUtf8(codec.eventType());
|
||||
body.writeInteger(codec.eventVersion());
|
||||
body.writeObject(payload -> codec.encode(event, payload));
|
||||
byte[] bodyBytes = body.toByteArray();
|
||||
byte[] checksum = digest(bodyBytes, 0, bodyBytes.length);
|
||||
PDEEncoderImpl checksumField = new PDEEncoderImpl();
|
||||
checksumField.writeBytes(checksum);
|
||||
byte[] prefix = checksumField.toByteArray();
|
||||
byte[] fields = Arrays.copyOf(prefix, Math.addExact(prefix.length, bodyBytes.length));
|
||||
System.arraycopy(bodyBytes, 0, fields, prefix.length, bodyBytes.length);
|
||||
PDEEncoderImpl record = new PDEEncoderImpl();
|
||||
record.writeEncodedObject(fields);
|
||||
byte[] bytes = record.toByteArray();
|
||||
if (bytes.length > MAX_RECORD_SIZE_BYTES) throw integrity("record exceeds 1024 bytes");
|
||||
return new Encoded(bytes, checksum);
|
||||
}
|
||||
static void requireSupportedFormat(AssetAZEvent event) {
|
||||
if (event instanceof JournalFormatVersionChanged changed && changed.journalFormatVersion() != 1) {
|
||||
throw new JournalException("Unsupported journal format: " + changed.journalFormatVersion());
|
||||
}
|
||||
}
|
||||
private static byte[] digest(byte[] bytes, int offset, int length) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
digest.update(bytes, offset, length);
|
||||
return digest.digest();
|
||||
} catch (NoSuchAlgorithmException impossible) { throw new AssertionError(impossible); }
|
||||
}
|
||||
private static JournalException integrity(String message) {
|
||||
return new JournalException("record integrity failure: " + message);
|
||||
}
|
||||
|
||||
/** Each instance owns exactly one fixed record buffer, reused without clearing. */
|
||||
static final class Reader implements AutoCloseable {
|
||||
private final FileChannel channel;
|
||||
private final EventCodecs codecs;
|
||||
private final byte[] buffer = new byte[MAX_RECORD_SIZE_BYTES];
|
||||
private long offset;
|
||||
private long sequence;
|
||||
private byte[] previous = initialChecksum();
|
||||
|
||||
Reader(Path path, EventCodecs codecs) throws IOException {
|
||||
channel = FileChannel.open(path, StandardOpenOption.READ);
|
||||
this.codecs = codecs;
|
||||
}
|
||||
long sequence() { return sequence; }
|
||||
byte[] checksum() { return previous; }
|
||||
Record next() throws IOException {
|
||||
long remaining = channel.size() - offset;
|
||||
if (remaining == 0) return null;
|
||||
try {
|
||||
if (remaining < 1) throw integrity("file shrank during reading");
|
||||
readFully(0, 1);
|
||||
int headerSize = 1 + PDEDecoderImpl.objectLengthWidth(buffer[0] & 255);
|
||||
if (headerSize > remaining) throw integrity("truncated OBJECT header");
|
||||
readFully(1, headerSize - 1);
|
||||
long size = PDEDecoderImpl.objectSize(buffer, 0, headerSize);
|
||||
if (size > MAX_RECORD_SIZE_BYTES) throw integrity("declared record exceeds 1024 bytes");
|
||||
if (size > remaining) throw integrity("declared record extends beyond journal file");
|
||||
readFully(headerSize, (int) size - headerSize);
|
||||
PDEDecoderImpl outer = new PDEDecoderImpl(buffer, 0, (int) size);
|
||||
PDEDecoderImpl fields = outer.readObject();
|
||||
outer.requireEnd();
|
||||
byte[] checksum = fields.readBytes();
|
||||
if (checksum.length != 32) throw integrity("checksum must be 32 bytes");
|
||||
byte[] calculated = digest(buffer, fields.position(), fields.limit() - fields.position());
|
||||
if (!MessageDigest.isEqual(checksum, calculated)) throw integrity("checksum mismatch");
|
||||
byte[] predecessor = fields.readBytes();
|
||||
if (predecessor.length != 32 || !MessageDigest.isEqual(previous, predecessor)) {
|
||||
throw integrity("previousChecksum mismatch");
|
||||
}
|
||||
long nextSequence = fields.readLong();
|
||||
if (sequence == Long.MAX_VALUE || nextSequence != sequence + 1) throw integrity("sequence discontinuity");
|
||||
String type = fields.readUtf8();
|
||||
int version = fields.readInt();
|
||||
PDEDecoderImpl payload = fields.readObject();
|
||||
fields.requireEnd();
|
||||
AssetAZEventCodec codec = codecs.reader(type, version);
|
||||
AssetAZEvent event = codec.decode(payload);
|
||||
// The codec owns its schema, but cannot leave an envelope payload unread.
|
||||
payload.requireEnd();
|
||||
if (event == null || event.getClass() != codec.eventClass()) {
|
||||
throw new EventSchemaException("codec returned wrong domain type");
|
||||
}
|
||||
requireSupportedFormat(event);
|
||||
offset += size;
|
||||
sequence = nextSequence;
|
||||
previous = checksum;
|
||||
return new Record(sequence, event, checksum);
|
||||
} catch (RuntimeException failure) {
|
||||
throw new JournalException("Journal byte offset " + offset + ", expected sequence "
|
||||
+ (sequence + 1) + ": " + failure.getMessage(), failure);
|
||||
}
|
||||
}
|
||||
private void readFully(int start, int length) throws IOException {
|
||||
ByteBuffer target = ByteBuffer.wrap(buffer, start, length);
|
||||
while (target.hasRemaining()) {
|
||||
if (channel.read(target) < 0) throw integrity("truncated record");
|
||||
}
|
||||
}
|
||||
@Override public void close() throws IOException { channel.close(); }
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package com.r35157.assetaz.services.journal.impl.ref;
|
||||
|
||||
import com.r35157.assetaz.events.AssetAZEvent;
|
||||
import com.r35157.assetaz.services.journal.AssetAZJournalConsumer;
|
||||
import com.r35157.assetaz.services.journal.AssetAZJournalConsumerState;
|
||||
import com.r35157.assetaz.services.journal.AssetAZJournalSubscription;
|
||||
import com.r35157.assetaz.services.journal.exception.DegradedException;
|
||||
import com.r35157.assetaz.services.journal.exception.JournalException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Set;
|
||||
import static com.r35157.assetaz.services.journal.AssetAZJournalConsumerState.*;
|
||||
|
||||
/** One independent, permanently registered sequential worker. */
|
||||
final class JournalSubscription implements AssetAZJournalSubscription {
|
||||
private static final Logger log = LoggerFactory.getLogger(JournalSubscription.class);
|
||||
private final Path path;
|
||||
private final EventCodecs codecs;
|
||||
private final long replayFrom;
|
||||
private final long registrationHead;
|
||||
private final Set<Class<? extends AssetAZEvent>> types;
|
||||
private final AssetAZJournalConsumer consumer;
|
||||
private final ArrayDeque<JournalRecords.Record> pending = new ArrayDeque<>();
|
||||
private long processedThrough;
|
||||
private AssetAZJournalConsumerState state = CATCHING_UP;
|
||||
private Throwable failure;
|
||||
|
||||
JournalSubscription(Path path, EventCodecs codecs, long replayFrom, long registrationHead,
|
||||
Set<Class<? extends AssetAZEvent>> types, AssetAZJournalConsumer consumer) {
|
||||
this.path = path; this.codecs = codecs; this.replayFrom = replayFrom;
|
||||
this.registrationHead = registrationHead; this.types = types; this.consumer = consumer;
|
||||
processedThrough = replayFrom - 1;
|
||||
}
|
||||
void start() {
|
||||
Thread.ofVirtual().name("assetaz-journal-" + identity()).start(this::run);
|
||||
}
|
||||
synchronized void publish(JournalRecords.Record record) {
|
||||
if (state == DEGRADED) return;
|
||||
pending.addLast(record);
|
||||
notifyAll();
|
||||
}
|
||||
@Override public synchronized long getProcessedThroughSequence() { return processedThrough; }
|
||||
@Override public synchronized @NotNull AssetAZJournalConsumerState getState() { return state; }
|
||||
@Override public synchronized void awaitProcessedThrough(long requiredSequence)
|
||||
throws DegradedException, InterruptedException {
|
||||
while (processedThrough < requiredSequence) {
|
||||
if (state == DEGRADED) throw new DegradedException(
|
||||
"Consumer " + identity() + " is degraded through " + processedThrough
|
||||
+ "; required " + requiredSequence, failure);
|
||||
wait();
|
||||
}
|
||||
}
|
||||
private void run() {
|
||||
long currentSequence = replayFrom;
|
||||
try {
|
||||
if (replayFrom <= registrationHead) {
|
||||
try (JournalRecords.Reader reader = new JournalRecords.Reader(path, codecs)) {
|
||||
while (reader.sequence() < registrationHead) {
|
||||
JournalRecords.Record record = reader.next();
|
||||
if (record == null) throw new JournalException("record integrity failure: replay ended before registration head");
|
||||
currentSequence = record.sequence();
|
||||
if (record.sequence() >= replayFrom) process(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean online = false;
|
||||
while (true) {
|
||||
JournalRecords.Record record;
|
||||
synchronized (this) {
|
||||
while (online && pending.isEmpty()) wait();
|
||||
record = pending.pollFirst();
|
||||
// Empty queue under the publication monitor chooses the cutover.
|
||||
// Arrivals after this point queue as normal post-ONLINE events.
|
||||
}
|
||||
if (record == null) {
|
||||
currentSequence = getProcessedThroughSequence();
|
||||
consumer.requestState(ONLINE);
|
||||
synchronized (this) { state = ONLINE; notifyAll(); }
|
||||
online = true;
|
||||
} else {
|
||||
currentSequence = record.sequence();
|
||||
process(record);
|
||||
}
|
||||
}
|
||||
} catch (Throwable exception) {
|
||||
if (exception instanceof InterruptedException) Thread.currentThread().interrupt();
|
||||
degrade(exception, currentSequence);
|
||||
}
|
||||
}
|
||||
private void process(JournalRecords.Record record) throws Exception {
|
||||
if (types.isEmpty() || types.contains(record.event().getClass())) {
|
||||
consumer.onEvent(record.sequence(), record.event());
|
||||
}
|
||||
synchronized (this) { processedThrough = record.sequence(); notifyAll(); }
|
||||
}
|
||||
private void degrade(Throwable exception, long sequence) {
|
||||
AssetAZJournalConsumerState oldState;
|
||||
synchronized (this) {
|
||||
oldState = state; failure = exception; state = DEGRADED;
|
||||
pending.clear(); notifyAll();
|
||||
}
|
||||
log.error("AssetAZ journal consumer {} failed at sequence {}, state {}, safe cursor {}",
|
||||
identity(), sequence, oldState, getProcessedThroughSequence(), exception);
|
||||
}
|
||||
private String identity() {
|
||||
return consumer.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(consumer));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.r35157.libs.pde;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/** Decoder restricted to a logical byte range, including each child object. */
|
||||
public interface PDEDecoder {
|
||||
int readInt();
|
||||
long readLong();
|
||||
@NotNull String readUtf8();
|
||||
@Nullable String readNullableUtf8();
|
||||
byte @NotNull [] readBytes();
|
||||
byte @Nullable [] readNullableBytes();
|
||||
@NotNull PDEDecoder readObject();
|
||||
void requireEnd();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.r35157.libs.pde;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/** Primitive PDE writer; domain conversions belong to codecs. */
|
||||
public interface PDEEncoder {
|
||||
void writeInteger(long value);
|
||||
void writeUtf8(@NotNull String value);
|
||||
void writeNullableUtf8(@Nullable String value);
|
||||
void writeBytes(byte @NotNull [] value);
|
||||
void writeNullableBytes(byte @Nullable [] value);
|
||||
void writeObject(@NotNull Consumer<PDEEncoder> writer);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.r35157.libs.pde;
|
||||
|
||||
/** Invalid PDE type, range, encoding or framing (not a domain schema error). */
|
||||
public final class PDEException extends IllegalArgumentException {
|
||||
public PDEException(String message) { super("PDE decoding/framing failure: " + message); }
|
||||
public PDEException(String message, Throwable cause) { super("PDE decoding/framing failure: " + message, cause); }
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.r35157.libs.pde.impl.ref;
|
||||
|
||||
import com.r35157.libs.pde.PDEDecoder;
|
||||
import com.r35157.libs.pde.PDEException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.CharacterCodingException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class PDEDecoderImpl implements PDEDecoder {
|
||||
private final byte[] bytes;
|
||||
private final int limit;
|
||||
private int position;
|
||||
|
||||
public PDEDecoderImpl(byte @NotNull [] bytes, int offset, int length) {
|
||||
Objects.checkFromIndexSize(offset, length, bytes.length);
|
||||
this.bytes = bytes; position = offset; limit = offset + length;
|
||||
}
|
||||
/** Absolute backing-array offset, for hashing original encoded ranges. */
|
||||
public int position() { return position; }
|
||||
public int limit() { return limit; }
|
||||
@Override public int readInt() {
|
||||
long value = readLong();
|
||||
if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) throw error("integer outside int range");
|
||||
return (int) value;
|
||||
}
|
||||
@Override public long readLong() {
|
||||
int type = next();
|
||||
if (type < 4 || type > 19) throw error("expected non-null integer");
|
||||
boolean negative = type >= 12;
|
||||
int width = type - (negative ? 11 : 3);
|
||||
long magnitude = unsigned(width);
|
||||
if (magnitude < 0) throw error("integer outside long range");
|
||||
return negative ? -1 - magnitude : magnitude;
|
||||
}
|
||||
@Override public @NotNull String readUtf8() {
|
||||
String result = readNullableUtf8();
|
||||
if (result == null) throw error("required UTF-8 is null");
|
||||
return result;
|
||||
}
|
||||
@Override public @Nullable String readNullableUtf8() {
|
||||
int length = fieldLength(48, 49, 65);
|
||||
if (length < 0) return null;
|
||||
require(length);
|
||||
try {
|
||||
String result = StandardCharsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(bytes, position, length)).toString();
|
||||
position += length;
|
||||
return result;
|
||||
} catch (CharacterCodingException failure) { throw new PDEException("invalid UTF-8", failure); }
|
||||
}
|
||||
@Override public byte @NotNull [] readBytes() {
|
||||
byte[] result = readNullableBytes();
|
||||
if (result == null) throw error("required bytes are null");
|
||||
return result;
|
||||
}
|
||||
@Override public byte @Nullable [] readNullableBytes() {
|
||||
int length = fieldLength(23, 24, 40);
|
||||
if (length < 0) return null;
|
||||
require(length);
|
||||
byte[] result = Arrays.copyOfRange(bytes, position, position + length);
|
||||
position += length;
|
||||
return result;
|
||||
}
|
||||
@Override public @NotNull PDEDecoderImpl readObject() {
|
||||
int width = objectLengthWidth(next());
|
||||
int length = length(width);
|
||||
require(length);
|
||||
PDEDecoderImpl child = new PDEDecoderImpl(bytes, position, length);
|
||||
position += length;
|
||||
return child;
|
||||
}
|
||||
@Override public void requireEnd() {
|
||||
if (position != limit) throw error("unexpected trailing values");
|
||||
}
|
||||
/** Number of following length bytes for a non-null OBJECT type. */
|
||||
public static int objectLengthWidth(int type) {
|
||||
if (type < 120 || type > 127) throw new PDEException("expected non-null OBJECT");
|
||||
return type - 119;
|
||||
}
|
||||
/** Inspect just an OBJECT header; the body need not be present yet. */
|
||||
public static long objectSize(byte @NotNull [] bytes, int offset, int headerLength) {
|
||||
PDEDecoderImpl header = new PDEDecoderImpl(bytes, offset, headerLength);
|
||||
int width = objectLengthWidth(header.next());
|
||||
long size = header.unsigned(width);
|
||||
header.requireEnd();
|
||||
if (size < 0 || size > Long.MAX_VALUE - headerLength) throw error("OBJECT length overflow");
|
||||
return size + headerLength;
|
||||
}
|
||||
private int fieldLength(int nullType, int shortBase, int lengthBase) {
|
||||
int type = next();
|
||||
if (type == nullType) return -1;
|
||||
if (type >= shortBase && type <= shortBase + 15) return type - shortBase;
|
||||
if (type >= lengthBase && type <= lengthBase + 7) return length(type - lengthBase + 1);
|
||||
throw error("unexpected primitive type " + type);
|
||||
}
|
||||
private int length(int width) {
|
||||
long value = unsigned(width);
|
||||
if (value < 0 || value > Integer.MAX_VALUE) throw error("encoded length outside supported range");
|
||||
return (int) value;
|
||||
}
|
||||
private long unsigned(int width) {
|
||||
require(width);
|
||||
long result = 0;
|
||||
for (int i = 0; i < width; i++) result |= (long) next() << (8 * i);
|
||||
return result;
|
||||
}
|
||||
private int next() { require(1); return bytes[position++] & 255; }
|
||||
private void require(int length) {
|
||||
if (length < 0 || length > limit - position) throw error("truncated value");
|
||||
}
|
||||
private static PDEException error(String message) { return new PDEException(message); }
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.r35157.libs.pde.impl.ref;
|
||||
|
||||
import com.r35157.libs.pde.PDEEncoder;
|
||||
import com.r35157.libs.pde.PDEException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.CharacterCodingException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/** Minimal Jenkov PDE: little-endian magnitudes/lengths, typed nulls and objects. */
|
||||
public final class PDEEncoderImpl implements PDEEncoder {
|
||||
private final ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
|
||||
@Override public void writeInteger(long value) {
|
||||
long magnitude = value < 0 ? -(value + 1) : value;
|
||||
int width = width(magnitude);
|
||||
output.write((value < 0 ? 11 : 3) + width);
|
||||
littleEndian(magnitude, width);
|
||||
}
|
||||
@Override public void writeUtf8(@NotNull String value) {
|
||||
Objects.requireNonNull(value, "value");
|
||||
try {
|
||||
var encoded = StandardCharsets.UTF_8.newEncoder().encode(CharBuffer.wrap(value));
|
||||
byte[] bytes = new byte[encoded.remaining()];
|
||||
encoded.get(bytes);
|
||||
field(49, 65, bytes);
|
||||
} catch (CharacterCodingException failure) {
|
||||
throw new PDEException("invalid UTF-8 input", failure);
|
||||
}
|
||||
}
|
||||
@Override public void writeNullableUtf8(@Nullable String value) {
|
||||
if (value == null) output.write(48); else writeUtf8(value);
|
||||
}
|
||||
@Override public void writeBytes(byte @NotNull [] value) {
|
||||
field(24, 40, Objects.requireNonNull(value, "value"));
|
||||
}
|
||||
@Override public void writeNullableBytes(byte @Nullable [] value) {
|
||||
if (value == null) output.write(23); else writeBytes(value);
|
||||
}
|
||||
@Override public void writeObject(@NotNull Consumer<PDEEncoder> writer) {
|
||||
PDEEncoderImpl child = new PDEEncoderImpl();
|
||||
Objects.requireNonNull(writer, "writer").accept(child);
|
||||
writeEncodedObject(child.toByteArray());
|
||||
}
|
||||
/** Wrap already encoded PDE fields without semantic re-encoding. */
|
||||
public void writeEncodedObject(byte @NotNull [] fields) {
|
||||
int width = width(fields.length);
|
||||
output.write(119 + width);
|
||||
littleEndian(fields.length, width);
|
||||
output.writeBytes(fields);
|
||||
}
|
||||
public byte @NotNull [] toByteArray() { return output.toByteArray(); }
|
||||
private void field(int shortBase, int lengthBase, byte[] value) {
|
||||
if (value.length <= 15) output.write(shortBase + value.length);
|
||||
else {
|
||||
int width = width(value.length);
|
||||
output.write(lengthBase + width - 1);
|
||||
littleEndian(value.length, width);
|
||||
}
|
||||
output.writeBytes(value);
|
||||
}
|
||||
private static int width(long value) {
|
||||
return Math.max(1, (64 - Long.numberOfLeadingZeros(value) + 7) / 8);
|
||||
}
|
||||
private void littleEndian(long value, int width) {
|
||||
for (int i = 0; i < width; i++) { output.write((int) value & 255); value >>>= 8; }
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.r35157.nenjim.hubd.impl.ref;
|
||||
|
||||
import com.r35157.assetaz.services.journal.impl.ref.AssetAZJournalServiceImpl;
|
||||
import com.r35157.assetaz.services.journal.codec.impl.ref.AssetOwnerRegisteredCodec;
|
||||
import com.r35157.assetaz.services.journal.codec.impl.ref.JournalFormatVersionChangedCodec;
|
||||
import com.fanitas.evelyn.core.Evelyn;
|
||||
import com.fanitas.evelyn.core.impl.ref.EvelynImpl;
|
||||
import com.fanitas.evelyn.service.burner.EvelynIOUBurnerService;
|
||||
@@ -152,6 +155,13 @@ public final class NenjimHubImpl implements NenjimHub {
|
||||
|
||||
private void initializeComponents(
|
||||
NenjimRegistryServiceManager registryServiceManager) {
|
||||
registryServiceManager.registerComponent(
|
||||
new NenjimComponentId("assetaz.journal.codec.asset-owner-registered.v1"),
|
||||
new AssetOwnerRegisteredCodec());
|
||||
registryServiceManager.registerComponent(
|
||||
new NenjimComponentId("assetaz.journal.codec.journal-format-version-changed.v1"),
|
||||
new JournalFormatVersionChangedCodec());
|
||||
|
||||
objectCache = new ObjectCacheImpl();
|
||||
registryServiceManager.registerComponent(OBJECT_CACHE_ID, objectCache);
|
||||
|
||||
@@ -371,6 +381,8 @@ public final class NenjimHubImpl implements NenjimHub {
|
||||
createProcessConstructorCatalogue() {
|
||||
return Map.ofEntries(
|
||||
Map.entry(JOURNAL_SERVICE_ID, this::constructJournalProcess),
|
||||
Map.entry(ASSETAZ_JOURNAL_SERVICE_ID, () -> new AssetAZJournalServiceImpl(
|
||||
registryService, Path.of("data/assetaz/journal/events.pde"))),
|
||||
Map.entry(TICKER_SERVICE_ID, this::constructTickerProcess),
|
||||
Map.entry(JUPITER_PERPS_ALARM_ID, this::constructJupiterPerpsAlarmProcess),
|
||||
Map.entry(EVELYN_SERVICE_PROD_ID, this::constructEvelynProdProcess),
|
||||
@@ -446,6 +458,8 @@ public final class NenjimHubImpl implements NenjimHub {
|
||||
new NenjimComponentId("nenjim.registry.service");
|
||||
private static final NenjimComponentId JOURNAL_SERVICE_ID =
|
||||
new NenjimComponentId("nenjim.journal.service");
|
||||
private static final NenjimComponentId ASSETAZ_JOURNAL_SERVICE_ID =
|
||||
new NenjimComponentId("assetaz.journal.service");
|
||||
private static final Path JOURNAL_ROOT =
|
||||
Path.of("data/nenjim/journals");
|
||||
private static final NenjimComponentId OBJECT_CACHE_ID =
|
||||
|
||||
Reference in New Issue
Block a user