Level-Up Go Applications with Ringbuffers
In this blog post, we are happy to work with our community member, Martin W. Kirst, who is a leading software architect for Oliver Wyman, a global strategic consultancy company. He has been crafting software and building large cloud platforms for three decades and has gained plenty of experience in various roles such as Software Engineering, Software Architecture, and Product Ownership across the Energy Retail, Logistic/Transport, Finance/Accounting, and Meteorology. He’s super passionate about technologies, tools and automation to drive the business. While developing the infrastructure for a large cloud-native workflow automation and orchestration platform, he got in touch with Hazelcast. It’s our please to welcome Martin’s contribution of Ringbuffer support in Hazelcast’s Go Client SDK. You can connect with Martin on Linkedin: https://www.linkedin.com/in/martin-w-kirst/
Introduction
Hazelcast Go Client v1.4 brings ringbuffer support into the Go language. Hazelcast has offered the ringbuffer feature for a while and enables a broad range of integration options for distributed applications. With the latest release, the Go community will benefit from the ringbuffer feature as well. Hazelcast’s ringbuffer integration is a replicated, but not partitioned, data structure that stores its data in a ring-like structure. You can think of it as a circular array with a given capacity. Each ringbuffer has a tail and a head. The tail is where the items are added and the head is where the items are overwritten or expired. Each client is connected to a ringbuffer that can independently read items between the head and tail, which makes the ringbuffer an ideal candidate for publish and subscribe or efficient data replication use cases.
Example code
The following example will illustrate how to use a Hazelcast ringbuffer in a scenario with distributed Go applications that forward/replicate message events. For simplicity, imagine there are three microservices: Publisher, Logger, and Alerter. As the names already indicate, the Publisher will emit message events only. The Logger will only log each message event to the console, and the Alerter will wait for a certain message event to create an alert. The Logger and Alerter services will demonstrate that each subscriber to the ringbuffer operates independently.
Let’s have a look at the Publisher code first.
Creating a new Hazelcast client connection and attaching to a ringbuffer can be done in just two lines (errors are omitted for brevity):
client, _ := hazelcast.StartNewClient(ctx)
rb, _ := client.GetRingbuffer(ctx, "my-ringbuffer")
Our trivial Publisher is supposed to send a message every second and with a 10% chance send an alert.
for true {
msg := fmt.Sprintf("Hello World, sender's time is %s", time.Now())
if rand.Intn(100) < 10 {
// a 10% chance of sending an alert
msg = fmt.Sprintf("Alert at %s", time.Now())
}
sequence, _ := rb.Add(ctx, msg, hazelcast.OverflowPolicyOverwrite)
log.Printf("Published with sequence=%d, msg=%s", sequence, msg)
time.Sleep(1 * time.Second)
}
Let’s have a look at the Logger, which will simply log every message to the console by reading one item at a time.
for sequence := int64(0); true; sequence++ {
item, _ := rb.ReadOne(ctx, sequence)
log.Printf("Received item idx=%d: %s", sequence, item)
}
Last but more interestingly, the Alerter will read many events but only print alert messages to the console.
for sequence := int64(0); true; {
resultSet, _ := rb.ReadMany(ctx, sequence, 5, 10, nil)
for i := 0; i < resultSet.Size(); i++ {
if msg, err := resultSet.Get(i); err == nil && strings.HasPrefix(msg.(string), "Alert") {
msgSequence, _ := resultSet.GetSequence(i)
log.Printf("Received alert sequence=%d msg=%s", msgSequence, msg)
}
}
sequence = resultSet.GetNextSequenceToReadFrom()
}
While looking at the Alerter’s code, you’ll recognize multiple message items are read from the ringbuffer at once. The ReadMany() function will block until the minimum number of messages (5 in this case) is available in the ringbuffer. So this microservice controls its own sequence number, to read from the ringbuffer independently from the Logger.
The following screenshots from the console will show you, the Publisher’s output.
While running Logger in one console and the Alerter in another one, you can observe how each service reading from the ringbuffer will receive the same sequence numbers, as sent by Hazelcast, but has independent control over when and what to read.
By using ringbuffers, clients are able to read items from the past, as long as the capacity is not reached. When it’s reached, the oldest items are overwritten (when using OverflowPolicy). This helps integrate services, including those having short service interrupts during deployments. In our scenario, the Logger deployment could take multiple minutes to start up without losing messages, because once the new Logger instance is deployed and ready, it would pickup messages from the past.
Summary
This blog post explained how to use a Hazelcast ringbuffer in a scenario with distributed Go applications which will forward/replicate message events. This example code gives you a first impression of how easy and simple implementation of Hazelcast ringbuffers in Go can be. Depending on your use cases you might be interested in fine-tuning the capacity or even a time-to-live (TTL) value for your ringbuffer. Such useful settings are done on Hazelcast server side and are well documented. Don’t hesitate to share your experience with us in our community Slack or Github repository.