Skip to content

Commit d57fe2e

Browse files
committed
chore: log stage-timing breakdown for slow tmp payloads
1 parent 50dad51 commit d57fe2e

3 files changed

Lines changed: 83 additions & 0 deletions

File tree

.env.defaults

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ SQLITE_PORT=3456
139139
SQLITE_RCLONE_ENABLED=false
140140
SQLITE_FTS5_ENABLED=false
141141
DATABASE_MAP_MAX_SIZE=500
142+
# log a stage-timing breakdown for 'tmp' payloads slower than this (ms)
143+
SLOW_TMP_PAYLOAD_MS=5000
142144

143145
################
144146
## web server ##

.env.schema

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ SQLITE_PORT=
163163
SQLITE_RCLONE_ENABLED=
164164
SQLITE_FTS5_ENABLED=
165165
DATABASE_MAP_MAX_SIZE=
166+
SLOW_TMP_PAYLOAD_MS=
166167

167168
################
168169
## web server ##

helpers/parse-payload.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ const CHECKPOINTS = ['PASSIVE', 'FULL', 'RESTART', 'TRUNCATE'];
114114
const HOSTNAME = os.hostname();
115115
const IP_ADDRESS = ip.address();
116116

117+
// log a consolidated stage-timing breakdown for any 'tmp' payload
118+
// whose queue-wait + handler time exceeds this threshold
119+
const SLOW_TMP_PAYLOAD_MS = env.SLOW_TMP_PAYLOAD_MS
120+
? Number.parseInt(env.SLOW_TMP_PAYLOAD_MS, 10)
121+
: ms('5s');
122+
117123
const PAYLOAD_ACTIONS = new Set([
118124
'sync', // no db
119125
'tmp', // no db
@@ -649,12 +655,20 @@ async function parsePayload(data, ws) {
649655
// create the temporary message for each alias
650656
const errors = {};
651657

658+
// stage timings for the consolidated slow-payload log below
659+
const handlerStartedAt = Date.now();
660+
const queueWaitMs = payload.sent_at
661+
? Math.max(0, now - payload.sent_at)
662+
: 0;
663+
const aliasTimings = [];
664+
652665
//
653666
// rate limit the payload.remoteAddress from
654667
// sending more than 1 GB per day or 1000 messages per day
655668
// but attempt to use the reverse PTR root domain of the remoteAddress
656669
//
657670
let sender = payload.remoteAddress;
671+
const reverseDnsStartedAt = Date.now();
658672
try {
659673
const [clientHostname] = await this.resolver.reverse(
660674
payload.remoteAddress
@@ -666,11 +680,14 @@ async function parsePayload(data, ws) {
666680
logger.warn(err);
667681
}
668682

683+
const reverseDnsMs = Date.now() - reverseDnsStartedAt;
684+
669685
const date = new Date().toISOString().split('T')[0];
670686

671687
//
672688
// parse headers from message
673689
//
690+
const parseStartedAt = Date.now();
674691
const splitter = new Splitter();
675692
const joiner = new Joiner();
676693
let headers;
@@ -689,11 +706,23 @@ async function parsePayload(data, ws) {
689706
// (arguments = `session`, `headers`, `body`, `useSender`)
690707
//
691708
const fingerprint = getFingerprint({}, headers, payload.raw);
709+
const parseMs = Date.now() - parseStartedAt;
692710

693711
await pMap(
694712
payload.aliases,
695713

696714
async (obj) => {
715+
// per-alias stage timer: mark() records elapsed time since the
716+
// previous mark under the given stage name (see slow-payload log)
717+
const aliasStartedAt = Date.now();
718+
let stageStartedAt = aliasStartedAt;
719+
const stages = {};
720+
const mark = (name) => {
721+
const ts = Date.now();
722+
stages[name] = ts - stageStartedAt;
723+
stageStartedAt = ts;
724+
};
725+
697726
try {
698727
const alias = await Aliases.findById(obj.id)
699728
.populate('domain', 'id name')
@@ -707,6 +736,8 @@ async function parsePayload(data, ws) {
707736
.lean()
708737
.exec();
709738

739+
mark('aliasLookup');
740+
710741
if (!alias) throw new TypeError('Alias does not exist');
711742

712743
if (!alias.user) throw new TypeError('User does not exist');
@@ -764,6 +795,8 @@ async function parsePayload(data, ws) {
764795
const { isOverQuota, storageUsed, maxQuotaPerAlias } =
765796
await Aliases.isOverQuota(alias, 0, this.client);
766797

798+
mark('quota');
799+
767800
if (isOverQuota) {
768801
const err = new Error(
769802
`${session.user.username} has exceeded quota with ${bytes(
@@ -915,6 +948,8 @@ async function parsePayload(data, ws) {
915948
}
916949
}
917950

951+
mark('rateLimit');
952+
918953
// check that we have available space
919954
const storagePath = getPathToDatabase({
920955
id: alias.id,
@@ -929,6 +964,8 @@ async function parsePayload(data, ws) {
929964
)} was available`
930965
);
931966

967+
mark('diskSpace');
968+
932969
// we should only use in-memory database is if was connected (IMAP session open)
933970
if (
934971
this.databaseMap &&
@@ -1226,6 +1263,8 @@ async function parsePayload(data, ws) {
12261263
});
12271264
}
12281265

1266+
mark('sieve');
1267+
12291268
// Use Sieve-determined folder and flags
12301269
const targetFolder = sieveResult.folder || 'INBOX';
12311270
const targetFlags = sieveResult.flags || [];
@@ -1302,6 +1341,8 @@ async function parsePayload(data, ws) {
13021341
}
13031342
}
13041343

1344+
mark('directAppend');
1345+
13051346
/*
13061347
//
13071348
// attempt to get in-memory password from IMAP servers
@@ -1393,6 +1434,8 @@ async function parsePayload(data, ws) {
13931434
if (!session.db) {
13941435
const tmpDb = await getTemporaryDatabase.call(this, session);
13951436

1437+
mark('tmpDbOpen');
1438+
13961439
let err;
13971440

13981441
try {
@@ -1767,9 +1810,13 @@ async function parsePayload(data, ws) {
17671810
logger.fatal(err);
17681811
}
17691812

1813+
mark('tmpStore');
1814+
17701815
if (tmpDb && !this.temporaryDatabaseMap)
17711816
await closeDatabase(tmpDb);
17721817

1818+
mark('tmpDbClose');
1819+
17731820
// send user push notification
17741821
if (!err)
17751822
sendApn(this.client, alias.id, targetFolder || 'INBOX')
@@ -1916,18 +1963,51 @@ async function parsePayload(data, ws) {
19161963
recipient: session.user.username
19171964
});
19181965
}
1966+
1967+
mark('imip');
19191968
} catch (err) {
19201969
err.payload = _.omit(payload, 'raw');
19211970
err.isCodeBug = isCodeBug(err);
19221971
logger.error(err);
19231972
errors[`${obj.address}`] = JSON.parse(
19241973
safeStringify(parseErr(err))
19251974
);
1975+
} finally {
1976+
aliasTimings.push({
1977+
alias: obj.address,
1978+
aliasId: obj.id,
1979+
totalMs: Date.now() - aliasStartedAt,
1980+
stages
1981+
});
19261982
}
19271983
},
19281984
{ concurrency }
19291985
);
19301986

1987+
//
1988+
// one consolidated log line per slow tmp request so latency can be
1989+
// attributed to queue-wait vs a specific per-alias handler stage
1990+
//
1991+
const handlerMs = Date.now() - handlerStartedAt;
1992+
if (queueWaitMs + handlerMs >= SLOW_TMP_PAYLOAD_MS) {
1993+
logger.warn('slow tmp payload', {
1994+
// opt in to Mongo log persistence (warn-level logs are
1995+
// otherwise dropped by the hook in helpers/logger.js)
1996+
ignore_hook: false,
1997+
payloadId: payload.id,
1998+
queueWaitMs,
1999+
handlerMs,
2000+
endToEndMs: queueWaitMs + handlerMs,
2001+
reverseDnsMs,
2002+
parseMs,
2003+
byteLength,
2004+
aliasCount: payload.aliases.length,
2005+
aliasTimings,
2006+
remoteAddress: payload.remoteAddress,
2007+
hostname: HOSTNAME
2008+
});
2009+
}
2010+
19312011
response = {
19322012
id: payload.id,
19332013
data: errors

0 commit comments

Comments
 (0)