简化开发模拟器历史数据管理
启动时清理上一轮 dev-sim Redis 历史,运行期间限制历史长度并重置演示计数,避免历史数据拖慢启动。
| | |
| | | return restoreState(stored); |
| | | } |
| | | |
| | | @Override |
| | | public SimulatorState resetHistoryForDemoStartup(SimulatorState state) |
| | | { |
| | | pendingEmbeddedHistory = List.of(); |
| | | Set<Object> historyKeys = redisTemplate.opsForSet().members(HISTORY_KEYS_KEY); |
| | | if (historyKeys != null && !historyKeys.isEmpty()) redisTemplate.delete(historyKeys); |
| | | redisTemplate.delete(HISTORY_KEYS_KEY); |
| | | |
| | | List<com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorState> collectors = state.collectors() |
| | | .stream() |
| | | .map(collector -> new com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorState( |
| | | collector.collectorId(), collector.deviceCode(), collector.ip(), collector.mac(), |
| | | collector.name(), collector.manufacturer(), collector.model(), collector.channelCount(), |
| | | collector.online(), collector.sampleIntervalMs(), 1L, |
| | | null, collector.channels(), List.of(), 0L, |
| | | collector.historyFaults())) |
| | | .toList(); |
| | | SimulatorState reset = new SimulatorState(state.version(), state.devices(), collectors, |
| | | state.nextDeviceId(), state.nextCollectorId(), state.tombstones()); |
| | | redisTemplate.opsForValue().set(STATE_KEY, JSON.toJSONString(reset)); |
| | | return reset; |
| | | } |
| | | |
| | | private SimulatorState restoreState(Object stored) |
| | | { |
| | | ParsedSimulatorState parsed = parseState(stored); |
| | |
| | | List<Object> records = new ArrayList<>( |
| | | append.records().stream().map(JSON::toJSONString).toList()); |
| | | typed.opsForList().rightPushAll(historyKey, records); |
| | | typed.opsForList().trim(historyKey, -MAX_HISTORY_RECORDS, -1); |
| | | typed.opsForSet().add(HISTORY_KEYS_KEY, historyKey); |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | @Test |
| | | void doesNotOverwriteDamagedEmbeddedHistoryBeforeValidation() |
| | | void clearsLegacyEmbeddedHistoryForDemoStartup() throws Exception |
| | | { |
| | | AtomicReference<Object> stored = new AtomicReference<>("{\"version\":1,\"devices\":[]," |
| | | + "\"collectors\":[{\"collectorId\":1,\"deviceCode\":\"dev-sim-collector-1\"," |
| | |
| | | + "\"historyFaults\":{\"missingSequences\":[],\"outOfOrder\":false}}]," |
| | | + "\"nextDeviceId\":1,\"nextCollectorId\":2,\"tombstones\":[]}"); |
| | | |
| | | assertThrows(IllegalStateException.class, |
| | | () -> new BtsDevelopmentSimulator(0, store(stored))); |
| | | |
| | | assertEquals(true, stored.get().toString().contains("\"history\":[{")); |
| | | try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, store(stored))) |
| | | { |
| | | assertEquals(0L, simulator.snapshot().collectors().get(0).historyRecordCount()); |
| | | } |
| | | assertEquals(false, stored.get().toString().contains("\"history\":[{")); |
| | | } |
| | | |
| | | @Test |
| | |
| | | { |
| | | this.stateStore = stateStore; |
| | | state = stateStore.loadOrInitialize(defaultState()); |
| | | state = stateStore.resetHistoryForDemoStartup(state); |
| | | validateRestoredState(); |
| | | stateStore.completeValidatedRestore(state); |
| | | simulator = ScriptedBtsTcpSimulator.responding(port, this::respond); |
| | |
| | | { |
| | | int CURRENT_VERSION = 1; |
| | | int MAX_HISTORY_BATCH_SIZE = 1000; |
| | | /** 开发演示只保留有限历史,避免 Redis 随运行时间无限增长。 */ |
| | | long MAX_HISTORY_RECORDS = 20_000L; |
| | | |
| | | /** 读取已有状态;仅在状态不存在时原子写入并返回默认状态。 */ |
| | | SimulatorState loadOrInitialize(SimulatorState defaultState); |
| | | |
| | | /** 开发模拟器启动时清理上一轮演示历史;正式业务存储不需要实现该动作。 */ |
| | | default SimulatorState resetHistoryForDemoStartup(SimulatorState state) { return state; } |
| | | |
| | | /** 完整保存下一版模拟器状态。 */ |
| | | void save(SimulatorState state); |
| | | |
| | |
| | | CollectorState next = copy(current, current.ip(), current.channelCount(), current.online(), |
| | | current.sampleIntervalMs(), |
| | | sequence, lastSampledAt, current.channels(), List.copyOf(latest.values()), |
| | | current.historyRecordCount() + appendedRecords.size(), |
| | | Math.min(BtsSimulatorStateStore.MAX_HISTORY_RECORDS, |
| | | current.historyRecordCount() + appendedRecords.size()), |
| | | current.historyFaults()); |
| | | return new CollectorAdvance(next, appendedRecords); |
| | | } |
| | |
| | | public synchronized void save(SimulatorState state, List<CollectorHistoryAppend> historyAppends) |
| | | { |
| | | for (CollectorHistoryAppend append : historyAppends) |
| | | collectorHistory.computeIfAbsent(append.collectorCode(), key -> new ArrayList<>()) |
| | | .addAll(append.records()); |
| | | { |
| | | ArrayList<CollectorMeasurementRecord> history = collectorHistory.computeIfAbsent( |
| | | append.collectorCode(), key -> new ArrayList<>()); |
| | | history.addAll(append.records()); |
| | | int overflow = history.size() - (int) MAX_HISTORY_RECORDS; |
| | | if (overflow > 0) history.subList(0, overflow).clear(); |
| | | } |
| | | this.state = state; |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | @Test |
| | | void collectorKeepsAppendOnlyHistoryBeyondPreviousDisplayLimit() throws Exception |
| | | void collectorKeepsOnlyBoundedDemoHistory() throws Exception |
| | | { |
| | | Instant startedAt = Instant.parse("2026-08-29T03:00:00Z"); |
| | | TrackingHistoryStore store = new TrackingHistoryStore(); |
| | |
| | | |
| | | store.resetReadStats(); |
| | | var page = simulator.collectorHistory(1, 0, 1); |
| | | assertEquals(60_012, page.total()); |
| | | assertEquals(1, page.records().get(0).sequenceId()); |
| | | assertEquals(BtsSimulatorStateStore.MAX_HISTORY_RECORDS, page.total()); |
| | | assertTrue(page.records().get(0).sequenceId() > 1); |
| | | assertEquals(1, store.historyReads); |
| | | assertEquals(1, store.recordsRead); |
| | | } |
| | |
| | | { |
| | | assertTrue(store.historyReads > 1); |
| | | assertEquals(1000, store.maximumRequestedLimit); |
| | | assertEquals(60_012, store.recordsRead); |
| | | assertEquals(BtsSimulatorStateStore.MAX_HISTORY_RECORDS, store.recordsRead); |
| | | } |
| | | } |
| | | |