├── compose ├── redis.yml ├── amqp.yml ├── benchmark.yml ├── nats-jetstream.yml ├── postgresql.yml ├── mysql.yml ├── kafka.yml └── kafka-multinode.yml ├── .gitignore ├── pkg ├── counter.go ├── subscribe.go ├── multiplier.go ├── publish.go ├── benchmark.go └── pubsub.go ├── run.sh ├── setup ├── startup.sh ├── instance.tf └── .terraform.lock.hcl ├── LICENSE ├── cmd └── main.go ├── go.mod ├── README.md └── go.sum /compose/redis.yml: -------------------------------------------------------------------------------- 1 | services: 2 | redis: 3 | image: redis:7 4 | ports: 5 | - "6379:6379" 6 | restart: unless-stopped 7 | -------------------------------------------------------------------------------- /compose/amqp.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rabbitmq: 3 | image: rabbitmq:4.0 4 | restart: unless-stopped 5 | ports: 6 | - 5672:5672 7 | -------------------------------------------------------------------------------- /compose/benchmark.yml: -------------------------------------------------------------------------------- 1 | services: 2 | benchmark: 3 | image: golang:1.23 4 | command: /bin/true 5 | volumes: 6 | - $GOPATH/pkg/mod/cache:/go/pkg/mod/cache 7 | env_file: 8 | - .env 9 | -------------------------------------------------------------------------------- /compose/nats-jetstream.yml: -------------------------------------------------------------------------------- 1 | services: 2 | nats: 3 | image: nats:latest 4 | ports: 5 | - "0.0.0.0:4222:4222" 6 | restart: unless-stopped 7 | command: ["-js"] 8 | ulimits: 9 | nofile: 10 | soft: 65536 11 | hard: 65536 12 | -------------------------------------------------------------------------------- /compose/postgresql.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres:17 4 | restart: unless-stopped 5 | ports: 6 | - 5432:5432 7 | environment: 8 | POSTGRES_USER: watermill 9 | POSTGRES_DB: watermill 10 | POSTGRES_PASSWORD: "password" 11 | -------------------------------------------------------------------------------- /compose/mysql.yml: -------------------------------------------------------------------------------- 1 | services: 2 | mysql: 3 | image: mysql:9.1 4 | restart: unless-stopped 5 | command: --max-connections 2048 6 | ports: 7 | - 3306:3306 8 | environment: 9 | MYSQL_DATABASE: watermill 10 | MYSQL_ALLOW_EMPTY_PASSWORD: "yes" 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | .mod-cache 15 | .terraform 16 | *.tfstate* 17 | compose/.env 18 | compose/*.json 19 | 20 | .idea 21 | vendor 22 | -------------------------------------------------------------------------------- /pkg/counter.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "sync/atomic" 5 | "time" 6 | ) 7 | 8 | type Counter struct { 9 | count uint64 10 | startTime time.Time 11 | } 12 | 13 | func NewCounter() *Counter { 14 | return &Counter{ 15 | count: 0, 16 | startTime: time.Now(), 17 | } 18 | } 19 | 20 | func (c *Counter) Add(n uint64) { 21 | atomic.AddUint64(&c.count, n) 22 | } 23 | 24 | func (c *Counter) Count() uint64 { 25 | return c.count 26 | } 27 | 28 | func (c *Counter) MeanPerSecond() float64 { 29 | return float64(c.count) / time.Since(c.startTime).Seconds() 30 | } 31 | -------------------------------------------------------------------------------- /compose/kafka.yml: -------------------------------------------------------------------------------- 1 | services: 2 | zookeeper: 3 | image: confluentinc/cp-zookeeper:latest 4 | restart: unless-stopped 5 | environment: 6 | ZOOKEEPER_CLIENT_PORT: 2181 7 | logging: 8 | driver: none 9 | 10 | kafka: 11 | image: confluentinc/cp-kafka:latest 12 | restart: unless-stopped 13 | logging: 14 | driver: none 15 | ports: 16 | - 9092:9092 17 | depends_on: 18 | - zookeeper 19 | environment: 20 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 21 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092 22 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 23 | KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" 24 | KAFKA_NUM_PARTITIONS: 16 25 | -------------------------------------------------------------------------------- /pkg/subscribe.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/ThreeDotsLabs/watermill" 8 | "github.com/ThreeDotsLabs/watermill/message" 9 | ) 10 | 11 | func (ps PubSub) ConsumeMessages(wg *sync.WaitGroup, counter *Counter) error { 12 | router, err := message.NewRouter(message.RouterConfig{}, watermill.NopLogger{}) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | router.AddNoPublisherHandler( 18 | "benchmark_read", 19 | ps.Topic, 20 | ps.Subscriber, 21 | func(msg *message.Message) error { 22 | defer counter.Add(1) 23 | defer wg.Done() 24 | 25 | msg.Ack() 26 | return nil 27 | }, 28 | ) 29 | 30 | return router.Run(context.Background()) 31 | } 32 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | readonly compose="$1" 5 | 6 | if [ -z "$compose" ]; then 7 | echo "Usage: $0 " 8 | exit 1 9 | fi 10 | 11 | compose_flags= 12 | if [ -f "./compose/$compose.yml" ]; then 13 | compose_flags="-f ./compose/$compose.yml" 14 | docker compose $compose_flags up -d --remove-orphans 15 | 16 | # TODO replace with waiting for port 17 | sleep 20 18 | fi 19 | 20 | if [ ! -d ./vendor ]; then 21 | docker compose -f ./compose/benchmark.yml run \ 22 | -v "$(pwd):/benchmark" \ 23 | -w /benchmark \ 24 | benchmark go mod vendor 25 | fi 26 | 27 | docker compose $compose_flags -f ./compose/benchmark.yml run \ 28 | -v "$(pwd):/benchmark" \ 29 | -w /benchmark \ 30 | benchmark go run -mod=vendor ./cmd/main.go -pubsub "$compose" 31 | -------------------------------------------------------------------------------- /setup/startup.sh: -------------------------------------------------------------------------------- 1 | apt-get update 2 | apt-get install -y ca-certificates curl 3 | install -m 0755 -d /etc/apt/keyrings 4 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc 5 | chmod a+r /etc/apt/keyrings/docker.asc 6 | 7 | echo \ 8 | "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ 9 | $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ 10 | tee /etc/apt/sources.list.d/docker.list > /dev/null 11 | apt-get update 12 | 13 | apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 14 | 15 | adduser benchmark docker 16 | 17 | su - benchmark -c "git clone https://github.com/ThreeDotsLabs/watermill-benchmark" 18 | su - benchmark -c "echo export GOPATH=~/go >> .bash_profile" 19 | -------------------------------------------------------------------------------- /setup/instance.tf: -------------------------------------------------------------------------------- 1 | provider "google" { 2 | project = "${var.project}" 3 | region = "europe-north1" 4 | } 5 | 6 | resource "google_compute_instance" "default" { 7 | name = "benchmark-${formatdate("YYYYMMDDhhmmss", timestamp())}" 8 | machine_type = "n1-highcpu-16" 9 | zone = "europe-north1-a" 10 | 11 | boot_disk { 12 | initialize_params { 13 | size = 128 14 | type = "pd-ssd" 15 | image = "ubuntu-2204-jammy-v20240927" 16 | } 17 | } 18 | 19 | network_interface { 20 | network = "default" 21 | access_config {} 22 | } 23 | 24 | metadata = { 25 | ssh-keys = "benchmark:${file(var.pub_key_path)}" 26 | } 27 | 28 | metadata_startup_script = "${file("startup.sh")}" 29 | } 30 | 31 | output "public_ip" { 32 | value = "${google_compute_instance.default.network_interface.0.access_config.0.nat_ip}" 33 | } 34 | 35 | variable "project" {} 36 | variable "pub_key_path" {} 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Three Dots Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/google" { 5 | version = "6.9.0" 6 | hashes = [ 7 | "h1:KUDx+m/KfVYyqsZDf0/h9JpWSFZXPWF+2rn37DBEC58=", 8 | "zh:53e9d7ffed63e2accff949ac7ca348d03be3e404e0b18c93ec596bcb52bac97a", 9 | "zh:6cbaf7e40fba2cff3d3fe4b3213de81c6157e327e996febad6949949b104b6ae", 10 | "zh:74562331eae7c88a8f934eb05971c361081c6e23d7a4564d1b11206558c037ed", 11 | "zh:ac65f1507886d92858ddeeff710c7dab942437c7421c63c1c7aeb139f2bb44af", 12 | "zh:b4a562b7c497661cd6c972097fea12449f183bb7e11bf6c62a750cc97bd3407e", 13 | "zh:b9cdc22e59c47604492bdf3e4d123037e2f5dd0f8a0ec0cf0b81fda165dd8581", 14 | "zh:c3f146d739b88de32339fb0091898bc9ad0aa3b257c8db4526076476c78e467a", 15 | "zh:c7567999d7563913360598bca4c9fce59f43e3a45726da4fa3f6d2752469c486", 16 | "zh:f3451c149844709713301641a5a2cfbf8add4abda927a457c5a2d67fa565887b", 17 | "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", 18 | "zh:f7df50451a534f59c472a190fc2e49caaed9ebdb22bd0b6e4fe2be423ce3a7a5", 19 | "zh:fd41e513b9546f2ab6b2cc36de36f4cd8ae3eb04ba3fca9b308ec6e387837366", 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /pkg/multiplier.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/hashicorp/go-multierror" 8 | "github.com/pkg/errors" 9 | 10 | "github.com/ThreeDotsLabs/watermill/message" 11 | ) 12 | 13 | type Constructor func() (message.Subscriber, error) 14 | 15 | type multiplier struct { 16 | subscriberConstructor func() (message.Subscriber, error) 17 | subscribersCount int 18 | subscribers []message.Subscriber 19 | } 20 | 21 | // TODO: Right now this is a copy-paste of Watermill's internal Multiplier 22 | func NewMultiplier(constructor Constructor, subscribersCount int) message.Subscriber { 23 | return &multiplier{ 24 | subscriberConstructor: constructor, 25 | subscribersCount: subscribersCount, 26 | } 27 | } 28 | 29 | func (s *multiplier) Subscribe(ctx context.Context, topic string) (msgs <-chan *message.Message, err error) { 30 | defer func() { 31 | if err != nil { 32 | if closeErr := s.Close(); closeErr != nil { 33 | err = multierror.Append(err, closeErr) 34 | } 35 | } 36 | }() 37 | 38 | out := make(chan *message.Message) 39 | 40 | subWg := sync.WaitGroup{} 41 | subWg.Add(s.subscribersCount) 42 | 43 | for i := 0; i < s.subscribersCount; i++ { 44 | sub, err := s.subscriberConstructor() 45 | if err != nil { 46 | return nil, errors.Wrap(err, "cannot create subscriber") 47 | } 48 | 49 | s.subscribers = append(s.subscribers, sub) 50 | 51 | msgs, err := sub.Subscribe(ctx, topic) 52 | if err != nil { 53 | return nil, errors.Wrap(err, "cannot subscribe") 54 | } 55 | 56 | go func() { 57 | for msg := range msgs { 58 | out <- msg 59 | } 60 | subWg.Done() 61 | }() 62 | } 63 | 64 | go func() { 65 | subWg.Wait() 66 | close(out) 67 | }() 68 | 69 | return out, nil 70 | } 71 | 72 | func (s *multiplier) Close() error { 73 | var err error 74 | 75 | for _, sub := range s.subscribers { 76 | if closeErr := sub.Close(); closeErr != nil { 77 | err = multierror.Append(err, closeErr) 78 | } 79 | } 80 | 81 | return err 82 | } 83 | -------------------------------------------------------------------------------- /pkg/publish.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | cryptoRand "crypto/rand" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | "github.com/oklog/ulid" 10 | 11 | "github.com/ThreeDotsLabs/watermill" 12 | "github.com/ThreeDotsLabs/watermill/message" 13 | ) 14 | 15 | func (ps PubSub) PublishMessages() (Results, error) { 16 | messagesLeft := ps.MessagesCount 17 | workers := 64 18 | 19 | wg := sync.WaitGroup{} 20 | wg.Add(workers) 21 | 22 | addMsg := make(chan *message.Message) 23 | 24 | for num := 0; num < workers; num++ { 25 | go func() { 26 | defer wg.Done() 27 | 28 | for msg := range addMsg { 29 | if err := ps.Publisher.Publish(ps.Topic, msg); err != nil { 30 | panic(err) 31 | } 32 | } 33 | }() 34 | } 35 | 36 | msgPayload, err := ps.payload() 37 | if err != nil { 38 | return Results{}, err 39 | } 40 | 41 | start := time.Now() 42 | 43 | var uuidFunc func() string 44 | if ps.UUIDFunc != nil { 45 | uuidFunc = ps.UUIDFunc 46 | } else { 47 | uuidFunc = watermill.NewULID 48 | } 49 | 50 | for ; messagesLeft > 0; messagesLeft-- { 51 | msg := message.NewMessage(uuidFunc(), msgPayload) 52 | addMsg <- msg 53 | } 54 | close(addMsg) 55 | 56 | wg.Wait() 57 | 58 | elapsed := time.Now().Sub(start) 59 | 60 | fmt.Printf("added %d messages in %s, %f msg/s\n", ps.MessagesCount, elapsed, float64(ps.MessagesCount)/elapsed.Seconds()) 61 | 62 | return Results{ 63 | Count: ps.MessagesCount, 64 | MessageSize: ps.MessageSize, 65 | MeanRate: float64(ps.MessagesCount) / elapsed.Seconds(), 66 | MeanThroughput: float64(ps.MessagesCount*ps.MessageSize) / elapsed.Seconds(), 67 | }, nil 68 | } 69 | 70 | func newBinaryULID() string { 71 | bytes, err := ulid.MustNew(ulid.Now(), cryptoRand.Reader).MarshalBinary() 72 | if err != nil { 73 | panic(err) 74 | } 75 | return string(bytes) 76 | } 77 | 78 | func (ps PubSub) payload() ([]byte, error) { 79 | msgPayload := make([]byte, ps.MessageSize) 80 | _, err := cryptoRand.Read(msgPayload) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | return msgPayload, nil 86 | } 87 | -------------------------------------------------------------------------------- /pkg/benchmark.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | "github.com/ThreeDotsLabs/watermill" 11 | "github.com/ThreeDotsLabs/watermill/message" 12 | ) 13 | 14 | type Results struct { 15 | Count int 16 | MessageSize int 17 | MeanRate float64 18 | MeanThroughput float64 19 | } 20 | 21 | // RunBenchmark runs benchmark on chosen pubsub and returns publishing and subscribing results. 22 | func RunBenchmark(pubSubName string, messagesCount int, messageSize int) (Results, Results, error) { 23 | topic := "benchmark_" + watermill.NewShortUUID() 24 | 25 | if err := initialise(pubSubName, topic); err != nil { 26 | return Results{}, Results{}, err 27 | } 28 | 29 | pubsub, err := NewPubSub(pubSubName, topic, messagesCount, messageSize) 30 | if err != nil { 31 | return Results{}, Results{}, err 32 | } 33 | 34 | pubResults, err := pubsub.PublishMessages() 35 | if err != nil { 36 | return Results{}, Results{}, err 37 | } 38 | 39 | var c *Counter 40 | 41 | doneChannel := make(chan struct{}) 42 | 43 | go func() { 44 | ticker := time.NewTicker(time.Second * 5) 45 | for { 46 | select { 47 | case <-doneChannel: 48 | return 49 | case <-ticker.C: 50 | if c != nil { 51 | fmt.Printf("processed: %d\n", c.count) 52 | } 53 | } 54 | } 55 | }() 56 | 57 | wg := sync.WaitGroup{} 58 | wg.Add(pubsub.MessagesCount) 59 | 60 | c = NewCounter() 61 | 62 | go func() { 63 | err := pubsub.ConsumeMessages(&wg, c) 64 | if err != nil { 65 | panic(err) 66 | } 67 | }() 68 | 69 | wg.Wait() 70 | 71 | mean := c.MeanPerSecond() 72 | 73 | doneChannel <- struct{}{} 74 | 75 | if err := pubsub.Close(); err != nil { 76 | return Results{}, Results{}, err 77 | } 78 | 79 | subResults := Results{ 80 | Count: int(c.Count()), 81 | MessageSize: pubsub.MessageSize, 82 | MeanRate: mean, 83 | MeanThroughput: mean * float64(pubsub.MessageSize), 84 | } 85 | 86 | return pubResults, subResults, nil 87 | } 88 | 89 | // It is required to create a subscriber for some PubSubs for initialisation. 90 | func initialise(pubSubName string, topic string) error { 91 | pubsub, err := NewPubSub(pubSubName, topic, 0, 0) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | if si, ok := pubsub.Subscriber.(message.SubscribeInitializer); ok && strings.Contains(pubSubName, "nats") { 97 | err = si.SubscribeInitialize(topic) 98 | if err != nil { 99 | return err 100 | } 101 | } 102 | if _, err := pubsub.Subscriber.Subscribe(context.Background(), topic); err != nil { 103 | return err 104 | } 105 | 106 | err = pubsub.Close() 107 | if err != nil { 108 | return err 109 | } 110 | 111 | return nil 112 | } 113 | -------------------------------------------------------------------------------- /compose/kafka-multinode.yml: -------------------------------------------------------------------------------- 1 | services: 2 | zookeeper: 3 | image: confluentinc/cp-zookeeper:5.0.1 4 | restart: unless-stopped 5 | logging: 6 | driver: none 7 | environment: 8 | ZOOKEEPER_SERVER_ID: 1 9 | ZOOKEEPER_CLIENT_PORT: "2181" 10 | ZOOKEEPER_TICK_TIME: "2000" 11 | 12 | kafka1: 13 | image: confluentinc/cp-kafka:latest 14 | restart: unless-stopped 15 | logging: 16 | driver: none 17 | ports: 18 | - 9091:9091 19 | depends_on: 20 | - zookeeper 21 | environment: 22 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 23 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka1:9091,PLAINTEXT_HOST://kafka1:29091 24 | KAFKA_BROKER_ID: 1 25 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3 26 | KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" 27 | KAFKA_NUM_PARTITIONS: 16 28 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT 29 | KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT 30 | 31 | kafka2: 32 | image: confluentinc/cp-kafka:latest 33 | restart: unless-stopped 34 | logging: 35 | driver: none 36 | ports: 37 | - 9092:9092 38 | depends_on: 39 | - zookeeper 40 | environment: 41 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 42 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka2:9092,PLAINTEXT_HOST://kafka2:29092 43 | KAFKA_BROKER_ID: 2 44 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3 45 | KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" 46 | KAFKA_NUM_PARTITIONS: 16 47 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT 48 | KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT 49 | 50 | kafka3: 51 | image: confluentinc/cp-kafka:latest 52 | restart: unless-stopped 53 | logging: 54 | driver: none 55 | ports: 56 | - 9093:9093 57 | depends_on: 58 | - zookeeper 59 | environment: 60 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 61 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka3:9093,PLAINTEXT_HOST://kafka3:29093 62 | KAFKA_BROKER_ID: 3 63 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3 64 | KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" 65 | KAFKA_NUM_PARTITIONS: 16 66 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT 67 | KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT 68 | 69 | kafka4: 70 | image: confluentinc/cp-kafka:latest 71 | restart: unless-stopped 72 | logging: 73 | driver: none 74 | ports: 75 | - 9094:9094 76 | depends_on: 77 | - zookeeper 78 | environment: 79 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 80 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka4:9094,PLAINTEXT_HOST://kafka4:29094 81 | KAFKA_BROKER_ID: 4 82 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3 83 | KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" 84 | KAFKA_NUM_PARTITIONS: 16 85 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT 86 | KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT 87 | 88 | kafka5: 89 | image: confluentinc/cp-kafka:latest 90 | restart: unless-stopped 91 | logging: 92 | driver: none 93 | ports: 94 | - 9095:9095 95 | depends_on: 96 | - zookeeper 97 | environment: 98 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 99 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka5:9095,PLAINTEXT_HOST://kafka5:29095 100 | KAFKA_BROKER_ID: 5 101 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3 102 | KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" 103 | KAFKA_NUM_PARTITIONS: 16 104 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT 105 | KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT 106 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/ThreeDotsLabs/watermill-benchmark/pkg" 11 | ) 12 | 13 | const ( 14 | defaultMessageSize = "16,64,256" 15 | ) 16 | 17 | var pubsubFlag = flag.String("pubsub", "", "") 18 | var messagesCount = flag.Int("count", 0, "") 19 | var messageSizes = flag.String("size", defaultMessageSize, "comma-separated list of message sizes") 20 | 21 | func main() { 22 | flag.Parse() 23 | sizes := strings.Split(*messageSizes, ",") 24 | 25 | var pubResults []pkg.Results 26 | var subResults []pkg.Results 27 | 28 | for _, size := range sizes { 29 | s, err := strconv.Atoi(size) 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | fmt.Printf("Starting benchmark for PubSub %s (%d messages, %d bytes each)\n", 35 | *pubsubFlag, *messagesCount, s) 36 | 37 | pubRes, subRes, err := pkg.RunBenchmark(*pubsubFlag, *messagesCount, s) 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | pubResults = append(pubResults, pubRes) 43 | subResults = append(subResults, subRes) 44 | } 45 | 46 | fmt.Printf("\n\n") 47 | fmt.Println(generateTable(pubResults, subResults)) 48 | } 49 | 50 | func padRight(str string, length int) string { 51 | if len(str) >= length { 52 | return str 53 | } 54 | return str + strings.Repeat(" ", length-len(str)) 55 | } 56 | 57 | func generateTable(pubResults, subResults []pkg.Results) string { 58 | headers := []string{ 59 | "Message size (bytes)", 60 | "Publish (messages / s)", 61 | "Subscribe (messages / s)", 62 | } 63 | 64 | // Calculate max width for each column based on headers and data 65 | colWidths := make([]int, len(headers)) 66 | for i, header := range headers { 67 | colWidths[i] = len(header) 68 | } 69 | 70 | // Check data widths for each column including thousand separators 71 | for i := 0; i < len(pubResults); i++ { 72 | // Column 1: Message size 73 | msgSizeWidth := len(fmt.Sprintf("%d", pubResults[i].MessageSize)) 74 | if msgSizeWidth > colWidths[0] { 75 | colWidths[0] = msgSizeWidth 76 | } 77 | 78 | // Column 2: Publish rate 79 | pubRateWidth := len(fmt.Sprintf("%d,%03d", int(pubResults[i].MeanRate)/1000, int(pubResults[i].MeanRate)%1000)) 80 | if pubRateWidth > colWidths[1] { 81 | colWidths[1] = pubRateWidth 82 | } 83 | 84 | // Column 3: Subscribe rate 85 | subRateWidth := len(fmt.Sprintf("%d,%03d", int(subResults[i].MeanRate)/1000, int(subResults[i].MeanRate)%1000)) 86 | if subRateWidth > colWidths[2] { 87 | colWidths[2] = subRateWidth 88 | } 89 | } 90 | 91 | var buf bytes.Buffer 92 | 93 | // Write header 94 | buf.WriteString("|") 95 | for i, header := range headers { 96 | buf.WriteString(" " + padRight(header, colWidths[i]) + " |") 97 | } 98 | buf.WriteString("\n") 99 | 100 | // Write separator 101 | buf.WriteString("|") 102 | for _, width := range colWidths { 103 | buf.WriteString("-" + strings.Repeat("-", width) + "-|") 104 | } 105 | buf.WriteString("\n") 106 | 107 | // Write data rows 108 | for i := 0; i < len(pubResults); i++ { 109 | buf.WriteString("|") 110 | 111 | // Message size (left-aligned) 112 | msgSize := fmt.Sprintf("%d", pubResults[i].MessageSize) 113 | buf.WriteString(" " + padRight(msgSize, colWidths[0]) + " |") 114 | 115 | // Publish rate (left-aligned) with thousand separator 116 | pubRate := fmt.Sprintf("%d,%03d", int(pubResults[i].MeanRate)/1000, int(pubResults[i].MeanRate)%1000) 117 | buf.WriteString(" " + padRight(pubRate, colWidths[1]) + " |") 118 | 119 | // Subscribe rate (left-aligned) with thousand separator 120 | subRate := fmt.Sprintf("%d,%03d", int(subResults[i].MeanRate)/1000, int(subResults[i].MeanRate)%1000) 121 | buf.WriteString(" " + padRight(subRate, colWidths[2]) + " |") 122 | 123 | buf.WriteString("\n") 124 | } 125 | 126 | return buf.String() 127 | } 128 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ThreeDotsLabs/watermill-benchmark 2 | 3 | go 1.23 4 | 5 | toolchain go1.23.0 6 | 7 | require ( 8 | github.com/Shopify/sarama v1.38.0 9 | github.com/ThreeDotsLabs/watermill v1.4.0 10 | github.com/ThreeDotsLabs/watermill-amqp/v3 v3.0.0 11 | github.com/ThreeDotsLabs/watermill-googlecloud v1.2.2 12 | github.com/ThreeDotsLabs/watermill-kafka/v3 v3.0.5 13 | github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.2 14 | github.com/ThreeDotsLabs/watermill-redisstream v1.4.2 15 | github.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0-rc.1 16 | github.com/go-sql-driver/mysql v1.4.1 17 | github.com/hashicorp/go-multierror v1.1.1 18 | github.com/lib/pq v1.10.2 19 | github.com/oklog/ulid v1.3.1 20 | github.com/pkg/errors v0.9.1 21 | github.com/redis/go-redis/v9 v9.6.1 22 | ) 23 | 24 | require ( 25 | cloud.google.com/go v0.115.1 // indirect 26 | cloud.google.com/go/auth v0.9.1 // indirect 27 | cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect 28 | cloud.google.com/go/compute/metadata v0.5.0 // indirect 29 | cloud.google.com/go/iam v1.2.0 // indirect 30 | cloud.google.com/go/pubsub v1.42.0 // indirect 31 | github.com/IBM/sarama v1.43.3 // indirect 32 | github.com/Rican7/retry v0.3.1 // indirect 33 | github.com/cenkalti/backoff/v3 v3.2.2 // indirect 34 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 35 | github.com/davecgh/go-spew v1.1.1 // indirect 36 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 37 | github.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 // indirect 38 | github.com/eapache/go-resiliency v1.7.0 // indirect 39 | github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect 40 | github.com/eapache/queue v1.1.0 // indirect 41 | github.com/felixge/httpsnoop v1.0.4 // indirect 42 | github.com/go-logr/logr v1.4.2 // indirect 43 | github.com/go-logr/stdr v1.2.2 // indirect 44 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 45 | github.com/golang/protobuf v1.5.4 // indirect 46 | github.com/golang/snappy v0.0.4 // indirect 47 | github.com/google/s2a-go v0.1.8 // indirect 48 | github.com/google/uuid v1.6.0 // indirect 49 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect 50 | github.com/googleapis/gax-go/v2 v2.13.0 // indirect 51 | github.com/hashicorp/errwrap v1.1.0 // indirect 52 | github.com/hashicorp/go-uuid v1.0.3 // indirect 53 | github.com/jcmturner/aescts/v2 v2.0.0 // indirect 54 | github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect 55 | github.com/jcmturner/gofork v1.7.6 // indirect 56 | github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect 57 | github.com/jcmturner/rpc/v2 v2.0.3 // indirect 58 | github.com/klauspost/compress v1.17.9 // indirect 59 | github.com/lithammer/shortuuid/v3 v3.0.7 // indirect 60 | github.com/nats-io/nats.go v1.37.0 // indirect 61 | github.com/nats-io/nkeys v0.4.7 // indirect 62 | github.com/nats-io/nuid v1.0.1 // indirect 63 | github.com/pierrec/lz4/v4 v4.1.21 // indirect 64 | github.com/rabbitmq/amqp091-go v1.10.0 // indirect 65 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect 66 | github.com/rogpeppe/go-internal v1.10.0 // indirect 67 | github.com/sony/gobreaker v1.0.0 // indirect 68 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect 69 | go.opencensus.io v0.24.0 // indirect 70 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect 71 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect 72 | go.opentelemetry.io/otel v1.29.0 // indirect 73 | go.opentelemetry.io/otel/metric v1.29.0 // indirect 74 | go.opentelemetry.io/otel/trace v1.29.0 // indirect 75 | golang.org/x/crypto v0.26.0 // indirect 76 | golang.org/x/net v0.28.0 // indirect 77 | golang.org/x/oauth2 v0.22.0 // indirect 78 | golang.org/x/sync v0.8.0 // indirect 79 | golang.org/x/sys v0.24.0 // indirect 80 | golang.org/x/text v0.17.0 // indirect 81 | golang.org/x/time v0.6.0 // indirect 82 | google.golang.org/api v0.194.0 // indirect 83 | google.golang.org/appengine v1.6.8 // indirect 84 | google.golang.org/genproto v0.0.0-20240823204242-4ba0660f739c // indirect 85 | google.golang.org/genproto/googleapis/api v0.0.0-20240823204242-4ba0660f739c // indirect 86 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c // indirect 87 | google.golang.org/grpc v1.65.0 // indirect 88 | google.golang.org/protobuf v1.34.2 // indirect 89 | ) 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Watermill Benchmark 2 | 3 | 4 | This is a set of tools for benchmarking [watermill](https://github.com/ThreeDotsLabs/watermill). 5 | 6 | **Warning:** This tool is meant to provide a rough estimate on how fast each Pub/Sub can process messages. 7 | It uses very simplified infrastructure to set things up and default configurations. 8 | 9 | Keep in mind that final performance depends on multiple factors. 10 | 11 | **It's not meant to be a definitive answer on which Pub/Sub is the fastest.** 12 | It should give you an idea of the ballpark performance you can expect. 13 | 14 | ## How it works 15 | 16 | * All tests are run on a single 16 CPU GCloud compute instance (`n1-highcpu-16`). 17 | * Docker Compose is used to run Pub/Sub infrastructure and benchmark code (except for Google Cloud Pub/Sub). 18 | * The tool will first produce a big number of messages on a generated topic. 19 | * Then it will subscribe to the topic and consume all of the messages. 20 | * Multiple message sizes can be chosen (by default: 16, 64 and 256 bytes). 21 | 22 | ## Results (as of 18 November 2024) 23 | 24 | ### Kafka (one node) 25 | 26 | | Message size (bytes) | Publish (messages / s) | Subscribe (messages / s) | 27 | |----------------------|------------------------|--------------------------| 28 | | 16 | 41,492 | 101,669 | 29 | | 64 | 40,189 | 106,264 | 30 | | 256 | 40,044 | 107,278 | 31 | 32 | ### NATS Jetstream (16 Subscribers) 33 | 34 | | Message size (bytes) | Publish (messages / s) | Subscribe (messages / s) | Subscribe (messages / s - async ack) | 35 | |----------------------|------------------------|--------------------------|--------------------------------------| 36 | | 16 | 50,668 | 34,713 | 59,728 | 37 | | 64 | 49,204 | 34,561 | 59,743 | 38 | | 256 | 48,242 | 34,097 | 59,385 | 39 | 40 | ### NATS Jetstream (48 Subscribers) 41 | 42 | | Message size (bytes) | Publish (messages / s) | Subscribe (messages /s ) | Subscribe (messages / s - async ack) | 43 | |----------------------|------------------------|--------------------------|--------------------------------------| 44 | | 16 | 50,680 | 46,377 | 86,348 | 45 | | 64 | 49,341 | 46,307 | 86,078 | 46 | | 256 | 48,744 | 46,035 | 86,499 | 47 | 48 | ### Redis 49 | 50 | | Message size (bytes) | Publish (messages / s) | Subscribe (messages / s) | 51 | |----------------------|------------------------|--------------------------| 52 | | 16 | 59,158 | 12,134 | 53 | | 64 | 58,988 | 12,392 | 54 | | 256 | 58,038 | 12,133 | 55 | 56 | ### SQL (MySQL) 57 | 58 | | Message size (bytes) | Publish (messages / s) | Subscribe (messages / s - batch size = 1) | Subscribe (messages / s - batch size = 100) | 59 | |----------------------|------------------------|-------------------------------------------|---------------------------------------------| 60 | | 16 | 6,371 | 283 | 2,794 | 61 | | 64 | 9,887 | 281 | 2,637 | 62 | | 256 | 9,596 | 271 | 2,766 | 63 | 64 | ### SQL (PostgreSQL) 65 | 66 | | Message size (bytes) | Publish (messages / s) | Subscribe (messages / s - batch size = 1) | Subscribe (messages / s - batch size = 100) | 67 | |----------------------|------------------------|-------------------------------------------|---------------------------------------------| 68 | | 16 | 2,552 | 122 | 9,460 | 69 | | 64 | 2,831 | 118 | 9,045 | 70 | | 256 | 2,744 | 104 | 7,843 | 71 | 72 | ### SQL (PostgreSQL Queue) 73 | 74 | | Subscribe Batch Size | Message size (bytes) | Publish (messages / s) | Subscribe (messages / s - batch size = 1) | Subscribe (messages / s - batch size = 100) | 75 | |----------------------|----------------------|------------------------|--------------------------------------------|---------------------------------------------| 76 | | 100 | 16 | 2,825 | 146 | 10,466 | 77 | | 100 | 64 | 2,842 | 147 | 9,626 | 78 | | 100 | 256 | 2,845 | 138 | 8,276 | 79 | 80 | | Message size (bytes) | Publish (messages / s) | Subscribe (messages / s) | 81 | |----------------------|------------------------|--------------------------| 82 | | 16 | 2,825 | 0,146 | 83 | | 64 | 2,842 | 0,147 | 84 | | 256 | 2,845 | 0,138 | 85 | 86 | ### Google Cloud Pub/Sub (16 subscribers) 87 | 88 | | Message size (bytes) | Publish (messages / s) | Subscribe (messages / s) | 89 | |----------------------|------------------------|--------------------------| 90 | | 16 | 3,027 | 28,589 | 91 | | 64 | 3,020 | 31,057 | 92 | | 256 | 2,918 | 32,722 | 93 | 94 | ### AMQP (RabbitMQ, 16 subscribers) 95 | 96 | | Message size (bytes) | Publish (messages / s) | Subscribe (messages / s) | 97 | |----------------------|------------------------|--------------------------| 98 | | 16 | 2,770 | 14,604 | 99 | | 64 | 2,752 | 12,128 | 100 | | 256 | 2,750 | 8,550 | 101 | 102 | ### GoChannel 103 | 104 | | Message size (bytes) | Publish (messages / s) | Subscribe (messages / s) | 105 | |----------------------|------------------------|--------------------------| 106 | | 16 | 315,776 | 138,743 | 107 | | 64 | 325,341 | 163,034 | 108 | | 256 | 341,223 | 145,718 | 109 | 110 | ## VM Setup 111 | 112 | The project includes [Terraform](https://www.terraform.io/) definition for setting up an instance on Google Cloud Platform. 113 | 114 | It will spin up a fresh Ubuntu 19.04 instance, install docker with dependencies and clone this repository. 115 | 116 | Set environment variables: 117 | 118 | ```bash 119 | # project name on GCP 120 | TF_VAR_project= 121 | # public part of the key that you will use to access SSH 122 | TF_VAR_pub_key_path= 123 | ``` 124 | 125 | Create the VM: 126 | 127 | ```bash 128 | cd setup 129 | terraform apply 130 | ``` 131 | 132 | The command will output the public IP address of the server. Use ssh with user `benchmark` to access it. 133 | 134 | After running all benchmarks, destroy the VM: 135 | 136 | ```bash 137 | terraform destroy 138 | ``` 139 | 140 | ## Configuration 141 | 142 | ### Google Pub/Sub 143 | 144 | Set environment variables in `compose/.env`: 145 | 146 | ```bash 147 | # path to json file within the project with GCP credentials 148 | GOOGLE_APPLICATION_CREDENTIALS=compose/key.json 149 | # project name on GCP 150 | GOOGLE_CLOUD_PROJECT= 151 | ``` 152 | 153 | ## Running 154 | 155 | Run benchmarks with: 156 | 157 | ```bash 158 | ./run.sh 159 | ``` 160 | -------------------------------------------------------------------------------- /pkg/pubsub.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | stdSQL "database/sql" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/Shopify/sarama" 12 | "github.com/ThreeDotsLabs/watermill" 13 | "github.com/ThreeDotsLabs/watermill-amqp/v3/pkg/amqp" 14 | "github.com/ThreeDotsLabs/watermill-googlecloud/pkg/googlecloud" 15 | "github.com/ThreeDotsLabs/watermill-kafka/v3/pkg/kafka" 16 | "github.com/ThreeDotsLabs/watermill-nats/v2/pkg/nats" 17 | "github.com/ThreeDotsLabs/watermill-redisstream/pkg/redisstream" 18 | "github.com/ThreeDotsLabs/watermill-sql/v4/pkg/sql" 19 | "github.com/ThreeDotsLabs/watermill/message" 20 | "github.com/ThreeDotsLabs/watermill/pubsub/gochannel" 21 | driver "github.com/go-sql-driver/mysql" 22 | _ "github.com/lib/pq" 23 | "github.com/redis/go-redis/v9" 24 | ) 25 | 26 | const ( 27 | defaultMessagesCount = 1000000 28 | ) 29 | 30 | var logger = watermill.NopLogger{} 31 | 32 | type PubSub struct { 33 | Publisher message.Publisher 34 | Subscriber message.Subscriber 35 | 36 | MessagesCount int 37 | MessageSize int 38 | 39 | Topic string 40 | 41 | UUIDFunc func() string 42 | } 43 | 44 | func NewPubSub(name string, topic string, messagesCount int, messageSize int) (PubSub, error) { 45 | definition, ok := pubSubDefinitions[name] 46 | if !ok { 47 | return PubSub{}, fmt.Errorf("unknown PubSub: %s", name) 48 | } 49 | 50 | pub, sub := definition.Constructor() 51 | 52 | if messagesCount == 0 { 53 | if definition.MessagesCount != 0 { 54 | messagesCount = definition.MessagesCount 55 | } else { 56 | messagesCount = defaultMessagesCount 57 | } 58 | } 59 | 60 | return PubSub{ 61 | Publisher: pub, 62 | Subscriber: sub, 63 | 64 | MessagesCount: messagesCount, 65 | MessageSize: messageSize, 66 | Topic: topic, 67 | 68 | UUIDFunc: definition.UUIDFunc, 69 | }, nil 70 | } 71 | 72 | func (ps PubSub) Close() error { 73 | if err := ps.Publisher.Close(); err != nil { 74 | return err 75 | } 76 | return ps.Subscriber.Close() 77 | } 78 | 79 | type PubSubDefinition struct { 80 | MessagesCount int 81 | UUIDFunc func() string 82 | Constructor func() (message.Publisher, message.Subscriber) 83 | } 84 | 85 | var pubSubDefinitions = map[string]PubSubDefinition{ 86 | "gochannel": { 87 | Constructor: func() (message.Publisher, message.Subscriber) { 88 | pubsub := gochannel.NewGoChannel(gochannel.Config{ 89 | Persistent: true, 90 | }, logger) 91 | return pubsub, pubsub 92 | }, 93 | }, 94 | "kafka": { 95 | MessagesCount: 5000000, 96 | Constructor: kafkaConstructor([]string{"kafka:9092"}), 97 | }, 98 | "kafka-multinode": { 99 | MessagesCount: 5000000, 100 | Constructor: kafkaConstructor([]string{"kafka1:9091", "kafka2:9092", "kafka3:9093", "kafka4:9094", "kafka5:9095"}), 101 | }, 102 | "nats-jetstream": { 103 | MessagesCount: 5000000, 104 | Constructor: func() (message.Publisher, message.Subscriber) { 105 | natsURL := os.Getenv("WATERMILL_NATS_URL") 106 | if natsURL == "" { 107 | natsURL = "nats://nats:4222" 108 | } 109 | 110 | jsConfig := nats.JetStreamConfig{ 111 | AutoProvision: false, 112 | } 113 | 114 | ackAsyncString := os.Getenv("WATERMILL_NATS_ACK_ASYNC") 115 | if strings.EqualFold(ackAsyncString, "true") { 116 | jsConfig.AckAsync = true 117 | } 118 | 119 | pub, err := nats.NewPublisher(nats.PublisherConfig{ 120 | URL: natsURL, 121 | Marshaler: &nats.NATSMarshaler{}, 122 | JetStream: jsConfig, 123 | }, logger) 124 | if err != nil { 125 | panic(err) 126 | } 127 | 128 | sub, err := nats.NewSubscriber(nats.SubscriberConfig{ 129 | URL: natsURL, 130 | QueueGroupPrefix: "benchmark", 131 | SubscribersCount: subscribersCount(), 132 | Unmarshaler: &nats.NATSMarshaler{}, 133 | JetStream: jsConfig, 134 | }, logger) 135 | if err != nil { 136 | panic(err) 137 | } 138 | 139 | return pub, sub 140 | }, 141 | }, 142 | 143 | "googlecloud": { 144 | Constructor: func() (message.Publisher, message.Subscriber) { 145 | pub, err := googlecloud.NewPublisher( 146 | googlecloud.PublisherConfig{ 147 | ProjectID: os.Getenv("GOOGLE_CLOUD_PROJECT"), 148 | Marshaler: googlecloud.DefaultMarshalerUnmarshaler{}, 149 | }, logger, 150 | ) 151 | if err != nil { 152 | panic(err) 153 | } 154 | 155 | sub := NewMultiplier( 156 | func() (message.Subscriber, error) { 157 | subscriber, err := googlecloud.NewSubscriber( 158 | googlecloud.SubscriberConfig{ 159 | ProjectID: os.Getenv("GOOGLE_CLOUD_PROJECT"), 160 | GenerateSubscriptionName: func(topic string) string { 161 | return topic 162 | }, 163 | Unmarshaler: googlecloud.DefaultMarshalerUnmarshaler{}, 164 | }, 165 | logger, 166 | ) 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | return subscriber, nil 172 | }, subscribersCount(), 173 | ) 174 | 175 | return pub, sub 176 | }, 177 | }, 178 | "mysql": { 179 | MessagesCount: 30000, 180 | UUIDFunc: newBinaryULID, 181 | Constructor: func() (message.Publisher, message.Subscriber) { 182 | conf := driver.NewConfig() 183 | conf.Net = "tcp" 184 | conf.User = "root" 185 | conf.Addr = "mysql" 186 | conf.DBName = "watermill" 187 | 188 | db, err := stdSQL.Open("mysql", conf.FormatDSN()) 189 | if err != nil { 190 | panic(err) 191 | } 192 | 193 | err = db.Ping() 194 | if err != nil { 195 | panic(err) 196 | } 197 | 198 | pub, err := sql.NewPublisher( 199 | db, 200 | sql.PublisherConfig{ 201 | AutoInitializeSchema: true, 202 | SchemaAdapter: MySQLSchema{}, 203 | }, 204 | logger, 205 | ) 206 | if err != nil { 207 | panic(err) 208 | } 209 | 210 | sub, err := sql.NewSubscriber( 211 | db, 212 | sql.SubscriberConfig{ 213 | SchemaAdapter: MySQLSchema{}, 214 | OffsetsAdapter: sql.DefaultMySQLOffsetsAdapter{}, 215 | ConsumerGroup: watermill.NewULID(), 216 | InitializeSchema: true, 217 | }, 218 | logger, 219 | ) 220 | if err != nil { 221 | panic(err) 222 | } 223 | 224 | return pub, sub 225 | }, 226 | }, 227 | "postgresql": { 228 | MessagesCount: 30000, 229 | UUIDFunc: watermill.NewUUID, 230 | Constructor: func() (message.Publisher, message.Subscriber) { 231 | dsn := "postgres://watermill:password@postgres:5432/watermill?sslmode=disable" 232 | db, err := stdSQL.Open("postgres", dsn) 233 | if err != nil { 234 | panic(err) 235 | } 236 | 237 | err = db.Ping() 238 | if err != nil { 239 | panic(err) 240 | } 241 | 242 | pub, err := sql.NewPublisher( 243 | db, 244 | sql.PublisherConfig{ 245 | AutoInitializeSchema: true, 246 | SchemaAdapter: PostgreSQLSchema{}, 247 | }, 248 | logger, 249 | ) 250 | if err != nil { 251 | panic(err) 252 | } 253 | 254 | sub, err := sql.NewSubscriber( 255 | db, 256 | sql.SubscriberConfig{ 257 | SchemaAdapter: PostgreSQLSchema{}, 258 | OffsetsAdapter: sql.DefaultPostgreSQLOffsetsAdapter{}, 259 | ConsumerGroup: watermill.NewULID(), 260 | InitializeSchema: true, 261 | }, 262 | logger, 263 | ) 264 | if err != nil { 265 | panic(err) 266 | } 267 | 268 | return pub, sub 269 | }, 270 | }, 271 | "postgresql-queue": { 272 | MessagesCount: 30000, 273 | UUIDFunc: watermill.NewUUID, 274 | Constructor: func() (message.Publisher, message.Subscriber) { 275 | dsn := "postgres://watermill:password@postgres:5432/watermill?sslmode=disable" 276 | db, err := stdSQL.Open("postgres", dsn) 277 | if err != nil { 278 | panic(err) 279 | } 280 | 281 | err = db.Ping() 282 | if err != nil { 283 | panic(err) 284 | } 285 | 286 | pub, err := sql.NewPublisher( 287 | db, 288 | sql.PublisherConfig{ 289 | AutoInitializeSchema: true, 290 | SchemaAdapter: sql.PostgreSQLQueueSchema{ 291 | GeneratePayloadType: func(topic string) string { 292 | return "BYTEA" 293 | }, 294 | }, 295 | }, 296 | logger, 297 | ) 298 | if err != nil { 299 | panic(err) 300 | } 301 | 302 | sub, err := sql.NewSubscriber( 303 | db, 304 | sql.SubscriberConfig{ 305 | SchemaAdapter: sql.PostgreSQLQueueSchema{ 306 | GeneratePayloadType: func(topic string) string { 307 | return "BYTEA" 308 | }, 309 | }, 310 | OffsetsAdapter: sql.PostgreSQLQueueOffsetsAdapter{}, 311 | InitializeSchema: true, 312 | }, 313 | logger, 314 | ) 315 | if err != nil { 316 | panic(err) 317 | } 318 | 319 | return pub, sub 320 | }, 321 | }, 322 | "amqp": { 323 | MessagesCount: 100000, 324 | Constructor: func() (message.Publisher, message.Subscriber) { 325 | config := amqp.NewDurablePubSubConfig( 326 | "amqp://rabbitmq:5672", 327 | func(topic string) string { 328 | return topic 329 | }, 330 | ) 331 | 332 | pub, err := amqp.NewPublisher(config, logger) 333 | if err != nil { 334 | panic(err) 335 | } 336 | 337 | sub := NewMultiplier( 338 | func() (message.Subscriber, error) { 339 | sub, err := amqp.NewSubscriber(config, logger) 340 | if err != nil { 341 | panic(err) 342 | } 343 | return sub, nil 344 | }, subscribersCount(), 345 | ) 346 | 347 | return pub, sub 348 | }, 349 | }, 350 | "redis": { 351 | Constructor: func() (message.Publisher, message.Subscriber) { 352 | subClient := redis.NewClient(&redis.Options{ 353 | Addr: "redis:6379", 354 | DB: 0, 355 | }) 356 | subscriber, err := redisstream.NewSubscriber( 357 | redisstream.SubscriberConfig{ 358 | Client: subClient, 359 | Unmarshaller: redisstream.DefaultMarshallerUnmarshaller{}, 360 | ConsumerGroup: "test_consumer_group", 361 | }, 362 | watermill.NewStdLogger(false, false), 363 | ) 364 | if err != nil { 365 | panic(err) 366 | } 367 | 368 | pubClient := redis.NewClient(&redis.Options{ 369 | Addr: "redis:6379", 370 | DB: 0, 371 | }) 372 | publisher, err := redisstream.NewPublisher( 373 | redisstream.PublisherConfig{ 374 | Client: pubClient, 375 | Marshaller: redisstream.DefaultMarshallerUnmarshaller{}, 376 | }, 377 | watermill.NewStdLogger(false, false), 378 | ) 379 | if err != nil { 380 | panic(err) 381 | } 382 | 383 | return publisher, subscriber 384 | }, 385 | }, 386 | } 387 | 388 | func subscribersCount() int { 389 | var ( 390 | mult = 1 391 | err error 392 | ) 393 | if ev := os.Getenv("SUBSCRIBER_CPU_MULTIPLIER"); ev != "" { 394 | mult, err = strconv.Atoi(ev) 395 | if err != nil { 396 | panic(fmt.Sprintf("invalid SUBSCRIBER_CPU_MULTIPLIER: %s", err.Error())) 397 | } 398 | } 399 | return runtime.NumCPU() * mult 400 | } 401 | 402 | func kafkaConstructor(brokers []string) func() (message.Publisher, message.Subscriber) { 403 | return func() (message.Publisher, message.Subscriber) { 404 | publisher, err := kafka.NewPublisher( 405 | kafka.PublisherConfig{ 406 | Brokers: brokers, 407 | Marshaler: kafka.DefaultMarshaler{}, 408 | }, 409 | logger, 410 | ) 411 | if err != nil { 412 | panic(err) 413 | } 414 | 415 | saramaConfig := kafka.DefaultSaramaSubscriberConfig() 416 | saramaConfig.Consumer.Offsets.Initial = sarama.OffsetOldest 417 | 418 | subscriber, err := kafka.NewSubscriber( 419 | kafka.SubscriberConfig{ 420 | Brokers: brokers, 421 | Unmarshaler: kafka.DefaultMarshaler{}, 422 | OverwriteSaramaConfig: saramaConfig, 423 | ConsumerGroup: "benchmark", 424 | }, 425 | logger, 426 | ) 427 | if err != nil { 428 | panic(err) 429 | } 430 | 431 | return publisher, subscriber 432 | } 433 | } 434 | 435 | type MySQLSchema struct { 436 | sql.DefaultMySQLSchema 437 | } 438 | 439 | func (m MySQLSchema) SchemaInitializingQueries(params sql.SchemaInitializingQueriesParams) ([]sql.Query, error) { 440 | createMessagesTable := strings.Join([]string{ 441 | "CREATE TABLE IF NOT EXISTS " + m.MessagesTable(params.Topic) + " (", 442 | "`offset` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,", 443 | "`uuid` BINARY(16) NOT NULL,", 444 | "`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,", 445 | "`payload` BLOB DEFAULT NULL,", 446 | "`metadata` JSON DEFAULT NULL", 447 | ");", 448 | }, "\n") 449 | 450 | return []sql.Query{{Query: createMessagesTable}}, nil 451 | } 452 | 453 | type PostgreSQLSchema struct { 454 | sql.DefaultPostgreSQLSchema 455 | } 456 | 457 | func (p PostgreSQLSchema) SchemaInitializingQueries(params sql.SchemaInitializingQueriesParams) ([]sql.Query, error) { 458 | createMessagesTable := ` 459 | CREATE TABLE IF NOT EXISTS ` + p.MessagesTable(params.Topic) + ` ( 460 | "offset" BIGSERIAL, 461 | "uuid" UUID NOT NULL, 462 | "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 463 | "payload" BYTEA DEFAULT NULL, 464 | "metadata" JSON DEFAULT NULL, 465 | "transaction_id" xid8 NOT NULL, 466 | PRIMARY KEY ("transaction_id", "offset") 467 | ); 468 | ` 469 | 470 | return []sql.Query{{Query: createMessagesTable}}, nil 471 | } 472 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ= 3 | cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc= 4 | cloud.google.com/go/auth v0.9.1 h1:+pMtLEV2k0AXKvs/tGZojuj6QaioxfUjOpMsG5Gtx+w= 5 | cloud.google.com/go/auth v0.9.1/go.mod h1:Sw8ocT5mhhXxFklyhT12Eiy0ed6tTrPMCJjSI8KhYLk= 6 | cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= 7 | cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= 8 | cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= 9 | cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= 10 | cloud.google.com/go/iam v1.2.0 h1:kZKMKVNk/IsSSc/udOb83K0hL/Yh/Gcqpz+oAkoIFN8= 11 | cloud.google.com/go/iam v1.2.0/go.mod h1:zITGuWgsLZxd8OwAlX+eMFgZDXzBm7icj1PVTYG766Q= 12 | cloud.google.com/go/kms v1.18.5 h1:75LSlVs60hyHK3ubs2OHd4sE63OAMcM2BdSJc2bkuM4= 13 | cloud.google.com/go/kms v1.18.5/go.mod h1:yXunGUGzabH8rjUPImp2ndHiGolHeWJJ0LODLedicIY= 14 | cloud.google.com/go/longrunning v0.5.12 h1:5LqSIdERr71CqfUsFlJdBpOkBH8FBCFD7P1nTWy3TYE= 15 | cloud.google.com/go/longrunning v0.5.12/go.mod h1:S5hMV8CDJ6r50t2ubVJSKQVv5u0rmik5//KgLO3k4lU= 16 | cloud.google.com/go/pubsub v1.42.0 h1:PVTbzorLryFL5ue8esTS2BfehUs0ahyNOY9qcd+HMOs= 17 | cloud.google.com/go/pubsub v1.42.0/go.mod h1:KADJ6s4MbTwhXmse/50SebEhE4SmUwHi48z3/dHar1Y= 18 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 19 | github.com/IBM/sarama v1.43.3 h1:Yj6L2IaNvb2mRBop39N7mmJAHBVY3dTPncr3qGVkxPA= 20 | github.com/IBM/sarama v1.43.3/go.mod h1:FVIRaLrhK3Cla/9FfRF5X9Zua2KpS3SYIXxhac1H+FQ= 21 | github.com/Rican7/retry v0.3.1 h1:scY4IbO8swckzoA/11HgBwaZRJEyY9vaNJshcdhp1Mc= 22 | github.com/Rican7/retry v0.3.1/go.mod h1:CxSDrhAyXmTMeEuRAnArMu1FHu48vtfjLREWqVl7Vw0= 23 | github.com/Shopify/sarama v1.38.0 h1:Q81EWxDT2Xs7kCaaiDGV30GyNCWd6K1Xmd4k2qpTWE8= 24 | github.com/Shopify/sarama v1.38.0/go.mod h1:djdek3V4gS0N9LZ+OhfuuM6rE1bEKeDffYY8UvsRNyM= 25 | github.com/Shopify/toxiproxy/v2 v2.5.0 h1:i4LPT+qrSlKNtQf5QliVjdP08GyAH8+BUIc9gT0eahc= 26 | github.com/Shopify/toxiproxy/v2 v2.5.0/go.mod h1:yhM2epWtAmel9CB8r2+L+PCmhH6yH2pITaPAo7jxJl0= 27 | github.com/ThreeDotsLabs/watermill v1.4.0 h1:c8T4QHY/MuxSXYQ1Cxn93cCZB5lkGgqhYA6L2jh2ghA= 28 | github.com/ThreeDotsLabs/watermill v1.4.0/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to= 29 | github.com/ThreeDotsLabs/watermill-amqp/v3 v3.0.0 h1:r5idq2qkd3M345iv3C3zAX+lFlEu7iW8QESNnuuv4eY= 30 | github.com/ThreeDotsLabs/watermill-amqp/v3 v3.0.0/go.mod h1:+8tCh6VCuBcQWhfETCwzRINKQ1uyeg9moH3h7jMKxQk= 31 | github.com/ThreeDotsLabs/watermill-googlecloud v1.2.2 h1:x194AUp/6h/thK6Tc2gITP0JhwV/g4JHpL91Y1dDeMA= 32 | github.com/ThreeDotsLabs/watermill-googlecloud v1.2.2/go.mod h1:sMU+5UoRRO1m/LBxju7tnwDCj7L/3IKwP9hjNSDYaOs= 33 | github.com/ThreeDotsLabs/watermill-kafka/v3 v3.0.5 h1:ud+4txnRgtr3kZXfXZ5+C7kVQEvsLc5HSNUEa0g+X1Q= 34 | github.com/ThreeDotsLabs/watermill-kafka/v3 v3.0.5/go.mod h1:t4o+4A6GB+XC8WL3DandhzPwd265zQuyWMQC/I+WIOU= 35 | github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.2 h1:9d7Vb2gepq73Rn/aKaAJWbBiJzS6nDyOm4O353jVsTM= 36 | github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.2/go.mod h1:stjbT+s4u/s5ime5jdIyvPyjBGwGeJewIN7jxH8gp4k= 37 | github.com/ThreeDotsLabs/watermill-redisstream v1.4.2 h1:FY6tsBcbhbJpKDOssU4bfybstqY0hQHwiZmVq9qyILQ= 38 | github.com/ThreeDotsLabs/watermill-redisstream v1.4.2/go.mod h1:69++855LyB+ckYDe60PiJLBcUrpckfDE2WwyzuVJRCk= 39 | github.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0-rc.1 h1:uYfnh1EoqXrzHu+bX/TboRyv4ou+EFcmkC1MABeQ0lI= 40 | github.com/ThreeDotsLabs/watermill-sql/v4 v4.0.0-rc.1/go.mod h1:ttA/lhzSh0YyDkosq1Cgc7IYz6Arrba0jIWfdnON0WA= 41 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 42 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 43 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 44 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 45 | github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= 46 | github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= 47 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 48 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 49 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 50 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 51 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 52 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 53 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 54 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 55 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 56 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 57 | github.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 h1:R2zQhFwSCyyd7L43igYjDrH0wkC/i+QBPELuY0HOu84= 58 | github.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0/go.mod h1:2MqLKYJfjs3UriXXF9Fd0Qmh/lhxi/6tHXkqtXxyIHc= 59 | github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA= 60 | github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= 61 | github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= 62 | github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= 63 | github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= 64 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 65 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 66 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 67 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 68 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 69 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 70 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 71 | github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= 72 | github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= 73 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 74 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 75 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 76 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 77 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 78 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= 79 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 80 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 81 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 82 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 83 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 84 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 85 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 86 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 87 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 88 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 89 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 90 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 91 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 92 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 93 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 94 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 95 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 96 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 97 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 98 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 99 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 100 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 101 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 102 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 103 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 104 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 105 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 106 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 107 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 108 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 109 | github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= 110 | github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= 111 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 112 | github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 113 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 114 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 115 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= 116 | github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= 117 | github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= 118 | github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= 119 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 120 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 121 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 122 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 123 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 124 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 125 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 126 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 127 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 128 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 129 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 130 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 131 | github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= 132 | github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= 133 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 134 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 135 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 136 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 137 | github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= 138 | github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 139 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 140 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 141 | github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= 142 | github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 143 | github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU= 144 | github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= 145 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= 146 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 147 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= 148 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 149 | github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= 150 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 151 | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= 152 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 153 | github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= 154 | github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= 155 | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= 156 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 157 | github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= 158 | github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 159 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 160 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 161 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 162 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 163 | github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= 164 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 165 | github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8= 166 | github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts= 167 | github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE= 168 | github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= 169 | github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= 170 | github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= 171 | github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= 172 | github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= 173 | github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= 174 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 175 | github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= 176 | github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 177 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 178 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 179 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 180 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 181 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 182 | github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= 183 | github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= 184 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= 185 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 186 | github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= 187 | github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= 188 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 189 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 190 | github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ= 191 | github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= 192 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 193 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 194 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 195 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 196 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 197 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 198 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 199 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 200 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 201 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 202 | github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= 203 | github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 204 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 205 | go.einride.tech/aip v0.67.1 h1:d/4TW92OxXBngkSOwWS2CH5rez869KpKMaN44mdxkFI= 206 | go.einride.tech/aip v0.67.1/go.mod h1:ZGX4/zKw8dcgzdLsrvpOOGxfxI2QSk12SlP7d6c0/XI= 207 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 208 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 209 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= 210 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= 211 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= 212 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= 213 | go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= 214 | go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= 215 | go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= 216 | go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= 217 | go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= 218 | go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= 219 | go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= 220 | go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= 221 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 222 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 223 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 224 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 225 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 226 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 227 | golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= 228 | golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 229 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 230 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 231 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 232 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 233 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 234 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 235 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 236 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 237 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 238 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 239 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 240 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 241 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 242 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 243 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 244 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 245 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 246 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 247 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 248 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 249 | golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= 250 | golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 251 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 252 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 253 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 254 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 255 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 256 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 257 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 258 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 259 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 260 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 261 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 262 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 263 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 264 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 265 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 266 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 267 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 268 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 269 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 270 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 271 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 272 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 273 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 274 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 275 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 276 | golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= 277 | golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 278 | golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= 279 | golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 280 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 281 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 282 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 283 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 284 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 285 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 286 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 287 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 288 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 289 | google.golang.org/api v0.194.0 h1:dztZKG9HgtIpbI35FhfuSNR/zmaMVdxNlntHj1sIS4s= 290 | google.golang.org/api v0.194.0/go.mod h1:AgvUFdojGANh3vI+P7EVnxj3AISHllxGCJSFmggmnd0= 291 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 292 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 293 | google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= 294 | google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= 295 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 296 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 297 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 298 | google.golang.org/genproto v0.0.0-20240823204242-4ba0660f739c h1:TYOEhrQMrNDTAd2rX9m+WgGr8Ku6YNuj1D7OX6rWSok= 299 | google.golang.org/genproto v0.0.0-20240823204242-4ba0660f739c/go.mod h1:2rC5OendXvZ8wGEo/cSLheztrZDZaSoHanUcd1xtZnw= 300 | google.golang.org/genproto/googleapis/api v0.0.0-20240823204242-4ba0660f739c h1:e0zB268kOca6FbuJkYUGxfwG4DKFZG/8DLyv9Zv66cE= 301 | google.golang.org/genproto/googleapis/api v0.0.0-20240823204242-4ba0660f739c/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= 302 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c h1:Kqjm4WpoWvwhMPcrAczoTyMySQmYa9Wy2iL6Con4zn8= 303 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 304 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 305 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 306 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 307 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 308 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 309 | google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= 310 | google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= 311 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 312 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 313 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 314 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 315 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 316 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 317 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 318 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 319 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 320 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 321 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 322 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 323 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 324 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 325 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 326 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 327 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 328 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 329 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 330 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 331 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 332 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 333 | --------------------------------------------------------------------------------