@@ -114,6 +114,12 @@ const CHECKPOINTS = ['PASSIVE', 'FULL', 'RESTART', 'TRUNCATE'];
114114const HOSTNAME = os . hostname ( ) ;
115115const 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+
117123const 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