merged from gramsrv upstream
This commit is contained in:
parent
79c64ee916
commit
21a0856587
651 changed files with 54774 additions and 4590 deletions
261
internal/app/channels/active_ids_shared_test.go
Normal file
261
internal/app/channels/active_ids_shared_test.go
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
package channels
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
type fakeActiveChannelIDsPageCache struct {
|
||||
mu sync.Mutex
|
||||
values map[store.ActiveChannelIDsPageKey][]int64
|
||||
getErr error
|
||||
putErr error
|
||||
gets int
|
||||
puts int
|
||||
}
|
||||
|
||||
func (f *fakeActiveChannelIDsPageCache) GetActiveChannelIDsPage(
|
||||
_ context.Context,
|
||||
key store.ActiveChannelIDsPageKey,
|
||||
) ([]int64, bool, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.gets++
|
||||
if f.getErr != nil {
|
||||
return nil, false, f.getErr
|
||||
}
|
||||
value, found := f.values[key]
|
||||
return append([]int64(nil), value...), found, nil
|
||||
}
|
||||
|
||||
func (f *fakeActiveChannelIDsPageCache) PutActiveChannelIDsPage(
|
||||
_ context.Context,
|
||||
key store.ActiveChannelIDsPageKey,
|
||||
value []int64,
|
||||
) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.puts++
|
||||
if f.putErr != nil {
|
||||
return f.putErr
|
||||
}
|
||||
if f.values == nil {
|
||||
f.values = make(map[store.ActiveChannelIDsPageKey][]int64)
|
||||
}
|
||||
f.values[key] = append([]int64(nil), value...)
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeActiveChannelIDsLoader struct {
|
||||
mu sync.Mutex
|
||||
values []int64
|
||||
err error
|
||||
calls int
|
||||
onLoad func()
|
||||
}
|
||||
|
||||
func (f *fakeActiveChannelIDsLoader) ListActiveChannelIDsForUser(
|
||||
_ context.Context,
|
||||
_, _ int64,
|
||||
_ int,
|
||||
) ([]int64, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.calls++
|
||||
if f.onLoad != nil {
|
||||
f.onLoad()
|
||||
}
|
||||
return append([]int64(nil), f.values...), f.err
|
||||
}
|
||||
|
||||
type fakeActiveChannelIDsMetrics struct {
|
||||
mu sync.Mutex
|
||||
outcomes map[string]int
|
||||
}
|
||||
|
||||
type mutableReadModelVersions struct {
|
||||
mu sync.Mutex
|
||||
hashes map[store.ReadModelKey]int64
|
||||
}
|
||||
|
||||
func (m *mutableReadModelVersions) ReadModelHash(
|
||||
_ context.Context,
|
||||
model string,
|
||||
ownerUserID int64,
|
||||
peerType domain.PeerType,
|
||||
peerID int64,
|
||||
) (int64, bool, error) {
|
||||
key := store.ReadModelKey{Model: model, OwnerUserID: ownerUserID, PeerType: peerType, PeerID: peerID}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
hash := m.hashes[key]
|
||||
return hash, hash != 0, nil
|
||||
}
|
||||
|
||||
func (m *mutableReadModelVersions) ReadModelHashes(
|
||||
_ context.Context,
|
||||
keys []store.ReadModelKey,
|
||||
) (map[store.ReadModelKey]int64, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
out := make(map[store.ReadModelKey]int64, len(keys))
|
||||
for _, key := range keys {
|
||||
out[key] = m.hashes[key]
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *mutableReadModelVersions) set(key store.ReadModelKey, hash int64) {
|
||||
m.mu.Lock()
|
||||
m.hashes[key] = hash
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (f *fakeActiveChannelIDsMetrics) ActiveChannelIDsCache(outcome string) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if f.outcomes == nil {
|
||||
f.outcomes = make(map[string]int)
|
||||
}
|
||||
f.outcomes[outcome]++
|
||||
}
|
||||
|
||||
func TestActiveChannelIDsSharedPageSurvivesServiceRestart(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
const ownerID int64 = 1001
|
||||
key := store.ReadModelKey{Model: "channel_active_memberships", OwnerUserID: ownerID, PeerType: domain.PeerTypeUser, PeerID: ownerID}
|
||||
versions := &fakeReadModelVersions{hashes: map[store.ReadModelKey]int64{key: 501}}
|
||||
shared := &fakeActiveChannelIDsPageCache{}
|
||||
firstLoader := &fakeActiveChannelIDsLoader{values: []int64{11, 12}}
|
||||
firstMetrics := &fakeActiveChannelIDsMetrics{}
|
||||
first := NewService(memory.NewChannelStore(),
|
||||
WithReadModelVersions(versions),
|
||||
WithActiveChannelIDsReadModel(shared, firstLoader, 32, 0, firstMetrics),
|
||||
)
|
||||
got, err := first.ActiveChannelIDsForUser(ctx, ownerID, 0, 1000)
|
||||
if err != nil || !slices.Equal(got, []int64{11, 12}) {
|
||||
t.Fatalf("first page = %v err=%v", got, err)
|
||||
}
|
||||
if firstLoader.calls != 1 || shared.puts != 1 || firstMetrics.outcomes["miss"] != 1 || firstMetrics.outcomes["fill"] != 1 {
|
||||
t.Fatalf("first load calls=%d puts=%d metrics=%v", firstLoader.calls, shared.puts, firstMetrics.outcomes)
|
||||
}
|
||||
|
||||
secondLoader := &fakeActiveChannelIDsLoader{err: errors.New("cold loader must not run")}
|
||||
secondMetrics := &fakeActiveChannelIDsMetrics{}
|
||||
second := NewService(memory.NewChannelStore(),
|
||||
WithReadModelVersions(versions),
|
||||
WithActiveChannelIDsReadModel(shared, secondLoader, 32, 0, secondMetrics),
|
||||
)
|
||||
got, err = second.ActiveChannelIDsForUser(ctx, ownerID, 0, 1000)
|
||||
if err != nil || !slices.Equal(got, []int64{11, 12}) {
|
||||
t.Fatalf("restart page = %v err=%v", got, err)
|
||||
}
|
||||
if secondLoader.calls != 0 || secondMetrics.outcomes["hit"] != 1 || secondMetrics.outcomes["served"] != 1 {
|
||||
t.Fatalf("restart loader=%d metrics=%v", secondLoader.calls, secondMetrics.outcomes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActiveChannelIDsSharedPageFailsClosedOnRedisError(t *testing.T) {
|
||||
const ownerID int64 = 1001
|
||||
key := store.ReadModelKey{Model: "channel_active_memberships", OwnerUserID: ownerID, PeerType: domain.PeerTypeUser, PeerID: ownerID}
|
||||
versions := &fakeReadModelVersions{hashes: map[store.ReadModelKey]int64{key: 601}}
|
||||
shared := &fakeActiveChannelIDsPageCache{getErr: errors.New("redis down")}
|
||||
loader := &fakeActiveChannelIDsLoader{values: []int64{11}}
|
||||
service := NewService(memory.NewChannelStore(),
|
||||
WithReadModelVersions(versions),
|
||||
WithActiveChannelIDsReadModel(shared, loader, 32, 0, nil),
|
||||
)
|
||||
if _, err := service.ActiveChannelIDsForUser(context.Background(), ownerID, 0, 1000); err == nil {
|
||||
t.Fatal("Redis error was silently bypassed")
|
||||
}
|
||||
if loader.calls != 0 {
|
||||
t.Fatalf("cold loader calls = %d, want 0", loader.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActiveChannelIDsSharedPageRetriesGenerationChange(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
const ownerID int64 = 1001
|
||||
key := store.ReadModelKey{Model: "channel_active_memberships", OwnerUserID: ownerID, PeerType: domain.PeerTypeUser, PeerID: ownerID}
|
||||
versions := &fakeReadModelVersions{hashes: map[store.ReadModelKey]int64{key: 701}}
|
||||
shared := &fakeActiveChannelIDsPageCache{}
|
||||
loader := &fakeActiveChannelIDsLoader{values: []int64{11}}
|
||||
loader.onLoad = func() {
|
||||
if loader.calls == 1 {
|
||||
versions.hashes[key] = 702
|
||||
loader.values = []int64{11, 12}
|
||||
}
|
||||
}
|
||||
metrics := &fakeActiveChannelIDsMetrics{}
|
||||
service := NewService(memory.NewChannelStore(),
|
||||
WithReadModelVersions(versions),
|
||||
WithActiveChannelIDsReadModel(shared, loader, 32, 0, metrics),
|
||||
)
|
||||
got, err := service.ActiveChannelIDsForUser(ctx, ownerID, 0, 1000)
|
||||
if err != nil || !slices.Equal(got, []int64{11, 12}) {
|
||||
t.Fatalf("page = %v err=%v", got, err)
|
||||
}
|
||||
if loader.calls != 2 || shared.puts != 1 || metrics.outcomes["generation_retry"] != 1 {
|
||||
t.Fatalf("loader=%d puts=%d metrics=%v", loader.calls, shared.puts, metrics.outcomes)
|
||||
}
|
||||
oldKey := store.ActiveChannelIDsPageKey{UserID: ownerID, Generation: 701, AfterChannelID: 0, Limit: 1000}
|
||||
if _, found := shared.values[oldKey]; found {
|
||||
t.Fatal("generation-raced page was stored under old key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestActiveChannelIDsSharedMissingGenerationOnlyCachesEmpty(t *testing.T) {
|
||||
shared := &fakeActiveChannelIDsPageCache{}
|
||||
loader := &fakeActiveChannelIDsLoader{values: []int64{11}}
|
||||
service := NewService(memory.NewChannelStore(),
|
||||
WithReadModelVersions(&fakeReadModelVersions{}),
|
||||
WithActiveChannelIDsReadModel(shared, loader, 32, 0, nil),
|
||||
)
|
||||
if _, err := service.ActiveChannelIDsForUser(context.Background(), 1001, 0, 1000); err == nil {
|
||||
t.Fatal("non-empty page without durable generation accepted")
|
||||
}
|
||||
if shared.puts != 0 {
|
||||
t.Fatalf("shared puts = %d, want 0", shared.puts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActiveChannelIDsLocalWriteInvalidatesCachedGenerationBeforeNotify(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
const ownerID int64 = 1001
|
||||
versionKey := store.ReadModelKey{Model: "channel_active_memberships", OwnerUserID: ownerID, PeerType: domain.PeerTypeUser, PeerID: ownerID}
|
||||
baseVersions := &mutableReadModelVersions{hashes: map[store.ReadModelKey]int64{versionKey: 801}}
|
||||
cachedVersions := store.NewCachedReadModelVersionStore(baseVersions, 0, 32)
|
||||
oldPageKey := store.ActiveChannelIDsPageKey{UserID: ownerID, Generation: 801, AfterChannelID: 0, Limit: 1000}
|
||||
shared := &fakeActiveChannelIDsPageCache{values: map[store.ActiveChannelIDsPageKey][]int64{oldPageKey: {11}}}
|
||||
loader := &fakeActiveChannelIDsLoader{values: []int64{11, 12}}
|
||||
service := NewService(memory.NewChannelStore(),
|
||||
WithReadModelVersions(cachedVersions),
|
||||
WithActiveChannelIDsReadModel(shared, loader, 32, 0, nil),
|
||||
)
|
||||
first, err := service.ActiveChannelIDsForUser(ctx, ownerID, 0, 1000)
|
||||
if err != nil || !slices.Equal(first, []int64{11}) {
|
||||
t.Fatalf("first = %v err=%v", first, err)
|
||||
}
|
||||
baseVersions.set(versionKey, 802)
|
||||
// Simulate the synchronous post-commit app hook before PostgreSQL NOTIFY is
|
||||
// delivered to this process.
|
||||
service.invalidateActiveChannelIDs(ownerID)
|
||||
second, err := service.ActiveChannelIDsForUser(ctx, ownerID, 0, 1000)
|
||||
if err != nil || !slices.Equal(second, []int64{11, 12}) {
|
||||
t.Fatalf("after local invalidation = %v err=%v", second, err)
|
||||
}
|
||||
if loader.calls != 1 {
|
||||
t.Fatalf("cold loader calls = %d, want 1 for new generation", loader.calls)
|
||||
}
|
||||
newPageKey := store.ActiveChannelIDsPageKey{UserID: ownerID, Generation: 802, AfterChannelID: 0, Limit: 1000}
|
||||
if !slices.Equal(shared.values[newPageKey], []int64{11, 12}) {
|
||||
t.Fatalf("new generation page = %v", shared.values[newPageKey])
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue