Skip to content

Commit 1abf314

Browse files
committed
Migrates to @redis/client
1 parent eba9c92 commit 1abf314

File tree

3 files changed

+49
-159
lines changed

3 files changed

+49
-159
lines changed

apps/pwabuilder-google-play/package-lock.json

Lines changed: 1 addition & 96 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/pwabuilder-google-play/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@azure/storage-blob": "^12.29.0",
2929
"@bubblewrap/core": "^1.24.0",
3030
"@opentelemetry/api": "^1.9.0",
31+
"@redis/client": "^5.10.0",
3132
"@redis/entraid": "^5.10.0",
3233
"@types/archiver": "^3.0.0",
3334
"@types/cors": "^2.8.13",
@@ -44,7 +45,6 @@
4445
"escape-html": "^1.0.3",
4546
"express": "^4.21.2",
4647
"fs-extra": "^11.1.0",
47-
"ioredis": "^5.8.0",
4848
"node-fetch": "^3.3.1",
4949
"password-generator": "^2.3.2",
5050
"path": "^0.12.7",

apps/pwabuilder-google-play/services/redisService.ts

Lines changed: 47 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { Redis } from "ioredis";
1+
import { createClient, RedisClientType } from "@redis/client";
22
import { DefaultAzureCredential } from "@azure/identity";
3-
import { EntraIdManagedIdentityAuthenticator } from "@redis/entraid";
3+
import { EntraIdCredentialsProviderFactory, REDIS_SCOPE_DEFAULT } from "@redis/entraid";
44

55
/**
66
* A service for interacting with a Redis database. In local development, this will be implemented as an in-memory database. For non-local environments, it will be implemented as a Redis database.
77
*/
8-
export interface DatabaseService {
8+
export interface RedisDatabaseService {
99
ready: Promise<void>;
1010
getJson<T>(key: string): Promise<T | null>;
1111
save<T>(key: string, value: T): Promise<void>;
@@ -18,11 +18,10 @@ export interface DatabaseService {
1818
/**
1919
* Database service that connects to PWABuilder's Redis instance in Azure.
2020
*/
21-
export class RedisService implements DatabaseService {
21+
export class RedisService implements RedisDatabaseService {
2222
public readonly ready: Promise<void>;
23-
private readonly redis: Redis;
23+
private readonly redis: RedisClientType;
2424
private readonly redisHost: string;
25-
private readonly authenticator: EntraIdManagedIdentityAuthenticator;
2625

2726
/**
2827
*
@@ -33,30 +32,38 @@ export class RedisService implements DatabaseService {
3332
throw new Error("REDIS_HOST environment variable is not set.");
3433
}
3534

36-
this.authenticator = new EntraIdManagedIdentityAuthenticator(new DefaultAzureCredential());
3735
this.redis = this.createRedisConnection();
3836
this.ready = this.initializeConnection();
3937
}
4038

41-
private createRedisConnection(): Redis {
42-
return new Redis({
43-
host: this.redisHost,
44-
port: 6380,
45-
tls: {
46-
servername: this.redisHost
47-
},
48-
connectTimeout: 30000, // 30 seconds to establish connection (Azure cold start)
49-
commandTimeout: 10000, // 10 seconds for individual commands
50-
lazyConnect: true, // Don't connect immediately
51-
maxRetriesPerRequest: 3, // Reasonable retries
52-
enableReadyCheck: false, // Disable ready check - authenticate first
53-
keepAlive: 30000, // Keep connections alive
54-
family: 4, // Force IPv4 for better Azure compatibility
55-
reconnectOnError: (err) => {
56-
const targetErrors = ['READONLY', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED'];
57-
return targetErrors.some(targetError => err.message.includes(targetError));
39+
private createRedisConnection(): RedisClientType {
40+
// Create credentials provider for authentication
41+
const credential = new DefaultAzureCredential();
42+
const credentialsProvider = EntraIdCredentialsProviderFactory.createForDefaultAzureCredential({
43+
credential,
44+
scopes: REDIS_SCOPE_DEFAULT,
45+
tokenManagerConfig: {
46+
expirationRefreshRatio: 0.8 // Refresh token after 80% of its lifetime
5847
}
5948
});
49+
50+
return createClient({
51+
socket: {
52+
host: this.redisHost,
53+
port: 6380,
54+
tls: true,
55+
connectTimeout: 30000, // 30 seconds to establish connection (Azure cold start)
56+
reconnectStrategy: (retries) => {
57+
// Exponential backoff with max 3000ms delay
58+
const delay = Math.min(retries * 100, 3000);
59+
console.info(`Reconnecting to Redis, attempt ${retries}, waiting ${delay}ms`);
60+
return delay;
61+
}
62+
},
63+
credentialsProvider,
64+
commandsQueueMaxLength: 1000,
65+
pingInterval: 30000 // Keep-alive ping every 30 seconds
66+
});
6067
}
6168

6269
private async initializeConnection(): Promise<void> {
@@ -66,35 +73,13 @@ export class RedisService implements DatabaseService {
6673
console.error("Redis connection error:", err);
6774
});
6875

69-
this.redis.on("close", () => {
70-
console.warn("Redis connection closed");
71-
});
72-
73-
// Connect to Redis
74-
await this.redis.connect();
75-
console.info("Redis connected, authenticating with managed identity...");
76-
77-
// Authenticate using EntraId authenticator (handles token refresh automatically)
78-
await this.authenticator.authenticateWithHello(this.redis);
79-
console.info("Redis authenticated successfully with managed identity");
80-
81-
// Set up reconnection handler to re-authenticate
8276
this.redis.on("reconnecting", () => {
8377
console.info("Redis reconnecting...");
8478
});
8579

86-
// Re-authenticate after reconnection
87-
this.redis.on("connect", async () => {
88-
if (this.redis.status === "reconnecting" || this.redis.status === "connecting") {
89-
console.info("Re-authenticating after reconnection...");
90-
try {
91-
await this.authenticator.authenticateWithHello(this.redis);
92-
console.info("Re-authentication successful");
93-
} catch (authError) {
94-
console.error("Redis re-authentication failed:", authError);
95-
}
96-
}
97-
});
80+
// Connect to Redis - authentication is handled automatically by credentialsProvider
81+
await this.redis.connect();
82+
console.info("Redis connected and authenticated with managed identity");
9883
} catch (error) {
9984
console.error("Failed to initialize Redis connection:", error);
10085
throw error;
@@ -141,7 +126,9 @@ export class RedisService implements DatabaseService {
141126
});
142127

143128
await Promise.race([
144-
this.redis.set(key, JSON.stringify(value), 'EX', expirationSeconds),
129+
this.redis.set(key, JSON.stringify(value), {
130+
EX: expirationSeconds
131+
}),
145132
timeoutPromise
146133
]);
147134

@@ -162,9 +149,8 @@ export class RedisService implements DatabaseService {
162149
const startTime = Date.now();
163150

164151
// Check Redis status first
165-
const redisStatus = this.redis.status;
166-
if (redisStatus !== 'ready') {
167-
console.warn(`Redis status is ${redisStatus}, skipping dequeue for key: ${key}`);
152+
if (!this.redis.isReady) {
153+
console.warn('Redis not ready, skipping dequeue for key:', key);
168154
return null;
169155
}
170156

@@ -173,7 +159,7 @@ export class RedisService implements DatabaseService {
173159
});
174160

175161
const json = await Promise.race([
176-
this.redis.lpop(key),
162+
this.redis.lPop(key),
177163
timeoutPromise
178164
]);
179165

@@ -194,8 +180,7 @@ export class RedisService implements DatabaseService {
194180
// If there's a connection error, provide diagnostics but don't try to reconnect here
195181
// Let the Redis client handle reconnection automatically
196182
try {
197-
const redisStatus = this.redis.status;
198-
console.error(`Redis status after dequeue failure: ${redisStatus}`);
183+
console.error('Redis ready status after dequeue failure:', this.redis.isReady);
199184
} catch (diagError) {
200185
console.error('Failed to get Redis status:', diagError);
201186
}
@@ -208,7 +193,7 @@ export class RedisService implements DatabaseService {
208193
* @param value The value to push onto the list. This will be converted to a JSON string.
209194
*/
210195
async enqueue<T>(key: string, value: T): Promise<number> {
211-
const totalItems = await this.redis.rpush(key, JSON.stringify(value));
196+
const totalItems = await this.redis.rPush(key, JSON.stringify(value));
212197
console.info(`Added ${key} to the queue ${key}. Queue length is now ${totalItems} `);
213198
return totalItems;
214199
}
@@ -219,7 +204,7 @@ export class RedisService implements DatabaseService {
219204
* @returns The length of the list.
220205
*/
221206
queueLength(key: string): Promise<number> {
222-
return this.redis.llen(key);
207+
return this.redis.lLen(key);
223208
}
224209

225210
/**
@@ -228,7 +213,7 @@ export class RedisService implements DatabaseService {
228213
*/
229214
async healthCheck(): Promise<boolean> {
230215
try {
231-
if (this.redis.status !== 'ready') {
216+
if (!this.redis.isReady) {
232217
return false;
233218
}
234219

@@ -247,7 +232,7 @@ export class RedisService implements DatabaseService {
247232
}
248233
}
249234

250-
class InMemoryDatabaseService implements DatabaseService {
235+
class InMemoryDatabaseService implements RedisDatabaseService {
251236
private readonly store: Map<string, string> = new Map(); // key is ID of the object, value is the JSON string.
252237
private readonly queues: Map<string, any[]> = new Map(); // key is the name of the queue, value is an array of JSON strings representing the items in the queue.
253238
public readonly ready = Promise.resolve();
@@ -303,5 +288,5 @@ if (isLocalDev) {
303288
console.info(`${process.env.NODE_ENV} environment detected.Using Redis for database service.`);
304289
}
305290
export const database = isLocalDev ?
306-
new InMemoryDatabaseService() as DatabaseService :
307-
new RedisService() as DatabaseService;
291+
new InMemoryDatabaseService() as RedisDatabaseService :
292+
new RedisService() as RedisDatabaseService;

0 commit comments

Comments
 (0)