In the Toronto area when the COVID-19 pandemic hit last year (spring 2020) one of the consequences (benefits?) was that all businesses where employees could reasonably work from home ended up being forced to do so.
This had an interesting effect on restaurants and the food service industry. In the suburbs food establishments saw a huge increase in sales, people were buying lunch near their homes instead of in downtown Toronto. Unfortunately, this also meant long lines for take out that would normally have taken a mere 5 minutes.
Some food stores went purely to online-ordering only, one of those was a Bubble Tea franchise named “Chatime”, you’ve probably heard of it, it’s apparently the biggest in the world. This actually caused a really frustrating problem for those wanting to order, my wife and I included.
Given the increased volume of orders, the Chatime near us would “turn-on” their online ordering system for a few minutes, wait for it to fill up with orders, and then turn it off again, in a desperate effort for them to keep up (orders apparently still took upwards of an hour to be made). For a week my wife and I tried many times to order, and each time we got to the order submission page… Uber Eats would give up and say the store is no longer open and would happily wipe out our cart at the same time (thanks). Frustrating puts it mildly.
So what do we do? Keep hitting F5 until it’s balled hoping to catch the store when it opens it’s online ordering again? Thankfully no.
Situations like this are one of the reasons I love the cloud, and specifically Serverless technologies. In roughly a day, I was able to put together a quick and dirty serverless app that would (effectively) hit F5 for us and then send an SMS message to let us know the moment online ordering opened back up.
It worked! First time we got the SMS message, we succeeded at placing an order!
It’s time I clean-up what I had built and put it out there for others to learn from, repurpose, etc…
For those that aren’t interested in a wordy walk-through and explanation, feel free to skip the rest and jump right into the code (a clap and follow would be nice though before you leave, if you’re also feeling generous :)
cdk-serverless-chatime-ordering-helper — https://github.com/aaronbrighton/cdk-serverless-chatime-ordering-helper
For the rest of you, let’s dive into it!
Amazon Pinpoint for Two-way SMS
Last spring, for whatever reason (maybe the feature actually wasn’t available yet, someone please correct me in the comments) I couldn’t find a way to cost-effectively handle in-bound SMS on AWS. So the initial implementation actually leveraged Twilio’s SMS w/ Webhooks and API for the SMS interfacing.
As I was cleaning up the code this past week, I realized I can cut Twilio and use Amazon Pinpoint directly. Which drastically simplifies the setup and explanation.
Amazon Pinpoint is… a flexible and scalable outbound and inbound marketing communications service. You can connect with customers over channels like email, SMS, push, or voice.
Pinpoint is the one piece I wasn’t able to automate using IaC (Infrastructure as Code), so if you’re following along, you’ll have to provision an SMS enabled long code phone number manually. This is pretty straight forward in the the AWS Management Console.
Subscriber Lambda and Amazon Location Service
Once an SMS message is received by the phone number in Amazon Pinpoint, it is relayed to some custom logic by way of an Amazon SNS (Simple Notification Service) topic. This is how Two-way SMS is integrated with downstream services in Pinpoint.
Amazon Location Service
Another external service that I ended up replacing as part of this cleanup was the Google Maps API. One of the things the app needs to do is convert a users Postal Code (Canadian) to LAT/LONG Coordinates. In the initial implementation I used Google Maps for this. At re:Invent 2020, Amazon announced Amazon Location service — so time to leverage this as well.
The first SMS message the end-user sends to this app will be their Postal Code. As you can see from the above diagram we then hit an external API hosted by Chatime. Their API expects a user to provide LAT/LONG Coordinates, so first we do the translation with Amazon Location service and then POST to Chatime’s Location API. We get back a list of Chatime locations near the user’s coordinates, as well as information like the Uber Eats online ordering URL. Code excerpt below:
Registering the store for monitoring
There’s a second exchange to be had between the app and the end-user before we start monitoring. We return them a list of the 3 closest Chatime locations that have an Uber Eats online store. They then have to respond back with the Store ID for the store they want to monitor / order from.
Once the selected Store ID is received by the app (it will get thrown at the Subscriber Lambda as well). We create an Amazon SNS topic for that Chatime store, with the Uber Eats URL as a tag (for “safe keeping”). Lastly, we subscribe the end-user to this SNS topic using “sms” as the protocol.
Probing Part 1: Amazon EventBridge and Populator Lambda
The next part of our application is the first half of the monitoring piece. We want to probe Uber Eats Chatime store pages at least once a minute. Therefore, we use our trusty “serverless cron” AKA Amazon EventBridge (formerly CloudWatch Rules).
The populator Lambda will scan the previously created SNS topics representing Chatime stores to be monitored, and will populate an Amazon SQS (Simple Queuing Service) queue with messages containing the Uber Eats URL (from the SNS Topic tags), and the SNS Topic ARN itself.
Probing Part 2: Amazon SQS and Worker Lambda
In the final steps, our Amazon SQS is configured to “fan-out” messages to a Worker Lambda with a batch size of 1 (meaning only 1 SQS message at a time will be sent to the Lambda — we don’t want to overload ;) the external Uber Eats store pages.
Our Worker Lambda throws a GET for the store page, and checks to see if there are any occurrences of the word “Currently unavailable” — this is what happens when a store shuts off their online ordering capability.
If the occurrence is found, the Lambda simply exits to try again ~60 seconds later. If the occurrence is not* found then we publish to the SNS topic we created earlier — which will result in an SMS message being sent to anyone who wanted to be notified if this store’s online order came online.
Probing Part 3: AWS Step Functions and Unsubscriber Lambda
Lastly, we want to remove the end-user’s subscription as we’ve now done our part. One of the “gotchas” of SNS Topics is that if you publish to them and then immediately try to delete those topics, there’s a chance not all of the notifications will be sent-out.
So we have a couple options:
- Sleep the Worker Lambda for ~30 seconds
- AWS Step Functions
Sleeping the Lambda is probably the first thought someone would have, but it’s not great for a number of reasons. One of those is you’re paying every ms the Lambda is running… so that’s just financially wasteful.
With Step Functions we can define a “Wait” step, say 30 seconds, before executing additional custom logic (our unsubscriber Lambda).
Our final step of this app is to remove the SNS topic completely along with all of it’s subscriptions, that’s what this Lambda does, the code is very simple.
How do I deploy this myself?
I won’t go into too much detail, as it’s largely already documented in the cdk-serverless-chatime-ordering-helper repo’s README.md.
If you have experience working with CDK (AWS Cloud Development Kit), then it should be fairly straightforward. If you don’t have experience with CDK, I highly recommend you familiar yourself with it! It’s a game changer for cloud development, especially serverless architectures.
There’s a really awesome low time-investment CDK workshop here: https://cdkworkshop.com/
To conclude… what does it look like for the end-user?
Thanks for reading!
If you found the content valuable, please press that heart button and follow, comment what you liked or didn’t like and let me know what I should cover next!
Top comments (1)
It is so elegant usage of SMS :)
Bubble tea is a dope, really. Always in search for a good one, especially with matcha!