Upgrading Serverless Superman to IBM Composer

When IBM Composer was released, my plan was to try to slowly introduce readers to it with various different tutorials. My post earlier this week is an example. But I've been thinking a lot lately about a particular problem that Composer can fix for me, so I'm skipping ahead to a more complex topic for the post today. I guess this is a long winded way of saying - if you are still learning Composer this post may be a bit complex, but I'm definitely going to do more simpler posts later.

The Superman Problem

Alright - so a few months ago I built one of my most complex applications on OpenWhisk, Serverless Superman. While I encourage you to read the blog post for details, the idea was basically to find random tweets about serverless and tweet them out with the word "serverless" replaced with "Superman". (I can't believe I get paid to build stuff like this.)

The process was somewhat complex. Here is my attempt at building a flow chart to show how it worked. Note - I suck at flow charts.

Flow Chart

The crucial part are the two branches there. If there are no tweets, the process needs to end. Also, sometimes I found tweets that were returned in the search but didn't actually include the word. Not quite sure what to think about that. I thought maybe it was in the hashtags but I didn't see it there either. So I've got a second thing to check as well before sending out the tweet.

All in all this works, but, you can't "exit" a sequence in OpenWhisk. You can only return an error. Now technically that isn't the case. I could use a combinator to try to get around this, but that felt over complex. I figured - who cares about the errors. I don't for sure.

But... it bothered me. Look at the error rate there:

Shot

I loaded up the details:

Shot

And began clicking. For almost every error, I saw what I expected - either "No tweets" or "No serverless mentions". Ie, not real errors. But then I found this:

Shot

That was totally unexpected and - as far as I know - not something that should have happened. The issue though is that so many of my errors are "noise" and not real errors. This is where switching to a Composer app can help.

The Composer Version

I know that Composer supports IF conditionals simple. I decided to rebuild the sequence as a Composer file. I'd take the opportunity to convert my "simple" actions into inline functions and then properly handle exiting when there are no tweets to create.

So as a reminder, my existing sequence consisted of custom "set up and massage" type actions as well as actions from my Twitter package. I've got a public Twitter package you can use to search and create tweets. I created a bound copy of the package with my app details so I can use it much easier.

Let's look at the Composer file.

composer.sequence(
    // set up parameters for the twitter search
    args => {
        let now = new Date();
        let datestr = now.getFullYear() + '-'+(now.getMonth()+1)+'-'+now.getDate();

        return {
            term:"serverless",
            since:datestr
        }

    },

    // now ask twitter for the results
    'mytwitter/getTweets',

    // twitter's API is missing some stuff, so we filter more and massage
    args => {

        // http://stackoverflow.com/a/7709819/52160
        let diffInMinutes = function(d1,d2) {
            var diffMs = (d1 - d2);
            var diffDays = Math.floor(diffMs / 86400000); // days
            var diffHrs = Math.floor((diffMs % 86400000) / 3600000); // hours
            var diffMins = Math.round(((diffMs % 86400000) % 3600000) / 60000); // minutes
            return diffMins;
        }

        //if a tweet is older than this in minutes, kill it
        const TOO_OLD = 30;
        
        let now = new Date();

        let result = args.tweets.filter( (tweet) => {
            //no replies
            if(tweet.in_reply_to_status_id) return false;
            //no RTs
            if(tweet.retweeted_status) return false;

            // http://stackoverflow.com/a/2766516/52160
            let date = new Date(
            tweet.created_at.replace(/^\w+ (\w+) (\d+) ([\d:]+) \+0000 (\d+)$/,
            "$1 $2 $4 $3 UTC"));

            let age = diffInMinutes(now, date);
            if(age > TOO_OLD) return false;

            return true;
        });

        //now map it
        result = result.map( (tweet) => {
            return {
                id:tweet.id,
                text:tweet.text,
                created_at:tweet.created_at,
                hashtags:tweet.entities.hashtags
            };
        });

        return { tweets:result };

    },

    //now we select one, if we even got one
    args => {
        let newText = '';

        if(args.tweets.length >= 1) {

            let chosen = args.tweets[ Math.floor(Math.random() * (args.tweets.length))];
            if(chosen.text.toLowerCase().indexOf('serverless') !== -1) {
                newText = chosen.text.replace(/serverless/ig, "Superman");
            }
        }

        return { status:newText };
    },

    //if we got something, tweet it
    composer.if(({status})=> status != '', 'mytwitter/sendTweet')
);

Let's take it from the top.

The first thing I did was inline my "setup" function. All this does is prepare the data to be sent to my Twitter action.

Then I run getTweets. That's a simple action call since all the work is packaged up. (And again, you can totally use my code too!)

My next inline action is a bit more complex. It handles doing additional filtering that Twitter's API cannot. I kinda think it's a bit too complex. Obviously what you inline and what you keep as an external action is up to you, and I'm willing to bet folks will go back and forth between what they feel comfortable doing, but for this update, I wanted to move everything inline just to see how it feels.

I'm ok with this block of code - but I definitely think folks may disagree.

The next action is another inline function - and yeah - you may ask - why not simply "combine" the two. Well again, I'm not saying it makes absolute sense, but in the previous version I had them as separate actions so I thought it made sense to keep them separate here too. The logic here handles picking the random tweet and updating the text.

Here too is where things begin to change. Previously that action returned an error if either of the two conditionals were false. In my case, I simply never set a value for newText.

That then lets the last part work very nicely - composer.if. If my status is not blank, run sendTweet. Otherwise do nothing.

So now the end result is either a tweet, or nothing. No errors. (Well, yes, I'll still have errors, but I'll have better errors now!) In case you're curious, check out how the Composer shell renders this:

Shot

Slick rick. Anyway, once this was done I literally just had to update the rule associated with my CRON-based trigger to point to the new app. And it worked:

Remember that there is currently a bug with wsk such that when you edit a rule, it disables it. Be sure to re-enable it right after the edit.

Any questions?

Like This?

If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can also subscribe to the email feed to get notified of new posts.

See Also