Refactor Kafka publishing without DB outbox

This commit is contained in:
richjohnrambo
2026-09-01 23:00:14 +08:00
parent 18237cf3ec
commit 929f847003
25 changed files with 416 additions and 478 deletions
-4
View File
@@ -263,10 +263,6 @@ type TenantHTTPInfrastructureAdapter interface {
TenantKafkaDeadLetterGet(t.TenantID, string) (*t.KafkaDeadLetter, error)
TenantKafkaDeadLetterMark(t.TenantID, string, string, string, string, time.Time) error
TenantKafkaDeadLetterMarkReplay(t.TenantID, string, string, string, time.Time) (*t.KafkaDeadLetter, error)
TenantKafkaOutboxCreate(t.TenantID, *t.KafkaOutboxRecord) error
TenantKafkaOutboxListPending(t.TenantID, int, time.Time) ([]t.KafkaOutboxRecord, error)
TenantKafkaOutboxMarkSent(t.TenantID, string, time.Time) error
TenantKafkaOutboxMarkFailed(t.TenantID, string, string, string, time.Time, time.Time) error
}
// TenantRegistrationInviteAdapter stores invitation definitions in the
-14
View File
@@ -44,20 +44,6 @@ func ensureIMHTTPInfrastructureTables(exec sqlExecutor) error {
INDEX idx_im_kafka_consume_log_biz(biz_type, biz_id),
INDEX idx_im_kafka_consume_log_topic(source_topic, id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
`CREATE TABLE IF NOT EXISTS im_kafka_outbox (
id BIGINT NOT NULL AUTO_INCREMENT, event_id VARCHAR(96) NOT NULL,
topic VARCHAR(128) NOT NULL, event_type VARCHAR(96) NOT NULL,
biz_type VARCHAR(64) NOT NULL DEFAULT '', biz_id VARCHAR(128) NOT NULL DEFAULT '',
key_value VARCHAR(256) NOT NULL DEFAULT '', envelope JSON NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'pending', attempts INT NOT NULL DEFAULT 0,
next_retry_at DATETIME(3), sent_at DATETIME(3), last_reason VARCHAR(64) NOT NULL DEFAULT '',
last_error VARCHAR(1000) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id), UNIQUE INDEX uk_im_kafka_outbox_event(event_id),
INDEX idx_im_kafka_outbox_pending(status, next_retry_at, id),
INDEX idx_im_kafka_outbox_biz(biz_type, biz_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
`CREATE TABLE IF NOT EXISTS im_kafka_dead_letters (
id BIGINT NOT NULL AUTO_INCREMENT, dead_id VARCHAR(96) NOT NULL,
event_id VARCHAR(96) NOT NULL, parent_event_id VARCHAR(96) NOT NULL DEFAULT '',
@@ -12,15 +12,14 @@ func TestEnsureIMHTTPInfrastructureTablesUsesSelectedTenantDatabase(t *testing.T
if err := ensureIMHTTPInfrastructureTables(exec); err != nil {
t.Fatal(err)
}
if len(exec.statements) != 8 {
t.Fatalf("created %d infrastructure tables, want 8", len(exec.statements))
if len(exec.statements) != 7 {
t.Fatalf("created %d infrastructure tables, want 7", len(exec.statements))
}
joined := strings.ToLower(strings.Join(exec.statements, "\n"))
for _, table := range []string{
"im_http_idempotency",
"im_business_events_outbox",
"im_kafka_consume_log",
"im_kafka_outbox",
"im_kafka_dead_letters",
"im_realname_appeals",
"im_conversation_settings",
@@ -33,4 +32,7 @@ func TestEnsureIMHTTPInfrastructureTablesUsesSelectedTenantDatabase(t *testing.T
if strings.Contains(joined, "tinode.") || strings.Contains(joined, "create database") || strings.Contains(joined, "tenant_id") {
t.Fatalf("infrastructure schema must stay inside the selected tenant database: %s", joined)
}
if strings.Contains(joined, "im_kafka_outbox") {
t.Fatalf("kafka outbox table must not be created: %s", joined)
}
}
-134
View File
@@ -512,140 +512,6 @@ func (a *adapter) TenantKafkaDeadLetterMarkReplay(id t.TenantID, deadID, resolve
return a.TenantKafkaDeadLetterGet(id, deadID)
}
func (a *adapter) TenantKafkaOutboxCreate(id t.TenantID, record *t.KafkaOutboxRecord) error {
tenantAdp, err := a.infrastructureAdapter(id)
if err != nil {
return err
}
if record == nil || record.EventID == "" || record.Topic == "" || record.EventType == "" || len(record.Envelope) == 0 {
return t.ErrMalformed
}
if tenantAdp != a {
return tenantAdp.TenantKafkaOutboxCreate(id, record)
}
now := t.TimeNow()
_, err = a.db.Exec(`INSERT INTO im_kafka_outbox
(event_id,topic,event_type,biz_type,biz_id,key_value,envelope,status,next_retry_at,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE updated_at=updated_at`,
record.EventID, record.Topic, record.EventType, record.BizType, record.BizID, record.Key,
record.Envelope, t.KafkaOutboxPending, record.NextRetryAt, now, now)
return err
}
func (a *adapter) TenantKafkaOutboxListPending(id t.TenantID, limit int, now time.Time) ([]t.KafkaOutboxRecord, error) {
tenantAdp, err := a.infrastructureAdapter(id)
if err != nil {
return nil, err
}
if limit <= 0 {
limit = 100
}
if tenantAdp != a {
return tenantAdp.TenantKafkaOutboxListPending(id, limit, now)
}
rows := []struct {
ID int64 `db:"id"`
EventID string `db:"event_id"`
Topic string `db:"topic"`
EventType string `db:"event_type"`
BizType string `db:"biz_type"`
BizID string `db:"biz_id"`
Key string `db:"key_value"`
Envelope []byte `db:"envelope"`
Status string `db:"status"`
Attempts int `db:"attempts"`
NextRetryAt sql.NullTime `db:"next_retry_at"`
SentAt sql.NullTime `db:"sent_at"`
LastReason string `db:"last_reason"`
LastError string `db:"last_error"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}{}
err = a.db.Select(&rows, `SELECT id,event_id,topic,event_type,biz_type,biz_id,key_value,envelope,status,attempts,
next_retry_at,sent_at,last_reason,last_error,created_at,updated_at
FROM im_kafka_outbox
WHERE status IN (?,?) AND (next_retry_at IS NULL OR next_retry_at<=?)
ORDER BY id ASC LIMIT ?`, t.KafkaOutboxPending, t.KafkaOutboxFailed, now, limit)
if err != nil {
return nil, err
}
out := make([]t.KafkaOutboxRecord, 0, len(rows))
for _, row := range rows {
rec := t.KafkaOutboxRecord{
ID: row.ID,
EventID: row.EventID,
Topic: row.Topic,
EventType: row.EventType,
BizType: row.BizType,
BizID: row.BizID,
Key: row.Key,
Envelope: row.Envelope,
Status: row.Status,
Attempts: row.Attempts,
LastReason: row.LastReason,
LastError: row.LastError,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
if row.NextRetryAt.Valid {
rec.NextRetryAt = &row.NextRetryAt.Time
}
if row.SentAt.Valid {
rec.SentAt = &row.SentAt.Time
}
out = append(out, rec)
}
return out, nil
}
func (a *adapter) TenantKafkaOutboxMarkSent(id t.TenantID, eventID string, now time.Time) error {
tenantAdp, err := a.infrastructureAdapter(id)
if err != nil {
return err
}
if eventID == "" {
return t.ErrMalformed
}
if tenantAdp != a {
return tenantAdp.TenantKafkaOutboxMarkSent(id, eventID, now)
}
res, err := a.db.Exec(`UPDATE im_kafka_outbox SET status=?,sent_at=?,attempts=attempts+1,last_reason='',
last_error='',updated_at=? WHERE event_id=? AND status<>?`,
t.KafkaOutboxSent, now, now, eventID, t.KafkaOutboxSent)
if err != nil {
return err
}
if affected, _ := res.RowsAffected(); affected == 0 {
return t.ErrNotFound
}
return nil
}
func (a *adapter) TenantKafkaOutboxMarkFailed(id t.TenantID, eventID, reason, message string, nextRetryAt, now time.Time) error {
tenantAdp, err := a.infrastructureAdapter(id)
if err != nil {
return err
}
if eventID == "" {
return t.ErrMalformed
}
if tenantAdp != a {
return tenantAdp.TenantKafkaOutboxMarkFailed(id, eventID, reason, message, nextRetryAt, now)
}
res, err := a.db.Exec(`UPDATE im_kafka_outbox SET status=?,attempts=attempts+1,next_retry_at=?,
last_reason=?,last_error=?,updated_at=? WHERE event_id=? AND status<>?`,
t.KafkaOutboxFailed, nextRetryAt, truncateKafkaString(reason, 64), truncateKafkaString(message, 1000),
now, eventID, t.KafkaOutboxSent)
if err != nil {
return err
}
if affected, _ := res.RowsAffected(); affected == 0 {
return t.ErrNotFound
}
return nil
}
func scanKafkaDeadLetter(rows *sqlx.Rows) (*t.KafkaDeadLetter, error) {
var row struct {
ID int64 `db:"id"`