| | |
| | | import java.time.format.DateTimeParseException; |
| | | import java.time.temporal.ChronoUnit; |
| | | import java.util.ArrayList; |
| | | import java.util.Comparator; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | |
| | | public static final int DEFAULT_SAMPLE_INTERVAL_MS = 1000; |
| | | public static final int MIN_SAMPLE_INTERVAL_MS = 100; |
| | | public static final int MAX_SAMPLE_INTERVAL_MS = 60000; |
| | | public static final int MAX_HISTORY_PAGE_SIZE = 1000; |
| | | public static final int MAX_HISTORY_PAGE_SIZE = BtsSimulatorStateStore.MAX_HISTORY_BATCH_SIZE; |
| | | private static final int MAX_SAMPLES_PER_ADVANCE = 1000; |
| | | private static final ZoneId DEVICE_ZONE = ZoneId.of("Asia/Hong_Kong"); |
| | | private static final DateTimeFormatter DEVICE_TIME_FORMATTER = |
| | |
| | | } |
| | | |
| | | /** 对只追加原始模拟历史应用可重复的下载故障视图,再按偏移量分页。 */ |
| | | public static CollectorHistoryPage history(CollectorState current, List<CollectorMeasurementRecord> rawHistory, |
| | | public static CollectorHistoryPage history(CollectorState current, BtsSimulatorStateStore stateStore, |
| | | long offset, int limit) |
| | | { |
| | | if (offset < 0) throw new IllegalArgumentException("offset must not be negative"); |
| | | if (limit < 1 || limit > MAX_HISTORY_PAGE_SIZE) |
| | | throw new IllegalArgumentException("limit must be between 1 and " + MAX_HISTORY_PAGE_SIZE); |
| | | |
| | | List<CollectorMeasurementRecord> download = faultedHistory(current, rawHistory); |
| | | int from = (int) Math.min(offset, download.size()); |
| | | int to = Math.min(from + limit, download.size()); |
| | | return new CollectorHistoryPage(current.deviceCode(), offset, to, to < download.size(), |
| | | download.size(), download.subList(from, to)); |
| | | CollectorHistoryFaults faults = current.historyFaults(); |
| | | if (faults.missingSequences().isEmpty() && faults.duplicateSequence() == null |
| | | && faults.conflictingSequence() == null && !faults.outOfOrder()) |
| | | { |
| | | BtsSimulatorStateStore.CollectorHistoryBatch batch = stateStore.loadCollectorHistoryPage( |
| | | current.deviceCode(), offset, limit, false); |
| | | return new CollectorHistoryPage(current.deviceCode(), offset, batch.nextOffset(), batch.hasMore(), |
| | | batch.total(), batch.records()); |
| | | } |
| | | return faultedHistoryPage(current, stateStore, offset, limit); |
| | | } |
| | | |
| | | /** 校验 Redis 恢复的采集器采样状态,拒绝损坏状态被静默替换。 */ |
| | | public static void validate(CollectorState state, List<CollectorMeasurementRecord> rawHistory) |
| | | public static void validate(CollectorState state, BtsSimulatorStateStore stateStore) |
| | | { |
| | | validateSampleInterval(state.sampleIntervalMs()); |
| | | if (state.nextSequence() < 1 || state.channels().size() != state.channelCount()) |
| | |
| | | validateMetrics(channel.metrics()); |
| | | } |
| | | validateLatestMeasurements(state); |
| | | validateHistory(state, rawHistory); |
| | | validateHistory(state, stateStore); |
| | | validateFaults(state.historyFaults()); |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | private static void validateHistory(CollectorState state, List<CollectorMeasurementRecord> rawHistory) |
| | | private static void validateHistory(CollectorState state, BtsSimulatorStateStore stateStore) |
| | | { |
| | | long maximumSequence = 0; |
| | | long previousSequence = 0; |
| | | long recordCount = 0; |
| | | long offset = 0; |
| | | Set<String> identities = new java.util.HashSet<>(); |
| | | for (CollectorMeasurementRecord record : rawHistory) |
| | | while (true) |
| | | { |
| | | String identity = record.sequenceId() + "\u0000" + record.channelCode() + "\u0000" + record.metricCode(); |
| | | BtsSimulatorStateStore.CollectorHistoryBatch batch = stateStore.loadCollectorHistoryPage( |
| | | state.deviceCode(), offset, MAX_HISTORY_PAGE_SIZE, false); |
| | | if (batch.offset() != offset || batch.records().size() > MAX_HISTORY_PAGE_SIZE) |
| | | throw new IllegalStateException("Invalid collector history page in Redis"); |
| | | for (CollectorMeasurementRecord record : batch.records()) |
| | | { |
| | | if (record.sequenceId() < previousSequence) |
| | | throw new IllegalStateException("Invalid collector history state in Redis"); |
| | | if (record.sequenceId() != previousSequence) |
| | | { |
| | | identities.clear(); |
| | | previousSequence = record.sequenceId(); |
| | | } |
| | | String identity = record.channelCode() + "\u0000" + record.metricCode(); |
| | | if (!validMeasurement(state, record.adapterCode(), record.collectorCode(), record.channelCode(), |
| | | record.sequenceId(), record.deviceTime(), record.deviceTimeRaw(), record.serverReceivedAt(), |
| | | record.metricCode(), record.value(), record.unit()) || !identities.add(identity) |
| | | || record.sequenceId() < previousSequence) |
| | | record.metricCode(), record.value(), record.unit()) || !identities.add(identity)) |
| | | throw new IllegalStateException("Invalid collector history state in Redis"); |
| | | maximumSequence = Math.max(maximumSequence, record.sequenceId()); |
| | | previousSequence = record.sequenceId(); |
| | | recordCount++; |
| | | } |
| | | if (!batch.hasMore()) |
| | | { |
| | | if (batch.total() != recordCount) |
| | | throw new IllegalStateException("Invalid collector history count in Redis"); |
| | | break; |
| | | } |
| | | if (batch.nextOffset() <= offset) |
| | | throw new IllegalStateException("Invalid collector history page in Redis"); |
| | | offset = batch.nextOffset(); |
| | | } |
| | | if (state.nextSequence() <= maximumSequence) |
| | | throw new IllegalStateException("Invalid collector history sequence in Redis"); |
| | | if (state.historyRecordCount() != rawHistory.size()) |
| | | if (state.historyRecordCount() != recordCount) |
| | | throw new IllegalStateException("Invalid collector history count in Redis"); |
| | | } |
| | | |
| | |
| | | return channelCode + "\u0000" + metricCode; |
| | | } |
| | | |
| | | private static List<CollectorMeasurementRecord> faultedHistory(CollectorState current, |
| | | List<CollectorMeasurementRecord> rawHistory) |
| | | private static CollectorHistoryPage faultedHistoryPage(CollectorState current, |
| | | BtsSimulatorStateStore stateStore, long offset, int limit) |
| | | { |
| | | // 故障视图需返回精确 total,因此逐批扫描原始历史;正常下载会直接读取目标页。 |
| | | CollectorHistoryFaults faults = current.historyFaults(); |
| | | Set<Long> missing = Set.copyOf(faults.missingSequences()); |
| | | List<CollectorMeasurementRecord> records = new ArrayList<>(); |
| | | for (CollectorMeasurementRecord record : rawHistory) |
| | | List<CollectorMeasurementRecord> page = new ArrayList<>(limit); |
| | | long rawOffset = 0; |
| | | long total = 0; |
| | | while (true) |
| | | { |
| | | BtsSimulatorStateStore.CollectorHistoryBatch batch = stateStore.loadCollectorHistoryPage( |
| | | current.deviceCode(), rawOffset, MAX_HISTORY_PAGE_SIZE, faults.outOfOrder()); |
| | | for (CollectorMeasurementRecord record : batch.records()) |
| | | { |
| | | if (missing.contains(record.sequenceId())) continue; |
| | | records.add(record); |
| | | if (Long.valueOf(record.sequenceId()).equals(faults.duplicateSequence())) records.add(record); |
| | | total = appendFaultedRecord(page, record, total, offset, limit); |
| | | if (Long.valueOf(record.sequenceId()).equals(faults.duplicateSequence())) |
| | | total = appendFaultedRecord(page, record, total, offset, limit); |
| | | if (Long.valueOf(record.sequenceId()).equals(faults.conflictingSequence())) |
| | | records.add(new CollectorMeasurementRecord(record.adapterCode(), record.collectorCode(), |
| | | record.channelCode(), record.sequenceId(), record.deviceTime(), record.deviceTimeRaw(), |
| | | record.serverReceivedAt(), record.metricCode(), |
| | | record.value().add(new BigDecimal("0.001")), record.unit())); |
| | | total = appendFaultedRecord(page, conflictingRecord(record), total, offset, limit); |
| | | } |
| | | if (faults.outOfOrder()) |
| | | records.sort(Comparator.comparingLong(CollectorMeasurementRecord::sequenceId).reversed() |
| | | .thenComparing(CollectorMeasurementRecord::channelCode) |
| | | .thenComparing(CollectorMeasurementRecord::metricCode)); |
| | | return records; |
| | | if (!batch.hasMore()) break; |
| | | if (batch.nextOffset() <= rawOffset) |
| | | throw new IllegalStateException("Invalid collector history page in Redis"); |
| | | rawOffset = batch.nextOffset(); |
| | | } |
| | | long from = Math.min(offset, total); |
| | | long nextOffset = from + page.size(); |
| | | return new CollectorHistoryPage(current.deviceCode(), offset, nextOffset, nextOffset < total, total, page); |
| | | } |
| | | |
| | | private static long appendFaultedRecord(List<CollectorMeasurementRecord> page, |
| | | CollectorMeasurementRecord record, long index, long offset, int limit) |
| | | { |
| | | if (index >= offset && page.size() < limit) page.add(record); |
| | | return index + 1; |
| | | } |
| | | |
| | | private static CollectorMeasurementRecord conflictingRecord(CollectorMeasurementRecord record) |
| | | { |
| | | return new CollectorMeasurementRecord(record.adapterCode(), record.collectorCode(), record.channelCode(), |
| | | record.sequenceId(), record.deviceTime(), record.deviceTimeRaw(), record.serverReceivedAt(), |
| | | record.metricCode(), record.value().add(new BigDecimal("0.001")), record.unit()); |
| | | } |
| | | |
| | | private static CollectorState copy(CollectorState current, String ip, int channelCount, boolean online, |