Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/backtest-kit/backtest-kit-redis-postgres-pgpool-docker/llms.txt

Use this file to discover all available pages before exploring further.

RedisService manages the ioredis connection lifecycle for the persistence layer. It exposes a single public method — waitForInit() — that resolves as soon as the underlying Redis client emits the ready event, indicating the connection is fully established and commands can be issued. All 16 cache services depend on RedisService and call waitForInit() before their first GET/SET operation.

API

waitForInit()
() => Promise<Redis>
Waits for the ioredis client to reach the ready state and resolves with the connected Redis instance.
  • Singleshot — powered by singleshot from functools-kit. The connection attempt runs at most once; all concurrent callers share the same promise.
  • Fast path — if redis.status === 'ready' at call time, the promise resolves immediately without registering any event listeners.
  • 15-second timeout — if the ready event has not fired within CONNECTION_TIMEOUT (15 000 ms), the method throws Error("Redis connection timeout") and clears the singleshot cache so the call can be retried.

Source Code

import { Redis } from "ioredis";
import { singleshot, sleep } from "functools-kit";
import { getRedis } from "../../../config/redis";
import { inject } from "../../core/di";
import LoggerService from "./LoggerService";
import TYPES from "../../core/types";

const CONNECTION_TIMEOUT = 15_000;
const TIMEOUT_SYMBOL = Symbol("timeout");

const waitForConnect = (redis: Redis, self: RedisService) =>
  new Promise<void>((resolve) => {
    redis.on("ready", () => {
      self.loggerService.log("redisService ready");
      resolve();
    });
  });

export class RedisService {

  readonly loggerService = inject<LoggerService>(TYPES.loggerService);

  public waitForInit = singleshot(async () => {
    this.loggerService.log("redisService waitForInit");
    const redis = await getRedis();
    if (redis.status === "ready") {
      return redis;
    }
    const result = await Promise.race([
      waitForConnect(redis, this),
      sleep(CONNECTION_TIMEOUT).then(() => TIMEOUT_SYMBOL),
    ]);
    if (result === TIMEOUT_SYMBOL) {
      this.waitForInit.clear();
      throw new Error("Redis connection timeout");
    }
    return redis;
  });

  protected init = async () => {
    this.loggerService.log("redisService init");
    await this.waitForInit();
  };
}

export default RedisService;

Connection Configuration

getRedis() in src/config/redis.ts constructs the ioredis Redis client. Like getPostgres(), it is wrapped in singleshot so the same client instance is reused across all cache services.
import { singleshot } from "functools-kit";
import { Redis } from "ioredis";
import {
  CC_REDIS_HOST,
  CC_REDIS_PASSWORD,
  CC_REDIS_PORT,
  CC_REDIS_USER,
} from "./params";

export const getRedis = singleshot(() => {
  const redis = new Redis({
    host:     CC_REDIS_HOST,
    port:     CC_REDIS_PORT,
    username: CC_REDIS_USER,
    password: CC_REDIS_PASSWORD,
  });

  setInterval(async () => {
    await redis.ping();
  }, 30000);

  process.on("SIGINT", async () => {
    await redis.disconnect(false);
  });

  return redis;
});

Connection Parameters

Environment VariableDefaultDescription
CC_REDIS_HOST"127.0.0.1"Redis server hostname or IP
CC_REDIS_PORT6379Redis server port
CC_REDIS_USER"default"Redis ACL username
CC_REDIS_PASSWORD"mysecurepassword"Redis ACL password
All four variables are read from src/config/params.ts. Override them in your deployment environment to point at a Redis Sentinel, Redis Cluster, or a managed Redis service.

Ready Detection

waitForInit() applies a two-branch check to avoid attaching a redundant event listener when the client is already healthy:
1

Immediate fast path

After getRedis() returns the client instance, redis.status is checked synchronously. If it equals 'ready', the promise resolves immediately:
const redis = await getRedis();
if (redis.status === "ready") {
  return redis; // no event listener needed
}
2

Event-based wait with timeout

Otherwise, a Promise.race runs between the ready-event listener and a 15-second sleep sentinel:
const result = await Promise.race([
  waitForConnect(redis, this),
  sleep(CONNECTION_TIMEOUT).then(() => TIMEOUT_SYMBOL),
]);
3

Timeout handling

If the sentinel wins the race, the singleshot cache is cleared and an error is thrown, allowing a retry:
if (result === TIMEOUT_SYMBOL) {
  this.waitForInit.clear();
  throw new Error("Redis connection timeout");
}

Keepalive Ping

A setInterval inside getRedis() issues a PING command every 30 seconds. This keeps the TCP connection alive through firewalls and load balancers that would otherwise drop idle connections:
setInterval(async () => {
  await redis.ping();
}, 30000);
The keepalive interval is started when the Redis object is first constructed, before the ready event fires. ioredis automatically queues the PING commands and flushes them once the connection is established.

Graceful Shutdown

When the Node.js process receives SIGINT, the Redis connection is disconnected cleanly. The false argument to disconnect() tells ioredis to wait for pending commands to complete before closing the socket:
process.on("SIGINT", async () => {
  await redis.disconnect(false);
});
If the process is killed with SIGKILL instead of SIGINT, the keepalive interval and graceful disconnect will not run. Use a process supervisor (e.g., Docker, PM2) that sends SIGTERM/SIGINT to ensure clean teardown.

Build docs developers (and LLMs) love