package com.common.service;
import com.common.entity.SyslogNonNormalMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.CannotAcquireLockException;
import org.springframework.dao.DeadlockLoserDataAccessException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.sql.Date;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.Supplier;
/**
* 把当前事实事务的内存聚合结果交给提交后的有界异步统计队列。
*
*
DIRECT 连续消费优先,不写 Outbox 或统计表,统计异常记录后放行。
* 历史 SHADOW 模式仍同步写影子投影,保留原有校验语义。
*/
@Slf4j
@Service
public class DirectStatsProjectionWriter {
private static final String DEVICE_MERGE_TEMPLATE =
"MERGE INTO %s t USING (SELECT ? stat_date,? device_id,? parse_delta,? non_log_delta FROM DUAL) s " +
"ON (t.stat_date=s.stat_date AND t.device_id=s.device_id) " +
"WHEN MATCHED THEN UPDATE SET t.parse_count=t.parse_count+s.parse_delta," +
"t.non_log_count=t.non_log_count+s.non_log_delta,t.updated_at=SYSDATE " +
"WHEN NOT MATCHED THEN INSERT (stat_date,device_id,parse_count,non_log_count," +
"last_receive_time,projection_seq,updated_at) " +
"VALUES (s.stat_date,s.device_id,s.parse_delta,s.non_log_delta,NULL,0,SYSDATE)";
private static final String RULE_MERGE_TEMPLATE =
"MERGE INTO %s t USING (SELECT ? stat_date,? normalize_rule_id,? max_log_time FROM DUAL) s " +
"ON (t.stat_date=s.stat_date AND t.normalize_rule_id=s.normalize_rule_id) " +
"WHEN MATCHED THEN UPDATE SET t.max_log_time=CASE " +
"WHEN s.max_log_time IS NULL THEN t.max_log_time " +
"WHEN t.max_log_time IS NULL OR t.max_log_time> rows) {
return project(rows == null ? 0 : rows.size(), () -> accumulator().addStandardizedRows(rows), false);
}
public WriteResult writeStandardizedRow(Map row) {
return project(row == null ? 0 : 1, () -> accumulator().addStandardizedRow(row), false);
}
public WriteResult writeNonNormalRows(List messages) {
return project(messages == null ? 0 : messages.size(), () -> accumulator().addNonNormalRows(messages), false);
}
public WriteResult writeNonNormalInsert(SyslogNonNormalMessage message) {
return project(message == null ? 0 : 1, () -> accumulator().addNonNormalInsert(message), false);
}
public WriteResult writeNonNormalUpdate(SyslogNonNormalMessage oldValue, SyslogNonNormalMessage patch) {
return project(oldValue == null || patch == null ? 0 : 1,
() -> accumulator().addNonNormalUpdate(oldValue, patch), true);
}
private WriteResult project(int rows, Supplier build, boolean exact) {
try {
if (!shouldBuildAggregation()) {
handleFactOnlyMode();
return WriteResult.inactive(rows);
}
long started = System.nanoTime();
DirectStatsAccumulator aggregate = build.get();
return exact ? writeExactDeviceGroups(aggregate, elapsedMs(started))
: write(aggregate, elapsedMs(started));
} catch (RuntimeException failure) {
if (!asynchronousProjection()) throw failure;
metrics.recordFailure();
asyncPublisher.recordLoss(rows, "AGGREGATION_OR_REGISTRATION_FAILED", failure);
return WriteResult.inactive(rows);
}
}
public void recordTransactionRetry() {
metrics.recordRetry();
}
public Map status() {
Map status = new LinkedHashMap<>(metrics.snapshot(modeProvider.getMode()));
status.put("read_enabled", readEnabled);
status.put("device_table", selectedDeviceTable());
status.put("rule_table", selectedRuleTable());
status.put("today_stats_date_enabled", todayStatsDateEnabled);
status.put("business_materialization_mode", asynchronousMaterialization()
? "KAFKA_DIRTY_KEY" : "SYNCHRONOUS");
status.put("daily_projection_write_mode", asynchronousProjection()
? "BEST_EFFORT_AFTER_COMMIT_KAFKA" : "SYNCHRONOUS_MERGE");
status.put("async_publisher", asyncPublisher.status());
return status;
}
private boolean shouldBuildAggregation() {
return modeProvider.writesDirectProjection();
}
private DirectStatsAccumulator accumulator() {
return new DirectStatsAccumulator(ZoneId.of(businessZone));
}
private void handleFactOnlyMode() {
if (!modeProvider.requiresRebuild()) {
return;
}
requireTransaction();
metrics.markRebuildRequired();
String table = safeTable(controlTable);
jdbc.update("MERGE INTO " + table + " t USING " +
"(SELECT 'SYSLOG_STATS' control_name FROM DUAL) s " +
"ON (t.control_name=s.control_name) " +
"WHEN MATCHED THEN UPDATE SET t.rebuild_required=1,t.updated_at=SYSDATE " +
"WHEN NOT MATCHED THEN INSERT (control_name,rebuild_required,updated_at) " +
"VALUES (s.control_name,1,SYSDATE)");
}
private WriteResult write(DirectStatsAccumulator accumulator, long aggregateMs) {
if (accumulator.isEmpty()) {
metrics.recordSuccess(accumulator.getInputRows(), 0, 0, aggregateMs, 0L, 0L);
return WriteResult.direct(accumulator.getInputRows(), 0, 0,
aggregateMs, 0L, 0L);
}
requireTransaction();
if (asynchronousProjection()) {
long enqueueStarted = System.nanoTime();
asyncPublisher.enqueueAfterCommit(StatsProjectionOutboxEvent.fromAccumulator(accumulator),
accumulator.getInputRows());
long enqueueMs = elapsedMs(enqueueStarted);
int deviceGroups = effectiveDeviceEntries(accumulator).size();
int ruleGroups = effectiveRuleEntries(accumulator).size();
metrics.recordSuccess(accumulator.getInputRows(), deviceGroups, ruleGroups,
aggregateMs, enqueueMs, 0L);
return WriteResult.direct(accumulator.getInputRows(), deviceGroups, ruleGroups,
aggregateMs, enqueueMs, 0L);
}
List> devices =
effectiveDeviceEntries(accumulator);
List> rules =
effectiveRuleEntries(accumulator);
long mergeMs;
long materializeMs = 0L;
try {
long mergeStarted = System.nanoTime();
mergeDevices(devices);
mergeRules(rules);
mergeMs = elapsedMs(mergeStarted);
if (modeProvider.materializesBusinessFields() && readEnabled) {
long materializeStarted = System.nanoTime();
if (asynchronousMaterialization()) {
materializationPublisher.enqueueAfterCommit(materializationCommands(devices, rules));
} else {
materializeDevices(devices);
materializeRules(rules);
}
materializeMs = elapsedMs(materializeStarted);
}
metrics.recordSuccess(accumulator.getInputRows(), devices.size(), rules.size(),
aggregateMs, mergeMs, materializeMs);
log.debug("直接统计事务块完成: factRows={}, deviceGroups={}, ruleGroups={}, " +
"aggregateMs={}, statsMergeMs={}, materializeMs={}",
accumulator.getInputRows(), devices.size(), rules.size(),
aggregateMs, mergeMs, materializeMs);
return WriteResult.direct(accumulator.getInputRows(), devices.size(), rules.size(),
aggregateMs, mergeMs, materializeMs);
} catch (RuntimeException failure) {
metrics.recordFailure();
if (isConcurrentFailure(failure)) {
throw new ConcurrentStatsWriteException("直接统计聚合键并发写入失败,可安全回滚后有限重试", failure);
}
throw new DirectStatsWriteException("直接统计写入失败,事实事务必须整体回滚", failure);
}
}
/**
* UPDATE/物理 DELETE 属于低频管理路径。负 delta 可能移除当前最大值或命中未建立历史基线的日期,
* 因此直接从事实表精确重算受影响的小分组,不能把负数插入一个不存在的聚合键。
*/
private WriteResult writeExactDeviceGroups(DirectStatsAccumulator accumulator, long aggregateMs) {
if (accumulator.getDevices().isEmpty()) {
metrics.recordSuccess(accumulator.getInputRows(), 0, 0, aggregateMs, 0L, 0L);
return WriteResult.direct(accumulator.getInputRows(), 0, 0,
aggregateMs, 0L, 0L);
}
requireTransaction();
// Additive positive/negative changes commute in the serialized projection consumer. This
// keeps non-normal UPDATE statistics outside the fact transaction as well.
if (asynchronousProjection()) {
return write(accumulator, aggregateMs);
}
List keys = new ArrayList<>(accumulator.getDevices().keySet());
try {
long mergeStarted = System.nanoTime();
List