feat: use postgres

This commit is contained in:
Ilia Denisov
2026-04-26 20:34:39 +02:00
committed by GitHub
parent 48b0056b49
commit fe829285a6
365 changed files with 29223 additions and 24049 deletions
@@ -3,6 +3,7 @@ package notificationuser_test
import (
"bytes"
"context"
"database/sql"
"encoding/base64"
"encoding/json"
"errors"
@@ -13,6 +14,7 @@ import (
"galaxy/integration/internal/harness"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require"
)
@@ -66,17 +68,13 @@ func TestNotificationUserTemporaryUnavailabilityDoesNotAdvanceOffset(t *testing.
return ok && offset.LastProcessedEntryID == messageID
}, time.Second, 50*time.Millisecond)
exists, err := h.redis.Exists(context.Background(), notificationMalformedIntentKey(messageID)).Result()
require.NoError(t, err)
require.Zero(t, exists)
exists, err = h.redis.Exists(context.Background(), notificationRouteKey(messageID, "email:user:"+recipient.UserID)).Result()
require.NoError(t, err)
require.Zero(t, exists)
require.False(t, h.malformedIntentExists(t, messageID))
require.False(t, h.routeExists(t, messageID, "email:user:"+recipient.UserID))
}
type notificationUserHarness struct {
redis *redis.Client
pg *sql.DB
userServiceURL string
@@ -141,31 +139,34 @@ func newNotificationUserHarness(t *testing.T) *notificationUserHarness {
userServiceBinary := harness.BuildBinary(t, "userservice", "./user/cmd/userservice")
notificationBinary := harness.BuildBinary(t, "notification", "./notification/cmd/notification")
userServiceProcess := harness.StartProcess(t, "userservice", userServiceBinary, map[string]string{
"USERSERVICE_LOG_LEVEL": "info",
"USERSERVICE_INTERNAL_HTTP_ADDR": userServiceAddr,
"USERSERVICE_REDIS_ADDR": redisRuntime.Addr,
"OTEL_TRACES_EXPORTER": "none",
"OTEL_METRICS_EXPORTER": "none",
})
userServiceEnv := harness.StartUserServicePersistence(t, redisRuntime.Addr).Env
userServiceEnv["USERSERVICE_LOG_LEVEL"] = "info"
userServiceEnv["USERSERVICE_INTERNAL_HTTP_ADDR"] = userServiceAddr
userServiceEnv["OTEL_TRACES_EXPORTER"] = "none"
userServiceEnv["OTEL_METRICS_EXPORTER"] = "none"
userServiceProcess := harness.StartProcess(t, "userservice", userServiceBinary, userServiceEnv)
waitForUserServiceReady(t, userServiceProcess, "http://"+userServiceAddr)
notificationProcess := harness.StartProcess(t, "notification", notificationBinary, map[string]string{
"NOTIFICATION_LOG_LEVEL": "info",
"NOTIFICATION_INTERNAL_HTTP_ADDR": notificationInternalAddr,
"NOTIFICATION_REDIS_ADDR": redisRuntime.Addr,
"NOTIFICATION_USER_SERVICE_BASE_URL": "http://" + userServiceAddr,
"NOTIFICATION_USER_SERVICE_TIMEOUT": "250ms",
"NOTIFICATION_INTENTS_READ_BLOCK_TIMEOUT": "100ms",
"NOTIFICATION_ROUTE_BACKOFF_MIN": "100ms",
"NOTIFICATION_ROUTE_BACKOFF_MAX": "100ms",
"OTEL_TRACES_EXPORTER": "none",
"OTEL_METRICS_EXPORTER": "none",
})
notificationPersistence := harness.StartNotificationServicePersistence(t, redisRuntime.Addr)
notificationEnv := notificationPersistence.Env
notificationPG, err := sql.Open("pgx", notificationPersistence.Postgres.DSNForSchema("notification", "notificationservice"))
require.NoError(t, err)
t.Cleanup(func() { _ = notificationPG.Close() })
notificationEnv["NOTIFICATION_LOG_LEVEL"] = "info"
notificationEnv["NOTIFICATION_INTERNAL_HTTP_ADDR"] = notificationInternalAddr
notificationEnv["NOTIFICATION_USER_SERVICE_BASE_URL"] = "http://" + userServiceAddr
notificationEnv["NOTIFICATION_USER_SERVICE_TIMEOUT"] = "250ms"
notificationEnv["NOTIFICATION_INTENTS_READ_BLOCK_TIMEOUT"] = "100ms"
notificationEnv["NOTIFICATION_ROUTE_BACKOFF_MIN"] = "100ms"
notificationEnv["NOTIFICATION_ROUTE_BACKOFF_MAX"] = "100ms"
notificationEnv["OTEL_TRACES_EXPORTER"] = "none"
notificationEnv["OTEL_METRICS_EXPORTER"] = "none"
notificationProcess := harness.StartProcess(t, "notification", notificationBinary, notificationEnv)
harness.WaitForHTTPStatus(t, notificationProcess, "http://"+notificationInternalAddr+"/readyz", http.StatusOK)
return &notificationUserHarness{
redis: redisClient,
pg: notificationPG,
userServiceURL: "http://" + userServiceAddr,
notificationProcess: notificationProcess,
userServiceProcess: userServiceProcess,
@@ -213,14 +214,27 @@ func (h *notificationUserHarness) publishUserIntent(t *testing.T, recipientUserI
func (h *notificationUserHarness) waitForRoute(t *testing.T, notificationID string, routeID string) notificationRouteRecord {
t.Helper()
key := notificationRouteKey(notificationID, routeID)
var route notificationRouteRecord
require.Eventually(t, func() bool {
payload, err := h.redis.Get(context.Background(), key).Bytes()
if err != nil {
return false
row := h.pg.QueryRowContext(context.Background(),
`SELECT notification_id, route_id, channel, recipient_ref, status, resolved_email, resolved_locale
FROM routes WHERE notification_id = $1 AND route_id = $2`,
notificationID, routeID,
)
if err := row.Scan(
&route.NotificationID,
&route.RouteID,
&route.Channel,
&route.RecipientRef,
&route.Status,
&route.ResolvedEmail,
&route.ResolvedLocale,
); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false
}
require.NoError(t, err)
}
require.NoError(t, decodeJSONPayload(payload, &route))
return true
}, 10*time.Second, 50*time.Millisecond)
@@ -230,14 +244,30 @@ func (h *notificationUserHarness) waitForRoute(t *testing.T, notificationID stri
func (h *notificationUserHarness) waitForMalformedIntent(t *testing.T, streamEntryID string) malformedIntentRecord {
t.Helper()
key := notificationMalformedIntentKey(streamEntryID)
var record malformedIntentRecord
require.Eventually(t, func() bool {
payload, err := h.redis.Get(context.Background(), key).Bytes()
if err != nil {
return false
row := h.pg.QueryRowContext(context.Background(),
`SELECT stream_entry_id, notification_type, producer, idempotency_key,
failure_code, failure_message, recorded_at
FROM malformed_intents WHERE stream_entry_id = $1`,
streamEntryID,
)
var recordedAt time.Time
if err := row.Scan(
&record.StreamEntryID,
&record.NotificationType,
&record.Producer,
&record.IdempotencyKey,
&record.FailureCode,
&record.FailureMessage,
&recordedAt,
); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false
}
require.NoError(t, err)
}
require.NoError(t, decodeStrictJSONPayload(payload, &record))
record.RecordedAtMS = recordedAt.UTC().UnixMilli()
return true
}, 10*time.Second, 50*time.Millisecond)
@@ -374,12 +404,26 @@ func decodeJSONPayload(payload []byte, target any) error {
return nil
}
func notificationRouteKey(notificationID string, routeID string) string {
return "notification:routes:" + encodeKeyComponent(notificationID) + ":" + encodeKeyComponent(routeID)
func (h *notificationUserHarness) routeExists(t *testing.T, notificationID string, routeID string) bool {
t.Helper()
var exists bool
err := h.pg.QueryRowContext(context.Background(),
`SELECT EXISTS(SELECT 1 FROM routes WHERE notification_id = $1 AND route_id = $2)`,
notificationID, routeID,
).Scan(&exists)
require.NoError(t, err)
return exists
}
func notificationMalformedIntentKey(streamEntryID string) string {
return "notification:malformed_intents:" + encodeKeyComponent(streamEntryID)
func (h *notificationUserHarness) malformedIntentExists(t *testing.T, streamEntryID string) bool {
t.Helper()
var exists bool
err := h.pg.QueryRowContext(context.Background(),
`SELECT EXISTS(SELECT 1 FROM malformed_intents WHERE stream_entry_id = $1)`,
streamEntryID,
).Scan(&exists)
require.NoError(t, err)
return exists
}
func notificationStreamOffsetKey() string {