Skip to content

Commit 428f87c

Browse files
committed
AUT-5433: Wait for both session stores before calling back
The dual session store was previously calling back to `express-session` before the secondary store operation completed. This meant the response could be sent and the browser could fire the next request before the secondary write finished, creating a window where the stores had different data. It also caused false positives in the consistency check, as the secondary read (during a `get()` call) would return data that had already been overwritten by the current request's `set()` call. Moving all callbacks to after both store operations complete should reduce the chance of stale session data between the stores. This may cause a slight increase in latency during the transition period until DynamoDB becomes the primary and exclusive store. It would be good to try to keep the transition period reasonably short, but having enough confidence around the consistency of data across the two stores.
1 parent 94773d5 commit 428f87c

2 files changed

Lines changed: 159 additions & 99 deletions

File tree

src/config/dual-session-store.ts

Lines changed: 88 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ export class DualSessionStore extends Store {
3636
{ sid, store: this.primaryLabel },
3737
"Session read from primary"
3838
);
39-
cb(null, primarySession);
4039
}
4140

4241
this.secondary.get(sid, (secondaryErr, secondarySession) => {
@@ -59,51 +58,92 @@ export class DualSessionStore extends Store {
5958
{ err: secondaryErr, sid, store: this.secondaryLabel },
6059
"Secondary consistency check read failed"
6160
);
62-
return;
61+
} else {
62+
logger.info(
63+
{ sid, store: this.secondaryLabel },
64+
"Session read from secondary"
65+
);
66+
this.performConsistencyChecks(primarySession, secondarySession, sid);
6367
}
6468

65-
logger.info(
66-
{ sid, store: this.secondaryLabel },
67-
"Session read from secondary"
68-
);
69-
this.performConsistencyChecks(primarySession, secondarySession, sid);
69+
cb(null, primarySession);
7070
});
7171
});
7272
}
7373

7474
set(sid: string, sess: SessionData, cb: (err?: any) => void): void {
75-
this.primary.set(sid, sess, (err) => {
76-
logger.info(
77-
{ sid, store: this.primaryLabel },
78-
"Session written to primary"
79-
);
80-
cb(err);
81-
});
75+
this.primary.set(sid, sess, (primaryErr) => {
76+
if (primaryErr) {
77+
logger.warn(
78+
{ err: primaryErr, sid, store: this.primaryLabel },
79+
"Primary session write failed"
80+
);
81+
} else {
82+
logger.info(
83+
{ sid, store: this.primaryLabel },
84+
"Session written to primary"
85+
);
86+
}
8287

83-
this.writeToSecondary("set", sid, sess);
84-
}
88+
this.secondary.set(sid, sess, (secondaryErr) => {
89+
if (secondaryErr) {
90+
logger.warn(
91+
{ err: secondaryErr, sid, store: this.secondaryLabel },
92+
"Secondary session write failed"
93+
);
94+
} else {
95+
logger.info(
96+
{ sid, store: this.secondaryLabel },
97+
"Session written to secondary"
98+
);
99+
}
85100

86-
destroy(sid: string, cb: (err?: any) => void): void {
87-
this.primary.destroy(sid, (err) => {
88-
logger.info(
89-
{ sid, store: this.primaryLabel },
90-
"Session destroyed in primary"
91-
);
92-
cb(err);
101+
if (primaryErr && secondaryErr) {
102+
cb(new DualStoreError(primaryErr, secondaryErr));
103+
} else if (primaryErr) {
104+
cb(primaryErr);
105+
} else {
106+
cb();
107+
}
108+
});
93109
});
110+
}
94111

95-
this.secondary.destroy(sid, (secondaryErr) => {
96-
if (secondaryErr) {
112+
destroy(sid: string, cb: (err?: any) => void): void {
113+
this.primary.destroy(sid, (primaryErr) => {
114+
if (primaryErr) {
97115
logger.warn(
98-
{ err: secondaryErr, sid, store: this.secondaryLabel },
99-
"Secondary session destroy failed"
116+
{ err: primaryErr, sid, store: this.primaryLabel },
117+
"Primary session destroy failed"
118+
);
119+
} else {
120+
logger.info(
121+
{ sid, store: this.primaryLabel },
122+
"Session destroyed in primary"
100123
);
101-
return;
102124
}
103-
logger.info(
104-
{ sid, store: this.secondaryLabel },
105-
"Session destroyed in secondary"
106-
);
125+
126+
this.secondary.destroy(sid, (secondaryErr) => {
127+
if (secondaryErr) {
128+
logger.warn(
129+
{ err: secondaryErr, sid, store: this.secondaryLabel },
130+
"Secondary session destroy failed"
131+
);
132+
} else {
133+
logger.info(
134+
{ sid, store: this.secondaryLabel },
135+
"Session destroyed in secondary"
136+
);
137+
}
138+
139+
if (primaryErr && secondaryErr) {
140+
cb(new DualStoreError(primaryErr, secondaryErr));
141+
} else if (primaryErr) {
142+
cb(primaryErr);
143+
} else {
144+
cb();
145+
}
146+
});
107147
});
108148
}
109149

@@ -113,36 +153,30 @@ export class DualSessionStore extends Store {
113153
{ sid, store: this.primaryLabel },
114154
"Session touched in primary"
115155
);
116-
cb();
117-
});
118156

119-
this.writeToSecondary("touch", sid, sess);
157+
this.touchSecondary(sid, sess, cb);
158+
});
120159
}
121160

122-
private writeToSecondary(
123-
operation: "set" | "touch",
124-
sid: string,
125-
sess: SessionData
126-
): void {
161+
private touchSecondary(sid: string, sess: SessionData, cb: () => void): void {
127162
try {
128-
this.secondary[operation](sid, sess, (secondaryErr) => {
129-
if (secondaryErr) {
130-
logger.warn(
131-
{ err: secondaryErr, sid, operation, store: this.secondaryLabel },
132-
"Secondary session write failed"
163+
if (this.secondary.touch) {
164+
this.secondary.touch(sid, sess, () => {
165+
logger.info(
166+
{ sid, store: this.secondaryLabel },
167+
"Session touched in secondary"
133168
);
134-
return;
135-
}
136-
logger.info(
137-
{ sid, operation, store: this.secondaryLabel },
138-
"Session written to secondary"
139-
);
140-
});
169+
cb();
170+
});
171+
} else {
172+
cb();
173+
}
141174
} catch (err) {
142175
logger.warn(
143-
{ err, sid, operation, store: this.secondaryLabel },
144-
"Secondary session write threw unexpectedly"
176+
{ err, sid, store: this.secondaryLabel },
177+
"Secondary session touch threw unexpectedly"
145178
);
179+
cb();
146180
}
147181
}
148182

0 commit comments

Comments
 (0)