Bunny
A cute library to write asynchronous microservices in Go
Requirements
This library requires Go 1.12 and Git
Quick Start
Create a new service
package main
import (
"context"
"github.com/sirupsen/logrus"
"gitlab.com/venini42/bunny"
bunnyctx "gitlab.com/venini42/bunny/pkg/context"
"gitlab.com/venini42/bunny/pkg/rabbitmq"
"os"
)
func main() {
logger := logrus.New()
// Create a new service instance
service, err := bunny.NewService(
bunny.ServiceName("example"),
bunny.ServiceLogger(logger),
)
if err != nil {
panic(err)
}
// Add a listener
service.AddListener("example", "example", func(ctx context.Context, delivery rabbitmq.Delivery, _ rabbitmq.ReplyFunc) error {
service.Logger.Infof("Received message on topic %s: %s", "example", string(body))
return nil
})
// Start the service. This is blocking until
// a fatal error occurs or a SIGTERM is notified
err = service.Start()
if err != nil {
panic(err)
}
os.Exit(0)
}
Listeners
Listeners are basically like controllers in a traditional
HTTP application. The handle a request that has a body, some headers, and may generate a response.
Note that unlike HTTP controllers, Bunny listeners are not required to return a response.
They can use the reply
function if the message is part of a
request/response cycle or an RPC API.
See Context
Async listeners
A listener can be completely asynchronous. This is
useful when subscribing to application events.
For example, let's update a search index when a new user is created:
service.AddListener("users", "users:created", func(ctx context.Context, body []byte, _ rabbitmq.ReplyFunc) error {
user := deserialize(body) // unmarshal the body into a struct
service.Logger.Infof("Updating search index for user id: %s", user.Id)
return search.UpdateIndex("users", user) // example code
})
RPC Listeners
A service often needs to reply to the caller and complete a request/response cycle.
A convenient reply
function is provided:
service.AddListener("users", "users:get-all", func(ctx context.Context, request rabbitmq.Delivery, reply rabbitmq.ReplyFunc) error {
users := db.GetAllUsers()
service.Logger.Infof("Found %d users", len(users))
body := serialize(users)
response := rabbitmq.Response {
Body: body
}
return reply(ctx, response)
})
Context
A context.Context
object is provided to every listener function.
Among other things, it contains some useful information about the current request.
At the moment it contains the following:
- the request
CorrelationId
. This should be propagated to all the involved
microservices that are invoked as part of the same request.
- The request
ReplyTo
param. Note that this is an empty string (""
) if not set.
This information can be easily accessed using the gitlab.com/venini42/bunny/pkg/context
package:
import (
"context"
bunnyctx "gitlab.com/venini42/bunny/pkg/context"
)
ctx := context.Background()
ctx = bunnyctx.WithCorrelationId(ctx, "some-uuid")
ctx = bunnyctx.WithReplyTo(ctx, "reply-here")
correlationId := bunnyctx.GetCorrelationId(ctx)
replyTo := bunnyctx.GetReplyTo(ctx)
Calling other services
Much like a service can have both async and RPC handlers,
it can also invoke other services responding the same way.
// Send an async message on the given exchange with the given routing
// or topic. It's basically fire-and-forget
service.Rabbit.Cast(ctx context.Context, request rabbitmq.Delivery) error
// Send a "sync" message, waiting for the response to come back and returning it.
// It leverages RabbitMQ's Direct ReplyTo feature to avoid creating temporary
// response queues, making it very lightweight
service.Rabbit.Call(ctx context.Context, request rabbitmq.Delivery) (Response, error) {
More information about Rabbit's Direct ReplyTo can be found here.
Tracing
TBD
Configuration
TBD
Examples
The example
folder contains a client and a service that
showcase both an async request and a RPC call with a reply