February 16, 2024
This story is, as the title suggests, about Gnus (the Emacs
newsreader, remember?) and ProtonMail Bridge. Gnus can
communicate over IMAP but is very slow in doing so. That’s why people
recommend fetching the mail locally and feeding it into the Dovecot
server, which runs on the same machine as Gnus. This setup eliminates
network round trips, and Gnus suddenly becomes very fast. I tried that
for a while, but it didn’t feel as comfortable. When I decided to switch
my mail provider to ProtonMail, I was really looking forward to using
Bridge because it acts as a local IMAP server, and I would no longer
need Dovecot and fetchmail. When I fired up Gnus for the first time, I
quickly realized that it wasn’t going to be that easy. Gnus was acting
crazy with the Bridge, and I almost gave up. I searched the entire
internet, but it seemed like no one else had the same issue. I reported
it to ProtonMail, but they told me they don’t support Gnus (what a
surprise). I didn’t report this issue to bug-gnu-emacs
because I was sure nobody would pick it up. I was so pissed off that I
decided to fix it myself.
At this point, I wasn’t even sure if it was a problem with Gnus or with the Bridge. Gnus works fine with all the major IMAP servers, and the Bridge works fine with all major clients. I started looking for clues in the Gnus source code, but it’s a huge package, and I didn’t find anything useful. So I compiled Gluon - the IMAP server embedded in the Bridge - and installed Dovecot once again. Next, I created test mailboxes on both servers and started debugging the responses. Nothing seemed out of the ordinary until…
Gluon started to respond to FETCH requests with a random message order, but Dovecot didn’t. An example communication with Gluon looked something like this:
Gnus: FETCH 1:3 ...
Gluon: * 3 RESPONSE ...
Gluon: * 1 RESPONSE ...
Gluon: * 2 RESPONSE ...
or this
Gnus: FETCH 1:3 ...
Gluon: * 2 RESPONSE ...
Gluon: * 3 RESPONSE ...
Gluon: * 1 RESPONSE ...
and it was completely random. I used edebug inside Emacs and confirmed that this was indeed the culprit of the issue. I looked into RFC 3501 to see if this is against the protocol, but apparently it is not (also see this StackOverflow discussion). I hoped I could just send this to Proton and let them fix their server, but unfortunately, they didn’t do anything wrong.
I was left with two options: (i) patch Gnus, (ii) patch Gluon. I considered both options and even started working on the client, but I quickly realized that it would be too much work, so I went back to Gluon. After skimming through its source code, I had the suspicion that Gluon’s weird responses had something to do with concurrency (Gluon is written in Go - a language known for its concurrent features).
After a while I found what I was looking for (this snippet is taken
from internal/state/mailbox_fetch.go
).
if !contexts.IsParallelismDisabledCtx(ctx) && (len(snapMessages) > minCountForParallelism || (len(snapMessages) > 1 && needsLiteral)) {
// If multiple fetch request are happening in parallel, reduce the number of goroutines in proportion to that
// to avoid overloading the user's machine.
parallelism = runtime.NumCPU() / int(activeFetchRequests)
// make sure that if division hits 0, we run single threaded rather than use MAXGOPROCS
if parallelism < 1 {
parallelism = 1
}
} else {
parallelism = 1
}
So there’s isParallelismDisabledCtx you say? :thinking: As it turned out, I could disable the whole parallelism thingy with a simple server option.
This is the final patch for the proton-bridge
package.
From 23f9c69a1552af1f946687f55de901488a2c9a38 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jakub=20Je=C4=8Dm=C3=ADnek?= <kuba@kubajecminek.cz>
Date: Fri, 16 Feb 2024 16:07:21 +0100
Subject: [PATCH] Gnus Fix: disable parallelism
---
internal/services/imapsmtpserver/imap.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/internal/services/imapsmtpserver/imap.go b/internal/services/imapsmtpserver/imap.go
index 63888b51..358173c6 100644
--- a/internal/services/imapsmtpserver/imap.go
+++ b/internal/services/imapsmtpserver/imap.go
@@ -120,6 +120,7 @@ func newIMAPServer(
gluon.WithReporter(reporter),
gluon.WithUIDValidityGenerator(uidValidityGenerator),
gluon.WithPanicHandler(panicHandler),
+ gluon.WithDisableParallelism(),
)
if err != nil {
return nil, err
--
2.42.0
This single line of code solved all my issues. I hope this will be useful to other people.