web-dev-qa-db-fra.com

Comment dois-je me connecter à une instance Redis à partir d'une fonction AWS Lambda?

J'essaie de créer une API pour une application Web d'une seule page en utilisant AWS Lambda et Serverless Framework . Je veux utiliser Redis Cloud pour le stockage, principalement pour sa combinaison de vitesse et de persistance des données. Je pourrais utiliser plus de fonctionnalités Redis Cloud à l'avenir, donc je préférerais éviter d'utiliser ElastiCache pour cela. Mon instance Redis Cloud s'exécute dans la même région AWS que ma fonction.

J'ai une fonction appelée related qui prend un hashtag d'une demande GET vers un point de terminaison API et vérifie s'il y a une entrée pour cela dans la base de données. S'il est là, il devrait renvoyer les résultats immédiatement. Sinon, il doit interroger RiteTag , écrire les résultats dans Redis, puis renvoyer les résultats à l'utilisateur.

Je suis assez nouveau dans ce domaine, donc je fais probablement quelque chose d'adorablement naïf. Voici le gestionnaire d'événements:

'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)
    }
  })
}

Ici se trouve le ../lib/related.js fichier:

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)
                }
              })
            }
          })
        }
      }
    })
  })
}

Tout cela fonctionne comme prévu, jusqu'à un certain point. Si j'exécute la fonction localement (en utilisant sls function run related), Je n'ai aucun problème: les balises sont lues et écrites dans la base de données Redis comme il se doit. Cependant, lorsque je le déploie (en utilisant sls dash deploy), il fonctionne la première fois qu'il est exécuté après le déploiement , puis cesse de fonctionner. Toutes les tentatives ultérieures de l'exécuter renvoient simplement null au navigateur (ou Postman, ou curl, ou l'application Web). Cela est vrai que la balise que j'utilise pour les tests soit déjà dans la base de données ou non. Si je redéploie ensuite, sans modifier la fonction elle-même, cela fonctionne à nouveau - une fois.

Sur ma machine locale, la fonction enregistre d'abord Connected: true à la console, puis les résultats de la requête, puis Connection closed. Sur AWS, il enregistre Connected: true, puis les résultats de la requête, et c'est tout. Lors de la deuxième exécution, il enregistre Connection closed. et rien d'autre. Lors de la troisième et de toutes les exécutions suivantes, il ne consigne rien du tout. Aucun des deux environnements ne signale d'erreurs.

Il semble assez clair que le problème est lié à la connexion à Redis. Si je ne le ferme pas dans les rappels, les tentatives ultérieures d'appeler la fonction expirent. J'ai également essayé d'utiliser redis.unref au lieu de redis.quit, mais cela ne semble pas faire de différence.

Toute aide serait grandement appréciée.

16
Nicholas

J'ai maintenant résolu mon propre problème, et j'espère que je pourrai aider quelqu'un qui connaîtra ce problème à l'avenir.

Il y a deux considérations principales lors de la connexion à une base de données comme je l'ai fait dans le code ci-dessus à partir d'une fonction Lambda:

  1. Une fois que context.succeed(), context.fail() ou context.done() est appelée, AWS peut geler tous les processus qui ne sont pas encore terminés. C'est ce qui a amené AWS à se connecter Connection closed Lors du deuxième appel à mon point de terminaison API: le processus a été gelé juste avant la fermeture de Redis, puis décongelé lors de l'appel suivant, auquel cas il a continué là où il s'était arrêté. , signalant que la connexion a été fermée. À retenir: si vous souhaitez fermer votre connexion à la base de données, assurez-vous qu'elle est complètement fermée avant vous appelez l'une de ces méthodes. Vous pouvez le faire en plaçant un rappel dans un gestionnaire d'événements qui est déclenché par une fermeture de connexion (.on('end'), dans mon cas).
  2. Si vous divisez votre code en fichiers séparés et require en haut de chaque fichier, comme je l'ai fait, Amazon mettra en cache autant de modules que possible en mémoire. Si cela pose problème, essayez de déplacer les appels require() à l'intérieur d'une fonction plutôt qu'en haut du fichier, puis exportez cette fonction. Ces modules seront ensuite réimportés à chaque exécution de la fonction.

Voici mon code mis à jour. Notez que j'ai également mis ma configuration Redis dans un fichier séparé, donc je peux l'importer dans d'autres fonctions Lambda sans dupliquer le code.

Le gestionnaire d'événements

'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)
    }
  })
}

Configuration de Redis

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))
}

La fonction

'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
                  })
                }
              })
            }
          })
        }
      }
    })
  })
}

Cela fonctionne exactement comme il se doit - et il est aussi extrêmement rapide.

23
Nicholas