Skip to content

Commit 8ec4fbc

Browse files
committed
net_imap: Make untagged EXISTS more efficient for COPY/MOVE/APPEND.
The current process for generating untagged EXISTS in response to new messages is very message-specific; the callback is executed every single time for every single message, no matter how many messages are part of the operation. This can be very inefficient when a user does a batch operation, such as appending a large number of messages to a mailbox, or copying/moving messages between mailboxes. This was further exacerbated by existing inefficiencies in this process, such as counting the number of messages in the maildir every time the untagged EXISTS callback is triggered. This meant adding a large number of messages e.g. via COPY/MOVE resulted in a polynomial number of passes over the contents of the maildir. Furthermore, a large number of EXISTS updates (e.g. thousands) can potentially fill up imap->pfd[1]. To make this more efficient, we now ensure that this callback is only triggered once for batch operations, by doing the following: * COPY/MOVE operations now trigger EVENT_MESSAGE_APPEND, not EVENT_MESSAGE_NEW. This allows net_imap to ignore all the APPEND events (but the NEW events), and also seems to be a more correct interpretation of RFC 5423 Section 4. * net_imap now ignores EVENT_MESSAGE_APPEND. * Add a new pseudo-event, EVENT_MESSAGE_APPEND_MULTIPLE, which is fired off only at the end of a batch APPEND/COPY/MOVE operation. This way, as far as untagged EXIST goes, we only need to send a single update, instead of a linear number of updates. * net_imap now now manually fires the EVENT_MESSAGE_APPEND_MULTIPLE event with the UIDs that were impacted. Overall, this yields a significant performance improvement when appending/copying/moving lots of messages (about 10x, from some performance benchmarks before/after this change).
1 parent e69809e commit 8ec4fbc

File tree

4 files changed

+49
-11
lines changed

4 files changed

+49
-11
lines changed

include/mod_mail.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ enum mailbox_event_type {
5353
EVENT_SERVER_METADATA_CHANGE = (1 << 19),
5454
EVENT_ANNOTATION_CHANGE = (1 << 20), /*!< Message annotation changed */
5555
/* Internal events (not part of any RFC, only used by the BBS) */
56-
EVENT_MAILBOX_UIDVALIDITY_CHANGE = (1 << 21) /*!< UIDVALIDITY reset */
56+
EVENT_MAILBOX_UIDVALIDITY_CHANGE = (1 << 21), /*!< UIDVALIDITY reset */
57+
EVENT_INTERNAL_MESSAGE_APPEND_MULTIPLE = (1 << 22), /*!< Cumulative update used for efficiency in lieu of APPEND */
5758
};
5859

5960
/*! \brief Get the name of a mailbox event type */

modules/mod_mail.c

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ const char *mailbox_event_type_name(enum mailbox_event_type type)
213213
return "AnnotationChange";
214214
case EVENT_MAILBOX_UIDVALIDITY_CHANGE:
215215
return "UIDVALIDITYChange";
216+
case EVENT_INTERNAL_MESSAGE_APPEND_MULTIPLE:
217+
return "InternalMessageAppendMultiple";
216218
/* No default case */
217219
}
218220
__builtin_unreachable();
@@ -279,7 +281,7 @@ void mailbox_dispatch_event_basic(enum mailbox_event_type type, struct bbs_node
279281
mailbox_dispatch_event(&e);
280282
}
281283

282-
void mailbox_notify_new_message(struct bbs_node *node, struct mailbox *mbox, const char *maildir, const char *newfile, size_t size)
284+
static void mailbox_notify_new_or_appended_message(struct bbs_node *node, struct mailbox *mbox, const char *maildir, const char *newfile, size_t size, enum mailbox_event_type event_type)
283285
{
284286
struct mailbox_event e;
285287
struct stat st;
@@ -308,11 +310,16 @@ void mailbox_notify_new_message(struct bbs_node *node, struct mailbox *mbox, con
308310
* for a MessageNew event, implicitly move the message from new to cur NOW, which will assign it a UID.
309311
* Otherwise, there is really no good workaround. */
310312

311-
mailbox_initialize_event(&e, EVENT_MESSAGE_NEW, node, mbox, maildir);
313+
mailbox_initialize_event(&e, event_type, node, mbox, maildir);
312314
e.msgsize = size;
313315
mailbox_dispatch_event(&e);
314316
}
315317

318+
void mailbox_notify_new_message(struct bbs_node *node, struct mailbox *mbox, const char *maildir, const char *newfile, size_t size)
319+
{
320+
return mailbox_notify_new_or_appended_message(node, mbox, maildir, newfile, size, EVENT_MESSAGE_NEW);
321+
}
322+
316323
void mailbox_notify_quota_exceeded(struct bbs_node *node, struct mailbox *mbox)
317324
{
318325
struct mailbox_event e;
@@ -1710,7 +1717,7 @@ static int copy_move_untagged_exists(struct bbs_node *node, struct mailbox *mbox
17101717

17111718
safe_strncpy(maildir, newpath, sizeof(maildir));
17121719
maildir_extract_from_filename(maildir);
1713-
mailbox_notify_new_message(node, mbox, maildir, newpath, size);
1720+
mailbox_notify_new_or_appended_message(node, mbox, maildir, newpath, size, EVENT_MESSAGE_APPEND);
17141721
return 0;
17151722
}
17161723

modules/mod_mail_events.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,12 @@ static void log_cb(struct mailbox_event *e)
206206
/*! \brief Callback for all mailbox events */
207207
static void mbox_event_callback(struct mailbox_event *event)
208208
{
209+
if (event->type == EVENT_INTERNAL_MESSAGE_APPEND_MULTIPLE) {
210+
/* We already log everything for each individual message,
211+
* we don't care about the batch event. */
212+
return;
213+
}
214+
209215
/* Serialize logging for events, so events aren't logged partially interleaved with each other.
210216
* Obviously, this may reduce performance. */
211217
bbs_mutex_lock(&loglock);

nets/net_imap.c

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,10 @@ static void imap_mbox_watcher(struct mailbox_event *event)
808808

809809
switch (event->type) {
810810
case EVENT_MESSAGE_APPEND:
811+
/* We ignore this event as far as sending untagged EXISTS, and just use EVENT_INTERNAL_MESSAGE_APPEND_MULTIPLE
812+
* to send a single update at the end of a batch operation. */
813+
break;
814+
case EVENT_INTERNAL_MESSAGE_APPEND_MULTIPLE: /* Pseudo event used for batch updates */
811815
case EVENT_MESSAGE_NEW:
812816
send_untagged_exists(event->node, event->mbox, event->maildir);
813817
break;
@@ -2044,6 +2048,19 @@ static int handle_fetch(struct imap_session *imap, char *s, int usinguid)
20442048
return handle_fetch_full(imap, s, usinguid, 1);
20452049
}
20462050

2051+
static void copy_move_append_multiupdate(struct imap_session *imap, const char *dstmaildir, unsigned int *newuids, int lengths)
2052+
{
2053+
/* Batch an update for all new UIDs for the target mailbox and dispatch an EVENT_INTERNAL_MESSAGE_APPEND_MULTIPLE event.
2054+
* mailbox_notify_new_or_appended_message has already been called at this point,
2055+
* so we don't need to process any individual messages. */
2056+
2057+
/* We don't actually need these; in send_untagged_exists, we calculate how many messages are in the maildir. */
2058+
UNUSED(newuids);
2059+
UNUSED(lengths);
2060+
2061+
mailbox_dispatch_event_basic(EVENT_INTERNAL_MESSAGE_APPEND_MULTIPLE, imap->node, imap->mbox, dstmaildir);
2062+
}
2063+
20472064
static int handle_copy(struct imap_session *imap, const char *sequences, const char *newbox, int usinguid)
20482065
{
20492066
struct dirent *entry, **entries;
@@ -2114,6 +2131,9 @@ static int handle_copy(struct imap_session *imap, const char *sequences, const c
21142131
if (olduids || newuids) {
21152132
olduidstr = gen_uintlist(olduids, lengths);
21162133
newuidstr = gen_uintlist(newuids, lengths);
2134+
2135+
copy_move_append_multiupdate(imap, newboxdir, newuids, lengths);
2136+
21172137
free_if(olduids);
21182138
free_if(newuids);
21192139
}
@@ -2122,8 +2142,6 @@ static int handle_copy(struct imap_session *imap, const char *sequences, const c
21222142
} else if (!numcopies && quotaleft <= 0) {
21232143
imap_reply(imap, "NO [OVERQUOTA] Insufficient quota remaining");
21242144
} else {
2125-
/* maildir_copy_msg_filename sends the untagged EXISTS for the new message in the destination mailbox.
2126-
* No untagged EXISTS should be sent to the client running the command. */
21272145
if (IMAP_HAS_ACL(imap->acl, IMAP_ACL_READ)) {
21282146
imap_reply(imap, "OK [COPYUID %u %s %s] COPY completed", uidvalidity, S_IF(olduidstr), S_IF(newuidstr));
21292147
} else {
@@ -2223,14 +2241,14 @@ static int handle_move(struct imap_session *imap, const char *sequences, const c
22232241
if (olduids || newuids) {
22242242
olduidstr = gen_uintlist(olduids, lengths);
22252243
newuidstr = gen_uintlist(newuids, lengths);
2244+
2245+
copy_move_append_multiupdate(imap, newboxdir, newuids, lengths);
2246+
22262247
free_if(olduids);
22272248
free_if(newuids);
22282249
}
22292250

2230-
/* maildir_move_msg_filename sends the untagged EXISTS for the new message in the destination mailbox.
2231-
* No untagged EXISTS should be sent to the client running the command.
2232-
*
2233-
* The untagged EXPUNGE responses are sent at this point, and since untagged EXPUNGES can't be sent
2251+
/* The untagged EXPUNGE responses are sent at this point, and since untagged EXPUNGES can't be sent
22342252
* while a command is not in progress, we need to flush this out BEFORE finalizing the response with an OK. */
22352253
if (expunged) {
22362254
imap->highestmodseq = maildir_indicate_expunged(EVENT_MESSAGE_EXPUNGE, imap->node, imap->mbox, imap->curdir, expunged, expungedseqs, exp_lengths, 0);
@@ -2636,7 +2654,7 @@ static int handle_append(struct imap_session *imap, char *s)
26362654
* that isn't the spirit of an "atomic" operation... */
26372655
}
26382656

2639-
/* We can return directly here because a isn't used when imap->client != NULL and therefore doesn't need to be freed */
2657+
/* We can return directly here because a uintlist isn't used when imap->client != NULL and therefore doesn't need to be freed */
26402658
if (imap->client) {
26412659
/* Send empty line to denote end of APPEND/MULTIAPPEND */
26422660
ssize_t res = bbs_write(imap->client->client.wfd, "\r\n", 2);
@@ -2652,6 +2670,8 @@ static int handle_append(struct imap_session *imap, char *s)
26522670
return 0;
26532671
}
26542672

2673+
copy_move_append_multiupdate(imap, appenddir, a, lengths);
2674+
26552675
if (append_response(imap, imap->acl, a, appends, uidvalidity, uidnext)) {
26562676
goto cleanup2;
26572677
}
@@ -3078,6 +3098,10 @@ static int handle_remote_move(struct imap_session *imap, char *dest, const char
30783098
* has not necessarily yet come. */
30793099
}
30803100

3101+
if (!destclient) {
3102+
copy_move_append_multiupdate(imap, traversalstack.dir, a, alengths);
3103+
}
3104+
30813105
/* XXX If destclient, in theory if the remote server supports UIDPLUS, we could keep track of the dest UID
30823106
* for each message, but we definitely can't do a passthrough of the response since there could be many messages involved. */
30833107
if (!destclient && usinguid && IMAP_HAS_ACL(imap->acl, IMAP_ACL_READ)) {

0 commit comments

Comments
 (0)