Skip to content
14 changes: 14 additions & 0 deletions cmd/hatchet-engine/engine/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ func runV0Config(ctx context.Context, sc *server.ServerConfig, cleanup *cleanup.
sv1, err := schedulerv1.New(
schedulerv1.WithAlerter(sc.Alerter),
schedulerv1.WithMessageQueue(sc.MessageQueueV1),
schedulerv1.WithPubSub(sc.PubSubV1),
schedulerv1.WithRepository(sc.V1),
schedulerv1.WithLogger(sc.Logger),
schedulerv1.WithPartition(p),
Expand Down Expand Up @@ -249,6 +250,7 @@ func runV0Config(ctx context.Context, sc *server.ServerConfig, cleanup *cleanup.
tasks, err := task.New(
task.WithAlerter(sc.Alerter),
task.WithMessageQueue(sc.MessageQueueV1),
task.WithPubSub(sc.PubSubV1),
task.WithV1Repository(sc.V1),
task.WithLogger(sc.Logger),
task.WithPartition(p),
Expand Down Expand Up @@ -281,6 +283,7 @@ func runV0Config(ctx context.Context, sc *server.ServerConfig, cleanup *cleanup.
olap, err := olap.New(
olap.WithAlerter(sc.Alerter),
olap.WithMessageQueue(sc.MessageQueueV1),
olap.WithPubSub(sc.PubSubV1),
olap.WithRepository(sc.V1),
olap.WithLogger(sc.Logger),
olap.WithPartition(p),
Expand Down Expand Up @@ -343,6 +346,7 @@ func runV0Config(ctx context.Context, sc *server.ServerConfig, cleanup *cleanup.
d, err := dispatcher.New(
dispatcher.WithAlerter(sc.Alerter),
dispatcher.WithMessageQueueV1(sc.MessageQueueV1),
dispatcher.WithPubSub(sc.PubSubV1),
dispatcher.WithRepositoryV1(sc.V1),
dispatcher.WithLogger(sc.Logger),
dispatcher.WithCache(cacheInstance),
Expand All @@ -367,6 +371,7 @@ func runV0Config(ctx context.Context, sc *server.ServerConfig, cleanup *cleanup.
// create the event ingestor
ei, err := ingestor.NewIngestor(
ingestor.WithMessageQueueV1(sc.MessageQueueV1),
ingestor.WithPubSub(sc.PubSubV1),
ingestor.WithRepositoryV1(sc.V1),
ingestor.WithLogIngestionEnabled(sc.Runtime.LogIngestionEnabled),
ingestor.WithLocalScheduler(localScheduler),
Expand All @@ -385,6 +390,7 @@ func runV0Config(ctx context.Context, sc *server.ServerConfig, cleanup *cleanup.
adminSvc, err := admin.NewAdminService(
admin.WithRepositoryV1(sc.V1),
admin.WithMessageQueueV1(sc.MessageQueueV1),
admin.WithPubSub(sc.PubSubV1),
admin.WithLocalScheduler(localScheduler),
admin.WithLocalDispatcher(d),
admin.WithOptimisticSchedulingEnabled(sc.Runtime.OptimisticSchedulingEnabled),
Expand All @@ -400,6 +406,7 @@ func runV0Config(ctx context.Context, sc *server.ServerConfig, cleanup *cleanup.
adminv1Svc, err := adminv1.NewAdminService(
adminv1.WithRepository(sc.V1),
adminv1.WithMessageQueue(sc.MessageQueueV1),
adminv1.WithPubSub(sc.PubSubV1),
adminv1.WithAnalytics(sc.Analytics),
adminv1.WithLocalScheduler(localScheduler),
adminv1.WithLocalDispatcher(d),
Expand Down Expand Up @@ -631,6 +638,7 @@ func runV1Config(ctx context.Context, sc *server.ServerConfig, cleanup *cleanup.
tasks, err := task.New(
task.WithAlerter(sc.Alerter),
task.WithMessageQueue(sc.MessageQueueV1),
task.WithPubSub(sc.PubSubV1),
task.WithV1Repository(sc.V1),
task.WithLogger(sc.Logger),
task.WithPartition(p),
Expand Down Expand Up @@ -666,6 +674,7 @@ func runV1Config(ctx context.Context, sc *server.ServerConfig, cleanup *cleanup.
olap, err := olap.New(
olap.WithAlerter(sc.Alerter),
olap.WithMessageQueue(sc.MessageQueueV1),
olap.WithPubSub(sc.PubSubV1),
olap.WithRepository(sc.V1),
olap.WithLogger(sc.Logger),
olap.WithPartition(p),
Expand Down Expand Up @@ -748,6 +757,7 @@ func runV1Config(ctx context.Context, sc *server.ServerConfig, cleanup *cleanup.
sv1, err := schedulerv1.New(
schedulerv1.WithAlerter(sc.Alerter),
schedulerv1.WithMessageQueue(sc.MessageQueueV1),
schedulerv1.WithPubSub(sc.PubSubV1),
schedulerv1.WithRepository(sc.V1),
schedulerv1.WithLogger(sc.Logger),
schedulerv1.WithPartition(p),
Expand Down Expand Up @@ -781,6 +791,7 @@ func runV1Config(ctx context.Context, sc *server.ServerConfig, cleanup *cleanup.
d, err := dispatcher.New(
dispatcher.WithAlerter(sc.Alerter),
dispatcher.WithMessageQueueV1(sc.MessageQueueV1),
dispatcher.WithPubSub(sc.PubSubV1),
dispatcher.WithRepositoryV1(sc.V1),
dispatcher.WithLogger(sc.Logger),
dispatcher.WithCache(cacheInstance),
Expand All @@ -806,6 +817,7 @@ func runV1Config(ctx context.Context, sc *server.ServerConfig, cleanup *cleanup.
// create the event ingestor
ei, err := ingestor.NewIngestor(
ingestor.WithMessageQueueV1(sc.MessageQueueV1),
ingestor.WithPubSub(sc.PubSubV1),
ingestor.WithRepositoryV1(sc.V1),
ingestor.WithLogIngestionEnabled(sc.Runtime.LogIngestionEnabled),
ingestor.WithLocalScheduler(localScheduler),
Expand All @@ -824,6 +836,7 @@ func runV1Config(ctx context.Context, sc *server.ServerConfig, cleanup *cleanup.
adminSvc, err := admin.NewAdminService(
admin.WithRepositoryV1(sc.V1),
admin.WithMessageQueueV1(sc.MessageQueueV1),
admin.WithPubSub(sc.PubSubV1),
admin.WithLocalScheduler(localScheduler),
admin.WithLocalDispatcher(d),
admin.WithOptimisticSchedulingEnabled(sc.Runtime.OptimisticSchedulingEnabled),
Expand All @@ -840,6 +853,7 @@ func runV1Config(ctx context.Context, sc *server.ServerConfig, cleanup *cleanup.
adminv1Svc, err := adminv1.NewAdminService(
adminv1.WithRepository(sc.V1),
adminv1.WithMessageQueue(sc.MessageQueueV1),
adminv1.WithPubSub(sc.PubSubV1),
adminv1.WithAnalytics(sc.Analytics),
adminv1.WithLocalScheduler(localScheduler),
adminv1.WithLocalDispatcher(d),
Expand Down
30 changes: 18 additions & 12 deletions frontend/docs/pages/self-hosting/configuration-options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -291,18 +291,24 @@ Variables marked with ⚠️ are conditionally required when specific features a

## Task Queue Configuration

| Variable | Description | Default Value |
| --------------------------------------------------- | -------------------------------------------------------------- | ------------- |
| `SERVER_MSGQUEUE_KIND` | Message queue kind (`rabbitmq` or `postgres`) | `rabbitmq` |
| `SERVER_MSGQUEUE_RABBITMQ_URL` | RabbitMQ URL | |
| `SERVER_MSGQUEUE_RABBITMQ_QOS` | RabbitMQ QoS (prefetch count) | `100` |
| `SERVER_MSGQUEUE_RABBITMQ_MAX_PUB_CHANS` | Max RabbitMQ publish channels | `20` |
| `SERVER_MSGQUEUE_RABBITMQ_MAX_SUB_CHANS` | Max RabbitMQ subscribe channels | `100` |
| `SERVER_MSGQUEUE_RABBITMQ_COMPRESSION_ENABLED` | Enable gzip compression of messages | `false` |
| `SERVER_MSGQUEUE_RABBITMQ_COMPRESSION_THRESHOLD` | Byte size above which messages are compressed | `5120` |
| `SERVER_MSGQUEUE_RABBITMQ_ENABLE_MESSAGE_REJECTION` | Reject (rather than requeue) messages past the max death count | `false` |
| `SERVER_MSGQUEUE_RABBITMQ_MAX_DEATH_COUNT` | Max redeliveries before a message is dead-lettered | `1000` |
| `SERVER_SINGLE_QUEUE_LIMIT` | Single queue limit | `100` |
| Variable | Description | Default Value |
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------- |
| `SERVER_MSGQUEUE_KIND` | Message queue kind (`rabbitmq` or `postgres`) | `rabbitmq` |
| `SERVER_MSGQUEUE_RABBITMQ_URL` | RabbitMQ URL | |
| `SERVER_MSGQUEUE_RABBITMQ_QOS` | RabbitMQ QoS (prefetch count) | `100` |
| `SERVER_MSGQUEUE_RABBITMQ_MAX_PUB_CHANS` | Max RabbitMQ publish channels | `20` |
| `SERVER_MSGQUEUE_RABBITMQ_MAX_SUB_CHANS` | Max RabbitMQ subscribe channels | `100` |
| `SERVER_MSGQUEUE_RABBITMQ_COMPRESSION_ENABLED` | Enable gzip compression of messages | `false` |
| `SERVER_MSGQUEUE_RABBITMQ_COMPRESSION_THRESHOLD` | Byte size above which messages are compressed | `5120` |
| `SERVER_MSGQUEUE_RABBITMQ_ENABLE_MESSAGE_REJECTION` | Reject (rather than requeue) messages past the max death count | `false` |
| `SERVER_MSGQUEUE_RABBITMQ_MAX_DEATH_COUNT` | Max redeliveries before a message is dead-lettered | `1000` |
| `SERVER_MSGQUEUE_PUBSUB_KIND` | Pub/sub kind for best-effort notifications (inherits `SERVER_MSGQUEUE_KIND` when unset) | |
| `SERVER_MSGQUEUE_PUBSUB_RABBITMQ_URL` | Pub/sub RabbitMQ URL (inherits `SERVER_MSGQUEUE_RABBITMQ_URL` when unset; always uses its own connections) | |
| `SERVER_MSGQUEUE_PUBSUB_RABBITMQ_MAX_PUB_CHANS` | Pub/sub RabbitMQ max publish channels | `10` |
| `SERVER_MSGQUEUE_PUBSUB_RABBITMQ_MAX_SUB_CHANS` | Pub/sub RabbitMQ max subscribe channels | `20` |
| `SERVER_MSGQUEUE_PUBSUB_POSTGRES_MAX_CONNS` | Pub/sub Postgres pool max connections (pool is built from the direct `DATABASE_URL`) | `5` |
| `SERVER_MSGQUEUE_PUBSUB_POSTGRES_MIN_CONNS` | Pub/sub Postgres pool min connections | `1` |
| `SERVER_SINGLE_QUEUE_LIMIT` | Single queue limit | `100` |

### Message Queue Buffers

Expand Down
36 changes: 26 additions & 10 deletions internal/msgqueue/rabbitmq/gzip.go → internal/msgqueue/gzip.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
package rabbitmq
package msgqueue

import (
"bytes"
"compress/gzip"
"fmt"
"io"

"sync"
)

// DefaultCompressionThreshold is the payload size above which messages are
// gzip-compressed when compression is enabled.
const DefaultCompressionThreshold = 5 * 1024 // 5KB

// Compressor holds gzip compression settings for a message queue
// implementation. Compression settings must agree between the durable queue
// and the pub/sub, since both publish to the same tenant topics.
type Compressor struct {
Enabled bool
Threshold int
}

type CompressionResult struct {
Payloads [][]byte
WasCompressed bool
Expand All @@ -23,7 +34,7 @@ type CompressionResult struct {
// No explicit size cap is needed: sync.Pool is self-limiting because the Go
// runtime evicts pooled objects during GC, so the pool cannot grow unbounded.
// In practice the pool size is also bounded by the number of goroutines
// concurrently compressing, which is small for a RabbitMQ publish path.
// concurrently compressing, which is small for a publish path.
var gzipWriterPool = sync.Pool{
New: func() any {
return gzip.NewWriter(nil)
Expand All @@ -38,15 +49,16 @@ func getPayloadSize(payloads [][]byte) int {
return totalSize
}

// compressPayloads compresses message payloads using gzip if they exceed the minimum size threshold.
// Returns compression results including the compressed payloads and compression statistics.
func (t *MessageQueueImpl) compressPayloads(payloads [][]byte) (*CompressionResult, error) {
// CompressPayloads compresses message payloads using gzip if they exceed the
// minimum size threshold. Returns compression results including the compressed
// payloads and compression statistics.
func (t Compressor) CompressPayloads(payloads [][]byte) (*CompressionResult, error) {
result := &CompressionResult{
Payloads: payloads,
WasCompressed: false,
}

if !t.compressionEnabled || len(payloads) == 0 {
if !t.Enabled || len(payloads) == 0 {
return result, nil
}

Expand All @@ -55,7 +67,7 @@ func (t *MessageQueueImpl) compressPayloads(payloads [][]byte) (*CompressionResu
result.OriginalSize = totalSize

// Only compress if total size exceeds threshold
if totalSize < t.compressionThreshold {
if totalSize < t.Threshold {
result.CompressedSize = totalSize
result.CompressionRatio = 1.0
return result, nil
Expand Down Expand Up @@ -97,8 +109,12 @@ func (t *MessageQueueImpl) compressPayloads(payloads [][]byte) (*CompressionResu
return result, nil
}

// decompressPayloads decompresses message payloads using gzip.
func (t *MessageQueueImpl) decompressPayloads(payloads [][]byte) ([][]byte, error) {
// DecompressPayloads decompresses gzip-compressed message payloads. It lives
// in the msgqueue package (rather than a single implementation) because a
// compressed message can cross backends: a producer whose durable queue is
// RabbitMQ with compression enabled hands the same *Message to whichever
// pub/sub is configured, so every subscriber must be able to decompress.
func DecompressPayloads(payloads [][]byte) ([][]byte, error) {
if len(payloads) == 0 {
return payloads, nil
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package rabbitmq
package msgqueue

import (
"bytes"
Expand All @@ -25,23 +25,23 @@ func generatePayloads(count, size int) [][]byte {
return payloads
}

func newMQ() *MessageQueueImpl {
return &MessageQueueImpl{
compressionEnabled: true,
compressionThreshold: 0,
func newCompressor() Compressor {
return Compressor{
Enabled: true,
Threshold: 0,
}
}

func TestCompressDecompressRoundtrip(t *testing.T) {
mq := newMQ()
mq := newCompressor()
payloads := generatePayloads(5, 10*1024)
result, err := mq.compressPayloads(payloads)
result, err := mq.CompressPayloads(payloads)

assert.NoError(t, err)
assert.True(t, result.WasCompressed, "expected WasCompressed to be true")
assert.Equal(t, len(payloads), len(result.Payloads), "expected %d payloads, got %d", len(payloads), len(result.Payloads))

decompressed, err := mq.decompressPayloads(result.Payloads)
decompressed, err := DecompressPayloads(result.Payloads)

assert.NoError(t, err)

Expand All @@ -52,69 +52,69 @@ func TestCompressDecompressRoundtrip(t *testing.T) {
}

func TestCompressPayloadsDisabled(t *testing.T) {
mq := &MessageQueueImpl{
compressionEnabled: false,
compressionThreshold: 0,
mq := Compressor{
Enabled: false,
Threshold: 0,
}

payloads := generatePayloads(3, 1024)
result, err := mq.compressPayloads(payloads)
result, err := mq.CompressPayloads(payloads)

assert.NoError(t, err)
assert.False(t, result.WasCompressed, "expected WasCompressed to be false when compression is disabled")
}

func TestCompressPayloadsBelowThreshold(t *testing.T) {
mq := &MessageQueueImpl{
compressionEnabled: true,
compressionThreshold: 100 * 1024,
mq := Compressor{
Enabled: true,
Threshold: 100 * 1024,
}

payloads := generatePayloads(1, 1024)
result, err := mq.compressPayloads(payloads)
result, err := mq.CompressPayloads(payloads)

assert.NoError(t, err)
assert.False(t, result.WasCompressed, "expected WasCompressed to be false when below threshold")
}

func BenchmarkCompressPayloads_1x10KiB(b *testing.B) {
mq := newMQ()
mq := newCompressor()
payloads := generatePayloads(1, 10*1024)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = mq.compressPayloads(payloads)
_, _ = mq.CompressPayloads(payloads)
}
}

func BenchmarkCompressPayloads_10x10KiB(b *testing.B) {
mq := newMQ()
mq := newCompressor()
payloads := generatePayloads(10, 10*1024)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = mq.compressPayloads(payloads)
_, _ = mq.CompressPayloads(payloads)
}
}

func BenchmarkCompressPayloads_10x100KiB(b *testing.B) {
mq := newMQ()
mq := newCompressor()
payloads := generatePayloads(10, 100*1024)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = mq.compressPayloads(payloads)
_, _ = mq.CompressPayloads(payloads)
}
}

func BenchmarkCompressPayloads_Concurrent(b *testing.B) {
mq := newMQ()
mq := newCompressor()
payloads := generatePayloads(5, 10*1024)
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _ = mq.compressPayloads(payloads)
_, _ = mq.CompressPayloads(payloads)
}
})
}
7 changes: 2 additions & 5 deletions internal/msgqueue/mq_pub_buffer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import (
"sync/atomic"
"testing"
"time"

"github.com/google/uuid"
)

// mockMessageQueue satisfies MessageQueue for tests. Only SendMessage is wired up.
Expand All @@ -24,11 +22,10 @@ func (m *mockMessageQueue) SendMessage(ctx context.Context, q Queue, msg *Messag
}
return nil
}
func (m *mockMessageQueue) Subscribe(_ Queue, _ AckHook, _ AckHook) (func() error, error) {
func (m *mockMessageQueue) Subscribe(_ Queue, _ MsgHandler, _ MsgHandler) (func() error, error) {
return func() error { return nil }, nil
}
func (m *mockMessageQueue) RegisterTenant(_ context.Context, _ uuid.UUID) error { return nil }
func (m *mockMessageQueue) IsReady() bool { return true }
func (m *mockMessageQueue) IsReady() bool { return true }

// TestPubBufferFlushesWhenFull verifies that when the channel is at capacity the
// capacityRelease mechanism breaks the post-flush interval wait and triggers an
Expand Down
Loading
Loading