nest icon indicating copy to clipboard operation
nest copied to clipboard

Cluster mode and Socketio sticky sessions

Open Bubuioc opened this issue 1 year ago • 10 comments
trafficstars

Is there an existing issue for this?

  • [X] I have searched the existing issues

Current behavior

Please someone. How can i implement sticky session for a Nest app running in cluster mode.The Nest docs have no info about this and Socketio have no docs for nest(.I have posted a question on Socketio github discussions and Nest discord but nothing.I already implemented adapter but the sticky sessions from ** @socket.io/sticky** i can't setup.Maybe someone did this.

Minimum reproduction code

a

Steps to reproduce

No response

Expected behavior

This is main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';
import { Logger, ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { SocketIOAdapter } from './core/socket/adapters/socket.io.adapter';
import * as cluster from 'cluster';
import * as os from 'os';
const clusterInstance = cluster as unknown as cluster.Cluster;
import { NODE_ENV } from './common/enums/node.env.enum';

async function bootstrap() {
  const logger = new Logger();
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
  app.enableCors({
    origin: '*',
  });

  const socketIOAdapter = new SocketIOAdapter(app);
  app.useWebSocketAdapter(socketIOAdapter);
  const configService: ConfigService = app.get(ConfigService);

  const port: number = configService.get('port');
  const nodeEnv: string = configService.get('NODE_ENV');

  if (nodeEnv !== NODE_ENV.PRODUCTION) {
    const config = new DocumentBuilder()
      .setTitle('Chat application')
      .setDescription('The corporative chat application written in Nest')
      .setVersion('1.0')
      .addBearerAuth({ type: 'http', name: 'accessToken' })
      .build();
    const document = SwaggerModule.createDocument(app, config);
    SwaggerModule.setup('api/docs', app, document);
  }

  await app.listen(port);

  logger.log(
    `Find docs on http://localhost:${port}/api/docs`,
    'NestApplication',
  );
}

const runningOnCluster: boolean =
  Object.values(NODE_ENV)
    .filter((env) => env !== NODE_ENV.LOCAL)
    .findIndex((env) => env === process.env.NODE_ENV) > -1;

if (runningOnCluster) {
  if (clusterInstance.isPrimary) {
    const cpuCount = os.cpus().length;
    for (let i = 0; i < cpuCount; i += 1) {
      clusterInstance.fork();
    }
    clusterInstance.on('online', (worker) => {
      Logger.log(`${worker.process.pid} is online`, 'NestApplication::Cluster');
    });
    clusterInstance.on('exit', ({ process }) => {
      Logger.log(`${process.pid} died`, 'NestApplication::Cluster');
    });
  } else {
    bootstrap();
  }
} else {
  bootstrap();
}

and this is redis streams adatper

import { INestApplicationContext } from '@nestjs/common';
import { IoAdapter } from '@nestjs/platform-socket.io';
import { Server, ServerOptions } from 'socket.io';
import { WsAuthMiddleware } from '../../../core/socket/middlewares/ws.auth.middleware';
import { createAdapter } from '@socket.io/redis-streams-adapter';
import { ConfigService } from '@nestjs/config';
import {
  DEFAULT_NAMESPACE,
  REDIS_CLIENT,
} from 'src/common/constants/constants';
import { WsQueryValidationMiddleware } from '../middlewares/ws.query.validation.middleware';
import { RedisClientType } from 'redis';

export class SocketIOAdapter extends IoAdapter {
  private configService: ConfigService;
  private redisClient: RedisClientType;
  private adapterConstructor: ReturnType<typeof createAdapter>;

  constructor(private app: INestApplicationContext) {
    super(app);
    this.configService = this.app.get(ConfigService);
    this.redisClient = this.app.get(REDIS_CLIENT);
    this._createClient();
  }

  private _createClient(): void {
    const streamName = this.configService.get<string>('redisConfig.streamName');
    this.adapterConstructor = createAdapter(this.redisClient, { streamName });
  }

  createIOServer(port: number, options?: ServerOptions): Server {
    const server: Server = super.createIOServer(port, {
      ...options,
      cors: {
        origin: (_req: any, callback: (arg0: null, arg1: boolean) => void) => {
          callback(null, true);
        },
        credentials: true,
      },
      transports: ['polling', 'websocket'],
      allowUpgrades: true,
    });
    server.adapter(this.adapterConstructor);

    server.of(DEFAULT_NAMESPACE).use(WsAuthMiddleware);
    server.of(DEFAULT_NAMESPACE).use(WsQueryValidationMiddleware);

    return server;
  }
}

Package

  • [ ] I don't know. Or some 3rd-party package
  • [ ] @nestjs/common
  • [ ] @nestjs/core
  • [ ] @nestjs/microservices
  • [ ] @nestjs/platform-express
  • [ ] @nestjs/platform-fastify
  • [X] @nestjs/platform-socket.io
  • [ ] @nestjs/platform-ws
  • [ ] @nestjs/testing
  • [ ] @nestjs/websockets
  • [ ] Other (see below)

Other package

No response

NestJS version

No response

Packages versions

    "@nestjs/websockets": "^10.3.8",
    "@socket.io/redis-streams-adapter": "^0.2.2",
    "@socket.io/sticky": "^1.0.4",

Node.js version

No response

In which operating systems have you tested?

  • [X] macOS
  • [X] Windows
  • [X] Linux

Other

No response

Bubuioc avatar Oct 10 '24 14:10 Bubuioc