28 KiB
GB32960 History DuckDB Hot Store Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Replace the current high-write-risk Parquet rewrite history path with a DuckDB hot history store that supports fast vin + time queries and full RAW frame replay.
Architecture: Add a new DuckDbHotEventFileStore implementation behind the existing EventFileStore interface, backed by append/upsert DuckDB tables instead of rewriting per-vehicle Parquet files. Keep the current HTTP history API and RAW archive reader intact, then switch vehicle-history-app to the hot DuckDB store by configuration.
Tech Stack: Java 26, Spring Boot, DuckDB JDBC, JUnit 5, AssertJ, existing Kafka VehicleEnvelope, existing ArchiveStore.
Scope
This plan implements Phase 1 from docs/superpowers/specs/2026-06-23-gb32960-production-readiness-design.md.
Included:
- DuckDB hot event table for
EventFileRecord. - Idempotent writes by
event_id. - Query by protocol/date/VIN/eventType/eventTime/order/limit.
- Lookup by
rawArchiveUri. - Spring configuration to select the hot store.
- Tests that prove history endpoints still replay RAW frames.
- Local verification using the existing
vehicle-history-app.
Not included in this plan:
- MySQL daily statistics.
- VIN-to-platform local mapping.
- Full telemetry point columnar table.
- Alarm timeline MySQL tables.
- Cold Parquet export.
Those will be separate plans after this foundation lands.
Files
- Create:
modules/sinks/event-file-store/src/main/java/com/lingniu/ingest/eventfilestore/DuckDbHotEventFileStore.java - Create:
modules/sinks/event-file-store/src/test/java/com/lingniu/ingest/eventfilestore/DuckDbHotEventFileStoreTest.java - Modify:
modules/sinks/event-file-store/src/main/java/com/lingniu/ingest/eventfilestore/config/EventFileStoreProperties.java - Modify:
modules/sinks/event-file-store/src/main/java/com/lingniu/ingest/eventfilestore/config/EventFileStoreAutoConfiguration.java - Modify:
modules/sinks/event-file-store/src/test/java/com/lingniu/ingest/eventfilestore/config/EventFileStoreAutoConfigurationTest.java - Modify:
modules/apps/vehicle-history-app/src/main/resources/application.yml - Modify:
modules/apps/vehicle-history-app/src/test/java/com/lingniu/ingest/historyapp/VehicleHistoryAppDefaultsTest.java - Modify:
modules/services/event-history-service/src/test/java/com/lingniu/ingest/eventhistory/Gb32960DecodedFrameServiceTest.java
Task 1: Add Hot Store Configuration
Files:
-
Modify:
modules/sinks/event-file-store/src/main/java/com/lingniu/ingest/eventfilestore/config/EventFileStoreProperties.java -
Modify:
modules/sinks/event-file-store/src/main/java/com/lingniu/ingest/eventfilestore/config/EventFileStoreAutoConfiguration.java -
Test:
modules/sinks/event-file-store/src/test/java/com/lingniu/ingest/eventfilestore/config/EventFileStoreAutoConfigurationTest.java -
Step 1: Write the failing auto-configuration test
Add a test that sets lingniu.ingest.event-file-store.storage=duckdb-hot and expects the bean class to be DuckDbHotEventFileStore.
@Test
void createsDuckDbHotStoreWhenStorageIsDuckDbHot() {
contextRunner
.withPropertyValues(
"lingniu.ingest.event-file-store.enabled=true",
"lingniu.ingest.event-file-store.storage=duckdb-hot",
"lingniu.ingest.event-file-store.path=" + tempDir.resolve("history"))
.run(context -> assertThat(context)
.hasSingleBean(EventFileStore.class)
.getBean(EventFileStore.class)
.isInstanceOf(DuckDbHotEventFileStore.class));
}
- Step 2: Run the new test and verify it fails
Run:
mvn -pl :event-file-store -Dtest=EventFileStoreAutoConfigurationTest#createsDuckDbHotStoreWhenStorageIsDuckDbHot test
Expected: compilation failure because DuckDbHotEventFileStore and storage property do not exist.
- Step 3: Add the
storageproperty
Add to EventFileStoreProperties:
/**
* Storage backend. `duckdb-hot` is the production backend; `parquet-sidecar`
* keeps the previous implementation available for compatibility tests.
*/
private String storage = "duckdb-hot";
public String getStorage() {
return storage;
}
public void setStorage(String storage) {
this.storage = storage;
}
- Step 4: Switch auto-configuration by storage mode
Change eventFileStore(...) to:
@Bean
@ConditionalOnMissingBean
public EventFileStore eventFileStore(EventFileStoreProperties properties,
ObjectProvider<ObjectMapper> objectMapper) {
ObjectMapper mapper = mapper(objectMapper);
Path root = Path.of(properties.getPath());
ZoneId zoneId = ZoneId.of(properties.getZoneId());
String storage = properties.getStorage() == null ? "" : properties.getStorage().trim();
return switch (storage) {
case "", "duckdb-hot" -> new DuckDbHotEventFileStore(root, zoneId, mapper);
case "parquet-sidecar" -> new DuckDbParquetEventFileStore(root, zoneId, mapper);
default -> throw new IllegalStateException(
"unsupported event-file-store storage: " + properties.getStorage());
};
}
Import DuckDbHotEventFileStore.
- Step 5: Run configuration tests
Run:
mvn -pl :event-file-store -Dtest=EventFileStoreAutoConfigurationTest test
Expected: tests pass after the store class exists in Task 2.
Task 2: Implement DuckDB Hot Store Schema and Writes
Files:
-
Create:
modules/sinks/event-file-store/src/main/java/com/lingniu/ingest/eventfilestore/DuckDbHotEventFileStore.java -
Create:
modules/sinks/event-file-store/src/test/java/com/lingniu/ingest/eventfilestore/DuckDbHotEventFileStoreTest.java -
Step 1: Write failing append/query/idempotency tests
Create DuckDbHotEventFileStoreTest:
class DuckDbHotEventFileStoreTest {
@TempDir
Path tempDir;
@Test
void appendsRecordsAndQueriesByVinTypeAndTime() throws Exception {
EventFileStore store = store();
store.append(rawRecord("raw-1", "VIN001", "2026-06-23T01:00:00Z"));
store.append(rawRecord("raw-2", "VIN002", "2026-06-23T01:00:01Z"));
store.append(rawRecord("raw-3", "VIN001", "2026-06-23T01:00:02Z"));
EventFileQuery query = new EventFileQuery(
ProtocolId.GB32960,
LocalDate.parse("2026-06-23"),
LocalDate.parse("2026-06-23"),
Instant.parse("2026-06-23T01:00:00Z"),
Instant.parse("2026-06-23T01:00:03Z"),
EventFileQuery.Order.DESC,
2,
"VIN001",
"RAW_ARCHIVE");
assertThat(store.query(query))
.extracting(EventFileRecord::eventId)
.containsExactly("raw-3", "raw-1");
}
@Test
void appendAllIsIdempotentByEventId() throws Exception {
EventFileStore store = store();
EventFileRecord original = rawRecord("same-id", "VIN001", "2026-06-23T01:00:00Z");
EventFileRecord replacement = new EventFileRecord(
"same-id",
ProtocolId.GB32960,
"RAW_ARCHIVE",
"VIN001",
Instant.parse("2026-06-23T01:00:05Z"),
Instant.parse("2026-06-23T01:00:06Z"),
"archive://replacement.bin",
Map.of("source", "replacement"),
"{\"replacement\":true}");
store.appendAll(List.of(original, replacement));
assertThat(store.query(new EventFileQuery(
ProtocolId.GB32960,
LocalDate.parse("2026-06-23"),
LocalDate.parse("2026-06-23"),
EventFileQuery.Order.ASC,
10,
"VIN001",
"RAW_ARCHIVE")))
.singleElement()
.satisfies(record -> {
assertThat(record.eventId()).isEqualTo("same-id");
assertThat(record.rawArchiveUri()).isEqualTo("archive://replacement.bin");
});
}
@Test
void findsRecordByRawArchiveUri() throws Exception {
EventFileStore store = store();
EventFileRecord record = rawRecord("raw-uri", "VIN001", "2026-06-23T01:00:00Z");
store.append(record);
EventFileRecord found = store.findByRawArchiveUri(record.rawArchiveUri());
assertThat(found).isNotNull();
assertThat(found.eventId()).isEqualTo("raw-uri");
}
private EventFileStore store() {
return new DuckDbHotEventFileStore(tempDir, ZoneId.of("Asia/Shanghai"), new ObjectMapper());
}
private static EventFileRecord rawRecord(String id, String vin, String eventTime) {
return new EventFileRecord(
id,
ProtocolId.GB32960,
"RAW_ARCHIVE",
vin,
Instant.parse(eventTime),
Instant.parse(eventTime).plusMillis(100),
"archive://" + id + ".bin",
Map.of("platformAccount", "Hyundai", "command", "REALTIME_REPORT"),
"{\"eventId\":\"" + id + "\"}");
}
}
- Step 2: Run the test and verify it fails
Run:
mvn -pl :event-file-store -Dtest=DuckDbHotEventFileStoreTest test
Expected: compilation failure because DuckDbHotEventFileStore does not exist.
- Step 3: Create the hot store class
Create DuckDbHotEventFileStore with this structure:
public final class DuckDbHotEventFileStore implements EventFileStore {
private static final TypeReference<Map<String, String>> STRING_MAP = new TypeReference<>() {};
private final Path root;
private final Path dbPath;
private final ZoneId partitionZone;
private final ObjectMapper objectMapper;
private volatile boolean initialized;
public DuckDbHotEventFileStore(Path root, ZoneId partitionZone) {
this(root, partitionZone, new ObjectMapper());
}
public DuckDbHotEventFileStore(Path root, ZoneId partitionZone, ObjectMapper objectMapper) {
if (root == null) {
throw new IllegalArgumentException("root must not be null");
}
this.root = root.toAbsolutePath();
this.dbPath = this.root.resolve("events.duckdb");
this.partitionZone = partitionZone == null ? ZoneId.of("Asia/Shanghai") : partitionZone;
this.objectMapper = objectMapper == null ? new ObjectMapper() : objectMapper;
}
@Override
public synchronized void appendAll(List<EventFileRecord> records) throws IOException {
if (records == null || records.isEmpty()) {
return;
}
ensureInitialized();
try (Connection connection = DriverManager.getConnection(jdbcUrl())) {
connection.setAutoCommit(false);
try {
upsertRecords(connection, records);
connection.commit();
} catch (SQLException | IOException e) {
connection.rollback();
throw e;
} finally {
connection.setAutoCommit(true);
}
} catch (SQLException e) {
throw new IOException("write duckdb hot event store failed", e);
}
}
@Override
public List<EventFileRecord> query(EventFileQuery query) throws IOException {
ensureInitialized();
// Implement in Task 3.
return List.of();
}
@Override
public EventFileRecord findByRawArchiveUri(String rawArchiveUri) throws IOException {
ensureInitialized();
// Implement in Task 3.
return null;
}
}
- Step 4: Add schema initialization
Add:
private void ensureInitialized() throws IOException {
if (initialized) {
return;
}
synchronized (this) {
if (initialized) {
return;
}
Files.createDirectories(root);
try (Connection connection = DriverManager.getConnection(jdbcUrl());
Statement statement = connection.createStatement()) {
statement.execute("""
CREATE TABLE IF NOT EXISTS event_records (
event_id VARCHAR PRIMARY KEY,
protocol VARCHAR NOT NULL,
event_type VARCHAR NOT NULL,
vin VARCHAR NOT NULL,
event_time_ms BIGINT NOT NULL,
ingest_time_ms BIGINT NOT NULL,
partition_date DATE NOT NULL,
raw_archive_uri VARCHAR NOT NULL,
metadata_json VARCHAR NOT NULL,
payload_json VARCHAR NOT NULL
)
""");
statement.execute("""
CREATE INDEX IF NOT EXISTS event_records_protocol_date_time_idx
ON event_records(protocol, partition_date, event_time_ms)
""");
statement.execute("""
CREATE INDEX IF NOT EXISTS event_records_vin_date_time_idx
ON event_records(protocol, vin, partition_date, event_time_ms)
""");
statement.execute("""
CREATE INDEX IF NOT EXISTS event_records_vin_type_date_time_idx
ON event_records(protocol, vin, event_type, partition_date, event_time_ms)
""");
statement.execute("""
CREATE INDEX IF NOT EXISTS event_records_raw_archive_uri_idx
ON event_records(raw_archive_uri)
""");
initialized = true;
} catch (SQLException e) {
throw new IOException("initialize duckdb hot event store failed", e);
}
}
}
- Step 5: Add idempotent batch upsert
Use DuckDB INSERT OR REPLACE inside one transaction:
private void upsertRecords(Connection connection, List<EventFileRecord> records)
throws SQLException, IOException {
try (PreparedStatement ps = connection.prepareStatement("""
INSERT OR REPLACE INTO event_records VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""")) {
for (EventFileRecord record : records) {
ps.setString(1, record.eventId());
ps.setString(2, record.protocol().name());
ps.setString(3, record.eventType());
ps.setString(4, record.vin());
ps.setLong(5, record.eventTime().toEpochMilli());
ps.setLong(6, record.ingestTime().toEpochMilli());
ps.setString(7, LocalDate.ofInstant(record.eventTime(), partitionZone).toString());
ps.setString(8, record.rawArchiveUri());
ps.setString(9, objectMapper.writeValueAsString(record.metadata()));
ps.setString(10, record.payloadJson());
ps.addBatch();
}
ps.executeBatch();
}
}
- Step 6: Run the hot store tests
Run:
mvn -pl :event-file-store -Dtest=DuckDbHotEventFileStoreTest test
Expected: query tests still fail until Task 3 implements reads; append initialization should compile.
Task 3: Implement Hot Store Queries and Raw URI Lookup
Files:
-
Modify:
modules/sinks/event-file-store/src/main/java/com/lingniu/ingest/eventfilestore/DuckDbHotEventFileStore.java -
Test:
modules/sinks/event-file-store/src/test/java/com/lingniu/ingest/eventfilestore/DuckDbHotEventFileStoreTest.java -
Step 1: Implement
query(EventFileQuery)
Use prepared statements for every external value:
@Override
public List<EventFileRecord> query(EventFileQuery query) throws IOException {
ensureInitialized();
String order = query.order() == EventFileQuery.Order.DESC ? "DESC" : "ASC";
StringBuilder where = new StringBuilder("""
WHERE protocol = ?
AND partition_date BETWEEN CAST(? AS DATE) AND CAST(? AS DATE)
""");
if (query.vin() != null) {
where.append(" AND vin = ?\n");
}
if (query.eventType() != null) {
where.append(" AND event_type = ?\n");
}
if (query.eventTimeFrom() != null) {
where.append(" AND event_time_ms >= ?\n");
}
if (query.eventTimeTo() != null) {
where.append(" AND event_time_ms <= ?\n");
}
String sql = """
SELECT event_id, protocol, event_type, vin, event_time_ms, ingest_time_ms,
raw_archive_uri, metadata_json, payload_json
FROM event_records
%s
ORDER BY event_time_ms %s, ingest_time_ms %s, event_id %s
LIMIT ?
""".formatted(where, order, order, order);
try (Connection connection = DriverManager.getConnection(jdbcUrl());
PreparedStatement ps = connection.prepareStatement(sql)) {
bindQuery(ps, query);
try (ResultSet rs = ps.executeQuery()) {
List<EventFileRecord> out = new ArrayList<>();
while (rs.next()) {
out.add(record(rs));
}
return out;
}
} catch (SQLException e) {
throw new IOException("query duckdb hot event store failed", e);
}
}
- Step 2: Add query binding helper
private static void bindQuery(PreparedStatement ps, EventFileQuery query) throws SQLException {
int index = 1;
ps.setString(index++, query.protocol().name());
ps.setString(index++, query.dateFrom().toString());
ps.setString(index++, query.dateTo().toString());
if (query.vin() != null) {
ps.setString(index++, query.vin());
}
if (query.eventType() != null) {
ps.setString(index++, query.eventType());
}
if (query.eventTimeFrom() != null) {
ps.setLong(index++, query.eventTimeFrom().toEpochMilli());
}
if (query.eventTimeTo() != null) {
ps.setLong(index++, query.eventTimeTo().toEpochMilli());
}
ps.setInt(index, query.limit());
}
- Step 3: Implement
findByRawArchiveUri
@Override
public EventFileRecord findByRawArchiveUri(String rawArchiveUri) throws IOException {
if (rawArchiveUri == null || rawArchiveUri.isBlank()) {
return null;
}
ensureInitialized();
try (Connection connection = DriverManager.getConnection(jdbcUrl());
PreparedStatement ps = connection.prepareStatement("""
SELECT event_id, protocol, event_type, vin, event_time_ms, ingest_time_ms,
raw_archive_uri, metadata_json, payload_json
FROM event_records
WHERE raw_archive_uri = ?
ORDER BY event_type = 'RAW_ARCHIVE' DESC, ingest_time_ms DESC, event_id DESC
LIMIT 1
""")) {
ps.setString(1, rawArchiveUri);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? record(rs) : null;
}
} catch (SQLException e) {
throw new IOException("query duckdb hot store by raw archive uri failed", e);
}
}
- Step 4: Add record mapper and helpers
private EventFileRecord record(ResultSet rs) throws SQLException, IOException {
return new EventFileRecord(
rs.getString("event_id"),
protocol(rs.getString("protocol")),
rs.getString("event_type"),
rs.getString("vin"),
Instant.ofEpochMilli(rs.getLong("event_time_ms")),
Instant.ofEpochMilli(rs.getLong("ingest_time_ms")),
rs.getString("raw_archive_uri"),
readMetadata(rs.getString("metadata_json")),
rs.getString("payload_json"));
}
private Map<String, String> readMetadata(String json) throws IOException {
if (json == null || json.isBlank()) {
return Map.of();
}
return objectMapper.readValue(json, STRING_MAP);
}
private static ProtocolId protocol(String value) {
if (value == null || value.isBlank()) {
return ProtocolId.UNKNOWN;
}
try {
return ProtocolId.valueOf(value);
} catch (IllegalArgumentException ex) {
return ProtocolId.UNKNOWN;
}
}
private String jdbcUrl() {
return "jdbc:duckdb:" + dbPath;
}
- Step 5: Run hot store tests
Run:
mvn -pl :event-file-store -Dtest=DuckDbHotEventFileStoreTest test
Expected: all DuckDbHotEventFileStoreTest tests pass.
Task 4: Preserve Legacy Store Tests and Update Defaults
Files:
-
Modify:
modules/sinks/event-file-store/src/test/java/com/lingniu/ingest/eventfilestore/DuckDbParquetEventFileStoreTest.java -
Modify:
modules/apps/vehicle-history-app/src/main/resources/application.yml -
Modify:
modules/apps/vehicle-history-app/src/test/java/com/lingniu/ingest/historyapp/VehicleHistoryAppDefaultsTest.java -
Step 1: Keep Parquet tests explicitly legacy
No behavior change is needed in DuckDbParquetEventFileStoreTest; leave it instantiating DuckDbParquetEventFileStore directly. Add a class comment:
/**
* Compatibility coverage for the legacy Parquet sidecar backend.
* Production history uses DuckDbHotEventFileStore through auto-configuration.
*/
class DuckDbParquetEventFileStoreTest {
- Step 2: Set vehicle-history-app storage default
Add to modules/apps/vehicle-history-app/src/main/resources/application.yml:
event-file-store:
enabled: ${EVENT_FILE_STORE_ENABLED:true}
storage: ${EVENT_FILE_STORE_STORAGE:duckdb-hot}
path: ${EVENT_FILE_STORE_PATH:./target/event-store/}
zone-id: ${EVENT_FILE_STORE_ZONE_ID:Asia/Shanghai}
batch-size: ${EVENT_FILE_STORE_BATCH_SIZE:1000}
flush-interval-millis: ${EVENT_FILE_STORE_FLUSH_INTERVAL_MILLIS:1000}
Keep existing indentation and only add storage; if batch-size already exists, update it to 1000.
- Step 3: Update app default test
In VehicleHistoryAppDefaultsTest, assert:
assertThat(context.getEnvironment()
.getProperty("lingniu.ingest.event-file-store.storage"))
.isEqualTo("duckdb-hot");
- Step 4: Run app default tests
Run:
mvn -pl :vehicle-history-app -Dtest=VehicleHistoryAppDefaultsTest test
Expected: default configuration test passes.
Task 5: Verify History Ingest Still Archives RAW Bytes
Files:
-
Modify:
modules/services/event-history-service/src/test/java/com/lingniu/ingest/eventhistory/EventHistoryEnvelopeIngestorTest.java -
Test:
modules/services/event-history-service/src/test/java/com/lingniu/ingest/eventhistory/Gb32960DecodedFrameServiceTest.java -
Step 1: Add an integration-style test using the hot store
Add a test that writes a RAW envelope through EventHistoryEnvelopeIngestor into DuckDbHotEventFileStore, then finds it by URI.
@Test
void rawArchiveEnvelopeCanBeFoundFromDuckDbHotStoreByUri(@TempDir Path tempDir) throws Exception {
EventFileStore store = new DuckDbHotEventFileStore(tempDir, ZoneId.of("Asia/Shanghai"), OBJECT_MAPPER);
CapturingArchiveStore archive = new CapturingArchiveStore();
EventHistoryEnvelopeIngestor ingestor =
new EventHistoryEnvelopeIngestor(store, new TelemetryEnvelopeRecordMapper(), archive);
String rawArchiveKey = "2026/06/23/GB32960/VINRAW001/raw-event-hot.bin";
String rawArchiveUri = "archive://" + rawArchiveKey;
byte[] rawBytes = new byte[]{0x23, 0x23, 0x02, 0x01};
VehicleEnvelope envelope = VehicleEnvelope.newBuilder()
.setSchemaVersion("1.0")
.setEventId("raw-event-hot")
.setVin("VINRAW001")
.setSource("GB32960")
.setProtocolVersion("V2016")
.setEventTimeMs(1_782_112_400_000L)
.setIngestTimeMs(1_782_112_401_000L)
.putMetadata(RawArchiveKeys.META_KEY, rawArchiveKey)
.putMetadata(RawArchiveKeys.META_URI, rawArchiveUri)
.setRawArchive(RawArchiveRef.newBuilder()
.setUri(rawArchiveUri)
.setSizeBytes(rawBytes.length)
.setData(ByteString.copyFrom(rawBytes))
.build())
.build();
EnvelopeIngestResult result = ingestor.tryIngest(envelope.toByteArray());
assertThat(result.status()).isEqualTo(EnvelopeIngestResult.Status.STORED);
assertThat(store.findByRawArchiveUri(rawArchiveUri))
.isNotNull()
.extracting(EventFileRecord::eventId)
.isEqualTo("raw-event-hot");
assertThat(archive.bytesByKey).containsEntry(rawArchiveKey, rawBytes);
}
- Step 2: Run the event history ingestor test
Run:
mvn -pl :event-history-service -Dtest=EventHistoryEnvelopeIngestorTest test
Expected: test passes.
- Step 3: Run decoded frame service tests
Run:
mvn -pl :event-history-service -Dtest=Gb32960DecodedFrameServiceTest test
Expected: existing replay/snapshot tests pass. If they use a fake store, no change is needed.
Task 6: Full Module Verification
Files:
-
No source changes unless failures expose missing imports or config assertions.
-
Step 1: Run sink module tests
Run:
mvn -pl :event-file-store test
Expected: all event-file-store tests pass, including both hot and legacy stores.
- Step 2: Run event-history-service tests
Run:
mvn -pl :event-history-service test
Expected: all event-history-service tests pass.
- Step 3: Package vehicle-history-app
Run:
mvn -pl :vehicle-history-app -am package -DskipTests
Expected: package succeeds.
Task 7: Local Runtime Verification
Files:
-
No committed source changes.
-
Step 1: Stop any old history service on port 20200
Run:
lsof -tiTCP:20200 -sTCP:LISTEN | xargs -r kill
Expected: no command output, or the old process exits.
- Step 2: Start vehicle-history-app with hot DuckDB store
Run:
EVENT_FILE_STORE_STORAGE=duckdb-hot \
EVENT_FILE_STORE_PATH=./target/live-history-event-store \
SINK_ARCHIVE_PATH=./target/live-history-archive \
KAFKA_BROKERS=114.55.58.251:9092 \
KAFKA_CONSUMER_ENABLED=true \
KAFKA_GROUP_HISTORY=vehicle-history-hot-$(date +%Y%m%d%H%M%S) \
java --sun-misc-unsafe-memory-access=allow \
-jar modules/apps/vehicle-history-app/target/vehicle-history-app.jar
Expected:
-
app starts on
http://127.0.0.1:20200; -
logs show Kafka consumer subscribed;
-
target/live-history-event-store/events.duckdbis created. -
Step 3: Verify health
Run:
curl -sS http://127.0.0.1:20200/actuator/health
Expected:
{"status":"UP"}
- Step 4: Verify a recent VIN query
Run with a VIN observed in live archive:
curl -sS 'http://127.0.0.1:20200/api/event-history/gb32960/telemetry-snapshots?vin=LNXNEGRR9SR318194&platformAccount=Hyundai&dateFrom=2026-06-23&dateTo=2026-06-24&order=DESC&limit=3'
Expected:
-
response is a JSON array;
-
if live traffic for that VIN exists after service start, at least one snapshot appears;
-
rawArchiveUrispoint to existing files undertarget/live-history-archive. -
Step 5: Verify raw frame replay
Use one rawArchiveUri from Step 4:
curl -sS 'http://127.0.0.1:20200/api/event-history/gb32960/frame?rawArchiveUri=archive://REPLACE_ME&platformAccount=Hyundai'
Expected:
- response contains
vin,command,eventTime, and parsedblocks; - no
raw archive is missingwarning for the selected URI.
Self-Review Checklist
- The new hot store does not rewrite Parquet files on every append.
appendAllwrites one batch in one transaction.- Duplicate
event_idreplay is idempotent. - Existing history HTTP APIs still use
EventFileStore, so controller contracts are unchanged. - RAW bytes still land in
ArchiveStore. findByRawArchiveUriis backed by DuckDB index.- No MySQL credential, RDS host, username, or password appears in the plan or source files.
- This plan does not claim the full production goal is complete; it only lands the history foundation.