71 lines
3.0 KiB
Java
71 lines
3.0 KiB
Java
|
|
package com.common.service;
|
||
|
|
|
||
|
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||
|
|
import org.springframework.beans.factory.annotation.Value;
|
||
|
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||
|
|
import org.springframework.stereotype.Component;
|
||
|
|
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||
|
|
|
||
|
|
import java.util.LinkedHashMap;
|
||
|
|
import java.util.Map;
|
||
|
|
import java.util.concurrent.atomic.AtomicLong;
|
||
|
|
|
||
|
|
/** Writes one aggregated outbox row in the same transaction as the corresponding fact batch. */
|
||
|
|
@Component
|
||
|
|
public class StatsProjectionOutboxWriter {
|
||
|
|
private final JdbcTemplate jdbc;
|
||
|
|
private final ObjectMapper objectMapper;
|
||
|
|
private final AtomicLong events = new AtomicLong();
|
||
|
|
private final AtomicLong failures = new AtomicLong();
|
||
|
|
|
||
|
|
@Value("${stats.projection-async.enabled:true}") private boolean enabled;
|
||
|
|
@Value("${stats.projection-async.outbox-table:stats_projection_outbox}") private String outboxTable;
|
||
|
|
|
||
|
|
public StatsProjectionOutboxWriter(JdbcTemplate jdbc, ObjectMapper objectMapper) {
|
||
|
|
this.jdbc = jdbc;
|
||
|
|
this.objectMapper = objectMapper.copy();
|
||
|
|
this.objectMapper.deactivateDefaultTyping();
|
||
|
|
}
|
||
|
|
|
||
|
|
public boolean isEnabled() { return enabled; }
|
||
|
|
|
||
|
|
public int append(StatsProjectionOutboxEvent event) {
|
||
|
|
if (!enabled || event == null || event.isEmpty()) return 0;
|
||
|
|
if (!TransactionSynchronizationManager.isActualTransactionActive()) {
|
||
|
|
throw new IllegalStateException("Stats projection outbox must be written inside a transaction");
|
||
|
|
}
|
||
|
|
event.validate();
|
||
|
|
try {
|
||
|
|
int inserted = jdbc.update("INSERT INTO " + safeTable(outboxTable) +
|
||
|
|
" (event_id,event_schema,payload,status,attempt_count,next_attempt_at," +
|
||
|
|
"created_at,updated_at) VALUES (?,?,?,'PENDING',0,SYSDATE,SYSDATE,SYSDATE)",
|
||
|
|
event.getEventId(), event.getSchema(), objectMapper.writeValueAsString(event));
|
||
|
|
events.incrementAndGet();
|
||
|
|
return inserted;
|
||
|
|
} catch (JsonProcessingException serializationFailure) {
|
||
|
|
failures.incrementAndGet();
|
||
|
|
throw new IllegalStateException("Failed to serialize stats projection outbox event", serializationFailure);
|
||
|
|
} catch (RuntimeException databaseFailure) {
|
||
|
|
failures.incrementAndGet();
|
||
|
|
throw databaseFailure;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public Map<String, Object> status() {
|
||
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
||
|
|
result.put("enabled", enabled);
|
||
|
|
result.put("outbox_table", outboxTable);
|
||
|
|
result.put("events_written_total", events.get());
|
||
|
|
result.put("write_failures_total", failures.get());
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
static String safeTable(String table) {
|
||
|
|
if (table == null || !table.matches("[A-Za-z][A-Za-z0-9_]*")) {
|
||
|
|
throw new IllegalStateException("Invalid stats projection table: " + table);
|
||
|
|
}
|
||
|
|
return table;
|
||
|
|
}
|
||
|
|
}
|