This is an automated email from the ASF dual-hosted git repository.
btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
The following commit(s) were added to refs/heads/master by this push:
new 91decc0042 JAMES-4140 Allow reseting IMAP command throttling upon
specific commands
91decc0042 is described below
commit 91decc0042d6cae62d79ac199de25bde0fb44825
Author: Benoit TELLIER <[email protected]>
AuthorDate: Wed Oct 1 19:51:23 2025 +0200
JAMES-4140 Allow reseting IMAP command throttling upon specific commands
Helps mitigate delays in Outlook upon sending email:
a full refresh is needed for the operation to complete
That's the only use cases the throttling is frontally
visible to end users.
---
docs/modules/servers/partials/configure/imap.adoc | 3 ++
.../imapserver/netty/IMAPCommandsThrottler.java | 58 +++++++++++++++-------
.../netty/IMAPCommandsThrottlerTest.java | 15 +++---
.../src/test/resources/commandsThrottling.xml | 1 +
4 files changed, 52 insertions(+), 25 deletions(-)
diff --git a/docs/modules/servers/partials/configure/imap.adoc
b/docs/modules/servers/partials/configure/imap.adoc
index af6911fe43..431495f86f 100644
--- a/docs/modules/servers/partials/configure/imap.adoc
+++ b/docs/modules/servers/partials/configure/imap.adoc
@@ -197,6 +197,8 @@ The user can declare the list of commands on which
throttling needs to be tracke
would always increase. Duration.
- `observationPeriod`: the count of observed commands is reset after this
period thus stopping delays. Duration.
- `maxDelay`: maximum value the client will be delayed for.
+- `resetOn`: String. Optional default to none. Needs to be a command name. If
specified, this command name will reset
+throttling on this specific entry.
Sample configuration:
@@ -209,6 +211,7 @@ Sample configuration:
<additionalDelayPerOperation>2ms</additionalDelayPerOperation>
<observationPeriod>10m</observationPeriod>
<maxDelay>1s</maxDelay>
+ <resetOn>APPEND</resetOn> <!-- Helps mitigate delays in Outlook upon
sending email: a full refresh is needed for the operation to complete -->
</select>
<append>
<thresholdCount>5</thresholdCount>
diff --git
a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPCommandsThrottler.java
b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPCommandsThrottler.java
index 53e2074f00..111410e979 100644
---
a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPCommandsThrottler.java
+++
b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPCommandsThrottler.java
@@ -35,10 +35,13 @@ import org.apache.james.imap.api.process.ImapSession;
import org.apache.james.imap.processor.IdProcessor;
import org.apache.james.util.DurationParser;
import org.apache.james.util.MDCStructuredLogger;
+import org.apache.james.util.StructuredLogger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Multimap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
@@ -50,6 +53,7 @@ public class IMAPCommandsThrottler extends
ChannelInboundHandlerAdapter {
private static final Logger LOGGER =
LoggerFactory.getLogger(IMAPCommandsThrottler.class);
public record ThrottlerConfigurationEntry(
+ Optional<String> resetOn,
int thresholdCount,
Duration additionalDelayPerOperation,
Duration observationPeriod,
@@ -57,6 +61,7 @@ public class IMAPCommandsThrottler extends
ChannelInboundHandlerAdapter {
public static ThrottlerConfigurationEntry
from(ImmutableHierarchicalConfiguration configuration) {
return new ThrottlerConfigurationEntry(
+ Optional.ofNullable(configuration.getString("resetOn", null)),
Optional.ofNullable(configuration.getString("thresholdCount",
null))
.map(Integer::parseInt)
.orElseThrow(() -> new
IllegalArgumentException("thresholdCount in compulsory for
ThrottlerConfigurationEntry")),
@@ -80,9 +85,10 @@ public class IMAPCommandsThrottler extends
ChannelInboundHandlerAdapter {
}
}
- public record ThrottlerConfiguration(Map<String,
ThrottlerConfigurationEntry> entryMap) {
+ public record ThrottlerConfiguration(Map<String,
ThrottlerConfigurationEntry> entryMap,
+ Multimap<String, String>
resetTriggers) {
public static ThrottlerConfiguration
from(HierarchicalConfiguration<ImmutableNode> configuration) {
- return new ThrottlerConfiguration(configuration.getNodeModel()
+ return ThrottlerConfiguration.from(configuration.getNodeModel()
.getNodeHandler()
.getRootNode()
.getChildren()
@@ -90,6 +96,15 @@ public class IMAPCommandsThrottler extends
ChannelInboundHandlerAdapter {
.map(key -> Pair.of(key.getNodeName().toUpperCase(Locale.US),
ThrottlerConfigurationEntry.from(configuration.immutableConfigurationAt(key.getNodeName()))))
.collect(ImmutableMap.toImmutableMap(Pair::key, Pair::value)));
}
+
+ public static ThrottlerConfiguration from(Map<String,
ThrottlerConfigurationEntry> entryMap) {
+ return new ThrottlerConfiguration(entryMap,
+ entryMap.entrySet().stream()
+ .filter(e -> e.getValue().resetOn().isPresent())
+ .collect(ImmutableListMultimap.toImmutableListMultimap(
+ e ->
e.getValue().resetOn().get().toUpperCase(Locale.US),
+ e -> e.getKey().toUpperCase(Locale.US))));
+ }
}
private final ThrottlerConfiguration configuration;
@@ -101,23 +116,42 @@ public class IMAPCommandsThrottler extends
ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof ImapRequest imapRequest) {
+ ImapSession session = (ImapSession)
ctx.channel().attr(IMAP_SESSION_ATTRIBUTE_KEY);
String key =
imapRequest.getCommand().getName().toUpperCase(Locale.US);
+
+ resetDelaysIfNeeded(key, session);
+
Optional.ofNullable(configuration.entryMap().get(key))
- .ifPresentOrElse(configurationEntry -> throttle(ctx, msg,
imapRequest, configurationEntry),
+ .ifPresentOrElse(configurationEntry -> throttle(session, ctx,
msg, imapRequest, configurationEntry),
() -> ctx.fireChannelRead(msg));
} else {
ctx.fireChannelRead(msg);
}
}
- private static void throttle(ChannelHandlerContext ctx, Object msg,
ImapRequest imapRequest, ThrottlerConfigurationEntry configurationEntry) {
- ImapSession session = (ImapSession)
ctx.channel().attr(IMAP_SESSION_ATTRIBUTE_KEY);
+ private void resetDelaysIfNeeded(String key, ImapSession session) {
+ configuration.resetTriggers.get(key).forEach(keyToReset -> {
+ session.setAttribute("imap-applicative-traffic-shaper-counter-" +
keyToReset, new AtomicLong(0));
+ structuredLogger(session).log(logger -> logger.info("Reseting
delay for {} upon {} on an IMAP session.", keyToReset, key));
+ });
+ }
+ private static StructuredLogger structuredLogger(ImapSession session) {
+ return MDCStructuredLogger.forLogger(LOGGER)
+ .field("sessionId", session.sessionId().asString())
+ .field("username", session.getUserName().asString())
+ .field("userAgent",
Optional.ofNullable(session.getAttribute(IdProcessor.USER_AGENT))
+ .filter(String.class::isInstance)
+ .map(String.class::cast)
+ .orElse(""));
+ }
+
+ private static void throttle(ImapSession session, ChannelHandlerContext
ctx, Object msg, ImapRequest imapRequest, ThrottlerConfigurationEntry
configurationEntry) {
AtomicLong atomicLong = retrieveAssociatedCounter(imapRequest,
session, configurationEntry);
Duration delay =
Duration.ofMillis(configurationEntry.delayMSFor(atomicLong.getAndIncrement()));
if (delay.isPositive()) {
- logDelay(imapRequest, session, delay);
+ structuredLogger(session).log(logger -> logger.info("Delayed
command {} on an IMAP session. Delay {} ms",
imapRequest.getCommand().getName(), delay.toMillis()));
Mono.delay(delay)
.then(Mono.fromRunnable(() -> ctx.fireChannelRead(msg)))
@@ -140,16 +174,4 @@ public class IMAPCommandsThrottler extends
ChannelInboundHandlerAdapter {
return res;
});
}
-
- private static void logDelay(ImapRequest imapRequest, ImapSession session,
Duration delay) {
- MDCStructuredLogger.forLogger(LOGGER)
- .field("username", session.getUserName().asString())
- .field("userAgent",
Optional.ofNullable(session.getAttribute(IdProcessor.USER_AGENT))
- .filter(String.class::isInstance)
- .map(String.class::cast)
- .orElse(""))
- .log(logger -> logger.info("Delayed command {} on an IMAP session.
Delay {} ms",
- imapRequest.getCommand().getName(),
- delay.toMillis()));
- }
}
diff --git
a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPCommandsThrottlerTest.java
b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPCommandsThrottlerTest.java
index 32fc769b1b..62e7bbfe96 100644
---
a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPCommandsThrottlerTest.java
+++
b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPCommandsThrottlerTest.java
@@ -22,6 +22,7 @@ package org.apache.james.imapserver.netty;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.Duration;
+import java.util.Optional;
import org.apache.commons.configuration2.HierarchicalConfiguration;
import org.apache.commons.configuration2.tree.ImmutableNode;
@@ -41,11 +42,11 @@ class IMAPCommandsThrottlerTest {
void shouldLoad() throws Exception {
HierarchicalConfiguration<ImmutableNode> config =
ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("commandsThrottling.xml"));
- var selectEntry = new ThrottlerConfigurationEntry(25,
Duration.ofMillis(2), Duration.ofMinutes(10), Duration.ofSeconds(1));
- var appendEntry = new ThrottlerConfigurationEntry(5,
Duration.ofMillis(10), Duration.ofMinutes(5), Duration.ofSeconds(2));
+ var selectEntry = new
ThrottlerConfigurationEntry(Optional.of("APPEND"), 25, Duration.ofMillis(2),
Duration.ofMinutes(10), Duration.ofSeconds(1));
+ var appendEntry = new
ThrottlerConfigurationEntry(Optional.empty(), 5, Duration.ofMillis(10),
Duration.ofMinutes(5), Duration.ofSeconds(2));
assertThat(ThrottlerConfiguration.from(config))
- .isEqualTo(new ThrottlerConfiguration(
+ .isEqualTo(ThrottlerConfiguration.from(
ImmutableMap.of(
"SELECT", selectEntry,
"APPEND", appendEntry)));
@@ -56,28 +57,28 @@ class IMAPCommandsThrottlerTest {
class DelayTest {
@Test
void shouldNotDelayWhenBelowThreshold() {
- var selectEntry = new ThrottlerConfigurationEntry(25,
Duration.ofMillis(2), Duration.ofMinutes(10), Duration.ofSeconds(1));
+ var selectEntry = new
ThrottlerConfigurationEntry(Optional.empty(), 25, Duration.ofMillis(2),
Duration.ofMinutes(10), Duration.ofSeconds(1));
assertThat(selectEntry.delayMSFor(24)).isZero();
}
@Test
void shouldDelayWhenThreshold() {
- var selectEntry = new ThrottlerConfigurationEntry(25,
Duration.ofMillis(2), Duration.ofMinutes(10), Duration.ofSeconds(1));
+ var selectEntry = new
ThrottlerConfigurationEntry(Optional.empty(), 25, Duration.ofMillis(2),
Duration.ofMinutes(10), Duration.ofSeconds(1));
assertThat(selectEntry.delayMSFor(25)).isEqualTo(50);
}
@Test
void shouldAdditionalDelayWhenAboveThreshold() {
- var selectEntry = new ThrottlerConfigurationEntry(25,
Duration.ofMillis(2), Duration.ofMinutes(10), Duration.ofSeconds(1));
+ var selectEntry = new
ThrottlerConfigurationEntry(Optional.empty(), 25, Duration.ofMillis(2),
Duration.ofMinutes(10), Duration.ofSeconds(1));
assertThat(selectEntry.delayMSFor(26)).isEqualTo(52);
}
@Test
void shouldNotExceedMaximumDelay() {
- var selectEntry = new ThrottlerConfigurationEntry(25,
Duration.ofMillis(2), Duration.ofMinutes(10), Duration.ofSeconds(1));
+ var selectEntry = new
ThrottlerConfigurationEntry(Optional.empty(), 25, Duration.ofMillis(2),
Duration.ofMinutes(10), Duration.ofSeconds(1));
assertThat(selectEntry.delayMSFor(2600)).isEqualTo(1000);
}
diff --git
a/server/protocols/protocols-imap4/src/test/resources/commandsThrottling.xml
b/server/protocols/protocols-imap4/src/test/resources/commandsThrottling.xml
index dc4f57afb4..3a85e19313 100644
--- a/server/protocols/protocols-imap4/src/test/resources/commandsThrottling.xml
+++ b/server/protocols/protocols-imap4/src/test/resources/commandsThrottling.xml
@@ -1,5 +1,6 @@
<perSessionCommandThrottling>
<select>
+ <resetOn>APPEND</resetOn>
<thresholdCount>25</thresholdCount>
<additionalDelayPerOperation>2ms</additionalDelayPerOperation>
<observationPeriod>10m</observationPeriod>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]