Your web browser is out of date. Update your browser for more security, speed and the best experience on this site.

Update your browser
CapTech Home Page

Blog November 20, 2018

Golang: A Simple Distributed Application Pattern

In the previous discussion, we talked

about how a simple concurrent pattern can be used to provide services access, and also a sturdy batch process

to carry out tasks over a periodic time interval. For this post, we are going to expand upon that pattern again

by using Golang, but adding a

built-in language construct that is somewhat unique to languages that are designed for concurrency.

The Need

Last time, the program communication was all internal to the application. The user talked to the services directly and to the background process through the services. This time around, we will do the same thing, but we will also add the ability for applications talk to each other. Ordinarily, in a loosely-coupled, distributed environment, our applications would be "physically" separated in different containers or VMs, and connected by queues, streams, or some other connective "tissue." In this case, we are trying to operate within one application and simulate the movement of data between programs with what we have on hand rather than additional outside servers. Helpfully, Golang has that covered for us with the notion of channels.

Internal Queuing with Channels

Channels are a first-class citizen in the Golang specification, which yields a few advantages. For one, you can use them just about anywhere (e.g. function return values and parms). Additionally, they are very easy to use, with just a few notational syntaxes to remember. And lastly, they are very lightweight so you can use as many as you need. This last point is important - in part because channels are so easy to use, some programmers feel the need to use them anywhere. While there are standard concurrency performance/side-effect risks with a cavalier usage of Go routines, the bigger issue is actually adding program complexity where none was necessary. If two things don't need to happen at the same time, why force it?

Channels have many uses, some of which we will explore later for more relevant discussions (e.g. pub/sub). Today, we are going to focus on using a set of channels to act as queues between separate applications.

The Application(s)

Our application is actually going to be four applications. The first app is as was described in the previous blog post: a handful of services and a heartbeat. We will expand the services a bit so that we can have visibility into our new applications, but other than that, the foundation of the application will remain the same.

Golang Distributed Application Stack

The new applications will be essentially single-purpose functions that process words as they enter, each one with a simple perspective on what a word should look like, then push that word out to a channel that will be picked up by the next link in the chain. If we diagram the application with more of an "information flow" feel to it, it would look something like this:

For the initial version of the application, a web service will take a word, put it on a channel bound for Function A. Function A will operate on it (i.e. make it lower case), then put it on a channel bound for Function B. This will continue until Function C completes the journey by performing its duty on whatever comes across its channel. As far as the business purpose of each of the functions:

  • Function A feels all words should be lowercase;
  • Function B, believes that TitleCase is the way to go;
  • Whereas Function C, knows that the world would be a better place if all words were shouted in UPPERCASE.

The Go routine that manages the application's meta data (APP MGMT PROCESS) still simply manages the application's heartbeat and logging process.

Definitions

Defining channels for use in our application is pretty straight-forward. First we define what our application is going to need:

var (
 lc chan string
 uc chan string
 title chan string
)

Then we carve out some memory to hold them:


lc = make(chan string, maxMessages)
uc = make(chan string, maxMessages)
title = make(chan string, maxMessages)

Channels have types, so only the specified type of data can be passed through them. They can be simple strings like we have defined here, but they can also be complex structs as well. Since we want to use these channels as simple queues, we have also defined a length in each of the make statements of maxMessages, which is defined as 100. Length allows channels to be buffered, which will come in handy in a later discussion about controlling our distributed architectures resiliency.

Practice

When a message gets placed on a channel with a format that looks like: channelName <- data, for example lc <- c.Param("word") this puts a word passed in from the web service onto the lowercase channel - any receiver for that channel gets notified that a message is waiting.

// AddToLowerCaseQ ...
func AddToLowerCaseQ(c *gin.Context) {
 lc <- c.Param("word")
 origAddress = append(origAddress, c.Param("word"))
 content := gin.H{"payload": "Accepted: " + fmt.Sprintf("lc: %d, Tc: %d, UC: %d, #: %d", len(lc), len(title), len(uc), len(origAddress))}
 c.JSON(http.StatusOK, content)
}<a href="https://www.captechconsulting.com/blogs/golang-a-simple-concurrent-web-services-pattern" target="_blank" rel="noreferrer noopener">
</a>

This is the service we are using to capture user input. The service accepts a string and then adds it to the lc channel. When that receiver gets notified, it pulls the data from the channel and performs its operation on that data. Let's look at the Go routine receiver for making lowercase words:

// Make lowercase.
go func() {
 for {
 select {
 case item := <-lc:
 item = strings.ToLower(item)
 lcAddress = append(lcAddress, item)
 title <- item
 }
 } // for
}() // go func

The go func() {} just puts the bracketed code into the background and the for {} says run it forever. Without the for everything would fall straight through and we would miss anything coming in on the channel we are watching. The workhorse of our routine however, is the select {} block. The select is similar to a switch, except the select only looks for communication elements. And here, we are watching for anything coming across the lc channel with the case item := <-lc block. If nothing is there, we go back around and check again, and again, and again until we get data. Once that happens, we convert it to lowercase, append it to our trophy case so we can show our work to the web service we wrote to show it off GetLCAddress(), then push the updated data to the title channel where the TitleCase Go routine performs its work on it.

Now, yes, we could probably enhance the select statement to look for all of the channels in the program and it would work fine for our simple needs, but the goal here is to simulate multiple applications in a distributed environment so each Go routine deals with its own concern.

If you notice in the TitleCase and UPPERCASE Go routines, we are waiting 100 & 50 milliseconds respectively before we place check the channel channel again. There is no functional reason why we are doing this, but we want to be able to show progress in the GetTotals() and GetAllAddress() services (otherwise everything happens too quickly), but also because in our next installment, we are going to talk about circuit breakers, and delays are a convenient tool to make that pattern work.

Outputting the Proof

First, let's give the app something to do. The Gettysburg Address has 271 words that we can donate to the process so we will start be sending them in through the foreground process:

./gettysburg.sh which is a simple wrapper around a bunch of curl statements.

While this is running, we can check the progress from another terminal by running: curl -X GET http://localhost:7718/totals

This should yield something similar to this: {"payload":"lc: 109, Tc: 56, UC: 55, #: 209"}

What this tells you is that 209 words have been submitted, 109 have been converted to lowercase, 56 to TitleCase, and 55 to UPPERCASE. If you keep running this web service call, you'll see the totals change accordingly. When the script is complete, the result will look like this: {"payload":"lc: 271, Tc: 271, UC: 271, #: 271"}.

If you want to take a look at the results as they are building, the all service will show you what each of the result arrays look like side-by-side (this is a little modified for clarity).

<a href="https://www.captechconsulting.com/blogs/golang-a-simple-concurrent-web-services-pattern" target="_blank" rel="noreferrer noopener">"LC":["four","score","and","seven","years","ago","our","</a>fathers","brought","forth","on","this","continent","a","new","nation","conceived","in","liberty","and","dedicated","to","the","proposition","that","all","men","are","created","equal","now","we","are","engaged","in","a","great","civil","war","testing","whether","that","nation","or","any","nation","so","conceived","and","so","dedicated","can","long","endure","we","are","met","on","a","great","battlefield","of","that","war","we","have","come","to","dedicate","a","portion","of","that","field","as","a","final","resting","place","for","those","who","here","gave","their","lives","that","that","nation","might","live","it","is","altogether","fitting","and","proper","that","we","should","do","this","but","in","a","larger","sense","we","can"],

"Title":["Four","Score","And","Seven","Years","Ago","Our","Fathers","Brought","Forth","On","This","Continent","A","New","Nation","Conceived","In","Liberty","And","Dedicated","To","The","Proposition","That","All","Men","Are","Created","Equal","Now","We","Are","Engaged","In","A","Great","Civil","War","Testing","Whether","That","Nation","Or","Any","Nation","So","Conceived","And","So","Dedicated","Can","Long","Endure","We","Are"],

"UC":["FOUR","SCORE","AND","SEVEN","YEARS","AGO","OUR","FATHERS","BROUGHT","FORTH","ON","THIS","CONTINENT","A","NEW","NATION","CONCEIVED","IN","LIBERTY","AND","DEDICATED","TO","THE","PROPOSITION","THAT","ALL","MEN","ARE","CREATED","EQUAL","NOW","WE","ARE","ENGAGED","IN","A","GREAT","CIVIL","WAR","TESTING","WHETHER","THAT","NATION","OR","ANY","NATION","SO","CONCEIVED","AND","SO","DEDICATED","CAN","LONG","ENDURE","WE"]<a href="https://www.captechconsulting.com/blogs/golang-a-simple-concurrent-web-services-pattern" target="_blank" rel="noreferrer noopener">
</a>

As you can tell, the artificial delays we put in place show how our Go routines can handle backups in processing without any issues (to an extent). And because of the way we are siphoning data through channels, we don't have to worry about race conditions and the words coming out in different sequences.

Once everything is done, if you want to check the final tally of our three processes, it's pretty simple to do. To see the lower case results: curl -X GET http://localhost:7718/lc - which should get you this:

{"payload":"four score and seven years ago our fathers brought forth on this continent a new nation conceived in liberty and dedicated to the proposition that all men are created equal now we are engaged in a great civil war testing whether that nation or any nation so conceived and so dedicated can long endure we are met on a great battlefield of that war we have come to dedicate a portion of that field as a final resting place for those who here gave their lives that that nation might live it is altogether fitting and proper that we should do this but in a larger sense we can not dedicate we can not consecrate we can not hallow this ground the brave men living and dead who struggled here have consecrated it far above our poor power to add or detract the world will little note nor long remember what we say here but it can never forget what they did here it is for us the living rather to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced it is rather for us to be here dedicated to the great task remaining before us that from these honored dead we take increased devotion to that cause for which they gave the last full measure of devotion that we here highly resolve that these dead shall not have died in vain that this nation under god shall have a new birth of freedom and that government of the people by the people for the people shall not perish from the earth"}<a href="https://www.captechconsulting.com/blogs/golang-a-simple-concurrent-web-services-pattern" target="_blank" rel="noreferrer noopener">
</a>

To see it in its TitleCase version, call the title case service: curl -X GET http://localhost:7718/title - which should get you this:

{"payload":"Four Score And Seven Years Ago Our Fathers Brought Forth On This Continent A New Nation Conceived In Liberty And Dedicated To The Proposition That All Men Are Created Equal Now We Are Engaged In A Great Civil War Testing Whether That Nation Or Any Nation So Conceived And So Dedicated Can Long Endure We Are Met On A Great Battlefield Of That War We Have Come To Dedicate A Portion Of That Field As A Final Resting Place For Those Who Here Gave Their Lives That That Nation Might Live It Is Altogether Fitting And Proper That We Should Do This But In A Larger Sense We Can Not Dedicate We Can Not Consecrate We Can Not Hallow This Ground The Brave Men Living And Dead Who Struggled Here Have Consecrated It Far Above Our Poor Power To Add Or Detract The World Will Little Note Nor Long Remember What We Say Here But It Can Never Forget What They Did Here It Is For Us The Living Rather To Be Dedicated Here To The Unfinished Work Which They Who Fought Here Have Thus Far So Nobly Advanced It Is Rather For Us To Be Here Dedicated To The Great Task Remaining Before Us That From These Honored Dead We Take Increased Devotion To That Cause For Which They Gave The Last Full Measure Of Devotion That We Here Highly Resolve That These Dead Shall Not Have Died In Vain That This Nation Under God Shall Have A New Birth Of Freedom And That Government Of The People By The People For The People Shall Not Perish From The Earth"}<a href="https://www.captechconsulting.com/blogs/golang-a-simple-concurrent-web-services-pattern" target="_blank" rel="noreferrer noopener">
</a>

And finally, for the UPPERCASE version: curl -X GET http://localhost:7718/uc - looks like this:

{"payload":"FOUR SCORE AND SEVEN YEARS AGO OUR FATHERS BROUGHT FORTH ON THIS CONTINENT A NEW NATION CONCEIVED IN LIBERTY AND DEDICATED TO THE PROPOSITION THAT ALL MEN ARE CREATED EQUAL NOW WE ARE ENGAGED IN A GREAT CIVIL WAR TESTING WHETHER THAT NATION OR ANY NATION SO CONCEIVED AND SO DEDICATED CAN LONG ENDURE WE ARE MET ON A GREAT BATTLEFIELD OF THAT WAR WE HAVE COME TO DEDICATE A PORTION OF THAT FIELD AS A FINAL RESTING PLACE FOR THOSE WHO HERE GAVE THEIR LIVES THAT THAT NATION MIGHT LIVE IT IS ALTOGETHER FITTING AND PROPER THAT WE SHOULD DO THIS BUT IN A LARGER SENSE WE CAN NOT DEDICATE WE CAN NOT CONSECRATE WE CAN NOT HALLOW THIS GROUND THE BRAVE MEN LIVING AND DEAD WHO STRUGGLED HERE HAVE CONSECRATED IT FAR ABOVE OUR POOR POWER TO ADD OR DETRACT THE WORLD WILL LITTLE NOTE NOR LONG REMEMBER WHAT WE SAY HERE BUT IT CAN NEVER FORGET WHAT THEY DID HERE IT IS FOR US THE LIVING RATHER TO BE DEDICATED HERE TO THE UNFINISHED WORK WHICH THEY WHO FOUGHT HERE HAVE THUS FAR SO NOBLY ADVANCED IT IS RATHER FOR US TO BE HERE DEDICATED TO THE GREAT TASK REMAINING BEFORE US THAT FROM THESE HONORED DEAD WE TAKE INCREASED DEVOTION TO THAT CAUSE FOR WHICH THEY GAVE THE LAST FULL MEASURE OF DEVOTION THAT WE HERE HIGHLY RESOLVE THAT THESE DEAD SHALL NOT HAVE DIED IN VAIN THAT THIS NATION UNDER GOD SHALL HAVE A NEW BIRTH OF FREEDOM AND THAT GOVERNMENT OF THE PEOPLE BY THE PEOPLE FOR THE PEOPLE SHALL NOT PERISH FROM THE EARTH"}<a href="https://www.captechconsulting.com/blogs/golang-a-simple-concurrent-web-services-pattern" target="_blank" rel="noreferrer noopener">
</a>

Some Cautions

Keep in mind that the need for good programming practices remains in effect. Use the right tool for the right job. Concurrency requires you to think about solving your problems in a different way. Just because something can occur at the same time doesn't mean that it needs to. Even though Golang's concurrency model is very easy to understand and use, you can still fall victim to standard concurrency problems like race conditions. Using channels helps order the communication and processing order, but still, be mindful - concurrency and distributed patterns require a different way of thinking (e.g. separation of concerns, "read one-transaction, solve one-problem," simultaneous behavior, queue traceability, and dead-ends).

Summary

What we did in this article was expand our original concurrent pattern into a pattern where we could communicate between any number of background processes via Go channels. This pattern lets us model a distributed application in a completely self-contained environment. From here, we can prototype, test, or even instruct on cloud-native designs that normally would utilize queues, pub/subs, and streams - without having to stand up those assets.

Next time, we will expand upon this pattern by adding resiliency so we can use our setup to throw volume at various pieces of our application to see how it those pieces react under pressure. That will prove useful for some basic architecture validation before we expend a lot of effort actually constructing infrastructure.

Source code for this article can be found here.