130 lines
4.0 KiB
Go
130 lines
4.0 KiB
Go
//go:build integration
|
|
|
|
package mq
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
amqp "github.com/rabbitmq/amqp091-go"
|
|
)
|
|
|
|
// A full task queue is SaaS-owned: the publisher must observe the rejection
|
|
// and retain its command, rather than expecting Dispatcher to create a queue
|
|
// or invent the missing message after restart.
|
|
func TestV3FullSaaSOwnedTaskQueueRejectsSecondCommand(t *testing.T) {
|
|
url := os.Getenv("RABBITMQ_URL")
|
|
if url == "" {
|
|
t.Skip("RABBITMQ_URL not set")
|
|
}
|
|
provisionerURL := os.Getenv("RABBITMQ_PROVISIONER_URL")
|
|
if provisionerURL == "" {
|
|
provisionerURL = url
|
|
}
|
|
const dispatcherID = "550e8400-e29b-41d4-a716-446655440099"
|
|
const taskQueue = "agent-call.d." + dispatcherID + ".task.queue-full-1.v3"
|
|
const routingKey = "d." + dispatcherID + ".task.queue-full-1.in"
|
|
provisioner, err := amqp.Dial(provisionerURL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer provisioner.Close()
|
|
channel, err := provisioner.Channel()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer channel.Close()
|
|
for _, exchange := range []string{CommandsExchangeV3, ResultsExchangeV3, DeadLetterExchangeV3} {
|
|
if err := channel.ExchangeDeclare(exchange, "topic", true, false, false, false, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if _, err := channel.QueueDeclare(taskQueue, true, false, false, false, amqp.Table{
|
|
"x-max-length": int32(1), "x-overflow": "reject-publish",
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer channel.QueueDelete(taskQueue, false, false, false)
|
|
if err := channel.QueueBind(taskQueue, routingKey, CommandsExchangeV3, false, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
controlQueue := V3ControlQueueName(dispatcherID)
|
|
resultQueue := "agent-call.saas.d." + dispatcherID + ".v3"
|
|
for _, queue := range []string{controlQueue, resultQueue} {
|
|
if _, err := channel.QueueDeclare(queue, true, false, false, false, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer channel.QueueDelete(queue, false, false, false)
|
|
}
|
|
if err := channel.QueueBind(controlQueue, "d."+dispatcherID+".control.in", CommandsExchangeV3, false, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := channel.QueueBind(resultQueue, "d."+dispatcherID+".out", ResultsExchangeV3, false, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := channel.Confirm(false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
confirms := channel.NotifyPublish(make(chan amqp.Confirmation, 2))
|
|
returned := channel.NotifyReturn(make(chan amqp.Return, 2))
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
publish := func(body string) bool {
|
|
t.Helper()
|
|
if err := channel.PublishWithContext(ctx, CommandsExchangeV3, routingKey, true, false, amqp.Publishing{
|
|
ContentType: "application/json", DeliveryMode: amqp.Persistent, Body: []byte(body),
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
select {
|
|
case confirmation := <-confirms:
|
|
if !confirmation.Ack {
|
|
return false
|
|
}
|
|
select {
|
|
case <-returned:
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("SaaS publisher received no RabbitMQ confirmation")
|
|
return false
|
|
}
|
|
}
|
|
const first = `{"event_id":"original-task-command"}`
|
|
if !publish(first) || publish(`{"event_id":"rejected-task-command"}`) {
|
|
t.Fatal("full SaaS-owned queue did not confirm the first command and reject the second")
|
|
}
|
|
queued, err := channel.QueueInspect(taskQueue)
|
|
if err != nil || queued.Messages != 1 {
|
|
t.Fatalf("queue-full publication invented or discarded the original command: count=%d err=%v", queued.Messages, err)
|
|
}
|
|
broker, err := OpenV3(url, dispatcherID, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer broker.Close()
|
|
got := make(chan string, 1)
|
|
consumer, err := broker.StartPredeclaredConsumer(ctx, taskQueue, func(_ context.Context, _ string, body []byte) error {
|
|
got <- string(body)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
select {
|
|
case body := <-got:
|
|
if body != first {
|
|
t.Fatalf("Dispatcher consumed an unpublished command: %s", body)
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("Dispatcher did not consume the SaaS-retained original command")
|
|
}
|
|
if err := consumer.Stop(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|