Skip to content

Commit ffcbb83

Browse files
committed
AUT-5433: Add primary/secondary semantics to dual session store
Reads fall back to secondary when primary fails. Writes always go to both stores but only the primary result gates the callback. Consistency checks compare both reads on the happy path.
1 parent c1cf160 commit ffcbb83

3 files changed

Lines changed: 238 additions & 136 deletions

File tree

src/config/dual-session-store.ts

Lines changed: 94 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,89 +4,147 @@ import { isDeepStrictEqual } from "node:util";
44

55
export class DualSessionStore extends Store {
66
constructor(
7-
private redis: Store,
8-
private dynamo: Store
7+
private readonly primary: Store,
8+
private readonly secondary: Store,
9+
private readonly primaryLabel: string,
10+
private readonly secondaryLabel: string
911
) {
1012
super();
1113
}
1214

13-
get(sid: string, cb: (err?: any, session?: SessionData | null) => void): void {
14-
this.redis.get(sid, (redisErr, redisSession) => {
15-
if (redisErr) {
16-
logger.warn({ err: redisErr, sid }, "Redis session read failed, falling back to DynamoDB");
15+
get(
16+
sid: string,
17+
cb: (err?: any, session?: SessionData | null) => void
18+
): void {
19+
this.primary.get(sid, (primaryErr, primarySession) => {
20+
if (primaryErr) {
21+
logger.warn(
22+
{ err: primaryErr, sid, store: this.primaryLabel },
23+
"Primary session read failed, falling back to secondary"
24+
);
1725
} else {
18-
logger.info({ sid }, "Session read from Redis");
19-
cb(null, redisSession);
26+
logger.info(
27+
{ sid, store: this.primaryLabel },
28+
"Session read from primary"
29+
);
30+
cb(null, primarySession);
2031
}
2132

22-
this.dynamo.get(sid, (dynamoErr, dynamoSession) => {
23-
if (redisErr) {
24-
cb(dynamoErr, dynamoSession);
33+
this.secondary.get(sid, (secondaryErr, secondarySession) => {
34+
if (primaryErr) {
35+
cb(secondaryErr, secondarySession);
2536
return;
2637
}
2738

28-
if (dynamoErr) {
29-
logger.warn({ err: dynamoErr, sid }, "DynamoDB consistency check read failed");
39+
if (secondaryErr) {
40+
logger.warn(
41+
{ err: secondaryErr, sid, store: this.secondaryLabel },
42+
"Secondary consistency check read failed"
43+
);
3044
return;
3145
}
3246

33-
logger.info({ sid }, "Session read from DynamoDB");
34-
this.performConsistencyChecks(redisSession, dynamoSession, sid);
47+
logger.info(
48+
{ sid, store: this.secondaryLabel },
49+
"Session read from secondary"
50+
);
51+
this.performConsistencyChecks(primarySession, secondarySession, sid);
3552
});
3653
});
3754
}
3855

3956
set(sid: string, sess: SessionData, cb: (err?: any) => void): void {
40-
this.redis.set(sid, sess, (err) => {
41-
logger.info({ sid }, "Session written to Redis");
57+
this.primary.set(sid, sess, (err) => {
58+
logger.info(
59+
{ sid, store: this.primaryLabel },
60+
"Session written to primary"
61+
);
4262
cb(err);
4363
});
4464

45-
this.writeToDynamo("set", sid, sess);
65+
this.writeToSecondary("set", sid, sess);
4666
}
4767

4868
destroy(sid: string, cb: (err?: any) => void): void {
49-
this.redis.destroy(sid, (err) => {
50-
logger.info({ sid }, "Session destroyed in Redis");
69+
this.primary.destroy(sid, (err) => {
70+
logger.info(
71+
{ sid, store: this.primaryLabel },
72+
"Session destroyed in primary"
73+
);
5174
cb(err);
5275
});
5376

54-
this.dynamo.destroy(sid, (dynamoErr) => {
55-
if (dynamoErr) {
56-
logger.warn({ err: dynamoErr, sid }, "DynamoDB session destroy failed");
77+
this.secondary.destroy(sid, (secondaryErr) => {
78+
if (secondaryErr) {
79+
logger.warn(
80+
{ err: secondaryErr, sid, store: this.secondaryLabel },
81+
"Secondary session destroy failed"
82+
);
5783
return;
5884
}
59-
logger.info({ sid }, "Session destroyed in DynamoDB");
85+
logger.info(
86+
{ sid, store: this.secondaryLabel },
87+
"Session destroyed in secondary"
88+
);
6089
});
6190
}
6291

6392
touch(sid: string, sess: SessionData, cb: () => void): void {
64-
this.redis.touch(sid, sess, () => {
65-
logger.info({ sid }, "Session touched in Redis");
93+
this.primary.touch(sid, sess, () => {
94+
logger.info(
95+
{ sid, store: this.primaryLabel },
96+
"Session touched in primary"
97+
);
6698
cb();
6799
});
68100

69-
this.writeToDynamo("touch", sid, sess);
101+
this.writeToSecondary("touch", sid, sess);
70102
}
71103

72-
private writeToDynamo(operation: "set" | "touch", sid: string, sess: SessionData): void {
104+
private writeToSecondary(
105+
operation: "set" | "touch",
106+
sid: string,
107+
sess: SessionData
108+
): void {
73109
try {
74-
this.dynamo[operation](sid, sess, (dynamoErr) => {
75-
if (dynamoErr) {
76-
logger.warn({ err: dynamoErr, sid, operation }, "DynamoDB session write failed");
110+
this.secondary[operation](sid, sess, (secondaryErr) => {
111+
if (secondaryErr) {
112+
logger.warn(
113+
{ err: secondaryErr, sid, operation, store: this.secondaryLabel },
114+
"Secondary session write failed"
115+
);
77116
return;
78117
}
79-
logger.info({ sid, operation }, "Session written to DynamoDB");
118+
logger.info(
119+
{ sid, operation, store: this.secondaryLabel },
120+
"Session written to secondary"
121+
);
80122
});
81123
} catch (err) {
82-
logger.warn({ err, sid, operation }, "DynamoDB session write threw unexpectedly");
124+
logger.warn(
125+
{ err, sid, operation, store: this.secondaryLabel },
126+
"Secondary session write threw unexpectedly"
127+
);
83128
}
84129
}
85130

86-
private performConsistencyChecks(redisSession: SessionData, dynamoSession: SessionData, sid: string): void {
131+
private performConsistencyChecks(
132+
primarySession: SessionData,
133+
secondarySession: SessionData,
134+
sid: string
135+
): void {
87136
try {
88-
if (!isDeepStrictEqual(redisSession ?? null, dynamoSession ?? null)) {
89-
logger.warn({ sid, redisExists: !!redisSession, dynamoExists: !!dynamoSession }, "Session consistency mismatch");
137+
if (
138+
!isDeepStrictEqual(primarySession ?? null, secondarySession ?? null)
139+
) {
140+
logger.warn(
141+
{
142+
sid,
143+
primaryExists: !!primarySession,
144+
secondaryExists: !!secondarySession,
145+
},
146+
"Session consistency mismatch"
147+
);
90148
}
91149
} catch (err) {
92150
logger.warn({ err }, "Error performing session store consistency checks");

src/config/session.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export function getSessionStore(redisConfig: RedisConfig): Store {
5959

6060
const dynamoStore = getDynamoSessionStore();
6161

62-
return new DualSessionStore(redisStore, dynamoStore);
62+
return new DualSessionStore(redisStore, dynamoStore, "Redis", "DynamoDB");
6363
}
6464

6565
export async function disconnectRedisClient(): Promise<void> {

0 commit comments

Comments
 (0)