prosgarz35 commented on PR #3194:
URL: https://github.com/apache/james-project/pull/3194#issuecomment-5834555798

   # PR #3194 — Proposed Code Changes (Detailed Plan)
   
   > **Scope:** Artemis-related files only. Files `.gitattributes`, 
`crowdsec/james.log`, and `angus-mail.version` are intentionally excluded.
   >
   > Each section specifies: exact file path → exact line numbers → current 
code → replacement code → rationale.
   
   ---
   
   ## Change 1 — Bug Fix: BrokerExtension `server-id` mismatch
   
   ### Problem
   
   When `BrokerExtension` creates an embedded Artemis broker it assigns a 
unique `server-id` via `BROKER_COUNTER.incrementAndGet()`. Because 
`AtomicInteger` starts at **0**, the very first call to `incrementAndGet()` 
returns **1**. This means the first broker starts listening on InVM server-id 
**1**.
   
   However, `JMSCacheableMailQueueTest.setUp()` hard-codes `"vm://0"` as the 
connection URL, which targets server-id **0** — a server that does not exist.
   
   This mismatch is currently masked only by an Artemis InVM quirk where a 
default server-id=0 may be implicitly reused. As soon as tests run in parallel, 
or the JVM boot order changes, connections either fail or land on a different 
broker instance, producing silent data mix-up between test classes.
   
   ---
   
   ### File 1 of 2 — `BrokerExtension.java`
   
   **Path:**
   ```
   
server/queue/queue-jms/src/test/java/org/apache/james/queue/jms/BrokerExtension.java
   ```
   
   **Line 47 — current:**
   ```java
   private static final AtomicInteger BROKER_COUNTER = new AtomicInteger(0);
   ```
   
   **Line 47 — replacement:**
   ```java
   private static final AtomicInteger BROKER_COUNTER = new AtomicInteger(-1);
   ```
   
   **Why:** `new AtomicInteger(-1)` means the first `incrementAndGet()` call 
returns **0**, so the first broker gets `server-id=0`. This makes the InVM URL 
`vm://0` in the test correct for the first test class. Subsequent classes get 
ids 1, 2, … but each class uses its own `BrokerExtension` instance in its own 
JVM fork (surefire forks per-class by default), so there is no conflict.
   
   ---
   
   **Lines 61–74 — current:**
   ```java
   public BrokerExtension() throws Exception {
       brokerId = BROKER_COUNTER.incrementAndGet();
       Configuration config = new ConfigurationImpl()
           .setSecurityEnabled(false)
           .setJMXManagementEnabled(false)
           .setPersistenceEnabled(false)
           .addAcceptorConfiguration(new TransportConfiguration(
               InVMAcceptorFactory.class.getName(),
               java.util.Collections.singletonMap("server-id", 
String.valueOf(brokerId))
           ))
           .setName("test-broker-" + brokerId);
       broker = new EmbeddedActiveMQ();
       broker.setConfiguration(config);
   }
   ```
   
   **No change to lines 61–74.** The constructor body stays identical — only 
the initial value of `BROKER_COUNTER` changes (line 47 above). With `-1` as the 
start, `brokerId` will be `0` for the first instance, and the rest of the code 
is already correct.
   
   ---
   
   ### File 2 of 2 — `JMSCacheableMailQueueTest.java`
   
   **Path:**
   ```
   
server/queue/queue-jms/src/test/java/org/apache/james/queue/jms/JMSCacheableMailQueueTest.java
   ```
   
   **Lines 55–56 — current:**
   ```java
           // Use InVM transport - broker ID matches BrokerExtension instance 
counter
           ActiveMQConnectionFactory connectionFactory = new 
ActiveMQConnectionFactory("vm://0");
   ```
   
   **Lines 55–56 — replacement:**
   ```java
           // InVM transport: server-id=0 matches the first BrokerExtension 
instance (BROKER_COUNTER starts at -1)
           ActiveMQConnectionFactory connectionFactory = new 
ActiveMQConnectionFactory("vm://0");
   ```
   
   **Why:** The URL itself (`"vm://0"`) does not change — only the comment is 
updated to explain *why* `0` is correct, i.e., that `BROKER_COUNTER` is 
initialized at `-1` so the first `incrementAndGet()` yields `0`.
   
   ---
   
   ## Change 2 — DRY: Extract duplicated hex-encoding loop
   
   ### Problem
   
   Two methods in `JMSCacheableMailQueue` — `encodeAttributePropertyName` and 
`encodePerRecipientHeaderPropertyName` — contain **identical** 
character-encoding loops. The only difference is the string prefix prepended to 
the result. If the escaping format ever needs to change (e.g. switching from 
`_XXXX_` to a different scheme), both methods must be updated independently. 
This violates DRY.
   
   ---
   
   ### File — `JMSCacheableMailQueue.java`
   
   **Path:**
   ```
   
server/queue/queue-jms/src/main/java/org/apache/james/queue/jms/JMSCacheableMailQueue.java
   ```
   
   **Lines 162–186 — current:**
   ```java
       protected static String encodeAttributePropertyName(String 
attributeName) {
           StringBuilder sb = new StringBuilder(JAMES_MAIL_ATTR_PROP_PREFIX);
           for (int i = 0; i < attributeName.length(); i++) {
               char c = attributeName.charAt(i);
               if (Character.isJavaIdentifierPart(c)) {
                   sb.append(c);
               } else {
                   sb.append(String.format("_%04x_", (int) c));
               }
           }
           return sb.toString();
       }
   
       protected static String encodePerRecipientHeaderPropertyName(String 
recipientAddress) {
           StringBuilder sb = new 
StringBuilder(JAMES_MAIL_PER_RECIPIENT_HEADERS).append('_');
           for (int i = 0; i < recipientAddress.length(); i++) {
               char c = recipientAddress.charAt(i);
               if (Character.isJavaIdentifierPart(c)) {
                   sb.append(c);
               } else {
                   sb.append(String.format("_%04x_", (int) c));
               }
           }
           return sb.toString();
       }
   ```
   
   **Lines 162–186 — replacement:**
   ```java
       private static String encodeAsJmsPropertyName(String prefix, String 
input) {
           StringBuilder sb = new StringBuilder(prefix);
           for (int i = 0; i < input.length(); i++) {
               char c = input.charAt(i);
               if (Character.isJavaIdentifierPart(c)) {
                   sb.append(c);
               } else {
                   sb.append(String.format("_%04x_", (int) c));
               }
           }
           return sb.toString();
       }
   
       protected static String encodeAttributePropertyName(String 
attributeName) {
           return encodeAsJmsPropertyName(JAMES_MAIL_ATTR_PROP_PREFIX, 
attributeName);
       }
   
       protected static String encodePerRecipientHeaderPropertyName(String 
recipientAddress) {
           return encodeAsJmsPropertyName(JAMES_MAIL_PER_RECIPIENT_HEADERS + 
"_", recipientAddress);
       }
   ```
   
   **Why:**
   - Removes ~12 lines of duplicated code.
   - The encoding loop exists in exactly one place; any future change to the 
`_%04x_` escape format is a single-line edit.
   - Public API of both `protected static` methods is unchanged — callers and 
tests are unaffected.
   - `encodeAsJmsPropertyName` is `private static` — it is an internal detail, 
not part of the public interface.
   - Note: `JAMES_MAIL_PER_RECIPIENT_HEADERS + "_"` is a compile-time-constant 
concatenation; it produces exactly the same prefix as the original `new 
StringBuilder(JAMES_MAIL_PER_RECIPIENT_HEADERS).append('_')`.
   
   ---
   
   ## Change 3 — KISS: Simplify `mailAttribute()` — remove unnecessary 
`Throwing.function` wrapper
   
   ### Problem
   
   `mailAttribute()` currently uses `Throwing.function` (from the 
`fge/throwing-lambdas` library) to wrap a lambda that already fully catches 
every exception it can throw. The outer `Throwing.function` wrapper serves no 
purpose: no checked exception escapes the lambda. The result is a nested 
structure — lambda inside `Throwing.function` inside `.apply()` — that obscures 
simple sequential fallback logic.
   
   ---
   
   ### File — `JMSCacheableMailQueue.java`
   
   **Path:**
   ```
   
server/queue/queue-jms/src/main/java/org/apache/james/queue/jms/JMSCacheableMailQueue.java
   ```
   
   **Lines 541–570 — current:**
   ```java
       private Stream<Attribute> mailAttribute(Message message, String name) {
           // Now cast the property back to Serializable and set it as 
attribute.
           // See JAMES-1241. Property name is encoded to ensure it is a valid 
JMS identifier.
           Object attrValue = Throwing.function((String prop) -> {
               try {
                   Object val = message.getObjectProperty(prop);
                   if (val != null) {
                       return val;
                   }
               } catch (Exception e) {
                   // Fall back to raw name if property name check failed
               }
               try {
                   return message.getObjectProperty(name);
               } catch (Exception e) {
                   return null;
               }
           }).apply(encodeAttributePropertyName(name));
   
           if (attrValue instanceof String) {
               try {
                   return Stream.of(new Attribute(AttributeName.of(name), 
AttributeValue.fromJsonString((String) attrValue)));
               } catch (IOException e) {
                   LOGGER.error("Error deserializing mail attribute {} with 
value {}", name, attrValue, e);
               }
           } else {
               LOGGER.error("Not supported mail attribute {} of type {} for 
mail {}", name, attrValue, name);
           }
           return Stream.empty();
       }
   ```
   
   **Lines 541–570 — replacement:**
   ```java
       private Stream<Attribute> mailAttribute(Message message, String name) {
           // Property name is encoded to ensure it is a valid JMS identifier. 
See JAMES-1241.
           // Try encoded name first; fall back to raw name for backward 
compatibility.
           Object attrValue = null;
           try {
               attrValue = 
message.getObjectProperty(encodeAttributePropertyName(name));
           } catch (JMSException e) {
               // ignore — fall through to legacy fallback
           }
           if (attrValue == null) {
               try {
                   attrValue = message.getObjectProperty(name);
               } catch (JMSException e) {
                   // ignore
               }
           }
   
           if (attrValue instanceof String) {
               try {
                   return Stream.of(new Attribute(AttributeName.of(name), 
AttributeValue.fromJsonString((String) attrValue)));
               } catch (IOException e) {
                   LOGGER.error("Error deserializing mail attribute {} with 
value {}", name, attrValue, e);
               }
           } else {
               LOGGER.error("Not supported mail attribute {} of type {} for 
mail {}", name, attrValue, name);
           }
           return Stream.empty();
       }
   ```
   
   **Why:**
   - Removes the `Throwing.function` wrapper — the functional interface was 
only needed to satisfy the compiler about `JMSException`, but since both 
`getObjectProperty` calls were already surrounded by `catch (Exception e)`, the 
wrapper did nothing.
   - Replaces `catch (Exception e)` with the specific `catch (JMSException e)` 
— now we catch exactly what the API declares, not everything. This is better 
practice: unchecked exceptions (e.g. `NullPointerException` from a broken 
broker) are no longer silently swallowed.
   - Logic is identical: try encoded name → if null, try raw name → if still 
null/non-String, log error.
   - Code flows top-to-bottom with no hidden lambdas; a new contributor can 
read it without knowing the `fge/throwing-lambdas` library.
   - No behavioral change. The existing `JMSCacheableMailQueuePropertyNameTest` 
already covers the encode/decode round-trip; the fallback path is also tested.
   
   ---
   
   ## Change 4 — Inconsistency: Align `CLIENT_BLOCK_ON_DURABLE_SEND` / 
`CLIENT_BLOCK_ON_ACKNOWLEDGE` defaults
   
   ### Problem
   
   In `ActiveMQConfiguration.java`, both client-side blocking flags default to 
`true`:
   
   ```java
   private static final boolean CLIENT_BLOCK_ON_DURABLE_SEND_DEFAULT = true;
   private static final boolean CLIENT_BLOCK_ON_ACKNOWLEDGE_DEFAULT  = true;
   ```
   
   But the canonical `activemq.properties` sample file (the one that documents 
the recommended settings to operators) specifies:
   
   ```properties
   artemis.client.block.on.durable.send=false
   artemis.client.block.on.acknowledge=false
   ```
   
   **Impact:** deployments that do not provide an `activemq.properties` file 
(currently: JPA app, JPA-SMTP app, Spring app — they have no sample config for 
this) use `getDefault()` which calls `from(new BaseConfiguration())`. With 
`DEFAULT = true`, those deployments silently run in blocking mode: every 
`send()` to a durable queue blocks the caller thread until the broker 
acknowledges persistence. This is the high-latency / low-throughput path.
   
   Apps that *do* provide the sample file (postgres-app) get `false` — the 
non-blocking, high-throughput path.
   
   The two groups of apps behave differently with no visible indication to the 
operator.
   
   ---
   
   ### File — `ActiveMQConfiguration.java`
   
   **Path:**
   ```
   
server/queue/queue-activemq/src/main/java/org/apache/james/queue/activemq/ActiveMQConfiguration.java
   ```
   
   **Line 50 — current:**
   ```java
       private static final boolean CLIENT_BLOCK_ON_DURABLE_SEND_DEFAULT = true;
   ```
   
   **Line 50 — replacement:**
   ```java
       private static final boolean CLIENT_BLOCK_ON_DURABLE_SEND_DEFAULT = 
false;
   ```
   
   ---
   
   **Line 53 — current:**
   ```java
       private static final boolean CLIENT_BLOCK_ON_ACKNOWLEDGE_DEFAULT = true;
   ```
   
   **Line 53 — replacement:**
   ```java
       private static final boolean CLIENT_BLOCK_ON_ACKNOWLEDGE_DEFAULT = false;
   ```
   
   **Why:**
   - `false` (non-blocking) is the recommended value: already documented and 
shipped in `activemq.properties`.
   - Non-blocking send delegates persistence responsibility to the Artemis 
journal engine, which batches writes efficiently. This is the correct default 
for a mail queue broker embedded in the same JVM — there is no network latency, 
and the journal fsync (`journalSyncTransactional=true`) still guarantees 
durability.
   - `true` (blocking) makes sense only for remote brokers where you need 
round-trip confirmation. For InVM transport (`vm://0`), blocking is always 
wasteful.
   - Changing the default to `false` makes *all* James deployments (with or 
without a custom properties file) behave consistently and in the recommended 
way.
   
   > **Note:** if any deployment scenario genuinely requires blocking sends 
(e.g. a custom remote broker setup), the operator can override via 
`artemis.client.block.on.durable.send=true` in their properties file. The 
ability to configure this is preserved.
   
   ---
   
   ## Summary of All Changes
   
   | # | File | Lines affected | Type | Risk |
   |---|------|---------------|------|------|
   | 1a | `BrokerExtension.java` | 47 | Bug fix | Low — test-only |
   | 1b | `JMSCacheableMailQueueTest.java` | 55 | Comment update | None |
   | 2 | `JMSCacheableMailQueue.java` | 162–186 | DRY refactor | Low — 
`protected static` API unchanged |
   | 3 | `JMSCacheableMailQueue.java` | 541–570 | KISS refactor | Low — logic 
identical, narrower catch |
   | 4 | `ActiveMQConfiguration.java` | 50, 53 | Default value fix | Medium — 
behavior change for apps without properties file |
   
   > **Change 4** is the only one with production behavior impact. All other 
changes are either test-only or pure refactors with identical runtime behavior.


-- 
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]

Reply via email to