package com.haobang.dispatch; import com.common.entity.DeviceDevice; import com.fasterxml.jackson.databind.ObjectMapper; import com.haobang.config.AppProperties; import com.haobang.wal.SyslogIngressEvent; import com.haobang.wal.SyslogWalService; import com.kafka.kafkaProducer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.nio.file.Path; import java.nio.file.Files; import java.time.LocalDateTime; import java.util.concurrent.CompletableFuture; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class SyslogWalDispatcherTest { @TempDir Path tempDirectory; private SyslogWalService wal; private SyslogWalDispatcher dispatcher; @AfterEach void tearDown() { if (dispatcher != null) dispatcher.shutdown(); if (wal != null) wal.shutdown(); } @Test void outOfOrderKafkaAckAdvancesOnlyContiguousCheckpoint() throws Exception { AppProperties properties = properties(tempDirectory); properties.setSyslogPointTimeWriteEnabled(true); wal = new SyslogWalService(properties, new ObjectMapper()); wal.initialize(); SyslogIngressEvent firstEvent = wal.prepareForWal(event("20260811-event-1", "first")); SyslogIngressEvent secondEvent = wal.prepareForWal(event("20260811-event-2", "second")); wal.tryAppend(firstEvent); wal.tryAppend(secondEvent); waitUntil(() -> wal.getAppendedCount() == 2L); DeviceDevice device = new DeviceDevice(); device.setId(126); device.setName("device"); device.setVendor("vendor"); DeviceContextResolver resolver = mock(DeviceContextResolver.class); when(resolver.resolve(anyString(), anyString())) .thenReturn(DeviceContextResolver.Resolution.registered(device)); CompletableFuture first = new CompletableFuture<>(); CompletableFuture second = new CompletableFuture<>(); kafkaProducer producer = mock(kafkaProducer.class); when(producer.messagePush(any(DeviceDevice.class), anyString(), anyString(), anyString(), any())) .thenAnswer(invocation -> "20260811-event-1".equals(invocation.getArgument(3)) ? first : second); dispatcher = new SyslogWalDispatcher(wal, resolver, producer, properties); dispatcher.start(); waitUntil(() -> dispatcher.getInflight() == 2L); second.complete(null); Thread.sleep(100L); assertEquals(0L, wal.getCheckpointSequence()); first.complete(null); waitUntil(() -> wal.getCheckpointSequence() == 2L); assertEquals(2L, dispatcher.getKafkaAcked()); assertEquals(0L, dispatcher.getRetryCount()); verify(producer).messagePush(any(DeviceDevice.class), eq("first"), eq("20260811200000000"), eq("20260811-event-1"), eq(firstEvent.getPointTimeNs())); verify(producer).messagePush(any(DeviceDevice.class), eq("second"), eq("20260811200000000"), eq("20260811-event-2"), eq(secondEvent.getPointTimeNs())); } @Test void terminalDeviceStateIsPersistedBeforeCheckpointAdvances() throws Exception { AppProperties properties = properties(tempDirectory); wal = new SyslogWalService(properties, new ObjectMapper()); wal.initialize(); wal.tryAppend(event("20260811-event-1", "first")); waitUntil(() -> wal.getAppendedCount() == 1L); DeviceContextResolver resolver = mock(DeviceContextResolver.class); when(resolver.resolve(anyString(), anyString())).thenReturn(DeviceContextResolver.Resolution.terminal( DeviceContextResolver.Type.UNKNOWN_DEVICE, null, "not registered")); kafkaProducer producer = mock(kafkaProducer.class); dispatcher = new SyslogWalDispatcher(wal, resolver, producer, properties); dispatcher.start(); waitUntil(() -> wal.getCheckpointSequence() == 1L); assertEquals(1L, dispatcher.getTerminalFiltered()); assertTrue(Files.list(tempDirectory.resolve("terminal")).findAny().isPresent()); verify(producer, never()).messagePush(any(), anyString(), anyString(), anyString(), any()); } private SyslogIngressEvent event(String id, String raw) { return new SyslogIngressEvent(id, LocalDateTime.of(2026, 8, 11, 20, 0), "192.168.4.19", 514, "TCP", raw); } private AppProperties properties(Path directory) { AppProperties value = new AppProperties(); value.setSyslogWalEnabled(true); value.setSyslogWalDirectory(directory.toString()); value.setSyslogWalQueueCapacity(16); value.setSyslogWalHighWatermarkPercent(70); value.setSyslogWalLowWatermarkPercent(40); value.setSyslogWalRetryIntervalMs(5); value.setSyslogWalDiskRetryIntervalMs(50); value.setSyslogWalFlushRecords(1); value.setSyslogWalFlushIntervalMs(5); value.setSyslogWalSegmentMaxBytes(1024 * 1024); value.setSyslogWalRetentionDays(0); value.setSyslogWalShutdownTimeoutMs(5000); value.setSyslogWalCheckpointFlushRecords(1); value.setSyslogWalCheckpointFlushIntervalMs(5); value.setSyslogWalDiskHighWatermarkPercent(98); value.setSyslogWalDiskCriticalWatermarkPercent(99); value.setSyslogWalMinFreeBytes(0); value.setSyslogCharset("UTF-8"); value.setSyslogDispatchWorkerThreads(2); value.setSyslogDispatchMaxInflight(8); value.setSyslogDispatchRetryBackoffMs(10); value.setSyslogDispatchShutdownTimeoutMs(2000); value.setSyslogDeviceContextCacheTtlMs(60000); value.setSyslogStatusLogIntervalMs(60000); return value; } private void waitUntil(Check condition) throws Exception { long deadline = System.currentTimeMillis() + 5000L; while (!condition.done() && System.currentTimeMillis() < deadline) Thread.sleep(10L); assertTrue(condition.done(), "condition not reached before timeout"); } private interface Check { boolean done() throws Exception; } }