| | |
| | | List<CollectorRealtimeMeasurement> latestMeasurements = retainConfiguredMeasurements( |
| | | current.latestMeasurements(), channels); |
| | | return copy(current, ip, channelCount, online, current.sampleIntervalMs(), current.nextSequence(), |
| | | lastSampledAt, channels, latestMeasurements, current.history(), current.historyFaults()); |
| | | lastSampledAt, channels, latestMeasurements, current.historyRecordCount(), current.historyFaults()); |
| | | } |
| | | |
| | | /** 更新采样频率和历史下载故障;测量原始历史保持只追加。 */ |
| | |
| | | validateFaults(historyFaults); |
| | | return copy(current, current.ip(), current.channelCount(), current.online(), sampleIntervalMs, |
| | | current.nextSequence(), current.lastSampledAt(), current.channels(), current.latestMeasurements(), |
| | | current.history(), historyFaults); |
| | | current.historyRecordCount(), historyFaults); |
| | | } |
| | | |
| | | /** 替换一个演示通道的指标配置;动态添加项与固定指标共用指标编码合同。 */ |
| | |
| | | current.latestMeasurements(), channels); |
| | | return copy(current, current.ip(), current.channelCount(), current.online(), current.sampleIntervalMs(), |
| | | current.nextSequence(), current.lastSampledAt(), channels, latestMeasurements, |
| | | current.history(), current.historyFaults()); |
| | | current.historyRecordCount(), current.historyFaults()); |
| | | } |
| | | |
| | | /** 按采集器自身频率推进采样;离线期间不生成记录,恢复后从下一序号继续。 */ |
| | | public static CollectorState advance(CollectorState current, Instant now) |
| | | public static CollectorAdvance advance(CollectorState current, Instant now) |
| | | { |
| | | if (!current.online()) return current; |
| | | if (!current.online()) return new CollectorAdvance(current, List.of()); |
| | | Instant sampleAt = current.lastSampledAt() == null |
| | | ? now |
| | | : current.lastSampledAt().plusMillis(current.sampleIntervalMs()); |
| | | if (sampleAt.isAfter(now)) return current; |
| | | if (sampleAt.isAfter(now)) return new CollectorAdvance(current, List.of()); |
| | | |
| | | List<CollectorMeasurementRecord> history = new ArrayList<>(current.history()); |
| | | List<CollectorMeasurementRecord> appendedRecords = new ArrayList<>(); |
| | | Map<String, CollectorRealtimeMeasurement> latest = new LinkedHashMap<>(); |
| | | for (CollectorRealtimeMeasurement measurement : current.latestMeasurements()) |
| | | latest.put(measurementKey(measurement.channelCode(), measurement.metricCode()), measurement); |
| | |
| | | channel.channelCode(), sequence, sampleAt, |
| | | DEVICE_TIME_FORMATTER.format(sampleAt.atZone(DEVICE_ZONE)), now, |
| | | metric.metricCode(), metric.value(), metric.unit()); |
| | | history.add(record); |
| | | appendedRecords.add(record); |
| | | latest.put(measurementKey(record.channelCode(), record.metricCode()), |
| | | new CollectorRealtimeMeasurement(record.adapterCode(), record.collectorCode(), |
| | | record.channelCode(), record.sequenceId(), record.deviceTime(), |
| | |
| | | generated++; |
| | | sampleAt = sampleAt.plusMillis(current.sampleIntervalMs()); |
| | | } |
| | | return copy(current, current.ip(), current.channelCount(), current.online(), current.sampleIntervalMs(), |
| | | sequence, lastSampledAt, current.channels(), List.copyOf(latest.values()), history, |
| | | CollectorState next = copy(current, current.ip(), current.channelCount(), current.online(), |
| | | current.sampleIntervalMs(), |
| | | sequence, lastSampledAt, current.channels(), List.copyOf(latest.values()), |
| | | current.historyRecordCount() + appendedRecords.size(), |
| | | current.historyFaults()); |
| | | return new CollectorAdvance(next, appendedRecords); |
| | | } |
| | | |
| | | /** 返回独立保存的最新实时快照,不从原始历史反推当前状态。 */ |
| | |
| | | } |
| | | |
| | | /** 对只追加原始模拟历史应用可重复的下载故障视图,再按偏移量分页。 */ |
| | | public static CollectorHistoryPage history(CollectorState current, long offset, int limit) |
| | | public static CollectorHistoryPage history(CollectorState current, List<CollectorMeasurementRecord> rawHistory, |
| | | 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); |
| | | 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(), |
| | |
| | | } |
| | | |
| | | /** 校验 Redis 恢复的采集器采样状态,拒绝损坏状态被静默替换。 */ |
| | | public static void validate(CollectorState state) |
| | | public static void validate(CollectorState state, List<CollectorMeasurementRecord> rawHistory) |
| | | { |
| | | validateSampleInterval(state.sampleIntervalMs()); |
| | | if (state.nextSequence() < 1 || state.channels().size() != state.channelCount()) |
| | |
| | | validateMetrics(channel.metrics()); |
| | | } |
| | | validateLatestMeasurements(state); |
| | | validateHistory(state); |
| | | validateHistory(state, rawHistory); |
| | | validateFaults(state.historyFaults()); |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | private static void validateHistory(CollectorState state) |
| | | private static void validateHistory(CollectorState state, List<CollectorMeasurementRecord> rawHistory) |
| | | { |
| | | long maximumSequence = 0; |
| | | long previousSequence = 0; |
| | | Set<String> identities = new java.util.HashSet<>(); |
| | | for (CollectorMeasurementRecord record : state.history()) |
| | | for (CollectorMeasurementRecord record : rawHistory) |
| | | { |
| | | String identity = record.sequenceId() + "\u0000" + record.channelCode() + "\u0000" + record.metricCode(); |
| | | if (!validMeasurement(state, record.adapterCode(), record.collectorCode(), record.channelCode(), |
| | |
| | | } |
| | | if (state.nextSequence() <= maximumSequence) |
| | | throw new IllegalStateException("Invalid collector history sequence in Redis"); |
| | | if (state.historyRecordCount() != rawHistory.size()) |
| | | throw new IllegalStateException("Invalid collector history count in Redis"); |
| | | } |
| | | |
| | | private static boolean validMeasurement(CollectorState state, String adapterCode, String collectorCode, |
| | |
| | | return channelCode + "\u0000" + metricCode; |
| | | } |
| | | |
| | | private static List<CollectorMeasurementRecord> faultedHistory(CollectorState current) |
| | | private static List<CollectorMeasurementRecord> faultedHistory(CollectorState current, |
| | | List<CollectorMeasurementRecord> rawHistory) |
| | | { |
| | | CollectorHistoryFaults faults = current.historyFaults(); |
| | | Set<Long> missing = Set.copyOf(faults.missingSequences()); |
| | | List<CollectorMeasurementRecord> records = new ArrayList<>(); |
| | | for (CollectorMeasurementRecord record : current.history()) |
| | | for (CollectorMeasurementRecord record : rawHistory) |
| | | { |
| | | if (missing.contains(record.sequenceId())) continue; |
| | | records.add(record); |
| | |
| | | private static CollectorState copy(CollectorState current, String ip, int channelCount, boolean online, |
| | | int sampleIntervalMs, long nextSequence, Instant lastSampledAt, |
| | | List<CollectorChannelState> channels, List<CollectorRealtimeMeasurement> latestMeasurements, |
| | | List<CollectorMeasurementRecord> history, |
| | | long historyRecordCount, |
| | | CollectorHistoryFaults faults) |
| | | { |
| | | return new CollectorState(current.collectorId(), current.deviceCode(), ip.trim(), current.mac(), |
| | | current.name(), current.manufacturer(), current.model(), channelCount, online, sampleIntervalMs, |
| | | nextSequence, lastSampledAt, channels, latestMeasurements, history, faults); |
| | | nextSequence, lastSampledAt, channels, latestMeasurements, historyRecordCount, faults); |
| | | } |
| | | |
| | | private static void validateSampleInterval(int sampleIntervalMs) |
| | |
| | | for (CollectorMetricState metric : metrics) |
| | | { |
| | | if (metric.metricCode() == null || metric.metricCode().isBlank() || metric.value() == null |
| | | || metric.unit() == null || metric.unit().isBlank() || !codes.add(metric.metricCode().trim())) |
| | | || !metric.metricCode().equals(metric.metricCode().trim()) |
| | | || metric.unit() == null || metric.unit().isBlank() |
| | | || !metric.unit().equals(metric.unit().trim()) || !codes.add(metric.metricCode())) |
| | | throw new IllegalArgumentException("Collector metric code, value and unit must be valid and unique"); |
| | | } |
| | | } |
| | |
| | | public CollectorRealtimeSnapshot { measurements = List.copyOf(measurements); } |
| | | } |
| | | |
| | | public record CollectorAdvance(CollectorState state, List<CollectorMeasurementRecord> appendedRecords) |
| | | { |
| | | public CollectorAdvance { appendedRecords = List.copyOf(appendedRecords); } |
| | | } |
| | | |
| | | public record CollectorHistoryPage(String collectorCode, long offset, long nextOffset, boolean hasMore, |
| | | long total, List<CollectorMeasurementRecord> records) |
| | | { |