From ff24d3db60352d1ec4468b3fccb4f26f2d2e52f9 Mon Sep 17 00:00:00 2001 From: Mikhail Khadarenka Date: Thu, 28 May 2026 13:00:23 +0200 Subject: [PATCH] Avoid blocking IMAP pool cleanup on logout The IMAP connection pool garbage collector used to call logout while scanning and mutating the shared cache. A slow or stuck IMAP server could therefore keep the cleanup path busy for a long time and delay access to pooled sessions. Protect the connection cache with a small NSLock, remove expired entries from the pool while holding that lock, and perform the network logout work afterwards on a background thread. This keeps the cache unavailable only for the short bookkeeping phase instead of for the duration of every IMAP LOGOUT command. Also guard the collector against non-connection placeholders already supported by cacheEntry:forURL:, and keep the debug pool count behind the same synchronization. --- sope-mime/NGImap4/NGImap4ConnectionManager.h | 3 +- sope-mime/NGImap4/NGImap4ConnectionManager.m | 86 +++++++++++++++++--- 2 files changed, 78 insertions(+), 11 deletions(-) diff --git a/sope-mime/NGImap4/NGImap4ConnectionManager.h b/sope-mime/NGImap4/NGImap4ConnectionManager.h index b5f65c1cd..df44614b5 100644 --- a/sope-mime/NGImap4/NGImap4ConnectionManager.h +++ b/sope-mime/NGImap4/NGImap4ConnectionManager.h @@ -30,12 +30,13 @@ This class manages and pools NGImap4Connection objects. */ -@class NSString, NSTimer, NSMutableDictionary, NSURL; +@class NSString, NSTimer, NSMutableDictionary, NSLock, NSURL; @class NGImap4Connection, NGImap4Client; @interface NGImap4ConnectionManager : NSObject { NSMutableDictionary *urlToEntry; + NSLock *entryLock; NSTimer *gcTimer; } diff --git a/sope-mime/NGImap4/NGImap4ConnectionManager.m b/sope-mime/NGImap4/NGImap4ConnectionManager.m index 2d361220e..1c8145a96 100644 --- a/sope-mime/NGImap4/NGImap4ConnectionManager.m +++ b/sope-mime/NGImap4/NGImap4ConnectionManager.m @@ -24,6 +24,10 @@ #include "NGImap4Client.h" #include "imCommon.h" +#import +#import +#import + @implementation NGImap4ConnectionManager static BOOL debugOn = NO; @@ -79,6 +83,7 @@ - (id)init { if ((self = [super init])) { if (!poolingOff) { self->urlToEntry = [[NSMutableDictionary alloc] initWithCapacity:256]; + self->entryLock = [[NSLock alloc] init]; self->gcTimer = [[NSTimer scheduledTimerWithTimeInterval: PoolScanInterval target:self selector:@selector(_garbageCollect:) @@ -91,6 +96,7 @@ - (id)init { - (void)dealloc { [self->gcTimer invalidate]; [self->urlToEntry release]; + [self->entryLock release]; [self->gcTimer release]; [super dealloc]; } @@ -104,39 +110,99 @@ - (id)cacheKeyForURL:(NSURL *)_url { } - (NGImap4Connection *)entryForURL:(NSURL *)_url { + NGImap4Connection *entry; + if (_url == nil) return nil; - - return [self->urlToEntry objectForKey:[self cacheKeyForURL:_url]]; + + [self->entryLock lock]; + entry = [[self->urlToEntry objectForKey:[self cacheKeyForURL:_url]] retain]; + [self->entryLock unlock]; + + return [entry autorelease]; } - (void)cacheEntry:(NGImap4Connection *)_entry forURL:(NSURL *)_url { if (_entry == nil) _entry = (id)[NSNull null]; + [self->entryLock lock]; [self->urlToEntry setObject:_entry forKey:[self cacheKeyForURL:_url]]; + [self->entryLock unlock]; +} + +- (unsigned int)poolEntryCount { + unsigned int count; + + [self->entryLock lock]; + count = [self->urlToEntry count]; + [self->entryLock unlock]; + + return count; +} + +- (void)_logoutClients:(NSArray *)_clients { + NSAutoreleasePool *pool; + NGImap4Client *client; + unsigned int i, count; + + pool = [[NSAutoreleasePool alloc] init]; + count = [_clients count]; + + for (i = 0; i < count; i++) + { + client = [_clients objectAtIndex:i]; + + NS_DURING + [client logout]; + NS_HANDLER + [self logWithFormat:@"could not logout expired IMAP4 connection: %@", + localException]; + NS_ENDHANDLER; + } + + [_clients release]; + [pool release]; } - (void)_garbageCollect:(NSTimer *)_timer { // TODO: scan for old IMAP4 channels NGImap4Connection *entry; + NSMutableArray *clientsToLogout; NSDate *now; NSArray *a; - int i; + NSString *key; + NSArray *clients; + unsigned int i, count; - a = [self->urlToEntry allKeys]; + clientsToLogout = [NSMutableArray arrayWithCapacity:16]; now = [NSDate date]; - for (i = 0; i < [a count]; i++) + [self->entryLock lock]; + a = [self->urlToEntry allKeys]; + count = [a count]; + + for (i = 0; i < count; i++) { - entry = [self->urlToEntry objectForKey: [a objectAtIndex: i]]; + key = [a objectAtIndex: i]; + entry = [self->urlToEntry objectForKey: key]; - if ([now timeIntervalSinceDate: [entry creationTime]] > PoolScanInterval) + if ([entry isKindOfClass:[NGImap4Connection class]] + && [now timeIntervalSinceDate: [entry creationTime]] > PoolScanInterval) { - [[entry client] logout]; - [self->urlToEntry removeObjectForKey: [a objectAtIndex: i]]; + [clientsToLogout addObject: [entry client]]; + [self->urlToEntry removeObjectForKey: key]; } } + [self->entryLock unlock]; + + if ([clientsToLogout count] > 0) + { + clients = [clientsToLogout copy]; + [NSThread detachNewThreadSelector:@selector(_logoutClients:) + toTarget:self + withObject:clients]; + } [self debugWithFormat:@"should collect IMAP4 channels (%d active)", - [self->urlToEntry count]]; + [self poolEntryCount]]; } - (NGImap4Connection *)connectionForURL:(NSURL *)_url password:(NSString *)_p {