Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How should I connect to a Redis instance from an AWS Lambda function?

Tags:

I'm trying to build an API for a single-page web app using AWS Lambda and the Serverless Framework. I want to use Redis Cloud for storage, mostly for its combination of speed and data persistence. I may use more Redis Cloud features in the future, so I'd prefer to avoid using ElastiCache for this. My Redis Cloud instance is running in the same AWS region as my function.

I have a function called related that takes a hashtag from a GET request to an API endpoint, and checks to see if there's an entry for it in the database. If it's there, it should return the results immediately. If not, it should query RiteTag, write the results to Redis, and then return the results to the user.

I'm pretty new to this, so I'm probably doing something adorably naive. Here's the event handler:

'use strict'  const lib = require('../lib/related')  module.exports.handler = function (event, context) {   lib.respond(event, (err, res) => {     if (err) {       return context.fail(err)     } else {       return context.succeed(res)     }   }) } 

Here's the ../lib/related.js file:

var redis = require('redis') var jsonify = require('redis-jsonify') var rt = require('./ritetag') var redisOptions = {   host: process.env.REDIS_URL,   port: process.env.REDIS_PORT,   password: process.env.REDIS_PASS } var client = jsonify(redis.createClient(redisOptions))  module.exports.respond = function (event, callback) {   var tag = event.hashtag.replace(/^#/, '')   var key = 'related:' + tag    client.on('connect', () => {     console.log('Connected:', client.connected)   })    client.on('end', () => {     console.log('Connection closed.')   })    client.on('ready', function () {     client.get(key, (err, res) => {       if (err) {         client.quit()         callback(err)       } else {         if (res) {           // Tag is found in Redis, so send results directly.           client.quit()           callback(null, res)         } else {           // Tag is not yet in Redis, so query Ritetag.           rt.hashtagDirectory(tag, (err, res) => {             if (err) {               client.quit()               callback(err)             } else {               client.set(key, res, (err) => {                 if (err) {                   callback(err)                 } else {                   client.quit()                   callback(null, res)                 }               })             }           })         }       }     })   }) } 

All of this works as expected, to a point. If I run the function locally (using sls function run related), I have no problems whatsoever—tags are read from and written to the Redis database as they should be. However, when I deploy it (using sls dash deploy), it works the first time it's run after deployment, and then stops working. All subsequent attempts to run it simply return null to the browser (or Postman, or curl, or the web app). This is true regardless of whether the tag I use for testing is already in the database or not. If I then re-deploy, making no changes to the function itself, it works again—once.

On my local machine, the function first logs Connected: true to the console, then the results of the query, then Connection closed. On AWS, it logs Connected: true, then the results of the query, and that's it. On the second run, it logs Connection closed. and nothing else. On the third and all subsequent runs, it logs nothing at all. Neither environment ever reports any errors.

It seems pretty clear that the problem is with the connection to Redis. If I don't close it in the callbacks, then subsequent attempts to call the function just time out. I've also tried using redis.unref instead of redis.quit, but that didn't seem to make any difference.

Any help would be greatly appreciated.

like image 615
Nicholas Avatar asked May 07 '16 23:05

Nicholas


People also ask

Can you use Redis with Lambda?

You can connect to Upstash database from your Lambda functions using your favorite Redis client. You do not need any extra configuration. The only thing to note is you should use the same region for your Lambda function and database to minimize latency.

How do I access my AWS Redis database?

Sign in to the AWS Management Console and open the ElastiCache console at https://console.aws.amazon.com/elasticache/ . From the navigation pane, choose Redis clusters. The clusters screen will appear with a list of Redis (cluster mode disabled) and Redis (cluster mode enabled) clusters.


1 Answers

I've now solved my own problem, and I hope I can be of help to someone experiencing this problem in the future.

There are two major considerations when connecting to a database like I did in the code above from a Lambda function:

  1. Once context.succeed(), context.fail(), or context.done() is called, AWS may freeze any processes that haven't finished yet. This is what was causing AWS to log Connection closed on the second call to my API endpoint—the process was frozen just before Redis finished closing, then thawed on the next call, at which point it continued right where it left off, reporting that the connection was closed. Takeaway: if you want to close your database connection, make sure it's fully closed before you call one of those methods. You can do this by putting a callback in an event handler that's triggered by a connection close (.on('end'), in my case).
  2. If you split your code into separate files and require them at the top of each file, like I did, Amazon will cache as many of those modules as possible in memory. If that's causing problems, try moving the require() calls inside a function instead of at the top of the file, then exporting that function. Those modules will then be re-imported whenever the function is run.

Here's my updated code. Note that I've also put my Redis configuration into a separate file, so I can import it into other Lambda functions without duplicating code.

The Event Handler

'use strict'  const lib = require('../lib/related')  module.exports.handler = function (event, context) {   lib.respond(event, (err, res) => {     if (err) {       return context.fail(err)     } else {       return context.succeed(res)     }   }) } 

Redis Configuration

module.exports = () => {   const redis = require('redis')   const jsonify = require('redis-jsonify')   const redisOptions = {     host: process.env.REDIS_URL,     port: process.env.REDIS_PORT,     password: process.env.REDIS_PASS   }    return jsonify(redis.createClient(redisOptions)) } 

The Function

'use strict'  const rt = require('./ritetag')  module.exports.respond = function (event, callback) {   const redis = require('./redis')()    const tag = event.hashtag.replace(/^#/, '')   const key = 'related:' + tag   let error, response    redis.on('end', () => {     callback(error, response)   })    redis.on('ready', function () {     redis.get(key, (err, res) => {       if (err) {         redis.quit(() => {           error = err         })       } else {         if (res) {           // Tag is found in Redis, so send results directly.           redis.quit(() => {             response = res           })         } else {           // Tag is not yet in Redis, so query Ritetag.           rt.hashtagDirectory(tag, (err, res) => {             if (err) {               redis.quit(() => {                 error = err               })             } else {               redis.set(key, res, (err) => {                 if (err) {                   redis.quit(() => {                     error = err                   })                 } else {                   redis.quit(() => {                     response = res                   })                 }               })             }           })         }       }     })   }) } 

This works exactly as it should—and it's blazing fast, too.

like image 140
Nicholas Avatar answered Nov 13 '22 02:11

Nicholas