159 lines
5.1 KiB
Go
159 lines
5.1 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
|
|
"telesrv/internal/store"
|
|
"telesrv/internal/store/postgres/sqlcgen"
|
|
)
|
|
|
|
func TestSelectDistinctLayerAdvanceBatchDefersSameSession(t *testing.T) {
|
|
first := authKeySessionLayerBatchRequest{rawAuthKeyID: [8]byte{1}, sessionID: 7}
|
|
duplicate := authKeySessionLayerBatchRequest{rawAuthKeyID: [8]byte{1}, sessionID: 7}
|
|
other := authKeySessionLayerBatchRequest{rawAuthKeyID: [8]byte{1}, sessionID: 8}
|
|
batch, remaining := selectDistinctLayerAdvanceBatch(
|
|
[]authKeySessionLayerBatchRequest{first, duplicate, other},
|
|
3,
|
|
)
|
|
if len(batch) != 2 || batch[0].sessionID != 7 || batch[1].sessionID != 8 {
|
|
t.Fatalf("batch = %#v", batch)
|
|
}
|
|
if len(remaining) != 1 || remaining[0].sessionID != 7 {
|
|
t.Fatalf("remaining = %#v", remaining)
|
|
}
|
|
}
|
|
|
|
func TestBatchedAuthKeySessionLayerStorePostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
const accountCount = 32
|
|
now := time.Now().UTC()
|
|
keys := NewAuthKeyStore(pool)
|
|
type seeded struct {
|
|
id [8]byte
|
|
sessionID int64
|
|
observationID int64
|
|
msgID int64
|
|
}
|
|
seededKeys := make([]seeded, 0, accountCount)
|
|
for index := 0; index < accountCount; index++ {
|
|
id := randomLayerTestAuthKeyID(t)
|
|
sessionID := int64(91000 + index)
|
|
if err := keys.Save(ctx, store.AuthKeyData{ID: id}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
firstMsgID := authKeySessionLayerTestMsgID(now, uint32(index+1))
|
|
first, applied, err := keys.AdvanceSessionLayer(ctx, id, sessionID, 227, firstMsgID)
|
|
if err != nil || !applied || first.ObservationID <= 0 {
|
|
t.Fatalf("seed %d = (%+v,%v,%v)", index, first, applied, err)
|
|
}
|
|
seededKeys = append(seededKeys, seeded{
|
|
id: id, sessionID: sessionID, observationID: first.ObservationID,
|
|
msgID: authKeySessionLayerTestMsgID(now, uint32(accountCount+index+1)),
|
|
})
|
|
}
|
|
t.Cleanup(func() {
|
|
for _, item := range seededKeys {
|
|
_ = keys.Delete(ctx, item.id)
|
|
}
|
|
})
|
|
|
|
counted := &layerBatchCountingDB{db: pool}
|
|
batchedBase := NewAuthKeyStore(counted)
|
|
batcher, err := NewBatchedAuthKeySessionLayerStore(batchedBase, AuthKeySessionLayerBatchConfig{
|
|
MaxSize: accountCount, MaxWait: 10 * time.Millisecond,
|
|
QueueSize: accountCount * 2, QueryTimeout: 5 * time.Second,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(batcher.Close)
|
|
|
|
start := make(chan struct{})
|
|
errs := make(chan error, accountCount)
|
|
var wg sync.WaitGroup
|
|
for _, item := range seededKeys {
|
|
item := item
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
<-start
|
|
current, applied, err := batcher.AdvanceSessionLayer(ctx, item.id, item.sessionID, 227, item.msgID)
|
|
if err != nil {
|
|
errs <- err
|
|
return
|
|
}
|
|
if !applied || current.MessageID != item.msgID || current.ObservationID != item.observationID {
|
|
errs <- errors.New("same-Layer batch changed durable generation or failed to advance")
|
|
}
|
|
}()
|
|
}
|
|
close(start)
|
|
wg.Wait()
|
|
close(errs)
|
|
for err := range errs {
|
|
t.Fatal(err)
|
|
}
|
|
if calls := counted.batchQueries.Load(); calls <= 0 || calls > 4 {
|
|
t.Fatalf("batch SQL calls = %d, want 1..4 for %d concurrent sessions", calls, accountCount)
|
|
}
|
|
for _, item := range seededKeys {
|
|
current, found, err := keys.GetSessionLayer(ctx, item.id, item.sessionID)
|
|
if err != nil || !found || current.MessageID != item.msgID || current.ObservationID != item.observationID {
|
|
t.Fatalf("durable result %x/%d = (%+v,%v,%v)", item.id, item.sessionID, current, found, err)
|
|
}
|
|
}
|
|
|
|
// A fast miss must synchronously execute the original full state machine,
|
|
// rather than treating a successful batch statement as success for every row.
|
|
missingSession := int64(99001)
|
|
missingMsgID := authKeySessionLayerTestMsgID(now, 1000)
|
|
created, applied, err := batcher.AdvanceSessionLayer(ctx, seededKeys[0].id, missingSession, 225, missingMsgID)
|
|
if err != nil || !applied || created.Layer != 225 || created.MessageID != missingMsgID || created.ObservationID <= 0 {
|
|
t.Fatalf("batch miss full fallback = (%+v,%v,%v)", created, applied, err)
|
|
}
|
|
|
|
batcher.Close()
|
|
if _, _, err := batcher.AdvanceSessionLayer(ctx, seededKeys[0].id, missingSession, 225, missingMsgID); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("advance after close err = %v", err)
|
|
}
|
|
}
|
|
|
|
type layerBatchCountingDB struct {
|
|
db sqlcgen.DBTX
|
|
batchQueries atomic.Int64
|
|
}
|
|
|
|
func (db *layerBatchCountingDB) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) {
|
|
return db.db.Exec(ctx, sql, args...)
|
|
}
|
|
|
|
func (db *layerBatchCountingDB) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) {
|
|
if strings.Contains(sql, "WITH input AS") && strings.Contains(sql, "candidates AS MATERIALIZED") {
|
|
db.batchQueries.Add(1)
|
|
}
|
|
return db.db.Query(ctx, sql, args...)
|
|
}
|
|
|
|
func (db *layerBatchCountingDB) QueryRow(ctx context.Context, sql string, args ...any) pgx.Row {
|
|
return db.db.QueryRow(ctx, sql, args...)
|
|
}
|
|
|
|
func (db *layerBatchCountingDB) Begin(ctx context.Context) (pgx.Tx, error) {
|
|
beginner, ok := db.db.(txBeginner)
|
|
if !ok {
|
|
return nil, errors.New("counted database does not support transactions")
|
|
}
|
|
return beginner.Begin(ctx)
|
|
}
|
|
|
|
var _ sqlcgen.DBTX = (*layerBatchCountingDB)(nil)
|