I have a web application that I'm writing in Go, using the Iris package. This app needs to listen to Azure Service Bus for messages, and take action when they come in. This is my first time writing Go, and I want to make sure I am using the concurrency model properly.
The requirements for the app lifecycle are fairly straightforward:
- when the main Iris app starts, start the ASB service, without blocking the main Iris app
- ASB service listens for messages coming in forever
- when the main Iris app shuts down, close the ASB subscription and shut the singleton down
Is this idiomatic/correct/safe?
This is my code atm:
// main.go
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading environment file")
}
apiPort := os.Getenv("API_PORT")
if apiPort == "" {
apiPort = "8080"
}
app := iris.New()
broker := messagebroker.GetMessageBroker()
go func() {
broker.Init()
}()
app.Logger().SetLevel("debug")
app.Get("/jobs", listJobs)
// graceful shutdown
idleConnsClosed := make(chan struct{})
iris.RegisterOnInterrupt(func() {
timeout := 10 * time.Second
ctx, cancel := stdContext.WithTimeout(stdContext.Background(), timeout)
defer cancel()
broker.Shutdown()
println("\nShutting Down Iris")
// close all hosts
app.Shutdown(ctx)
close(idleConnsClosed)
})
app.Listen(fmt.Sprintf(":%s", apiPort), iris.WithoutInterruptHandler, iris.WithoutServerError((iris.ErrServerClosed)))
<-idleConnsClosed
}
func listJobs(ctx iris.Context) {
ctx.Text("Ok")
}
// message-broker.go
type MessageBroker interface {
Shutdown() bool
Init()
}
type messageBroker struct{}
var broker *messageBroker
var once sync.Once
var stopListener context.CancelFunc
func (mb messageBroker) Init() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading environment file")
}
connectString := os.Getenv("ASB_CONNECT_STRING")
if connectString == "" {
log.Fatal("No Service Bus credentials")
}
println("\nConnecting to Service Bus")
client, err := azservicebus.NewClientFromConnectionString(connectString, nil)
if err != nil {
panic(err)
}
receiver, err := client.NewReceiverForQueue("jobstestqueue", nil)
if err != nil {
panic(err)
}
println("\nConnected to Service Bus")
ctx, cancel := context.WithCancel(context.Background())
stopListener = cancel
out := make(chan string)
for {
messages, err := receiver.ReceiveMessages(ctx, 1, nil)
if err != nil {
panic(err)
}
go func() {
for _, message := range messages {
var body []byte = message.Body
out <- string(body)
err = receiver.CompleteMessage(ctx, message, nil)
if err != nil {
var sbErr *azservicebus.Error
if errors.As(err, &sbErr) && sbErr.Code == azservicebus.CodeLockLost {
// The message lock has expired. This isn't fatal for the client, but it does mean
// that this message can be received by another Receiver (or potentially this one!).
fmt.Printf("Message lock expired\n")
// You can extend the message lock by calling receiver.RenewMessageLock(msg) before the
// message lock has expired.
continue
}
panic(err)
}
}
}()
for {
select {
case msg := <-out:
fmt.Printf("Message received with body: %s\n", msg)
}
}
}
}
func (mb messageBroker) Shutdown() bool {
println("\nClosing broker subscriptions")
stopListener()
return true
}
// Returns the message broker singleton
func GetMessageBroker() MessageBroker {
once.Do(func() {
broker = &messageBroker{}
})
return *broker
}
```