Redis(ioredis)-无法捕获连接错误以便优雅地处理它们

18
我正在尝试优雅地处理redis错误,以便在错误发生时绕过它并做其他事情,而不是使我的应用程序崩溃。
但到目前为止,我无法捕获ioredis抛出的异常,它绕过了我的try/catch并终止了当前进程。这种当前行为不允许我优雅地处理错误,并从替代系统(而不是redis)获取数据。
import { createLogger } from '@unly/utils-simple-logger';
import Redis from 'ioredis';
import epsagon from './epsagon';

const logger = createLogger({
  label: 'Redis client',
});

/**
 * Creates a redis client
 *
 * @param url Url of the redis client, must contain the port number and be of the form "localhost:6379"
 * @param password Password of the redis client
 * @param maxRetriesPerRequest By default, all pending commands will be flushed with an error every 20 retry attempts.
 *          That makes sure commands won't wait forever when the connection is down.
 *          Set to null to disable this behavior, and every command will wait forever until the connection is alive again.
 * @return {Redis}
 */
export const getClient = (url = process.env.REDIS_URL, password = process.env.REDIS_PASSWORD, maxRetriesPerRequest = 20) => {
  const client = new Redis(`redis://${url}`, {
    password,
    showFriendlyErrorStack: true, // See https://github.com/luin/ioredis#error-handling
    lazyConnect: true, // XXX Don't attempt to connect when initializing the client, in order to properly handle connection failure on a use-case basis
    maxRetriesPerRequest,
  });

  client.on('connect', function () {
    logger.info('Connected to redis instance');
  });

  client.on('ready', function () {
    logger.info('Redis instance is ready (data loaded from disk)');
  });

  // Handles redis connection temporarily going down without app crashing
  // If an error is handled here, then redis will attempt to retry the request based on maxRetriesPerRequest
  client.on('error', function (e) {
    logger.error(`Error connecting to redis: "${e}"`);
    epsagon.setError(e);

    if (e.message === 'ERR invalid password') {
      logger.error(`Fatal error occurred "${e.message}". Stopping server.`);
      throw e; // Fatal error, don't attempt to fix
    }
  });

  return client;
};

我正在模拟错误的密码/url,以查看如何处理redis配置不当。为了处理调用方的错误,我将lazyConnect设置为true

但是,当我将url定义为localhoste:6379 (而不是localhost:6379)时,我会收到以下错误:

server 2019-08-10T19:44:00.926Z [Redis client] error:  Error connecting to redis: "Error: getaddrinfo ENOTFOUND localhoste localhoste:6379"
(x 20)
server 2019-08-10T19:44:11.450Z [Read cache] error:  Reached the max retries per request limit (which is 20). Refer to "maxRetriesPerRequest" option for details.

这是我的代码:

  // Fetch a potential query result for the given query, if it exists in the cache already
  let cachedItem;

  try {
    cachedItem = await redisClient.get(queryString); // This emit an error on the redis client, because it fails to connect (that's intended, to test the behaviour)
  } catch (e) {
    logger.error(e); // It never goes there, as the error isn't "thrown", but rather "emitted" and handled by redis its own way
    epsagon.setError(e);
  }

  // If the query is cached, return the results from the cache
  if (cachedItem) {
    // return item
  } else {} // fetch from another endpoint (fallback backup)

我的理解是,Redis错误通过 client.emit('error', error) 处理,这是异步的,调用者不会抛出错误,这不允许调用者使用try/catch来处理错误。

Redis的错误是否应该以非常特定的方式处理?难道不能像我们通常处理大多数错误一样捕获它们吗?

此外,默认情况下,Redis在抛出一个致命异常之前会重试连接20次。但我想以自己的方式处理任何异常。

我已经通过提供错误的连接数据测试了Redis客户端的行为,这使得连接无法建立,因为在那个URL上没有可用的Redis实例,我的目标是最终捕获所有类型的Redis错误并优雅地处理它们。


此日志来自“错误”事件,并记录在我的 AWS CloudWatch 日志中。 我所说的“崩溃”,是指当无法创建连接时,当前进程从 redis 终止,我希望捕获该错误,目前我无法控制它,因为它绕过了我尝试优雅地处理错误的 try/catch 尝试,破坏了我正在构建的工作流程。 - Vadorequest
谢谢@Nickolay,我在测试中迷失了方向,你帮我想明白了。确实,在client:on:error事件中不能有任何throw,我可能在测试时遇到了一些竞态条件,或者修复了一个错误,但现在这样做很好! - Vadorequest
1
太酷了!我已经把它发布为答案了。 - Nickolay
即使在我的client.on("error")代码中没有throw,我的nodejs应用程序仍然崩溃了。 - Aung Myint Thein
你的优雅错误捕获是如何实现的?@Vadorequest - Aung Myint Thein
显示剩余3条评论
2个回答

14

连接错误会在客户端Redis对象上作为error事件报告

根据文档中的“自动重新连接”章节,当与Redis的连接丢失(或者首次建立连接失败)时,ioredis将自动尝试重新连接。只有在maxRetriesPerRequest次尝试之后,待处理的命令才会“带着错误”被清空,也就是到达这里的catch部分:

  try {
    cachedItem = await redisClient.get(queryString); // This emit an error on the redis client, because it fails to connect (that's intended, to test the behaviour)
  } catch (e) {
    logger.error(e); // It never goes there, as the error isn't "thrown", but rather "emitted" and handled by redis its own way
    epsagon.setError(e);
  }

由于您在第一个错误时停止了程序:

  client.on('error', function (e) {
    // ...
    if (e.message === 'ERR invalid password') {
      logger.error(`Fatal error occurred "${e.message}". Stopping server.`);
      throw e; // Fatal error, don't attempt to fix

如果重试和随后的“错误清除”从未运行,那么请忽略client.on('error'中的错误,并且您应该会从await redisClient.get()返回错误。


1
对我来说没有用。连接到集群时,唯一会抛出错误的方法是在clusterRetryStrategy上返回“null”:clusterRetryStrategy: (times: number) => { return null;}。如果有人对集群有建议,请告诉我。谢谢。 - user906573

10

以下是我团队在TypeScript项目中使用IORedis所做的工作:

  let redis;
  const redisConfig: Redis.RedisOptions = {
    port: parseInt(process.env.REDIS_PORT, 10),
    host: process.env.REDIS_HOST,
    autoResubscribe: false,
    lazyConnect: true,
    maxRetriesPerRequest: 0, // <-- this seems to prevent retries and allow for try/catch
  };

  try {

    redis = new Redis(redisConfig);

    const infoString = await redis.info();
    console.log(infoString)

  } catch (err) {

    console.log(chalk.red('Redis Connection Failure '.padEnd(80, 'X')));
    console.log(err);
    console.log(chalk.red(' Redis Connection Failure'.padStart(80, 'X')));
    // do nothing

  } finally {
    await redis.disconnect();
  }

你把这个放在一个函数里了吗?每次请求时都连接Redis吗?我仍然被未处理的错误困扰。 - Aung Myint Thein
@AungMyintThein 将 enable_offline_queue 设置为 false,并阅读此链接及其相关问题 https://stackoverflow.com/questions/66893022/how-to-configure-node-redis-client-to-throw-errors-immediately-when-connection - Stupid Man
谢谢,显式的 disconnect() 防止了无限重试。即使删除了参数:lazyConnectmaxRetriesPerRequest,仍然可以继续工作。 - OXiGEN

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接