chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
|
|
@ -3,13 +3,13 @@ package postgres
|
|||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
// TestMaxContiguousPtsStopsAtHole 用真实 PG 验证 RecentUserPts 窗口查询 + 连续计算:
|
||||
// 存在在途空洞时只报告最大连续 pts,补洞后回升。
|
||||
func TestMaxContiguousPtsStopsAtHole(t *testing.T) {
|
||||
// TestAppendRejectsPtsHole 用真实 PG 验证显式 pts 写入不能制造空洞。
|
||||
func TestAppendRejectsPtsHole(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
|
||||
|
|
@ -39,27 +39,34 @@ func TestMaxContiguousPtsStopsAtHole(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
for _, p := range []int{1, 2, 3, 5, 6} { // pts=4 为在途空洞
|
||||
for _, p := range []int{1, 2, 3} {
|
||||
appendPts(p)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, "DELETE FROM user_update_watermarks WHERE user_id = $1", owner.ID); err != nil {
|
||||
t.Fatalf("delete watermark fallback row: %v", err)
|
||||
err = events.Append(ctx, owner.ID, domain.UpdateEvent{
|
||||
Type: domain.UpdateEventNoop,
|
||||
Pts: 5,
|
||||
PtsCount: 1,
|
||||
Date: 1700000005,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("append pts=5 succeeded, want gap rejection")
|
||||
}
|
||||
got, err := events.MaxContiguousPts(ctx, owner.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("MaxContiguousPts: %v", err)
|
||||
}
|
||||
if got != 3 {
|
||||
t.Fatalf("contiguous = %d, want 3(止于 pts=4 空洞,最大已提交为 6)", got)
|
||||
t.Fatalf("contiguous = %d, want 3 after rejected gap", got)
|
||||
}
|
||||
|
||||
appendPts(4) // 在途事务提交/补洞
|
||||
appendPts(4)
|
||||
appendPts(5)
|
||||
got, err = events.MaxContiguousPts(ctx, owner.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("MaxContiguousPts after fill: %v", err)
|
||||
}
|
||||
if got != 6 {
|
||||
t.Fatalf("contiguous after fill = %d, want 6", got)
|
||||
if got != 5 {
|
||||
t.Fatalf("contiguous after ordered append = %d, want 5", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -96,8 +103,8 @@ func TestAppendWithDispatchWritesEventAndOutboxAtomically(t *testing.T) {
|
|||
}
|
||||
var excludeAuthKeyID [8]byte
|
||||
excludeAuthKeyID[0] = 9
|
||||
if err := NewUpdateEventStore(pool).AppendWithDispatch(ctx, owner.ID, event, excludeAuthKeyID, 77); err != nil {
|
||||
t.Fatalf("AppendWithDispatch: %v", err)
|
||||
if _, err := NewUpdateEventStore(pool).AppendAllocatedWithDispatch(ctx, owner.ID, event, excludeAuthKeyID, 77); err != nil {
|
||||
t.Fatalf("AppendAllocatedWithDispatch: %v", err)
|
||||
}
|
||||
|
||||
got, err := NewUpdateEventStore(pool).ListAfter(ctx, owner.ID, 0, 10)
|
||||
|
|
@ -125,3 +132,182 @@ func TestAppendWithDispatchWritesEventAndOutboxAtomically(t *testing.T) {
|
|||
t.Fatalf("outbox = %+v, want event dispatch excluding current session", outbox)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatchOutboxLifecycleKeepsDurableEvents(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
|
||||
suffix := randomSuffix(t)
|
||||
owner, err := NewUserStore(pool).Create(ctx, domain.User{
|
||||
AccessHash: 3,
|
||||
Phone: "+1558" + suffix + "01",
|
||||
FirstName: "Outbox",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create user: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = $1", owner.ID)
|
||||
})
|
||||
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("begin tx: %v", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
events := NewUpdateEventStore(tx)
|
||||
outbox := NewDispatchOutboxStore(tx, WithLeaseTimeout(time.Second))
|
||||
appendEvent := func(pts int, sessionID int64) {
|
||||
t.Helper()
|
||||
event := domain.UpdateEvent{
|
||||
UserID: owner.ID,
|
||||
Type: domain.UpdateEventDialogPinned,
|
||||
Pts: pts,
|
||||
PtsCount: 1,
|
||||
Date: 1700000100 + pts,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID + int64(pts)},
|
||||
Bool: pts%2 == 0,
|
||||
}
|
||||
if _, err := events.AppendAllocatedWithDispatch(ctx, owner.ID, event, [8]byte{}, sessionID); err != nil {
|
||||
t.Fatalf("AppendAllocatedWithDispatch pts=%d: %v", pts, err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE dispatch_outbox
|
||||
SET next_attempt_at = now() - interval '10 years',
|
||||
updated_at = now() - interval '10 years'
|
||||
WHERE target_user_id = $1
|
||||
AND pts = $2
|
||||
`, owner.ID, pts); err != nil {
|
||||
t.Fatalf("make outbox ready pts=%d: %v", pts, err)
|
||||
}
|
||||
}
|
||||
outboxRows := func(pts int) int {
|
||||
t.Helper()
|
||||
var count int
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT count(*)::int
|
||||
FROM dispatch_outbox
|
||||
WHERE target_user_id = $1
|
||||
AND pts = $2
|
||||
`, owner.ID, pts).Scan(&count); err != nil {
|
||||
t.Fatalf("count outbox pts=%d: %v", pts, err)
|
||||
}
|
||||
return count
|
||||
}
|
||||
eventRows := func(pts int) int {
|
||||
t.Helper()
|
||||
var count int
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT count(*)::int
|
||||
FROM user_update_events
|
||||
WHERE user_id = $1
|
||||
AND pts = $2
|
||||
`, owner.ID, pts).Scan(&count); err != nil {
|
||||
t.Fatalf("count event pts=%d: %v", pts, err)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
appendEvent(1, 101)
|
||||
claimed, err := outbox.ClaimPending(ctx, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("ClaimPending first event: %v", err)
|
||||
}
|
||||
if len(claimed) != 1 || claimed[0].TargetUserID != owner.ID || claimed[0].Pts != 1 || claimed[0].Attempts != 1 || claimed[0].ExcludeSessionID != 101 {
|
||||
t.Fatalf("claimed first = %+v, want owner pts=1 attempts=1", claimed)
|
||||
}
|
||||
if err := outbox.MarkDelivered(ctx, owner.ID, claimed[0].ID); err != nil {
|
||||
t.Fatalf("MarkDelivered: %v", err)
|
||||
}
|
||||
if got := outboxRows(1); got != 0 {
|
||||
t.Fatalf("outbox rows for delivered pts=1 = %d, want 0", got)
|
||||
}
|
||||
if got := eventRows(1); got != 1 {
|
||||
t.Fatalf("durable event rows for delivered pts=1 = %d, want 1", got)
|
||||
}
|
||||
|
||||
appendEvent(2, 102)
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE dispatch_outbox
|
||||
SET status = 'dispatching',
|
||||
attempts = 1,
|
||||
next_attempt_at = now() - interval '10 years',
|
||||
updated_at = now() - interval '10 years'
|
||||
WHERE target_user_id = $1
|
||||
AND pts = 2
|
||||
`, owner.ID); err != nil {
|
||||
t.Fatalf("make stale dispatching: %v", err)
|
||||
}
|
||||
claimed, err = outbox.ClaimPending(ctx, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("ClaimPending stale event: %v", err)
|
||||
}
|
||||
if len(claimed) != 1 || claimed[0].TargetUserID != owner.ID || claimed[0].Pts != 2 || claimed[0].Attempts != 2 {
|
||||
t.Fatalf("claimed stale = %+v, want owner pts=2 attempts=2", claimed)
|
||||
}
|
||||
if err := outbox.MarkFailed(ctx, owner.ID, claimed[0].ID, "temporary"); err != nil {
|
||||
t.Fatalf("MarkFailed temporary: %v", err)
|
||||
}
|
||||
var status string
|
||||
var attempts int
|
||||
var lastError string
|
||||
var retryFuture bool
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT status, attempts, last_error, next_attempt_at > now()
|
||||
FROM dispatch_outbox
|
||||
WHERE target_user_id = $1
|
||||
AND id = $2
|
||||
`, owner.ID, claimed[0].ID).Scan(&status, &attempts, &lastError, &retryFuture); err != nil {
|
||||
t.Fatalf("query temporary failure: %v", err)
|
||||
}
|
||||
if status != "pending" || attempts != 2 || lastError != "temporary" || !retryFuture {
|
||||
t.Fatalf("temporary failure status=%s attempts=%d err=%q retryFuture=%v, want pending attempts=2 future retry", status, attempts, lastError, retryFuture)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE dispatch_outbox
|
||||
SET status = 'dispatching',
|
||||
attempts = 5,
|
||||
updated_at = now() - interval '10 years'
|
||||
WHERE target_user_id = $1
|
||||
AND id = $2
|
||||
`, owner.ID, claimed[0].ID); err != nil {
|
||||
t.Fatalf("prepare terminal failure: %v", err)
|
||||
}
|
||||
if err := outbox.MarkFailed(ctx, owner.ID, claimed[0].ID, "permanent"); err != nil {
|
||||
t.Fatalf("MarkFailed permanent: %v", err)
|
||||
}
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT status, attempts, last_error
|
||||
FROM dispatch_outbox
|
||||
WHERE target_user_id = $1
|
||||
AND id = $2
|
||||
`, owner.ID, claimed[0].ID).Scan(&status, &attempts, &lastError); err != nil {
|
||||
t.Fatalf("query terminal failure: %v", err)
|
||||
}
|
||||
if status != "failed" || attempts != 5 || lastError != "permanent" {
|
||||
t.Fatalf("terminal failure status=%s attempts=%d err=%q, want failed attempts=5", status, attempts, lastError)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE dispatch_outbox
|
||||
SET updated_at = now() - interval '10 years'
|
||||
WHERE target_user_id = $1
|
||||
AND id = $2
|
||||
`, owner.ID, claimed[0].ID); err != nil {
|
||||
t.Fatalf("age failed row: %v", err)
|
||||
}
|
||||
deleted, err := outbox.DeleteFailed(ctx, 24*time.Hour, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteFailed: %v", err)
|
||||
}
|
||||
if deleted != 1 {
|
||||
t.Fatalf("DeleteFailed deleted=%d, want 1", deleted)
|
||||
}
|
||||
if got := outboxRows(2); got != 0 {
|
||||
t.Fatalf("outbox rows for failed pts=2 = %d, want 0", got)
|
||||
}
|
||||
if got := eventRows(2); got != 1 {
|
||||
t.Fatalf("durable event rows for failed pts=2 = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue