prosgarz35 opened a new pull request, #3198:
URL: https://github.com/apache/james-project/pull/3198
# IMAP IDLE Improvements: Non-blocking Event Synchronization & Heartbeat
Lifecycle Safety
## 1. Context & Motivation
Apache James has been progressively migrating its protocol processing stack
to a non-blocking reactive model using **Project Reactor**.
However, the IMAP `IDLE` implementation in `IdleProcessor` still contained
legacy blocking synchronization primitives and unhandled lifecycle edge cases.
Under production workloads with concurrent mail delivery and unstable client
network connectivity (especially mobile clients like iOS Mail, Android K-9,
etc.), these issues caused:
1. **Thread Starvation**: Exhaustion of Reactor/Netty worker threads due to
blocking calls inside reactive event callbacks.
2. **Resource & Timer Leaks**: Zombie heartbeat tasks remaining scheduled
and retaining session references when sockets drop unexpectedly.
This document describes the problems identified and the rationale behind the
targeted, low-risk fixes.
---
## 2. Issues Identified
### Issue 1: Blocking `CountDownLatch::await` Inside Reactive Event Pipeline
- **Location**:
`org.apache.james.imap.processor.IdleProcessor.IdleMailboxListener#reactiveEvent`
- **Symptom**:
```java
@Override
public Publisher<Void> reactiveEvent(Event event) {
return Mono.fromRunnable(Throwing.runnable(countDownLatch::await))
.then(Mono.defer(() -> unsolicitedResponses(session, responder,
false)))
.then(Mono.fromRunnable(responder::flush));
}
```
- **Root Cause**:
`CountDownLatch` was used to ensure that mailbox change events (such as
`Added`, `Expunged`, `FlagsUpdated`) would not trigger `unsolicitedResponses`
until the initial `IDLE` handshake (`+ idling` response and initial untagged
updates) completed.
However, `reactiveEvent` runs on reactive scheduler threads (such as
`reactor-boundedElastic-*` or Netty worker event loops). Invoking
`countDownLatch::await` blocks the OS thread.
- **Impact**:
During bursts of incoming emails across multiple IDLE sessions, threads in
the pool are held up waiting on the latch. This leads to thread pool
starvation, causing latency spikes and connection timeouts across unrelated
IMAP commands (e.g. `LOGIN`, `FETCH`).
---
### Issue 2: Unchecked Heartbeat Rescheduling on Broken/Half-Closed
Connections
- **Location**: `org.apache.james.imap.processor.IdleProcessor#idle`
- **Symptom**:
The heartbeat task checked only `session.getState() !=
ImapSessionState.LOGOUT && idleActive.get()`.
When a remote client disconnects abnormally (e.g. cellular network drop,
NAT timeout without a clean TCP FIN/RST):
- The session state may not transition to `LOGOUT` immediately.
- Writing the untagged `* OK Still here` heartbeat can fail or throw an
exception.
- If unhandled or uncaught, either the exception halts the handler without
proper cleanup, or recursive rescheduling continues to retain references to
`ImapSession`, `Responder`, and associated channel buffers.
- **Impact**:
Gradual heap memory retention and unnecessary scheduling overhead for dead
client connections.
---
## 3. Proposed & Applied Solutions
Both changes are strictly localized to `IdleProcessor.java`, preserve 100%
backward compatibility, and avoid touching public APIs.
### Fix 1: Replace `CountDownLatch` with Reactive `Sinks.One<Void>`
- Instead of a blocking synchronization primitive, use `Sinks.One<Void>
idleReadySink = Sinks.one()`.
- Trigger completion when initialization finishes:
```java
return Mono.fromRunnable(() -> idle(request, session, responder,
idleReadySink))
.then(unsolicitedResponses(session, responder, false))
.onErrorResume(...)
.doFinally(signalType -> idleReadySink.tryEmitEmpty());
```
- In `IdleMailboxListener#reactiveEvent`, replace the blocking call with
non-blocking reactive subscription:
```java
@Override
public Publisher<Void> reactiveEvent(Event event) {
return idleReadySink.asMono()
.then(Mono.defer(() -> unsolicitedResponses(session, responder,
false)))
.then(Mono.fromRunnable(responder::flush));
}
```
- **Benefit**: Retains identical event sequencing guarantees, but **never
blocks the underlying execution thread**.
### Fix 2: Safeguard Heartbeat Execution and Rescheduling
- Wrap the heartbeat send operation in a defensive `try-catch` block.
- If writing to the responder fails due to a disconnected/broken pipe,
immediately mark `idleActive.set(false)` and abort further scheduling.
- Re-verify `idleActive.get() && session.getState() !=
ImapSessionState.LOGOUT` right before invoking `session.schedule(this,
heartbeatInterval)`.
- **Benefit**: Ensures self-healing termination of heartbeat tasks upon
connection failure, preventing memory and timer leaks.
---
## 4. Verification & Testing
- Verified with Maven 3.9+ and OpenJDK:
- Clean compilation of module `protocols-imap`.
- Zero Checkstyle violations (`checkstyle:check passed`).
- No changes to public interfaces or behavioral contract with IMAP clients.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]