How do I send Protobuf Messages via a Kafka Producer - go

I am using the Sarama Library to send messages through a Producer.
This allows me to send strings. My goal is to send Protobuf Messages
msg := &sarama.ProducerMessage{
Topic: *topic,
Value: sarama.StringEncoder(content),
}
This is a sample proto class that I have
message Pixel {
// Session identifier stuff
int64 timestamp = 1; // Milliseconds from the epoch
string session_id = 2; // Unique Identifier... for parent level0top
string client_name = 3; // Client-name/I-key
string ip = 10;
repeated string ip_list = 11;
string datacenter = 12;
string proxy_type = 13;
Please can you provide me an example of how I can send protobuf messages.

You need to use proto#Marshal and sarama#ByteEncoder on producer side and proto#Unmarshal on consumer side.
Producer:
pixelToSend := &pixel.Pixel{SessionId: t.String()}
pixelToSendBytes, err := proto.Marshal(pixelToSend)
if err != nil {
log.Fatalln("Failed to marshal pixel:", err)
}
msg := &sarama.ProducerMessage{
Topic: topic,
Value: sarama.ByteEncoder(pixelToSendBytes),
}
Consumer:
receivedPixel := &pixel.Pixel{}
err := proto.Unmarshal(msg.Value, receivedPixel)
if err != nil {
log.Fatalln("Failed to unmarshal pixel:", err)
}
log.Printf("Pixel received: %s", receivedPixel)
Complete example:
package main
import (
pixel "example/pixel"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/Shopify/sarama"
"github.com/golang/protobuf/proto"
)
func main() {
topic := "your-topic-name"
brokerList := []string{"localhost:29092"}
producer, err := newSyncProducer(brokerList)
if err != nil {
log.Fatalln("Failed to start Sarama producer:", err)
}
go func() {
ticker := time.NewTicker(time.Second)
for {
select {
case t := <-ticker.C:
pixelToSend := &pixel.Pixel{SessionId: t.String()}
pixelToSendBytes, err := proto.Marshal(pixelToSend)
if err != nil {
log.Fatalln("Failed to marshal pixel:", err)
}
msg := &sarama.ProducerMessage{
Topic: topic,
Value: sarama.ByteEncoder(pixelToSendBytes),
}
producer.SendMessage(msg)
log.Printf("Pixel sent: %s", pixelToSend)
}
}
}()
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
partitionConsumer, err := newPartitionConsumer(brokerList, topic)
if err != nil {
log.Fatalln("Failed to create Sarama partition consumer:", err)
}
log.Println("Waiting for messages...")
for {
select {
case msg := <-partitionConsumer.Messages():
receivedPixel := &pixel.Pixel{}
err := proto.Unmarshal(msg.Value, receivedPixel)
if err != nil {
log.Fatalln("Failed to unmarshal pixel:", err)
}
log.Printf("Pixel received: %s", receivedPixel)
case <-signals:
log.Print("Received termination signal. Exiting.")
return
}
}
}
func newSyncProducer(brokerList []string) (sarama.SyncProducer, error) {
config := sarama.NewConfig()
config.Producer.Return.Successes = true
// TODO configure producer
producer, err := sarama.NewSyncProducer(brokerList, config)
if err != nil {
return nil, err
}
return producer, nil
}
func newPartitionConsumer(brokerList []string, topic string) (sarama.PartitionConsumer, error) {
conf := sarama.NewConfig()
// TODO configure consumer
consumer, err := sarama.NewConsumer(brokerList, conf)
if err != nil {
return nil, err
}
partitionConsumer, err := consumer.ConsumePartition(topic, 0, sarama.OffsetOldest)
if err != nil {
return nil, err
}
return partitionConsumer, err
}

Related

XACK is not deleting the message, even if it is processed successfully?

I am trying to implement redis stream where we have a producer.
package producer
import (
"RedisStream/models"
"encoding/json"
"fmt"
"github.com/garyburd/redigo/redis"
)
type Producer struct {
streamName string
}
func NewProducer(streamName string) *Producer {
return &Producer{streamName: streamName}
}
func (p *Producer) WriteEvents(conn redis.Conn, key string) {
// Create a new struct
employee := models.Employee{
Name: "ashutosh",
Employer: "self-employee",
}
// Convert struct to JSON
e, _ := json.Marshal(employee)
// Send key and value to Redis stream
_, err := conn.Do("XADD", p.streamName, "*", key, e)
if err != nil {
fmt.Println(err)
}
fmt.Println("Successfully sent data to Redis stream")
}
then I have implemented a consumer
func (c *Consumer) ReadEventsCons1() {
// Connect to Redis
conn, err := redis.Dial("tcp", ":6379")
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()
for {
// Read key and value from Redis stream
reply, err := conn.Do("XREADGROUP", "GROUP", c.groupName[0], "ashu", "COUNT", "1", "STREAMS", c.streamName, ">")
vs, err := redis.Values(reply, err)
if err != nil {
if errors.Is(err, redis.ErrNil) {
continue
}
fmt.Printf("Error: %+v", err)
}
// Get the first and only value in the array since we're only
// reading from one stream "some-stream-name" here.
vs, err = redis.Values(vs[0], nil)
if err != nil {
fmt.Printf("Error: %+v", err)
}
// Ignore the stream name as the first value as we already have
// that in hand! Just get the second value which is guaranteed to
// exist per the docs, and parse it as some stream entries.
res, err := entries(vs[1], nil)
if err != nil {
fmt.Errorf("error parsing entries: %w", err)
}
for _, val := range res {
for k, v := range val.Fields {
empl := &models.Employee{}
_ = json.Unmarshal(v, empl)
fmt.Printf("From Consumer Ashu: Key: %s and val: %+v \n", k, empl)
}
reply, err := redis.Int(conn.Do("XACK", c.streamName, c.groupName[0], val.ID))
if reply != 1 {
fmt.Printf("failed to ack: err: %+v", err)
}
}
}
}
Once a consumer from a consumergroup successfully processed a message, I sent acknowledgement to redis.But messages still resides in redis stream. because post running
XLEN streamName
I can see length is growing. This may create memory challenge, since messages are residing in perpetuity. Is there any intelligent way to handle this issue?

Why Kafka message delivery from Producer to Consumer is so slow?

We have multiple microservices written in GoLang exchanging messages over the Kafka message bus. A microservice writes on a Kafka topic with a partition count of 3 with a replica factor of 2. We use AWS MSK for kafka brooker. We are using the Shopify Kafka client to connect with brokers.
Here is my Producer code -
package kf
import (
"fmt"
"github.com/Shopify/sarama"
"github.com/segmentio/kafka-go"
"net"
"strconv"
)
type Producer struct {
flowEventProducer sarama.SyncProducer
topic string
}
func InitProducer(brokers []string, topic string) *Producer {
CreateKafkaTopic(brokers[0], topic)
p := &Producer{}
prod, err := newFlowWriter(brokers)
if err != nil {
panic("failed to connect to producer")
}
p.flowEventProducer = prod
p.topic = topic
return p
}
func CreateKafkaTopic(kafkaURL, topic string) {
conn, err := kafka.Dial("tcp", kafkaURL)
if err != nil {
panic(err.Error())
}
controller, err := conn.Controller()
if err != nil {
panic(err.Error())
}
var controllerConn *kafka.Conn
controllerConn, err = kafka.Dial("tcp", net.JoinHostPort(controller.Host, strconv.Itoa(controller.Port)))
if err != nil {
panic(err.Error())
}
defer controllerConn.Close()
topicConfigs := []kafka.TopicConfig{
{
Topic: topic,
NumPartitions: 3,
ReplicationFactor: 2,
},
}
err = controllerConn.CreateTopics(topicConfigs...)
if err != nil {
panic(err.Error())
}
defer conn.Close()
}
func newFlowWriter(brokers []string) (sarama.SyncProducer, error) {
config := sarama.NewConfig()
version := "2.6.2"
kafkaVer, err := sarama.ParseKafkaVersion(version)
if err != nil {
panic("failed to parse kafka version, producer will not run")
}
config.Producer.Partitioner = sarama.NewHashPartitioner
config.Net.MaxOpenRequests = 10
config.Producer.RequiredAcks = sarama.WaitForLocal
config.Producer.Return.Successes = true
config.Version = kafkaVer
producer, err := sarama.NewSyncProducer(brokers, config)
return producer, err
}
func (p *Producer) WriteMessage(uuid string, data []byte) error {
msg := &sarama.ProducerMessage{
Topic: p.topic,
Key: sarama.ByteEncoder(uuid),
Value: sarama.ByteEncoder(data),
}
part, off, err := p.flowEventProducer.SendMessage(msg)
if err != nil {
return err
} else {
fmt.Printf("message wriiten on part:%d and offset: %d", part, off)
}
return nil
}
Here is my consumer -
package kf
import (
"context"
"encoding/json"
"fmt"
"github.com/Shopify/sarama"
)
type Consumer struct {
flowEventReader sarama.ConsumerGroup
topic string
brokerUrls []string
}
type data struct {
Name string `json:"name"`
Employee string `json:"employee"`
}
func InitConsumer(brokers []string, topic string) *Consumer {
c := &Consumer{}
c.topic = topic
c.brokerUrls = brokers
var (
err error
)
conf := createSaramaKafkaConf()
c.flowEventReader, err = sarama.NewConsumerGroup(c.brokerUrls, "myconf", conf)
if err != nil {
panic("failed to create consumer group on kafka cluster")
}
return c
}
type KafkaConsumerGroupHandler struct {
Cons *Consumer
}
func (c *Consumer) HandleMessages() {
// Consume from kafka and process
for {
var err = c.flowEventReader.Consume(context.Background(), []string{c.topic}, &KafkaConsumerGroupHandler{Cons: c})
if err != nil {
fmt.Println("FAILED")
continue
}
}
}
func (*KafkaConsumerGroupHandler) Setup(_ sarama.ConsumerGroupSession) error { return nil }
func (*KafkaConsumerGroupHandler) Cleanup(_ sarama.ConsumerGroupSession) error { return nil }
func (l *KafkaConsumerGroupHandler) ConsumeClaim(sess sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
for msg := range claim.Messages() {
l.Cons.logMessage(msg)
sess.MarkMessage(msg, "")
}
return nil
}
func (c *Consumer) logMessage(msg *sarama.ConsumerMessage) {
d := &data{}
err := json.Unmarshal(msg.Value, d)
if err != nil {
fmt.Println(err)
}
fmt.Printf("messages: key: %s and val:%+v", string(msg.Key), d)
}
func createSaramaKafkaConf() *sarama.Config {
conf := sarama.NewConfig()
version := "2.6.2"
kafkaVer, err := sarama.ParseKafkaVersion(version)
if err != nil {
panic("failed to parse kafka version, executor will not run")
}
conf.Version = kafkaVer
conf.Consumer.Offsets.Initial = sarama.OffsetOldest
conf.Consumer.Group.Rebalance.GroupStrategies = []sarama.BalanceStrategy{sarama.BalanceStrategyRoundRobin}
return conf
}
If we put load in microservices and producer starts producing messages of order of 500 events with each having size of ~1kb. we are encountering a delay of 30 seconds in message delivery. We want instant message delivery post-production. I think Kafka is very much capable of for my use-case. Please, help me in figuring out the issue for this delay.

How to return a channel

I'm creating a worker to consume messages from a RabitMQ queue. To achieve that, I created the following file named queue.go
package ExternalServices
import (
"../domain"
"encoding/json"
"github.com/streadway/amqp"
"os"
)
const (
catalogQueue = "catalog-queue"
)
func EnqueueMessageCatalog(catalog *domain.Catalog) error {
marshal, err := json.Marshal(*catalog)
if err != nil {
return err
}
jsonVal := string(marshal)
err = enqueue(catalogQueue, jsonVal)
return err
}
func DequeueMessageCatalog() ([]domain.Catalog, error) {
msgs, err := dequeue(catalogQueue)
if err != nil {
return nil, err
}
allCatalogs := make([]domain.Catalog, len(msgs))
for _, currMsg := range msgs {
var currCatalog domain.Catalog
err = json.Unmarshal([]byte(currMsg), &currCatalog)
if err != nil {
return nil, err
}
}
return allCatalogs, nil
}
func openConnection() (*amqp.Connection, *amqp.Channel, error) {
conn, err := amqp.Dial(os.Getenv("RabbitMQConStr"))
if err != nil {
return nil, nil, err
}
ch, err := conn.Channel()
if err != nil {
conn.Close()
return nil, nil, err
}
return conn, ch, nil
}
func ensureQueueExists(queueName string, ch *amqp.Channel) (amqp.Queue, error) {
q, err := ch.QueueDeclare(
queueName, // name
false, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
return q, err
}
func enqueue(queueName string, message string) error {
con, ch, err := openConnection()
if err != nil {
return err
}
defer con.Close()
defer ch.Close()
q, err := ensureQueueExists(queueName, ch)
if err != nil {
return err
}
err = ch.Publish(
"", // exchange
q.Name, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "application/json",
Body: []byte(message),
})
return err
}
func dequeue(queueName string) ([]string, error) {
con, ch, err := openConnection()
if err != nil {
return nil, err
}
defer con.Close()
defer ch.Close()
q, err := ensureQueueExists(queueName, ch)
if err != nil {
return nil, err
}
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
if err != nil {
return nil, err
}
jsons := make([]string, len(msgs))
i := 0
for currMsg:= range msgs {
jsons[i] = string(currMsg.Body)
i += 1
}
return jsons, nil
}
However, I got a bit confused at the dequeue function. I want my worker to be notified every time a messages arrives at my queue, so I guess the proper way to do so is to create a string chan to my worker, since I don't want to expose the message channel returned by Consume to it.
This is my worker so far.
package worker
import (
"../external-services"
"log"
)
func StartWorker() {
go func() {
messages, err := ExternalServices.DequeueMessageCatalog();
if err != nil {
// todo log
}
for d := range messages {
log.Printf("Received a message: %s", d)
}
}()
}
How can I modify my dequeue function so it returns a string chan?
After modifying this method to return the string chan, do the lines defer con.Close() and defer ch.Close() need to be deleted from this method?
It's my first project in GoLang so anything you think can increase the quality of the code will be much appreciated :-D
maybe something like this:
msgs, err := ch.Consume(...)
/* handle error */
stringCh := make(chan string)
done := make(chan struct{})
go func() {
defer con.Close()
defer ch.Close()
defer close(stringCh)
for {
select {
case currMsg := <-msgs:
stringCh <- string(currMsg.Body)
case <-done:
return
}
}
}()
return stringCh, done
This is only a sketchy example. Basic idea is spawn another goroutine listen to the message chan returned by Consume. Other details like how to graceful shutdown, dequeue interface,... depend on your needs.
After reading #YSTai response, I realised I miss the go routine creation. This is how my code end up.
worker.go
package main
import (
"../domain"
"../externalservices"
"log"
"strings"
"sync"
)
/*
StartWorker initializes a program that will wait for messages enqueued and process them
*/
func StartWorker() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
catalogReceived := make(chan domain.Catalog)
defer close(catalogReceived)
for true {
go func() {
externalservices.DequeueMessageCatalog(catalogReceived)
catalog := <-catalogReceived
website := domain.Website{
Name: strings.ToUpper(catalog.Name),
Zip: catalog.Zip}
externalservices.InsertWebSite(&website)
}()
}
}()
log.Printf(" [*] Waiting for messages")
wg.Wait()
}
func main() {
StartWorker()
}
queue.go
package externalservices
import (
"../domain"
"encoding/json"
"github.com/streadway/amqp"
"os"
)
const (
catalogQueue = "catalog-queue"
)
func EnqueueMessageCatalog(catalog *domain.Catalog) error {
marshal, err := json.Marshal(*catalog)
if err != nil {
return err
}
jsonVal := string(marshal)
err = enqueue(catalogQueue, jsonVal)
return err
}
// DequeueMessageCatalog is nice
func DequeueMessageCatalog(messageChannel chan domain.Catalog) {
message := make(chan []byte)
defer close(message)
for true {
go func() {
dequeue(catalogQueue, message)
}()
currCatalog := domain.Catalog{}
json.Unmarshal([]byte(<-message), &currCatalog)
messageChannel <- currCatalog
}
}
func openConnection() (*amqp.Connection, *amqp.Channel, error) {
connString := os.Getenv("RabbitMQConStr")
conn, err := amqp.Dial(connString)
if err != nil {
return nil, nil, err
}
ch, err := conn.Channel()
if err != nil {
conn.Close()
return nil, nil, err
}
return conn, ch, nil
}
func ensureQueueExists(queueName string, ch *amqp.Channel) (amqp.Queue, error) {
q, err := ch.QueueDeclare(
queueName, // name
false, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
return q, err
}
func enqueue(queueName string, message string) error {
con, ch, err := openConnection()
if err != nil {
return err
}
defer con.Close()
defer ch.Close()
q, err := ensureQueueExists(queueName, ch)
if err != nil {
return err
}
err = ch.Publish(
"", // exchange
q.Name, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "application/json",
Body: []byte(message),
})
return err
}
func dequeue(queueName string, message chan []byte) error {
con, ch, err := openConnection()
if err != nil {
return err
}
defer con.Close()
defer ch.Close()
q, err := ensureQueueExists(queueName, ch)
if err != nil {
return err
}
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
true, // no-wait
nil, // args
)
if err != nil {
return err
}
for currMsg := range msgs {
message <- currMsg.Body
}
return nil
}

Golang TCPConn Gob Communication

I'm having issues with the gob protocol (or maybe networking in general, where my knowledge is weak), and I don't understand why the following code does not work properly. It is just supposed to be a simple example of maintaining an open TCP connection, and sending multiple gobs through it. The code will send, and receive, but often corrupts its data.
Thank you in advance.
package main
import (
"encoding/gob"
"fmt"
"net"
"strconv"
"time"
)
type Message struct {
Msg string
}
func main() {
gob.Register(new(Message))
clientAddr, err := net.ResolveTCPAddr("tcp", "localhost:12346")
if err != nil {
fmt.Println(err)
}
serverAddr, err := net.ResolveTCPAddr("tcp", "localhost:12345")
if err != nil {
fmt.Println(err)
}
serverListener, err := net.ListenTCP("tcp", serverAddr)
if err != nil {
fmt.Println(err)
}
conn, err := net.DialTCP("tcp", clientAddr, serverAddr)
if err != nil {
fmt.Println(err)
}
serverConn, err := serverListener.AcceptTCP()
if err != nil {
fmt.Println(err)
}
done := false
go func() {
for !done {
recieveMessage(serverConn)
}
}()
for i := 1; i < 1000; i++ {
sent := Message{strconv.Itoa(i)}
sendMessage(sent, conn)
}
time.Sleep(time.Second)
done = true
}
func sendMessage(msg Message, conn *net.TCPConn) {
enc := gob.NewEncoder(conn)
err := enc.Encode(msg)
if err != nil {
fmt.Println(err)
}
}
func recieveMessage(conn *net.TCPConn) {
msg := new(Message)
dec := gob.NewDecoder(conn) // Will read from network.
err := dec.Decode(msg)
if err != nil {
fmt.Println(err)
}
fmt.Println("Client recieved:", msg.Msg)
}
The problem is that the decoder can buffer data from the next message. When this happens, the next new decoder starts in the middle of a message. The fix is to use a single encoder and decoder.
func main() {
...
dec := gob.NewDecoder(conn) // Will read from network.
enc := gob.NewEncoder(serverConn)
go func() {
for !done {
recieveMessage(dec)
}
}()
for i := 1; i < 1000; i++ {
sent := Message{strconv.Itoa(i)}
sendMessage(sent, enc)
}
...
}
func sendMessage(msg Message, enc *gob.Encoder) {
err := enc.Encode(msg)
if err != nil {
fmt.Println(err)
}
}
func recieveMessage(dec *gob.Decoder) {
msg := new(Message)
err := dec.Decode(msg)
if err != nil {
fmt.Println(err)
}
fmt.Println("Client recieved:", msg.Msg)
}
Run it in the playground

Golang amqp reconnect

I want to test the restart connection to the rabbitmq server.
On wrote small script to test.
http://play.golang.org/p/l3ZWzG0Qqb
But it's not working.
In step 10, I close the channel and connection. And open them again. And re-create chan amqp.Confirmation ( :75) . And continue the cycle.
But after that, from the chan confirms nothing return.
UPD: code here.
package main
import (
"fmt"
"github.com/streadway/amqp"
"log"
"os"
"time"
)
const SERVER = "amqp://user:pass#localhost:5672/"
const EXCHANGE_NAME = "publisher.test.1"
const EXCHANGE_TYPE = "direct"
const ROUTING_KEY = "publisher.test"
var Connection *amqp.Connection
var Channel *amqp.Channel
func setup(url string) (*amqp.Connection, *amqp.Channel, error) {
conn, err := amqp.Dial(url)
if err != nil {
return nil, nil, err
}
ch, err := conn.Channel()
if err != nil {
return nil, nil, err
}
return conn, ch, nil
}
func main() {
url := SERVER
Connection, Channel, err := setup(url)
if err != nil {
fmt.Println("err publisher setup:", err)
return
}
confirms := Channel.NotifyPublish(make(chan amqp.Confirmation, 1))
if err := Channel.Confirm(false); err != nil {
log.Fatalf("confirm.select destination: %s", err)
}
for i := 1; i <= 3000000; i++ {
log.Println(i)
if err != nil {
fmt.Println("err consume:", err)
return
}
if err := Channel.Publish(EXCHANGE_NAME, ROUTING_KEY, false, false, amqp.Publishing{
Body: []byte(fmt.Sprintf("%d", i)),
}); err != nil {
fmt.Println("err publish:", err)
log.Printf("%+v", err)
os.Exit(1)
return
}
// only ack the source delivery when the destination acks the publishing
confirmed := <-confirms
if confirmed.Ack {
log.Printf("confirmed delivery with delivery tag: %d", confirmed.DeliveryTag)
} else {
log.Printf("failed delivery of delivery tag: %d", confirmed.DeliveryTag)
// TODO. Reconnect will be here
}
if i == 10 {
Channel.Close()
Connection.Close()
while := true
for while {
log.Println("while")
time.Sleep(time.Second * 1)
Connection, Channel, err = setup(url)
if err == nil {
while = false
confirms = Channel.NotifyPublish(make(chan amqp.Confirmation, 1))
log.Printf("%+v", confirms)
}
}
}
time.Sleep(time.Millisecond * 300)
}
os.Exit(1)
}
You should put channel in confirm mode. by calling the channel.Confirm() method.
After closing the connection and even after getting new channel on the same connection, you should call Confirm() method again, since the channel is different from the old channel, and the default for all new channel is not to send confirm.

Resources