mirror of
https://github.com/hatchet-dev/hatchet.git
synced 2026-01-31 05:08:51 -06:00
fix: compute payload size correctly for pg_notify
This commit is contained in:
@@ -44,12 +44,8 @@ func (p *PostgresMessageQueue) pubNonDurableMessages(ctx context.Context, queueN
|
||||
|
||||
if err == nil {
|
||||
eg.Go(func() error {
|
||||
// if the message is greater than 8kb, store the message in the database
|
||||
if len(msgBytes) > 8000 {
|
||||
return p.repo.AddMessage(ctx, queueName, msgBytes)
|
||||
}
|
||||
|
||||
// if the message is less than 8kb, publish the message to the channel
|
||||
// Notify will automatically fall back to database storage if the
|
||||
// wrapped message exceeds pg_notify's 8KB limit
|
||||
return p.repo.Notify(ctx, queueName, string(msgBytes))
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
@@ -15,8 +16,8 @@ import (
|
||||
)
|
||||
|
||||
type PubSubMessage struct {
|
||||
QueueName string `json:"queue_name"`
|
||||
Payload []byte `json:"payload"`
|
||||
QueueName string `json:"queue_name"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
}
|
||||
|
||||
type MessageQueueRepository interface {
|
||||
@@ -59,7 +60,19 @@ func (m *messageQueueRepository) Listen(ctx context.Context, name string, f func
|
||||
}
|
||||
|
||||
func (m *messageQueueRepository) Notify(ctx context.Context, name string, payload string) error {
|
||||
return m.m.notify(ctx, name, payload)
|
||||
wrappedPayload, err := m.m.wrapMessage(name, payload)
|
||||
if err != nil {
|
||||
m.l.Error().Err(err).Msg("error wrapping message")
|
||||
return err
|
||||
}
|
||||
|
||||
// PostgreSQL's pg_notify has an 8000 byte limit
|
||||
// If the wrapped message exceeds this, fall back to database storage
|
||||
if len(wrappedPayload) > 8000 {
|
||||
return m.AddMessage(ctx, name, []byte(payload))
|
||||
}
|
||||
|
||||
return m.m.notify(ctx, wrappedPayload)
|
||||
}
|
||||
|
||||
func (m *messageQueueRepository) AddMessage(ctx context.Context, queue string, payload []byte) error {
|
||||
|
||||
@@ -179,21 +179,29 @@ func (m *multiplexedListener) listen(ctx context.Context, name string, f func(ct
|
||||
}
|
||||
}
|
||||
|
||||
// notify sends a notification through the Postgres channel.
|
||||
func (m *multiplexedListener) notify(ctx context.Context, name string, payload string) error {
|
||||
// wrapMessage wraps a payload in a PubSubMessage and marshals it.
|
||||
// Returns the marshaled bytes or an error.
|
||||
func (m *multiplexedListener) wrapMessage(name string, payload string) ([]byte, error) {
|
||||
var jsonPayload json.RawMessage
|
||||
|
||||
// Handle empty payload - use null as valid JSON
|
||||
if payload == "" {
|
||||
jsonPayload = json.RawMessage("null")
|
||||
} else {
|
||||
jsonPayload = json.RawMessage(payload)
|
||||
}
|
||||
|
||||
pubSubMsg := &PubSubMessage{
|
||||
QueueName: name,
|
||||
Payload: []byte(payload),
|
||||
Payload: jsonPayload,
|
||||
}
|
||||
|
||||
payloadBytes, err := json.Marshal(pubSubMsg)
|
||||
|
||||
if err != nil {
|
||||
m.l.Error().Err(err).Msg("error marshalling notification payload")
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = m.pool.Exec(ctx, "select pg_notify($1,$2)", multiplexChannel, string(payloadBytes))
|
||||
return json.Marshal(pubSubMsg)
|
||||
}
|
||||
|
||||
// notify sends a notification through the Postgres channel.
|
||||
// wrappedPayload should be the already-marshaled PubSubMessage.
|
||||
func (m *multiplexedListener) notify(ctx context.Context, wrappedPayload []byte) error {
|
||||
_, err := m.pool.Exec(ctx, "select pg_notify($1,$2)", multiplexChannel, string(wrappedPayload))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -258,3 +260,117 @@ func TestMultiplexedListener_ConcurrentAccess(t *testing.T) {
|
||||
t.Errorf("Expected %d messages received, got %d", expectedCount, actualCount)
|
||||
}
|
||||
}
|
||||
|
||||
const testQueueName = "test-queue"
|
||||
|
||||
var byteOverhead []byte = []byte(`{"queue_name":"test-queue","payload":}`)
|
||||
var byteOverheadSize = len(byteOverhead)
|
||||
|
||||
// TestPubSubMessageWrappedSize verifies that we correctly calculate the size
|
||||
// of messages after wrapping in PubSubMessage, accounting for JSON marshaling
|
||||
// overhead without double base64 encoding. PostgreSQL's pg_notify has an 8000 byte limit.
|
||||
func TestPubSubMessageWrappedSize(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
queueName string
|
||||
payloadSize int // size of the original JSON payload
|
||||
expectUnderLimit bool // expect to be under pg_notify's 8000 byte limit
|
||||
}{
|
||||
{
|
||||
name: "small message stays under limit",
|
||||
queueName: testQueueName,
|
||||
payloadSize: 1000,
|
||||
expectUnderLimit: true,
|
||||
},
|
||||
{
|
||||
name: "7000 byte message should stay under limit",
|
||||
queueName: testQueueName,
|
||||
payloadSize: 7000,
|
||||
expectUnderLimit: true,
|
||||
},
|
||||
{
|
||||
name: "7999 byte message at the boundary",
|
||||
queueName: testQueueName,
|
||||
payloadSize: 7999 - byteOverheadSize,
|
||||
expectUnderLimit: true, // 7950 + 38 byte PubSubMessage wrapper = 7988, just under 8000
|
||||
},
|
||||
{
|
||||
name: "8001 byte message should exceed limit",
|
||||
queueName: testQueueName,
|
||||
payloadSize: 8001 - byteOverheadSize,
|
||||
expectUnderLimit: false, // 7970 + 38 byte PubSubMessage wrapper = 8008, exceeds 8000
|
||||
},
|
||||
}
|
||||
|
||||
logger := zerolog.Nop()
|
||||
m := &multiplexedListener{
|
||||
l: &logger,
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create a JSON payload of exactly the specified size
|
||||
payload := createJSONPayload(tt.payloadSize)
|
||||
|
||||
// Use the actual wrapMessage method to wrap and marshal
|
||||
wrappedBytes, err := m.wrapMessage(tt.queueName, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to wrap message: %v", err)
|
||||
}
|
||||
|
||||
wrappedSize := len(wrappedBytes)
|
||||
underLimit := wrappedSize <= 8000
|
||||
|
||||
t.Logf("Overhead size: %d bytes", byteOverheadSize)
|
||||
|
||||
t.Logf("Original payload size: %d, Wrapped size: %d", len(payload), wrappedSize)
|
||||
|
||||
if underLimit != tt.expectUnderLimit {
|
||||
t.Errorf("Expected under 8000 bytes: %v, but got size: %d (under: %v)",
|
||||
tt.expectUnderLimit, wrappedSize, underLimit)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestEmptyPayloadHandling verifies that empty payloads are handled correctly
|
||||
func TestEmptyPayloadHandling(t *testing.T) {
|
||||
logger := zerolog.Nop()
|
||||
m := &multiplexedListener{
|
||||
l: &logger,
|
||||
}
|
||||
|
||||
// Test wrapping an empty payload
|
||||
wrappedBytes, err := m.wrapMessage("test-queue", "")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to wrap empty message: %v", err)
|
||||
}
|
||||
|
||||
// Should produce valid JSON with null payload
|
||||
expected := `{"queue_name":"test-queue","payload":null}`
|
||||
actual := string(wrappedBytes)
|
||||
if actual != expected {
|
||||
t.Errorf("Expected %s, got %s", expected, actual)
|
||||
}
|
||||
|
||||
t.Logf("Empty payload wrapped as: %s", actual)
|
||||
t.Logf("Size: %d bytes", len(wrappedBytes))
|
||||
}
|
||||
|
||||
// createJSONPayload creates a JSON string of exactly the specified size
|
||||
func createJSONPayload(size int) string {
|
||||
// Create a simple JSON object with a large string field
|
||||
// The JSON structure will be: {"data":"<padding>"}
|
||||
// Structure overhead: { (1) + "data" (6) + : (1) + " (1) + " (1) + } (1) = 11 bytes
|
||||
const jsonStructureOverhead = 11
|
||||
paddingSize := size - jsonStructureOverhead
|
||||
if paddingSize < 0 {
|
||||
paddingSize = 0
|
||||
}
|
||||
padding := strings.Repeat("x", paddingSize)
|
||||
payload := map[string]string{
|
||||
"data": padding,
|
||||
}
|
||||
bytes, _ := json.Marshal(payload)
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user