skip to Main Content

I am having some problems with sockets. I’m using express-session to manage session and it is storing session on redis.

I want to use pool for connections of redis. Express session uses RedisStore and RedisStore needs RedisClient, I cannot give it pool instead of RedisClient,

My Code :

const Redis = require('ioredis')
const session = require('express-session')
const RedisStore = require('connect-redis').default

var redisClient  = new Redis({ host: `redis`, port: 6379 })

redisClient.on('ready', () => { console.log(`Redis is ready @${redisClient.options.host}`)})
redisClient.on('error', (err) => { console.log('Redis error: ', err) })

const sessionAge = 24 * 60 * 1000

module.exports = session({
  store: new RedisStore({ client: redisClient }),
  key: 'sid',
  secret: 'secret',
  saveUninitialized: false,
  resave: false,
  rolling: false,
  cookie: { secure: false, maxAge: sessionAge, sameSite: 'lax' },
  expires: sessionAge
})

2

Answers


  1. Chosen as BEST ANSWER

    I've found a way to create a fake RedisStore that does Redis transactions using the methods I created.

    const session = require('express-session')
    const RedisStore = require('connect-redis').default
    const { RedisPool } = require('ioredis-conn-pool')
    
    let pool = new RedisPool({
        redis: {
          host: `redis`,
          port: 6379
        },
        pool: {
          min: 10,
          max: 100
        }
      })
    
    const redisStore = new RedisStore({ client: {} })
    
    const noop = (_err, _data) => {}
    
    redisStore.get = async function (sid, cb = noop) {
      const key = this.prefix + sid
      let redisClient
      try {
        redisClient = await pool.getConnection()
        const data = await redisClient.get(key)
        if (!data) return cb()
        return cb(null, await this.serializer.parse(data))
      } catch (err) {
        return cb(err)
      } finally {
        if (redisClient) {
          await pool.release(redisClient)
        }
      }
    }
    
    redisStore.set = async function (sid, sess, cb = noop) {
      const key = this.prefix + sid
      const ttl = this._getTTL(sess)
      let redisClient
      try {
        const val = this.serializer.stringify(sess)
        if (ttl > 0) {
          redisClient = await pool.getConnection()
          if (this.disableTTL) {
            await redisClient.set(key, val)
          } else {
            const isRedis = 'scanIterator' in redisClient
            if (isRedis) {
              await redisClient.set(key, val, { EX: ttl })
            } else {
              await redisClient.set(key, val, 'EX', ttl)
            }
          }
          return cb()
        } else {
          return this.destroy(sid, cb)
        }
      } catch (err) {
        return cb(err)
      } finally {
        if (redisClient) {
          await pool.release(redisClient)
        }
      }
    }
    
    redisStore.destroy = async function (sid, cb = noop) {
      const key = this.prefix + sid
      let redisClient
      try {
        redisClient = await pool.getConnection()
        await redisClient.del([key])
        return cb()
      } catch (err) {
        return cb(err)
      } finally {
        if (redisClient) {
          await pool.release(redisClient)
        }
      }
    }
    
    redisStore.touch = async function (sid, sess, cb = noop) {
      const key = this.prefix + sid
      if (this.disableTouch || this.disableTTL) return cb()
      let redisClient
      try {
        redisClient = await pool.getConnection()
        await redisClient.expire(key, this._getTTL(sess))
        return cb()
      } catch (err) {
        return cb(err)
      } finally {
        if (redisClient) {
          await pool.release(redisClient)
        }
      }
    }
    
    const sessionAge = 24 * 60 * 1000
    
    module.exports = session({
      store: redisStore,
      key: 'sid',
      secret: 'secret',
      saveUninitialized: false,
      resave: false,
      rolling: false,
      cookie: { secure: false, maxAge: sessionAge, sameSite: 'lax' },
      expires: sessionAge
    })
    

  2. You are correct: to implement connection pooling for Redis in your Express.js application using express-session and connect-redis, you can indeed use the ioredis package.
    ioredis supports connection pooling, meaning it manages multiple connections under the hood.

     Express App
        │
        ├─> express-session
        │      │
        │      └─> RedisStore ──> ioredis (Redis Client)
        │
        └─> Redis Server
    

    But you seem to be under the impression that you need to explicitly configure or manage connection pooling for Redis within their application.

    However, ioredis automatically handles connection pooling. When you a new instance of Redis, as you did with var redisClient = new Redis({ host: 'redis', port: 6379 }), ioredis is already managing the connections efficiently. Each command issued through ioredis is queued and executed using an available connection in a pool managed internally by ioredis.
    There is no need to manually set up pooling mechanisms as ioredis optimizes connection usage and management out of the box.

    The only additional considerations would be around fine-tuning the Redis client configurations:

    const Redis = require('ioredis');
    const session = require('express-session');
    const RedisStore = require('connect-redis')(session);
    
    // ioredis automatically manages a pool of connections
    var redisClient = new Redis({
      host: 'redis',
      port: 6379,
      // Additional options to manage the pool:
      maxRetriesPerRequest: null,  // Do not give up on a request
      enableReadyCheck: true,      // Only use ready instances
      retryStrategy: (times) => Math.min(times * 50, 2000)  // Increase delay between retries
    });
    
    redisClient.on('ready', () => { console.log(`Redis is ready @${redisClient.options.host}`) });
    redisClient.on('error', (err) => { console.log('Redis error: ', err) });
    
    const sessionAge = 24 * 60 * 60 * 1000;  // Fixed to correct unit (milliseconds for one day)
    
    module.exports = session({
      store: new RedisStore({ client: redisClient }),
      key: 'sid',
      secret: 'secret',
      saveUninitialized: false,
      resave: false,
      rolling: false,
      cookie: { secure: false, maxAge: sessionAge, sameSite: 'lax' },
      expires: sessionAge
    });
    

    Note: redis/ioredis issue 470 uses new redis.Cluster() for connection pooling, but for a single Redis instance, ioredis handles connection pooling internally.

    The sessionAge was corrected to use milliseconds equivalent to 24 hours as express-session expects this value in milliseconds.
    The error handling and logging in place will provide visibility into the health of the Redis connection.


    Do you mean a single connection is created?"

    When using ioredis, a single connection is not created per request. Instead, ioredis manages a pool of connections internally. That means when you create a new instance of the Redis class, it handles multiple connections under the hood, reusing and managing these connections efficiently across multiple requests. So, for every request, your application does not create a new Redis connection; it uses the pool managed by ioredis.

    Your solution involves manually managing a connection pool using a third-party library, ioredis-conn-pool, which is not part of the standard ioredis package. It allows for explicit control over the number of connections (with a minimum and maximum threshold), which can be beneficial in scenarios where very granular control over connection management is needed. You modify the methods of RedisStore to manage fetching and releasing connections from the pool for each session operation (get, set, destroy, touch), which adds complexity but offers full control over how connections are utilized.

    That seems… a bit harder to maintain and debug. And it relies on an additional third-party library (ioredis-conn-pool), which might have its own maintenance and compatibility issues.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search