eda033ff9b
Addresses errcheck, gosec, revive, staticcheck, and unused findings
across the codebase (unchecked error returns, unsafe file inclusion
warnings on operator/test-controlled paths, missing package comments,
unused parameters, deprecated API usage). Also fixes two suppression
comments that were silently no-ops due to wrong syntax (#nosec needs
a leading '#', nolint reasons need '//' not '--').
With the backlog clear, drop continue-on-error from the CI lint job
per the plan left in b4b4806.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
142 lines
3.7 KiB
Go
142 lines
3.7 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/igodwin/notifier/internal/domain"
|
|
)
|
|
|
|
// TestConcurrentSendGetListNoRace hammers Send, GetNotification, and
|
|
// ListNotifications concurrently while worker goroutines process notifications
|
|
// pulled from the queue in the background. It is meant to be run with
|
|
// `go test -race`: if the service ever stored or returned a raw pointer that a
|
|
// worker also mutates (the bug this test guards against), the race detector
|
|
// flags a data race here.
|
|
func TestConcurrentSendGetListNoRace(t *testing.T) {
|
|
svc := createTestService(t)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
|
defer cancel()
|
|
|
|
if err := svc.Start(ctx); err != nil {
|
|
t.Fatalf("Failed to start service: %v", err)
|
|
}
|
|
defer func() { _ = svc.Stop() }()
|
|
|
|
const numSenders = 8
|
|
const sendsPerSender = 25
|
|
|
|
var idsMu sync.Mutex
|
|
var ids []string
|
|
|
|
var sendersWg sync.WaitGroup
|
|
for s := 0; s < numSenders; s++ {
|
|
sendersWg.Add(1)
|
|
go func(sender int) {
|
|
defer sendersWg.Done()
|
|
for i := 0; i < sendsPerSender; i++ {
|
|
notification := &domain.Notification{
|
|
ID: uuid.New().String(),
|
|
Type: domain.TypeStdout,
|
|
Status: domain.StatusPending,
|
|
Subject: fmt.Sprintf("race-test-%d-%d", sender, i),
|
|
Body: "race test body",
|
|
Recipients: []string{"race@example.com"},
|
|
CC: []string{"cc@example.com"},
|
|
Metadata: map[string]interface{}{"sender": sender},
|
|
CreatedAt: time.Now(),
|
|
MaxRetries: 1,
|
|
}
|
|
|
|
if _, err := svc.Send(ctx, notification); err != nil {
|
|
t.Errorf("Send failed: %v", err)
|
|
continue
|
|
}
|
|
|
|
idsMu.Lock()
|
|
ids = append(ids, notification.ID)
|
|
idsMu.Unlock()
|
|
}
|
|
}(s)
|
|
}
|
|
|
|
// Readers race against the senders and against the worker pool mutating
|
|
// notifications as they're processed.
|
|
stopReaders := make(chan struct{})
|
|
var readersWg sync.WaitGroup
|
|
for r := 0; r < 4; r++ {
|
|
readersWg.Add(1)
|
|
go func() {
|
|
defer readersWg.Done()
|
|
for {
|
|
select {
|
|
case <-stopReaders:
|
|
return
|
|
default:
|
|
}
|
|
|
|
idsMu.Lock()
|
|
n := len(ids)
|
|
var id string
|
|
if n > 0 {
|
|
id = ids[n-1]
|
|
}
|
|
idsMu.Unlock()
|
|
|
|
if id != "" {
|
|
if notif, err := svc.GetNotification(ctx, id); err == nil {
|
|
// Touch the returned notification's reference fields;
|
|
// if it were aliased with the stored/queued copy a
|
|
// concurrent worker mutation would trip the race
|
|
// detector right here.
|
|
_ = notif.Status
|
|
_ = append([]string(nil), notif.Recipients...)
|
|
}
|
|
}
|
|
|
|
list, err := svc.ListNotifications(ctx, &domain.NotificationFilter{})
|
|
if err != nil {
|
|
t.Errorf("ListNotifications failed: %v", err)
|
|
continue
|
|
}
|
|
for _, notif := range list {
|
|
_ = notif.Status
|
|
_ = append([]string(nil), notif.Recipients...)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
sendersWg.Wait()
|
|
close(stopReaders)
|
|
readersWg.Wait()
|
|
|
|
// Give the worker pool a moment to drain the queue, then sanity check the
|
|
// service is still consistent.
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
stats, err := svc.GetStats(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetStats failed: %v", err)
|
|
}
|
|
if stats.TotalSent == numSenders*sendsPerSender {
|
|
return
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
|
|
stats, err := svc.GetStats(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetStats failed: %v", err)
|
|
}
|
|
t.Logf("final stats: sent=%d failed=%d queued=%d pending=%d", stats.TotalSent, stats.TotalFailed, stats.TotalQueued, stats.TotalPending)
|
|
if stats.TotalSent != numSenders*sendsPerSender {
|
|
t.Errorf("expected all %d notifications to reach Sent, got %d", numSenders*sendsPerSender, stats.TotalSent)
|
|
}
|
|
}
|