owpengram-server/internal/store/postgres/channel_update_retention_integration_test.go

232 lines
8.7 KiB
Go

package postgres
import (
"context"
"fmt"
"strings"
"testing"
"time"
"telesrv/internal/domain"
)
func TestChannelUpdateRetentionFloorDifferenceAndDirtyCheckpointPostgres(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
users := NewUserStore(pool)
owner, err := users.Create(ctx, domain.User{
AccessHash: 71,
Phone: "+1766" + suffix + "01",
FirstName: "RetentionOwner",
})
if err != nil {
t.Fatalf("create owner: %v", err)
}
var channelID int64
t.Cleanup(func() {
if channelID != 0 {
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", channelID)
}
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = $1", owner.ID)
})
channels := NewChannelStore(pool)
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: owner.ID,
Title: "Retention PG " + suffix,
Megagroup: true,
Date: 1_700_020_000,
})
if err != nil {
t.Fatalf("create channel: %v", err)
}
channelID = created.Channel.ID
sent := make([]domain.SendChannelMessageResult, 0, 3)
for i := 1; i <= 3; i++ {
result, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
UserID: owner.ID, ChannelID: channelID, RandomID: int64(7000 + i), Message: "retention", Date: 1_700_020_000 + i,
})
if err != nil {
t.Fatalf("send message %d: %v", i, err)
}
sent = append(sent, result)
}
pruned, err := channels.PruneChannelUpdateEvents(ctx, channelID, sent[1].Event.Pts, 100)
if err != nil {
t.Fatalf("prune channel updates: %v", err)
}
if pruned.Deleted != 3 || pruned.Checkpoint.RetainedThroughPts != sent[1].Event.Pts {
t.Fatalf("prune result = %+v, want deleted=3 floor=%d", pruned, sent[1].Event.Pts)
}
var floor, latestDate, latestPts, remaining int
if err := pool.QueryRow(ctx, `
SELECT cp.retained_through_pts, cp.latest_event_date, cp.latest_pts,
(SELECT COUNT(*) FROM channel_update_events e WHERE e.channel_id = cp.channel_id)::int
FROM channel_update_checkpoints cp
WHERE cp.channel_id = $1`, channelID).Scan(&floor, &latestDate, &latestPts, &remaining); err != nil {
t.Fatalf("read retention checkpoint: %v", err)
}
if floor != sent[1].Event.Pts || latestDate != sent[2].Event.Date || latestPts != sent[2].Event.Pts || remaining != 1 {
t.Fatalf("checkpoint/db = floor:%d latest:%d/%d remaining:%d", floor, latestDate, latestPts, remaining)
}
below, err := channels.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{
UserID: owner.ID, ChannelID: channelID, Pts: floor - 1, Limit: 100,
})
if err != nil {
t.Fatalf("difference below retained floor: %v", err)
}
if !below.TooLong || below.Pts != sent[2].Event.Pts {
t.Fatalf("difference below floor = %+v, want too-long snapshot at pts %d", below, sent[2].Event.Pts)
}
atFloor, err := channels.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{
UserID: owner.ID, ChannelID: channelID, Pts: floor, Limit: 100,
})
if err != nil {
t.Fatalf("difference at retained floor: %v", err)
}
if atFloor.TooLong || len(atFloor.Events) != 1 || atFloor.Events[0].Pts != sent[2].Event.Pts {
t.Fatalf("difference at floor = %+v, want normal incremental event pts %d", atFloor, sent[2].Event.Pts)
}
allPruned, err := channels.PruneChannelUpdateEvents(ctx, channelID, sent[2].Event.Pts, 100)
if err != nil {
t.Fatalf("prune remaining channel update: %v", err)
}
if allPruned.Deleted != 1 {
t.Fatalf("remaining prune = %+v, want deleted=1", allPruned)
}
dirty, err := channels.ListDirtyActiveChannelsForUser(ctx, owner.ID, sent[2].Event.Date-1, 0, 10)
if err != nil {
t.Fatalf("list dirty channels after prune: %v", err)
}
if len(dirty) != 1 || dirty[0].ChannelID != channelID || dirty[0].Pts != sent[2].Event.Pts {
t.Fatalf("dirty channels after prune = %+v, want channel %d pts %d", dirty, channelID, sent[2].Event.Pts)
}
}
func TestDeleteExpiredChannelUpdateEventsContinuesPastCandidatePagePostgres(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
users := NewUserStore(pool)
owner, err := users.Create(ctx, domain.User{
AccessHash: time.Now().UnixNano(),
Phone: "+1767" + suffix + "01",
FirstName: "RetentionPageOwner",
})
if err != nil {
t.Fatalf("create retention page owner: %v", err)
}
channelIDs := make([]int64, 0, 320)
t.Cleanup(func() {
if len(channelIDs) > 0 {
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = ANY($1::bigint[])", channelIDs)
}
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = $1", owner.ID)
})
channels := NewChannelStore(pool)
for i := 0; i < 320; i++ {
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: owner.ID,
Title: fmt.Sprintf("Retention page %s/%03d", suffix, i),
Megagroup: true,
// Keep these rows ahead of ordinary developer/test data in the global seek.
Date: 1,
})
if err != nil {
t.Fatalf("create retention candidate %d: %v", i, err)
}
channelIDs = append(channelIDs, created.Channel.ID)
}
deleted, err := channels.DeleteExpiredChannelUpdateEvents(ctx, time.Second, len(channelIDs))
if err != nil {
t.Fatalf("delete expired channel updates across pages: %v", err)
}
if deleted != len(channelIDs) {
t.Fatalf("deleted expired channel updates = %d, want %d (must continue after page 256)", deleted, len(channelIDs))
}
var remaining, advanced int
if err := pool.QueryRow(ctx, `
SELECT
(SELECT count(*) FROM channel_update_events WHERE channel_id = ANY($1::bigint[]))::int,
(SELECT count(*) FROM channel_update_checkpoints
WHERE channel_id = ANY($1::bigint[]) AND retained_through_pts = 1)::int`, channelIDs).Scan(&remaining, &advanced); err != nil {
t.Fatalf("read paged channel retention result: %v", err)
}
if remaining != 0 || advanced != len(channelIDs) {
t.Fatalf("paged retention remaining/advanced = %d/%d, want 0/%d", remaining, advanced, len(channelIDs))
}
}
func TestDeleteExpiredChannelUpdateEventsIsolatesGapAndContinuesPostgres(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
owner, err := NewUserStore(pool).Create(ctx, domain.User{
AccessHash: time.Now().UnixNano(),
Phone: "+1768" + suffix + "01",
FirstName: "RetentionGapOwner",
})
if err != nil {
t.Fatalf("create owner: %v", err)
}
channels := NewChannelStore(pool)
bad, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: owner.ID, Title: "Retention bad " + suffix, Megagroup: true, Date: 1,
})
if err != nil {
t.Fatalf("create bad channel: %v", err)
}
healthy, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
CreatorUserID: owner.ID, Title: "Retention healthy " + suffix, Megagroup: true, Date: 2,
})
if err != nil {
t.Fatalf("create healthy channel: %v", err)
}
t.Cleanup(func() {
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = ANY($1::bigint[])", []int64{bad.Channel.ID, healthy.Channel.ID})
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = $1", owner.ID)
})
// Deliberately model a persisted invariant violation: floor=0 but the first event ends at pts=2
// with pts_count=1. The bad channel is the oldest global candidate and must be reported without
// preventing the healthy channel behind it from advancing.
if _, err := pool.Exec(ctx, `
WITH moved_event AS (
UPDATE channel_update_events SET pts = 2, date = 1 WHERE channel_id = $1 RETURNING channel_id
), moved_channel AS (
UPDATE channels SET pts = 2 WHERE id = $1 RETURNING id
)
UPDATE channel_update_checkpoints
SET latest_pts = 2, latest_event_date = 1
WHERE channel_id = $1
`, bad.Channel.ID); err != nil {
t.Fatalf("inject channel retention gap: %v", err)
}
deleted, err := channels.DeleteExpiredChannelUpdateEvents(ctx, time.Second, 10)
if err == nil || !strings.Contains(err.Error(), "has gap") {
t.Fatalf("gap retention err = %v, want reported invariant violation", err)
}
if deleted < 1 {
t.Fatalf("deleted across bad+healthy channels = %d, want at least the healthy channel's event", deleted)
}
var badFloor, badRows, healthyFloor, healthyRows int
if scanErr := pool.QueryRow(ctx, `
SELECT
(SELECT retained_through_pts FROM channel_update_checkpoints WHERE channel_id = $1)::int,
(SELECT count(*) FROM channel_update_events WHERE channel_id = $1)::int,
(SELECT retained_through_pts FROM channel_update_checkpoints WHERE channel_id = $2)::int,
(SELECT count(*) FROM channel_update_events WHERE channel_id = $2)::int
`, bad.Channel.ID, healthy.Channel.ID).Scan(&badFloor, &badRows, &healthyFloor, &healthyRows); scanErr != nil {
t.Fatalf("read isolated retention state: %v", scanErr)
}
if badFloor != 0 || badRows != 1 || healthyFloor != 1 || healthyRows != 0 {
t.Fatalf("isolated state bad=%d/%d healthy=%d/%d, want 0/1 and 1/0", badFloor, badRows, healthyFloor, healthyRows)
}
}