Breaking the Ice With core.async
I know, it's been a while. I thought I'd finally break the ice with a little something I learned while doing a side project using Clojure's core.async library.
First, the project. The customer support folks at my company use Zendesk for keeping track of tickets. The software team recently started using Slack as our chat software and I thought that it'd be neat to feed new Zendesk tickets into a Slack channel. Sometimes there's a bit of a disconnect between the customer support team and the software team, and this might help narrow the gap. So I looked around for an existing integration, decided there was none, and started coding. Well, I was wrong. Slack already does have a published integration. Lesson learned: the best code is the code that you don't write yourself.
Before I realized this lesson, I got about halfway to writing my own little integration in Clojure using core.async. It was a lot of fun.
I wound up with a few functions that executed go
blocks:
update-state-loop: int (freq) -> go channel.
This function updates the program's state, represented by a Clojureatom
everyfreq
amount of time. Program state included: (1) the last time since we updated, and (2) how many tickets we sent that day. If I had spent more time understanding the Zendesk API, I could probably eliminate this state from the program all together by setting start and end bounds on my queries.latest-tickets-loop: channel (in), channel (out), freq -> go channel.
This function takes data fromin
, and puts the newest tickets (depending on the state of the program) toout
.messenger-loop: ch -> go channel.
This function takes data fromch
and doesprintln
on each one. Later, this function could be replacted with a function that would send off reformatted tickets to post on Slack.
There is also a function that fetched data from Zendesk every freq
amount of
time:
data-ch: int (freq) -> go channel
Looking at these function signatures makes me realize two things:
It looks like I wrote functions with "-loop" at the end to indicate that they execute and return go channels. Hmm, didn't notice that before. Interesting. I'm not sure whether I should distinguish between regular channels and go channels, so I just will.
Whoops, I got the units upside down on freq. This variable should be called
period
. The units of frequency are inverse of time: "1 / time," whereas I'm using the parameter to represent a time interval.
Anyway, with this hacky stuff I attempted to wire these three loops together
into a run
function:
(defn run
[]
(let [freq (* 30 1000)
stream (data-ch freq)
pipe (chan)]
(update-state-loop freq)
(latest-tickets-loop stream pipe freq)
(messenger-loop pipe)))
But when I executed run
, nothing happenend. Mysterious. I had some luck that
day and a muse spoke to me: "wrap the let
-block in a blocking take" it said.
And I did like this:
(defn run
[]
(<!!
(let [freq (* 30 1000)
stream (data-ch freq)
pipe (chan)]
(update-state-loop freq)
(latest-tickets-loop stream pipe freq)
(messenger-loop pipe))))
Voila, it worked! But why? I had to think about it. I realized that pipe
was
created without a buffer, so it was a blocked channel. Attempts to write to it
block until something else tries to pull from it. So when I wrapped everything
in the blocking take, I was taking from (messenger-loop pipe)
, a go channel,
which then took from pipe
which then pulled from stream and opened things up
for data-ch
to put data into the stream
.
The code is useless, but hey, at least I enjoyed writing it, and it gave me an
excuse to break blog silence. I'd be interested in what you think about this
sort of design: separate asynchronous go blocks executing procedures that could
easily be done synchronously in one big (while true ...)
expression. Am I
just trigger happy with my new toy, the go-routine? Probably =)