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]

Reply via email to