refactor(shared-ts,iam,core-edu,content,msg): remove outbox polling publisher

M8: Debezium CDC now handles outbox table to Kafka (ADR-032)

- Remove OutboxPublisher class from shared-ts

- Remove publisher from iam/core-edu/content/msg lifecycle and modules

- OutboxService retained for transactional outbox table writes

- Debezium monitors binlog and pushes to Kafka automatically
This commit is contained in:
SpecialX
2026-07-15 01:27:45 +08:00
parent a3f4fd013e
commit 47a062606f
16 changed files with 34 additions and 1227 deletions

View File

@@ -3,7 +3,6 @@ import { AppModule } from "./app.module.js";
import { env } from "./config/env.js";
import { connectKafka, disconnectKafka } from "./config/kafka.js";
import { connectRedis, disconnectRedis } from "./config/redis.js";
import { outboxPublisher } from "./shared/outbox/outbox.publisher.js";
import { GlobalErrorFilter } from "./shared/errors/global-error.filter.js";
import { initTracer, shutdownTracer } from "./shared/observability/tracer.js";
import { logger } from "./shared/observability/logger.js";
@@ -24,9 +23,9 @@ async function bootstrap(): Promise<void> {
res.end(await registry.metrics());
});
// Connect Kafka producer/consumer before starting the outbox publisher.
// Non-blocking: if Kafka is unavailable, service still starts; outbox
// publisher will retry sends and messages stay pending until Kafka recovers.
// Connect Kafka producer/consumer.
// Non-blocking: if Kafka is unavailable, service still starts.
// v2.1M8OutboxPublisher 轮询线程已移除outbox 投递由 Debezium CDC 接管。
void connectKafka();
// Connect Redis for distributed lock (homework submission idempotency).
@@ -34,10 +33,6 @@ async function bootstrap(): Promise<void> {
// operations will fall back to DB unique index for idempotency.
void connectRedis();
// Start the transactional outbox publisher - polls pending messages
// and publishes them to Kafka topics defined in TOPIC_MAP.
await outboxPublisher.start();
await app.listen(env.PORT);
// 启动 gRPC server9 Service / 40 RPC
@@ -51,7 +46,6 @@ async function bootstrap(): Promise<void> {
const shutdown = async (signal: string): Promise<void> => {
logger.info({ signal }, "Shutting down gracefully...");
await stopGrpcServer();
await outboxPublisher.stop();
await disconnectRedis();
await disconnectKafka();
await shutdownTracer();

View File

@@ -7,7 +7,6 @@ import {
import { closeDb } from "../../config/database.js";
import { disconnectRedis } from "../../config/redis.js";
import { disconnectKafka } from "../../config/kafka.js";
import { outboxPublisher } from "../outbox/outbox.publisher.js";
const SERVICE_NAME = "core-edu";
@@ -24,7 +23,6 @@ export class LifecycleService implements OnModuleInit, OnApplicationShutdown {
`service ${SERVICE_NAME} shutting down (signal=${signal ?? "unknown"})`,
);
try {
await outboxPublisher.stop();
await disconnectRedis();
await disconnectKafka();
await closeDb();

View File

@@ -1,119 +0,0 @@
import { logger } from "../observability/logger.js";
import { producer } from "../../config/kafka.js";
import { outboxRepository } from "./outbox.repository.js";
import type { OutboxMessage } from "./outbox.schema.js";
/**
* TOPIC_MAP: 事件类型 → Kafka topic
*
* 仲裁依据ISSUE-002 / ISSUE-004 / events.proto 头注释
* 命名规范edu.teaching.<aggregate>.<action>
*/
const TOPIC_MAP: Record<string, string> = {
// Exam events
"exam.created": "edu.teaching.exam.created",
"exam.updated": "edu.teaching.exam.updated",
"exam.published": "edu.teaching.exam.published",
"exam.submitted": "edu.teaching.exam.submitted",
"exam.graded": "edu.teaching.exam.graded",
"exam.deleted": "edu.teaching.exam.deleted",
// Exam realtime events (P3.14: 供 msg → push-gateway → student-portal WebSocket)
"exam.extended": "edu.teaching.exam.extended",
"exam.force_submitted": "edu.teaching.exam.force_submitted",
"exam.question_reordered": "edu.teaching.exam.question_reordered",
// Homework events
"homework.assigned": "edu.teaching.homework.assigned",
"homework.submitted": "edu.teaching.homework.submitted",
"homework.graded": "edu.teaching.homework.graded",
// Grade events
"grade.recorded": "edu.teaching.grade.recorded",
"grade.updated": "edu.teaching.grade.updated",
// Attendance events
"attendance.recorded": "edu.teaching.attendance.recorded",
// Class events (ISSUE-004: 统一为 edu.teaching.class.transferred)
"class.transferred": "edu.teaching.class.transferred",
};
const POLL_INTERVAL_MS = 5000;
const BATCH_SIZE = 100;
const MAX_RETRY = 5;
const RETRY_BACKOFF_BASE_MS = 1000;
export class OutboxPublisher {
private intervalId: NodeJS.Timeout | null = null;
private isPolling = false;
async start(): Promise<void> {
logger.info("OutboxPublisher started");
this.intervalId = setInterval(() => {
void this.poll();
}, POLL_INTERVAL_MS);
}
async stop(): Promise<void> {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
logger.info("OutboxPublisher stopped");
}
private async poll(): Promise<void> {
if (this.isPolling) return;
this.isPolling = true;
try {
const messages = await outboxRepository.findPending(BATCH_SIZE);
for (const message of messages) {
await this.publish(message);
}
} catch (error) {
logger.error({ error }, "Outbox poll failed");
} finally {
this.isPolling = false;
}
}
private async publish(message: OutboxMessage): Promise<void> {
const topic = TOPIC_MAP[message.eventType] ?? "edu.teaching.fallback";
try {
await producer.send({
topic,
messages: [
{
key: message.aggregateId,
value: message.payload,
headers: {
eventType: message.eventType,
aggregateType: message.aggregateType,
eventId: message.eventId,
},
},
],
});
await outboxRepository.markProcessed(message.id);
logger.info(
{
id: message.id,
eventId: message.eventId,
eventType: message.eventType,
topic,
},
"Outbox message published",
);
} catch (error) {
logger.error(
{ error, id: message.id, eventId: message.eventId },
"Outbox publish failed",
);
const nextRetry = message.retryCount + 1;
if (nextRetry >= MAX_RETRY) {
await outboxRepository.markFailed(message.id);
} else {
const backoff = RETRY_BACKOFF_BASE_MS * Math.pow(2, nextRetry);
await outboxRepository.incrementRetry(message.id, backoff);
}
}
}
}
export const outboxPublisher = new OutboxPublisher();