perf(api): Redis-back PL24 account tokens (survive restarts, shared api+worker)
PL24 account JWTs were in-memory only, so every (re)deploy forced a fresh login on the next decode, and api + worker each logged in independently — fighting over PL24's one-session-per-account (squeezeOut). Persist account tokens to Redis (keyed by account, TTL = JWT exp) and hydrate from Redis before logging in. Best-effort: any Redis error falls through to a normal login, so auth never depends on Redis. Service tokens stay in-memory (warmed by the boot pre-warm). Complements the OnModuleInit pre-warm — now even the base login survives restarts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
|
||||
import { Injectable, Logger, type OnModuleInit, UnauthorizedException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { RedisService } from "../../redis/redis.service";
|
||||
import { PL24_ENDPOINTS } from "./pl24.constants";
|
||||
import {
|
||||
PL24AuthorizeRequest,
|
||||
@@ -43,7 +44,10 @@ export class PL24AuthService implements OnModuleInit {
|
||||
private readonly proxyUrl: string | null;
|
||||
private readonly timeout: number;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private readonly redis: RedisService,
|
||||
) {
|
||||
this.baseUrl = this.configService.get<string>("pl24.baseUrl", "https://www.partslink24.com");
|
||||
this.companyCode = this.configService.get<string>("pl24.companyCode", "");
|
||||
this.username = this.configService.get<string>("pl24.username", "");
|
||||
@@ -304,6 +308,13 @@ export class PL24AuthService implements OnModuleInit {
|
||||
if (!forceNew && this.tokenData && this.isTokenValid(this.tokenData)) {
|
||||
return this.tokenData;
|
||||
}
|
||||
if (!forceNew) {
|
||||
const cached = await this.hydrateAccountFromRedis("tr");
|
||||
if (cached) {
|
||||
this.tokenData = cached;
|
||||
return this.tokenData;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log("Logging in to PL24 (account 1 tr)...");
|
||||
|
||||
@@ -360,6 +371,7 @@ export class PL24AuthService implements OnModuleInit {
|
||||
expiresAt: new Date(payload.exp * 1000),
|
||||
services: payload.services || [],
|
||||
};
|
||||
await this.persistAccountToRedis("tr", this.tokenData);
|
||||
|
||||
this.logger.log(
|
||||
`PL24 login (tr) successful. Token expires at ${this.tokenData.expiresAt.toISOString()}`,
|
||||
@@ -423,6 +435,13 @@ export class PL24AuthService implements OnModuleInit {
|
||||
if (!forceNew && this.tokenData2 && this.isTokenValid(this.tokenData2)) {
|
||||
return this.tokenData2;
|
||||
}
|
||||
if (!forceNew) {
|
||||
const cached = await this.hydrateAccountFromRedis("de");
|
||||
if (cached) {
|
||||
this.tokenData2 = cached;
|
||||
return this.tokenData2;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.companyCode2 || !this.username2 || !this.password2) {
|
||||
throw new UnauthorizedException("PL24 account 2 (de) credentials not configured");
|
||||
@@ -481,6 +500,7 @@ export class PL24AuthService implements OnModuleInit {
|
||||
expiresAt: new Date(payload.exp * 1000),
|
||||
services: payload.services || [],
|
||||
};
|
||||
await this.persistAccountToRedis("de", this.tokenData2);
|
||||
|
||||
this.logger.log(
|
||||
`PL24 login (de) successful. Token expires at ${this.tokenData2.expiresAt.toISOString()}`,
|
||||
@@ -511,6 +531,33 @@ export class PL24AuthService implements OnModuleInit {
|
||||
return this.proxyAgent;
|
||||
}
|
||||
|
||||
// ── Redis-backed account-token cache (survives restarts; shared api+worker) ──
|
||||
// Best-effort: any Redis error falls through to a normal login, so PL24 auth
|
||||
// never depends on Redis being up. Sharing the token across processes also
|
||||
// avoids the squeezeOut session-fight (PL24 allows one session per account).
|
||||
private acctKey(account: "tr" | "de"): string {
|
||||
return `pl24:auth:acct:${account}`;
|
||||
}
|
||||
|
||||
private async hydrateAccountFromRedis(account: "tr" | "de"): Promise<PL24TokenData | null> {
|
||||
try {
|
||||
const raw = await this.redis.getJson<PL24TokenData>(this.acctKey(account));
|
||||
if (!raw) return null;
|
||||
const data: PL24TokenData = { ...raw, expiresAt: new Date(raw.expiresAt) };
|
||||
if (!this.isTokenValid(data)) return null;
|
||||
this.logger.log(`PL24 ${account} token hydrated from Redis (login skipped)`);
|
||||
return data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async persistAccountToRedis(account: "tr" | "de", data: PL24TokenData): Promise<void> {
|
||||
const ttlSeconds = Math.floor((data.expiresAt.getTime() - Date.now()) / 1000);
|
||||
if (ttlSeconds <= 0) return;
|
||||
await this.redis.setJson(this.acctKey(account), data, ttlSeconds).catch(() => {});
|
||||
}
|
||||
|
||||
private isTokenValid(token: PL24TokenData): boolean {
|
||||
const bufferMs = 60 * 1000;
|
||||
return token.expiresAt.getTime() - bufferMs > Date.now();
|
||||
|
||||
Reference in New Issue
Block a user