This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/dev/pr-12698-372d3cd7b4d7715eb6fc811c1b43e23cb6309c4d
in repository https://gitbox.apache.org/repos/asf/seatunnel.git

commit 83603e19b7b09d6114c6f6c9e89bfdafe2796183
Author: chovygo <[email protected]>
AuthorDate: Sun Oct 11 13:02:02 2026 +0000

    [Improve] [Connector-V2] Improve ActiveMQ sink: fix resource leak and add 
config options (#12698)
    
    Co-authored-by: zhaowei <[email protected]>
---
 docs/en/connectors/sink/Activemq.md                |  11 +
 docs/zh/connectors/sink/Activemq.md                |  19 +-
 .../seatunnel/activemq/client/ActivemqClient.java  |  87 +++-
 .../activemq/config/ActivemqSinkOptions.java       |  77 ++++
 .../activemq/sink/ActivemqSinkFactory.java         |  20 +-
 .../seatunnel/activemq/ActivemqFactoryTest.java    |   9 +
 .../activemq/client/ActivemqClientTest.java        | 510 +++++++++++++++++++++
 7 files changed, 720 insertions(+), 13 deletions(-)

diff --git a/docs/en/connectors/sink/Activemq.md 
b/docs/en/connectors/sink/Activemq.md
index c3aa17156c..834b6063e0 100644
--- a/docs/en/connectors/sink/Activemq.md
+++ b/docs/en/connectors/sink/Activemq.md
@@ -38,6 +38,15 @@ For consuming queues, see the separate [ActiveMQ 
source](../source/Activemq.md).
 | nested_map_and_list_enabled             | boolean | no       | -             
| Whether structured message properties and `MapMessage` entries can contain 
nested `Map` and `List` objects.                                                
           |
 | warn_about_unstarted_connection_timeout | int     | no       | -             
| Timeout in milliseconds before ActiveMQ warns that a connection was not 
started correctly. Set a value less than `0` to disable the warning in the 
ActiveMQ client. |
 | consumer_expiry_check_enabled            | boolean | no       | -            
 | Whether the ActiveMQ client checks message expiration in each 
`MessageConsumer` before dispatching messages.                                  
                                                                |
+| max_thread_pool_size                    | int     | no       | 1000          
| Maximum thread pool size for the ActiveMQ connection factory's internal 
thread pool used for dispatching messages. Defaults to `1000`.                  
            |
+| send_timeout                            | int     | no       | 0             
| Send timeout in milliseconds for the ActiveMQ connection factory. A value of 
`0` means no timeout. Defaults to `0`.                                          
      |
+| use_compression                         | boolean | no       | false         
| Whether message bodies are compressed before being sent to the broker. 
Defaults to `false`.                                                            
            |
+| connect_response_timeout               | int     | no       | 0             
| Timeout in milliseconds for waiting for the broker's response during 
connection establishment. A value of `0` means no timeout. Defaults to `0`. |
+| delivery_mode                           | int     | no       | 2             
| JMS delivery mode: `1` for NON_PERSISTENT or `2` for PERSISTENT. Defaults to 
`2` (PERSISTENT). |
+| priority                                | int     | no       | 4             
| JMS message priority from 0 (lowest) to 9 (highest). Defaults to `4`. |
+| producer_window_size                    | int     | no       | 0             
| Flow control window size in bytes for async sends. A value of `0` means no 
flow control. Defaults to `0`. |
+| time_to_live                            | long    | no       | 0             
| Message time-to-live in milliseconds. A value of `0` means never expires. 
Defaults to `0`. |
+| use_async_send                          | boolean | no       | false         
| When true, sends return without waiting for broker confirmation. Defaults to 
`false`. |
 
 ## Notes
 
@@ -46,6 +55,8 @@ For consuming queues, see the separate [ActiveMQ 
source](../source/Activemq.md).
 - The connector writes each SeaTunnel row as one JSON text message to 
`queue_name`. There is no separate `format` option for this sink.
 - Configure the broker address with `uri`. `host` and `port` are not ActiveMQ 
sink options.
 - Use any SeaTunnel source before this sink. The ActiveMQ connector only 
controls how the final rows are sent to the queue.
+- When `use_async_send = true` or `delivery_mode = 1` (NON_PERSISTENT), the 
ActiveMQ client sends without waiting for broker confirmation and this sink 
does not register an `ExceptionListener`, so a failed send may not be surfaced 
to the job. Use these options only when acceptable message loss is tolerable.
+- The factory-level options (`max_thread_pool_size`, `send_timeout`, 
`use_compression`, `connect_response_timeout`, `producer_window_size`, 
`use_async_send`) are only applied when explicitly set. When not set, the value 
from the matching `jms.*` parameter in the `uri` (if present) or the ActiveMQ 
client default is used.
 
 ## Example
 
diff --git a/docs/zh/connectors/sink/Activemq.md 
b/docs/zh/connectors/sink/Activemq.md
index 7cba3be4e3..5ce6ece4e5 100644
--- a/docs/zh/connectors/sink/Activemq.md
+++ b/docs/zh/connectors/sink/Activemq.md
@@ -4,6 +4,12 @@ import ChangeLog from '../changelog/connector-activemq.md';
 
 > ActiveMQ Sink 连接器
 
+## 支持的引擎
+
+> Spark<br/>
+> Flink<br/>
+> SeaTunnel Zeta<br/>
+
 ## 描述
 
 用于把 SeaTunnel 数据写入 ActiveMQ 队列。每一行数据都会被序列化成一条 JSON 文本消息。
@@ -31,7 +37,16 @@ import ChangeLog from '../changelog/connector-activemq.md';
 | dispatch_async                        | boolean | 否    | -   | Broker 
是否异步分发消息。                                                                       
                |
 | nested_map_and_list_enabled           | boolean | 否    | -   | 是否允许结构化消息属性和 
`MapMessage` 条目中包含嵌套的 `Map`、`List` 对象。                                          
     |
 | warn_about_unstarted_connection_timeout | int   | 否    | -   | 
连接没有正确启动时,ActiveMQ 客户端发出警告前等待的毫秒数。设置为小于 `0` 的值可以关闭这个警告。                         
     |
-| consumer_expiry_check_enabled           | boolean | 否    | -   | 是否在每个 
`MessageConsumer` 分发消息前检查消息是否已经过期。                                              
    |
+| consumer_expiry_check_enabled           | boolean | 否    | -     | 是否在每个 
`MessageConsumer` 分发消息前检查消息是否已经过期。                                              
    |
+| max_thread_pool_size                    | int     | 否    | 1000  | ActiveMQ 
连接工厂内部用于消息分发的线程池最大大小。默认值为 `1000`。                                            |
+| send_timeout                            | int     | 否    | 0     | ActiveMQ 
连接工厂的发送超时时间,单位毫秒。值为 `0` 表示不超时。默认值为 `0`。                                  |
+| use_compression                         | boolean | 否    | false | 
是否在发送消息前对消息体进行压缩。默认值为 `false`。                                                  
        |
+| connect_response_timeout               | int     | 否    | 0     | 连接建立时等待 
Broker 响应的超时时间,单位毫秒。值为 `0` 表示不超时。默认值为 `0`。                              |
+| delivery_mode                          | int     | 否    | 2     | JMS 
投递模式:`1` 为非持久化,`2` 为持久化。默认值为 `2`(持久化)。                                          
    |
+| priority                               | int     | 否    | 4     | JMS 
消息优先级,0(最低)到 9(最高)。默认值为 `4`。                                                    
        |
+| producer_window_size                   | int     | 否    | 0     | 
异步发送时的流控窗口大小,单位字节。值为 `0` 表示不限流。默认值为 `0`。                                        
  |
+| time_to_live                           | long    | 否    | 0     | 
消息存活时间,单位毫秒。值为 `0` 表示永不过期。默认值为 `0`。                                             
       |
+| use_async_send                         | boolean | 否    | false | 为 true 
时发送不等 Broker 确认直接返回,提升吞吐量。默认值为 `false`。                                         
  |
 
 ## 注意事项
 
@@ -40,6 +55,8 @@ import ChangeLog from '../changelog/connector-activemq.md';
 - 连接器会把每一行 SeaTunnel 数据作为一条 JSON 文本消息写入 `queue_name`,当前没有单独的 `format` 配置。
 - Broker 地址请使用 `uri` 配置,`host` 和 `port` 不是 ActiveMQ Sink 的配置项。
 - 该 Sink 前面可以接任意 SeaTunnel Source。ActiveMQ 连接器只负责把最终的数据行发送到队列。
+- 当 `use_async_send = true` 或 `delivery_mode = 1`(非持久化)时,ActiveMQ 客户端发送消息后不等 
Broker 确认即返回,且本连接器未注册 `ExceptionListener`,因此发送失败可能不会通知到作业。仅在可接受消息丢失的场景下使用这些选项。
+- 
工厂级选项(`max_thread_pool_size`、`send_timeout`、`use_compression`、`connect_response_timeout`、`producer_window_size`、`use_async_send`)仅在显式配置时生效。未配置时,使用
 `uri` 中对应的 `jms.*` 参数(如果存在)或 ActiveMQ 客户端默认值。
 
 ## 示例
 
diff --git 
a/seatunnel-connectors-v2/connector-activemq/src/main/java/org/apache/seatunnel/connectors/seatunnel/activemq/client/ActivemqClient.java
 
b/seatunnel-connectors-v2/connector-activemq/src/main/java/org/apache/seatunnel/connectors/seatunnel/activemq/client/ActivemqClient.java
index d351344446..b470a30ac2 100644
--- 
a/seatunnel-connectors-v2/connector-activemq/src/main/java/org/apache/seatunnel/connectors/seatunnel/activemq/client/ActivemqClient.java
+++ 
b/seatunnel-connectors-v2/connector-activemq/src/main/java/org/apache/seatunnel/connectors/seatunnel/activemq/client/ActivemqClient.java
@@ -39,13 +39,22 @@ import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.Activemq
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.CHECK_FOR_DUPLICATE;
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.CLIENT_ID;
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.CLOSE_TIMEOUT;
+import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.CONNECT_RESPONSE_TIMEOUT;
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.CONSUMER_EXPIRY_CHECK_ENABLED;
+import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.DELIVERY_MODE;
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.DISPATCH_ASYNC;
+import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.MAX_THREAD_POOL_SIZE;
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.NESTED_MAP_AND_LIST_ENABLED;
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.PASSWORD;
+import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.PRIORITY;
+import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.PRODUCER_WINDOW_SIZE;
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.QUEUE_NAME;
+import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.SEND_TIMEOUT;
+import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.TIME_TO_LIVE;
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.URI;
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.USERNAME;
+import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.USE_ASYNC_SEND;
+import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.USE_COMPRESSION;
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.WARN_ABOUT_UNSTARTED_CONNECTION_TIMEOUT;
 
 @Slf4j
@@ -53,17 +62,44 @@ public class ActivemqClient {
     private final ReadonlyConfig config;
     private final ActiveMQConnectionFactory connectionFactory;
     private final Connection connection;
+    private final Session session;
+    private final MessageProducer producer;
 
     public ActivemqClient(ReadonlyConfig config) {
         this.config = config;
         try {
+            int deliveryMode = config.get(DELIVERY_MODE);
+            if (deliveryMode != 1 && deliveryMode != 2) {
+                throw new IllegalArgumentException(
+                        "delivery_mode must be 1 (NON_PERSISTENT) or 2 
(PERSISTENT), got: "
+                                + deliveryMode);
+            }
+            int priority = config.get(PRIORITY);
+            if (priority < 0 || priority > 9) {
+                throw new IllegalArgumentException(
+                        "priority must be between 0 and 9, got: " + priority);
+            }
+
             this.connectionFactory = getConnectionFactory();
             log.info("connection factory created");
             this.connection = createConnection(config);
+            this.connection.start();
+            this.session = connection.createSession(false, 
Session.AUTO_ACKNOWLEDGE);
+            Destination destination = 
session.createQueue(config.get(QUEUE_NAME));
+            this.producer = session.createProducer(destination);
+            this.producer.setDeliveryMode(deliveryMode);
+            this.producer.setTimeToLive(config.get(TIME_TO_LIVE));
+            this.producer.setPriority(priority);
             log.info("connection created");
 
         } catch (Exception e) {
             log.error("Error while creating AMQ client", e);
+            // Best-effort cleanup of partially-created resources to avoid 
leaks
+            try {
+                close();
+            } catch (Exception closeError) {
+                e.addSuppressed(closeError);
+            }
             throw new ActivemqConnectorException(
                     ActivemqConnectorErrorCode.CREATE_ACTIVEMQ_CLIENT_FAILED,
                     "Error while create AMQ client ",
@@ -72,7 +108,9 @@ public class ActivemqClient {
     }
 
     public ActiveMQConnectionFactory getConnectionFactory() {
-        log.info("broker url : " + config.get(URI));
+        // Strip credentials from URI before logging to avoid leaking 
sensitive information
+        String safeUri = config.get(URI).replaceAll("(://)[^@:]+:[^@]+@", 
"$1***@");
+        log.info("broker url : " + safeUri);
         ActiveMQConnectionFactory factory = new 
ActiveMQConnectionFactory(config.get(URI));
 
         if (config.get(ALWAYS_SESSION_ASYNC) != null) {
@@ -109,39 +147,66 @@ public class ActivemqClient {
         if (config.get(NESTED_MAP_AND_LIST_ENABLED) != null) {
             
factory.setNestedMapAndListEnabled(config.get(NESTED_MAP_AND_LIST_ENABLED));
         }
+
+        // Options declared with .defaultValue() use getOptional() so the 
setter is only called
+        // when the user explicitly set the option. Otherwise the 
ActiveMQConnectionFactory would
+        // always apply the SeaTunnel default, silently overwriting jms.* 
parameters from the URI.
+        
config.getOptional(MAX_THREAD_POOL_SIZE).ifPresent(factory::setMaxThreadPoolSize);
+        config.getOptional(SEND_TIMEOUT).ifPresent(factory::setSendTimeout);
+        
config.getOptional(USE_COMPRESSION).ifPresent(factory::setUseCompression);
+        
config.getOptional(CONNECT_RESPONSE_TIMEOUT).ifPresent(factory::setConnectResponseTimeout);
+        
config.getOptional(PRODUCER_WINDOW_SIZE).ifPresent(factory::setProducerWindowSize);
+        config.getOptional(USE_ASYNC_SEND).ifPresent(factory::setUseAsyncSend);
         return factory;
     }
 
     public void write(byte[] msg) {
         try {
-            this.connection.start();
-            Session session = this.connection.createSession(false, 
Session.AUTO_ACKNOWLEDGE);
-            Destination destination = 
session.createQueue(config.get(QUEUE_NAME));
-            MessageProducer producer = session.createProducer(destination);
             String messageBody = new String(msg, StandardCharsets.UTF_8);
-            TextMessage objectMessage = session.createTextMessage(messageBody);
-            producer.send(objectMessage);
+            TextMessage textMessage = session.createTextMessage(messageBody);
+            producer.send(textMessage);
 
         } catch (JMSException e) {
             throw new ActivemqConnectorException(
                     ActivemqConnectorErrorCode.SEND_MESSAGE_FAILED,
-                    String.format(
-                            "Cannot send AMQ message %s at %s",
-                            config.get(QUEUE_NAME), config.get(CLIENT_ID)),
+                    String.format("Cannot send AMQ message to queue %s", 
config.get(QUEUE_NAME)),
                     e);
         }
     }
 
     public void close() {
+        JMSException firstError = null;
+        try {
+            if (producer != null) {
+                producer.close();
+            }
+        } catch (JMSException e) {
+            firstError = e;
+        }
+        try {
+            if (session != null) {
+                session.close();
+            }
+        } catch (JMSException e) {
+            if (firstError == null) {
+                firstError = e;
+            }
+        }
         try {
             if (connection != null) {
                 connection.close();
             }
         } catch (JMSException e) {
+            if (firstError == null) {
+                firstError = e;
+            }
+        }
+        if (firstError != null) {
             throw new ActivemqConnectorException(
                     ActivemqConnectorErrorCode.CLOSE_CONNECTION_FAILED,
                     String.format(
-                            "Error while closing AMQ connection with  %s", 
config.get(QUEUE_NAME)));
+                            "Error while closing AMQ connection with %s", 
config.get(QUEUE_NAME)),
+                    firstError);
         }
     }
 
diff --git 
a/seatunnel-connectors-v2/connector-activemq/src/main/java/org/apache/seatunnel/connectors/seatunnel/activemq/config/ActivemqSinkOptions.java
 
b/seatunnel-connectors-v2/connector-activemq/src/main/java/org/apache/seatunnel/connectors/seatunnel/activemq/config/ActivemqSinkOptions.java
index 1d4300b004..7806f84072 100644
--- 
a/seatunnel-connectors-v2/connector-activemq/src/main/java/org/apache/seatunnel/connectors/seatunnel/activemq/config/ActivemqSinkOptions.java
+++ 
b/seatunnel-connectors-v2/connector-activemq/src/main/java/org/apache/seatunnel/connectors/seatunnel/activemq/config/ActivemqSinkOptions.java
@@ -122,4 +122,81 @@ public class ActivemqSinkOptions implements Serializable {
                     .withDescription(
                             "Controls whether message expiration checking is 
done in each "
                                     + "MessageConsumer prior to dispatching a 
message.");
+
+    public static final Option<Integer> MAX_THREAD_POOL_SIZE =
+            Options.key("max_thread_pool_size")
+                    .intType()
+                    .defaultValue(1000)
+                    .withDescription(
+                            "Sets the maximum thread pool size for the 
ActiveMQ connection factory's "
+                                    + "internal thread pool used for 
dispatching messages.");
+
+    public static final Option<Integer> SEND_TIMEOUT =
+            Options.key("send_timeout")
+                    .intType()
+                    .defaultValue(0)
+                    .withDescription(
+                            "Sets the send timeout in milliseconds for the 
ActiveMQ connection factory. "
+                                    + "A value of 0 means no timeout.");
+
+    public static final Option<Boolean> USE_COMPRESSION =
+            Options.key("use_compression")
+                    .booleanType()
+                    .defaultValue(false)
+                    .withDescription(
+                            "When true, message bodies are compressed before 
being sent to the broker.");
+
+    public static final Option<Integer> CONNECT_RESPONSE_TIMEOUT =
+            Options.key("connect_response_timeout")
+                    .intType()
+                    .defaultValue(0)
+                    .withDescription(
+                            "Timeout in milliseconds for waiting for the 
broker's response during "
+                                    + "connection establishment. A value of 0 
means no timeout. "
+                                    + "Defaults to 0.");
+
+    public static final Option<Integer> DELIVERY_MODE =
+            Options.key("delivery_mode")
+                    .intType()
+                    .defaultValue(2)
+                    .withDescription(
+                            "JMS delivery mode: 1 for NON_PERSISTENT (message 
kept in memory, "
+                                    + "lost if broker crashes) or 2 for 
PERSISTENT (message written "
+                                    + "to disk before acknowledge). Defaults 
to 2 (PERSISTENT).");
+
+    public static final Option<Integer> PRIORITY =
+            Options.key("priority")
+                    .intType()
+                    .defaultValue(4)
+                    .withDescription(
+                            "JMS message priority from 0 (lowest) to 9 
(highest). The broker "
+                                    + "delivers higher-priority messages 
before lower-priority ones. "
+                                    + "Defaults to 4.");
+
+    public static final Option<Integer> PRODUCER_WINDOW_SIZE =
+            Options.key("producer_window_size")
+                    .intType()
+                    .defaultValue(0)
+                    .withDescription(
+                            "Flow control window size in bytes for async 
sends. A value of 0 means "
+                                    + "no flow control. Used together with 
use_async_send to prevent "
+                                    + "unbounded memory growth. Defaults to 
0.");
+
+    public static final Option<Long> TIME_TO_LIVE =
+            Options.key("time_to_live")
+                    .longType()
+                    .defaultValue(0L)
+                    .withDescription(
+                            "Message time-to-live in milliseconds. A value of 
0 means the message "
+                                    + "never expires. The broker discards 
messages after the TTL "
+                                    + "expires. Defaults to 0.");
+
+    public static final Option<Boolean> USE_ASYNC_SEND =
+            Options.key("use_async_send")
+                    .booleanType()
+                    .defaultValue(false)
+                    .withDescription(
+                            "When true, the producer sends messages without 
waiting for the broker "
+                                    + "to confirm receipt, improving 
throughput at the risk of message "
+                                    + "loss if the broker fails. Defaults to 
false.");
 }
diff --git 
a/seatunnel-connectors-v2/connector-activemq/src/main/java/org/apache/seatunnel/connectors/seatunnel/activemq/sink/ActivemqSinkFactory.java
 
b/seatunnel-connectors-v2/connector-activemq/src/main/java/org/apache/seatunnel/connectors/seatunnel/activemq/sink/ActivemqSinkFactory.java
index 55d14771a1..d7ec4aea58 100644
--- 
a/seatunnel-connectors-v2/connector-activemq/src/main/java/org/apache/seatunnel/connectors/seatunnel/activemq/sink/ActivemqSinkFactory.java
+++ 
b/seatunnel-connectors-v2/connector-activemq/src/main/java/org/apache/seatunnel/connectors/seatunnel/activemq/sink/ActivemqSinkFactory.java
@@ -31,13 +31,22 @@ import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.Activemq
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.CHECK_FOR_DUPLICATE;
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.CLIENT_ID;
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.CLOSE_TIMEOUT;
+import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.CONNECT_RESPONSE_TIMEOUT;
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.CONSUMER_EXPIRY_CHECK_ENABLED;
+import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.DELIVERY_MODE;
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.DISPATCH_ASYNC;
+import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.MAX_THREAD_POOL_SIZE;
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.NESTED_MAP_AND_LIST_ENABLED;
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.PASSWORD;
+import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.PRIORITY;
+import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.PRODUCER_WINDOW_SIZE;
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.QUEUE_NAME;
+import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.SEND_TIMEOUT;
+import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.TIME_TO_LIVE;
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.URI;
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.USERNAME;
+import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.USE_ASYNC_SEND;
+import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.USE_COMPRESSION;
 import static 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions.WARN_ABOUT_UNSTARTED_CONNECTION_TIMEOUT;
 
 @AutoService(Factory.class)
@@ -63,7 +72,16 @@ public class ActivemqSinkFactory implements TableSinkFactory 
{
                         CONSUMER_EXPIRY_CHECK_ENABLED,
                         DISPATCH_ASYNC,
                         NESTED_MAP_AND_LIST_ENABLED,
-                        WARN_ABOUT_UNSTARTED_CONNECTION_TIMEOUT)
+                        WARN_ABOUT_UNSTARTED_CONNECTION_TIMEOUT,
+                        MAX_THREAD_POOL_SIZE,
+                        SEND_TIMEOUT,
+                        USE_COMPRESSION,
+                        CONNECT_RESPONSE_TIMEOUT,
+                        DELIVERY_MODE,
+                        PRIORITY,
+                        PRODUCER_WINDOW_SIZE,
+                        TIME_TO_LIVE,
+                        USE_ASYNC_SEND)
                 .build();
     }
 
diff --git 
a/seatunnel-connectors-v2/connector-activemq/src/test/java/org/apache/seatunnel/connectors/seatunnel/activemq/ActivemqFactoryTest.java
 
b/seatunnel-connectors-v2/connector-activemq/src/test/java/org/apache/seatunnel/connectors/seatunnel/activemq/ActivemqFactoryTest.java
index 2141330010..4517eb055b 100644
--- 
a/seatunnel-connectors-v2/connector-activemq/src/test/java/org/apache/seatunnel/connectors/seatunnel/activemq/ActivemqFactoryTest.java
+++ 
b/seatunnel-connectors-v2/connector-activemq/src/test/java/org/apache/seatunnel/connectors/seatunnel/activemq/ActivemqFactoryTest.java
@@ -90,6 +90,15 @@ class ActivemqFactoryTest {
         config.put(ActivemqSinkOptions.CLOSE_TIMEOUT.key(), 1000);
         config.put(ActivemqSinkOptions.CONSUMER_EXPIRY_CHECK_ENABLED.key(), 
true);
         
config.put(ActivemqSinkOptions.WARN_ABOUT_UNSTARTED_CONNECTION_TIMEOUT.key(), 
-1);
+        config.put(ActivemqSinkOptions.MAX_THREAD_POOL_SIZE.key(), 100);
+        config.put(ActivemqSinkOptions.SEND_TIMEOUT.key(), 5000);
+        config.put(ActivemqSinkOptions.USE_COMPRESSION.key(), true);
+        config.put(ActivemqSinkOptions.CONNECT_RESPONSE_TIMEOUT.key(), 5000);
+        config.put(ActivemqSinkOptions.DELIVERY_MODE.key(), 1);
+        config.put(ActivemqSinkOptions.PRIORITY.key(), 9);
+        config.put(ActivemqSinkOptions.PRODUCER_WINDOW_SIZE.key(), 1048576);
+        config.put(ActivemqSinkOptions.TIME_TO_LIVE.key(), 60000);
+        config.put(ActivemqSinkOptions.USE_ASYNC_SEND.key(), true);
         Assertions.assertDoesNotThrow(() -> validate(config));
     }
 
diff --git 
a/seatunnel-connectors-v2/connector-activemq/src/test/java/org/apache/seatunnel/connectors/seatunnel/activemq/client/ActivemqClientTest.java
 
b/seatunnel-connectors-v2/connector-activemq/src/test/java/org/apache/seatunnel/connectors/seatunnel/activemq/client/ActivemqClientTest.java
new file mode 100644
index 0000000000..c2f9bfcb94
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-activemq/src/test/java/org/apache/seatunnel/connectors/seatunnel/activemq/client/ActivemqClientTest.java
@@ -0,0 +1,510 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.activemq.client;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import 
org.apache.seatunnel.connectors.seatunnel.activemq.config.ActivemqSinkOptions;
+import 
org.apache.seatunnel.connectors.seatunnel.activemq.exception.ActivemqConnectorException;
+
+import org.apache.activemq.ActiveMQConnectionFactory;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.InOrder;
+import org.mockito.MockedConstruction;
+
+import javax.jms.Connection;
+import javax.jms.JMSException;
+import javax.jms.MessageProducer;
+import javax.jms.Queue;
+import javax.jms.Session;
+import javax.jms.TextMessage;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class ActivemqClientTest {
+
+    private static final String TEST_URI = "tcp://localhost:61616";
+    private static final String TEST_QUEUE = "test-queue";
+    private static final String TEST_URI_WITH_CREDENTIALS =
+            "tcp://admin:secretPass@localhost:61616";
+
+    private static Map<String, Object> baseConfig() {
+        Map<String, Object> config = new HashMap<>();
+        config.put(ActivemqSinkOptions.URI.key(), TEST_URI);
+        config.put(ActivemqSinkOptions.QUEUE_NAME.key(), TEST_QUEUE);
+        return config;
+    }
+
+    /**
+     * L1 + L2: Session and MessageProducer are created once in the 
constructor, and
+     * connection.start() is called once in the constructor.
+     */
+    @Test
+    void constructorCreatesSessionProducerAndStartsConnection() throws 
Exception {
+        Connection mockConnection = mock(Connection.class);
+        Session mockSession = mock(Session.class);
+        Queue mockQueue = mock(Queue.class);
+        MessageProducer mockProducer = mock(MessageProducer.class);
+
+        try (MockedConstruction<ActiveMQConnectionFactory> ignored =
+                mockConstruction(
+                        ActiveMQConnectionFactory.class,
+                        (factory, ctx) ->
+                                
when(factory.createConnection()).thenReturn(mockConnection))) {
+            when(mockConnection.createSession(false, Session.AUTO_ACKNOWLEDGE))
+                    .thenReturn(mockSession);
+            when(mockSession.createQueue(TEST_QUEUE)).thenReturn(mockQueue);
+            
when(mockSession.createProducer(mockQueue)).thenReturn(mockProducer);
+
+            new ActivemqClient(ReadonlyConfig.fromMap(baseConfig()));
+
+            // L2: connection.start() called once in constructor
+            verify(mockConnection).start();
+            // L1: session created once in constructor
+            verify(mockConnection).createSession(false, 
Session.AUTO_ACKNOWLEDGE);
+            // L1: producer created once in constructor
+            verify(mockSession).createProducer(mockQueue);
+        }
+    }
+
+    /**
+     * L1: write() reuses the session and producer created in the constructor 
instead of creating
+     * new JMS resources for each message.
+     */
+    @Test
+    void writeReusesSessionAndProducer() throws Exception {
+        Connection mockConnection = mock(Connection.class);
+        Session mockSession = mock(Session.class);
+        Queue mockQueue = mock(Queue.class);
+        MessageProducer mockProducer = mock(MessageProducer.class);
+        TextMessage mockTextMessage = mock(TextMessage.class);
+
+        try (MockedConstruction<ActiveMQConnectionFactory> ignored =
+                mockConstruction(
+                        ActiveMQConnectionFactory.class,
+                        (factory, ctx) ->
+                                
when(factory.createConnection()).thenReturn(mockConnection))) {
+            when(mockConnection.createSession(false, Session.AUTO_ACKNOWLEDGE))
+                    .thenReturn(mockSession);
+            when(mockSession.createQueue(TEST_QUEUE)).thenReturn(mockQueue);
+            
when(mockSession.createProducer(mockQueue)).thenReturn(mockProducer);
+            
when(mockSession.createTextMessage("hello")).thenReturn(mockTextMessage);
+
+            ActivemqClient client = new 
ActivemqClient(ReadonlyConfig.fromMap(baseConfig()));
+            client.write("hello".getBytes(StandardCharsets.UTF_8));
+
+            // L1: createSession still called only once (constructor only, not 
in write)
+            verify(mockConnection, times(1)).createSession(false, 
Session.AUTO_ACKNOWLEDGE);
+            // L1: createProducer still called only once (constructor only, 
not in write)
+            verify(mockSession, times(1)).createProducer(mockQueue);
+            // L2: connection.start() not called again in write()
+            verify(mockConnection, times(1)).start();
+            // write() creates a TextMessage and sends it via the reused 
producer
+            verify(mockSession).createTextMessage("hello");
+            verify(mockProducer).send(mockTextMessage);
+        }
+    }
+
+    /**
+     * L4 + exception chain: write() wraps JMSException with the queue name 
(not the broker URI,
+     * which may contain credentials) in the error message, and preserves the 
original exception as
+     * the cause.
+     */
+    @Test
+    void writeWrapsJmsExceptionWithQueueInMessage() throws Exception {
+        Connection mockConnection = mock(Connection.class);
+        Session mockSession = mock(Session.class);
+        Queue mockQueue = mock(Queue.class);
+        MessageProducer mockProducer = mock(MessageProducer.class);
+        JMSException sendError = new JMSException("send failed");
+
+        Map<String, Object> config = baseConfig();
+        config.put(ActivemqSinkOptions.URI.key(), TEST_URI_WITH_CREDENTIALS);
+
+        try (MockedConstruction<ActiveMQConnectionFactory> ignored =
+                mockConstruction(
+                        ActiveMQConnectionFactory.class,
+                        (factory, ctx) ->
+                                
when(factory.createConnection()).thenReturn(mockConnection))) {
+            when(mockConnection.createSession(false, Session.AUTO_ACKNOWLEDGE))
+                    .thenReturn(mockSession);
+            when(mockSession.createQueue(TEST_QUEUE)).thenReturn(mockQueue);
+            
when(mockSession.createProducer(mockQueue)).thenReturn(mockProducer);
+            when(mockSession.createTextMessage("hello")).thenThrow(sendError);
+
+            ActivemqClient client = new 
ActivemqClient(ReadonlyConfig.fromMap(config));
+
+            ActivemqConnectorException ex =
+                    assertThrows(
+                            ActivemqConnectorException.class,
+                            () -> 
client.write("hello".getBytes(StandardCharsets.UTF_8)));
+
+            // Issue 2: error message contains queue name, not the broker URI
+            assertTrue(ex.getMessage().contains(TEST_QUEUE));
+            // Issue 2: credentials from the URI must not appear in the message
+            assertFalse(ex.getMessage().contains("secretPass"));
+            assertFalse(ex.getMessage().contains(TEST_URI_WITH_CREDENTIALS));
+            // Exception chain preserved (3-arg constructor)
+            assertEquals(sendError, ex.getCause());
+        }
+    }
+
+    /** L1: close() closes Producer, Session, and Connection in reverse 
creation order. */
+    @Test
+    void closeClosesAllResourcesInReverseOrder() throws Exception {
+        Connection mockConnection = mock(Connection.class);
+        Session mockSession = mock(Session.class);
+        Queue mockQueue = mock(Queue.class);
+        MessageProducer mockProducer = mock(MessageProducer.class);
+
+        try (MockedConstruction<ActiveMQConnectionFactory> ignored =
+                mockConstruction(
+                        ActiveMQConnectionFactory.class,
+                        (factory, ctx) ->
+                                
when(factory.createConnection()).thenReturn(mockConnection))) {
+            when(mockConnection.createSession(false, Session.AUTO_ACKNOWLEDGE))
+                    .thenReturn(mockSession);
+            when(mockSession.createQueue(TEST_QUEUE)).thenReturn(mockQueue);
+            
when(mockSession.createProducer(mockQueue)).thenReturn(mockProducer);
+
+            ActivemqClient client = new 
ActivemqClient(ReadonlyConfig.fromMap(baseConfig()));
+            client.close();
+
+            InOrder order = inOrder(mockProducer, mockSession, mockConnection);
+            order.verify(mockProducer).close();
+            order.verify(mockSession).close();
+            order.verify(mockConnection).close();
+        }
+    }
+
+    /**
+     * L3: close() preserves the first JMSException as the cause and still 
closes all remaining
+     * resources (session and connection) even when an earlier close fails.
+     */
+    @Test
+    void closePreservesFirstExceptionAndClosesRemainingResources() throws 
Exception {
+        Connection mockConnection = mock(Connection.class);
+        Session mockSession = mock(Session.class);
+        Queue mockQueue = mock(Queue.class);
+        MessageProducer mockProducer = mock(MessageProducer.class);
+        JMSException producerError = new JMSException("producer close failed");
+        JMSException sessionError = new JMSException("session close failed");
+
+        try (MockedConstruction<ActiveMQConnectionFactory> ignored =
+                mockConstruction(
+                        ActiveMQConnectionFactory.class,
+                        (factory, ctx) ->
+                                
when(factory.createConnection()).thenReturn(mockConnection))) {
+            when(mockConnection.createSession(false, Session.AUTO_ACKNOWLEDGE))
+                    .thenReturn(mockSession);
+            when(mockSession.createQueue(TEST_QUEUE)).thenReturn(mockQueue);
+            
when(mockSession.createProducer(mockQueue)).thenReturn(mockProducer);
+            doThrow(producerError).when(mockProducer).close();
+            doThrow(sessionError).when(mockSession).close();
+
+            ActivemqClient client = new 
ActivemqClient(ReadonlyConfig.fromMap(baseConfig()));
+
+            ActivemqConnectorException ex =
+                    assertThrows(ActivemqConnectorException.class, () -> 
client.close());
+
+            // L3: first error (producer) is preserved as the cause
+            assertEquals(producerError, ex.getCause());
+            // All resources still closed despite errors
+            verify(mockProducer).close();
+            verify(mockSession).close();
+            verify(mockConnection).close();
+        }
+    }
+
+    /**
+     * L3: close() wraps a connection-only close failure with the original 
JMSException as the
+     * cause.
+     */
+    @Test
+    void closeWithConnectionErrorOnlyWrapsCause() throws Exception {
+        Connection mockConnection = mock(Connection.class);
+        Session mockSession = mock(Session.class);
+        Queue mockQueue = mock(Queue.class);
+        MessageProducer mockProducer = mock(MessageProducer.class);
+        JMSException connError = new JMSException("connection close failed");
+
+        try (MockedConstruction<ActiveMQConnectionFactory> ignored =
+                mockConstruction(
+                        ActiveMQConnectionFactory.class,
+                        (factory, ctx) ->
+                                
when(factory.createConnection()).thenReturn(mockConnection))) {
+            when(mockConnection.createSession(false, Session.AUTO_ACKNOWLEDGE))
+                    .thenReturn(mockSession);
+            when(mockSession.createQueue(TEST_QUEUE)).thenReturn(mockQueue);
+            
when(mockSession.createProducer(mockQueue)).thenReturn(mockProducer);
+            doThrow(connError).when(mockConnection).close();
+
+            ActivemqClient client = new 
ActivemqClient(ReadonlyConfig.fromMap(baseConfig()));
+
+            ActivemqConnectorException ex =
+                    assertThrows(ActivemqConnectorException.class, () -> 
client.close());
+
+            // L3: connection close error is preserved as the cause
+            assertEquals(connError, ex.getCause());
+            verify(mockProducer).close();
+            verify(mockSession).close();
+            verify(mockConnection).close();
+        }
+    }
+
+    /**
+     * When factory-level options are not explicitly set, their setters must 
NOT be called so that
+     * {@code jms.*} parameters embedded in the broker URI are preserved. 
Producer-level options
+     * (delivery_mode, time_to_live, priority) always apply their JMS defaults.
+     */
+    @Test
+    void doesNotCallFactorySettersWhenOptionsNotSet() throws Exception {
+        Connection mockConnection = mock(Connection.class);
+        Session mockSession = mock(Session.class);
+        Queue mockQueue = mock(Queue.class);
+        MessageProducer mockProducer = mock(MessageProducer.class);
+
+        try (MockedConstruction<ActiveMQConnectionFactory> ignored =
+                mockConstruction(
+                        ActiveMQConnectionFactory.class,
+                        (factory, ctx) ->
+                                
when(factory.createConnection()).thenReturn(mockConnection))) {
+            when(mockConnection.createSession(false, Session.AUTO_ACKNOWLEDGE))
+                    .thenReturn(mockSession);
+            when(mockSession.createQueue(TEST_QUEUE)).thenReturn(mockQueue);
+            
when(mockSession.createProducer(mockQueue)).thenReturn(mockProducer);
+
+            new ActivemqClient(ReadonlyConfig.fromMap(baseConfig()));
+
+            ActiveMQConnectionFactory factory = ignored.constructed().get(0);
+            // Issue 1: factory setters must NOT be called when options use 
defaults
+            verify(factory, times(0)).setMaxThreadPoolSize(anyInt());
+            verify(factory, times(0)).setSendTimeout(anyInt());
+            verify(factory, times(0)).setUseCompression(anyBoolean());
+            verify(factory, times(0)).setConnectResponseTimeout(anyInt());
+            verify(factory, times(0)).setProducerWindowSize(anyInt());
+            verify(factory, times(0)).setUseAsyncSend(anyBoolean());
+            // Producer defaults are always applied (they default to JMS 
defaults)
+            verify(mockProducer).setDeliveryMode(2);
+            verify(mockProducer).setTimeToLive(0L);
+            verify(mockProducer).setPriority(4);
+        }
+    }
+
+    /**
+     * When the broker URI contains {@code jms.*} query parameters and the 
user does not set the
+     * corresponding SeaTunnel options, the factory setters must NOT be called 
so that the URI
+     * parameters are preserved. This test uses {@code mockConstruction} to 
verify that the
+     * default-value setters are never invoked; it does not exercise a real 
factory.
+     */
+    @Test
+    void preservesUriJmsParamsWhenOptionsNotSet() throws Exception {
+        String uriWithJmsParams =
+                
"tcp://localhost:61616?jms.useAsyncSend=true&jms.producerWindowSize=1048576";
+        Connection mockConnection = mock(Connection.class);
+        Session mockSession = mock(Session.class);
+        Queue mockQueue = mock(Queue.class);
+        MessageProducer mockProducer = mock(MessageProducer.class);
+
+        Map<String, Object> config = baseConfig();
+        config.put(ActivemqSinkOptions.URI.key(), uriWithJmsParams);
+
+        try (MockedConstruction<ActiveMQConnectionFactory> ignored =
+                mockConstruction(
+                        ActiveMQConnectionFactory.class,
+                        (factory, ctx) ->
+                                
when(factory.createConnection()).thenReturn(mockConnection))) {
+            when(mockConnection.createSession(false, Session.AUTO_ACKNOWLEDGE))
+                    .thenReturn(mockSession);
+            when(mockSession.createQueue(TEST_QUEUE)).thenReturn(mockQueue);
+            
when(mockSession.createProducer(mockQueue)).thenReturn(mockProducer);
+
+            new ActivemqClient(ReadonlyConfig.fromMap(config));
+
+            ActiveMQConnectionFactory factory = ignored.constructed().get(0);
+            // Issue 1: URI jms.* params must not be overwritten by SeaTunnel 
defaults
+            verify(factory, times(0)).setUseAsyncSend(false);
+            verify(factory, times(0)).setProducerWindowSize(0);
+        }
+    }
+
+    /**
+     * Verifies that explicitly configured properties are applied to the 
connection factory and the
+     * producer during construction.
+     */
+    @Test
+    void appliesOptionalProperties() throws Exception {
+        Connection mockConnection = mock(Connection.class);
+        Session mockSession = mock(Session.class);
+        Queue mockQueue = mock(Queue.class);
+        MessageProducer mockProducer = mock(MessageProducer.class);
+
+        Map<String, Object> config = baseConfig();
+        config.put(ActivemqSinkOptions.MAX_THREAD_POOL_SIZE.key(), 100);
+        config.put(ActivemqSinkOptions.SEND_TIMEOUT.key(), 5000);
+        config.put(ActivemqSinkOptions.USE_COMPRESSION.key(), true);
+        config.put(ActivemqSinkOptions.CONNECT_RESPONSE_TIMEOUT.key(), 5000);
+        config.put(ActivemqSinkOptions.PRODUCER_WINDOW_SIZE.key(), 1048576);
+        config.put(ActivemqSinkOptions.USE_ASYNC_SEND.key(), true);
+        config.put(ActivemqSinkOptions.DELIVERY_MODE.key(), 1);
+        config.put(ActivemqSinkOptions.TIME_TO_LIVE.key(), 60000L);
+        config.put(ActivemqSinkOptions.PRIORITY.key(), 9);
+
+        try (MockedConstruction<ActiveMQConnectionFactory> ignored =
+                mockConstruction(
+                        ActiveMQConnectionFactory.class,
+                        (factory, ctx) ->
+                                
when(factory.createConnection()).thenReturn(mockConnection))) {
+            when(mockConnection.createSession(false, Session.AUTO_ACKNOWLEDGE))
+                    .thenReturn(mockSession);
+            when(mockSession.createQueue(TEST_QUEUE)).thenReturn(mockQueue);
+            
when(mockSession.createProducer(mockQueue)).thenReturn(mockProducer);
+
+            new ActivemqClient(ReadonlyConfig.fromMap(config));
+
+            ActiveMQConnectionFactory factory = ignored.constructed().get(0);
+            // ConnectionFactory explicit values
+            verify(factory).setMaxThreadPoolSize(100);
+            verify(factory).setSendTimeout(5000);
+            verify(factory).setUseCompression(true);
+            verify(factory).setConnectResponseTimeout(5000);
+            verify(factory).setProducerWindowSize(1048576);
+            verify(factory).setUseAsyncSend(true);
+            // Producer explicit values
+            verify(mockProducer).setDeliveryMode(1);
+            verify(mockProducer).setTimeToLive(60000L);
+            verify(mockProducer).setPriority(9);
+        }
+    }
+
+    /**
+     * P1: When the constructor fails partway through, partially-created 
resources (e.g. connection)
+     * are cleaned up via close() to avoid leaks, and the original 
construction exception is
+     * propagated as the cause.
+     */
+    @Test
+    void constructorFailureCleansUpPartiallyCreatedResources() throws 
Exception {
+        Connection mockConnection = mock(Connection.class);
+        JMSException constructionError = new JMSException("createSession 
failed");
+
+        try (MockedConstruction<ActiveMQConnectionFactory> ignored =
+                mockConstruction(
+                        ActiveMQConnectionFactory.class,
+                        (factory, ctx) ->
+                                
when(factory.createConnection()).thenReturn(mockConnection))) {
+            // connection.start() succeeds, then createSession fails
+            when(mockConnection.createSession(false, Session.AUTO_ACKNOWLEDGE))
+                    .thenThrow(constructionError);
+
+            ActivemqConnectorException ex =
+                    assertThrows(
+                            ActivemqConnectorException.class,
+                            () -> new 
ActivemqClient(ReadonlyConfig.fromMap(baseConfig())));
+
+            // Original construction error is preserved as the cause
+            assertEquals(constructionError, ex.getCause());
+            // P1: connection was cleaned up despite construction failure
+            verify(mockConnection).close();
+        }
+    }
+
+    /**
+     * Issue 5: When the constructor fails and best-effort close() also 
throws, the close error is
+     * attached as a suppressed exception on the original cause instead of 
being swallowed.
+     */
+    @Test
+    void constructorFailurePreservesCloseErrorAsSuppressed() throws Exception {
+        Connection mockConnection = mock(Connection.class);
+        JMSException constructionError = new JMSException("createSession 
failed");
+        JMSException closeError = new JMSException("connection close failed");
+
+        try (MockedConstruction<ActiveMQConnectionFactory> ignored =
+                mockConstruction(
+                        ActiveMQConnectionFactory.class,
+                        (factory, ctx) ->
+                                
when(factory.createConnection()).thenReturn(mockConnection))) {
+            when(mockConnection.createSession(false, Session.AUTO_ACKNOWLEDGE))
+                    .thenThrow(constructionError);
+            doThrow(closeError).when(mockConnection).close();
+
+            ActivemqConnectorException ex =
+                    assertThrows(
+                            ActivemqConnectorException.class,
+                            () -> new 
ActivemqClient(ReadonlyConfig.fromMap(baseConfig())));
+
+            // Original construction error is preserved as the cause
+            assertEquals(constructionError, ex.getCause());
+            // Issue 5: close() wraps JMSException in 
ActivemqConnectorException, which is then
+            // attached as a suppressed exception instead of being swallowed
+            assertEquals(1, ex.getCause().getSuppressed().length);
+            assertTrue(ex.getCause().getSuppressed()[0] instanceof 
ActivemqConnectorException);
+        }
+    }
+
+    /**
+     * Issue 3: delivery_mode must be 1 (NON_PERSISTENT) or 2 (PERSISTENT). An 
invalid value should
+     * fail fast with a clear message instead of surfacing as a generic 
construction error.
+     */
+    @Test
+    void constructorRejectsInvalidDeliveryMode() {
+        Map<String, Object> config = baseConfig();
+        config.put(ActivemqSinkOptions.DELIVERY_MODE.key(), 5);
+
+        ActivemqConnectorException ex =
+                assertThrows(
+                        ActivemqConnectorException.class,
+                        () -> new 
ActivemqClient(ReadonlyConfig.fromMap(config)));
+
+        assertTrue(ex.getCause() instanceof IllegalArgumentException);
+        assertTrue(ex.getCause().getMessage().contains("delivery_mode"));
+    }
+
+    /**
+     * Issue 3: priority must be between 0 and 9. An out-of-range value should 
fail fast with a
+     * clear message.
+     */
+    @Test
+    void constructorRejectsInvalidPriority() {
+        Map<String, Object> config = baseConfig();
+        config.put(ActivemqSinkOptions.PRIORITY.key(), 15);
+
+        ActivemqConnectorException ex =
+                assertThrows(
+                        ActivemqConnectorException.class,
+                        () -> new 
ActivemqClient(ReadonlyConfig.fromMap(config)));
+
+        assertTrue(ex.getCause() instanceof IllegalArgumentException);
+        assertTrue(ex.getCause().getMessage().contains("priority"));
+    }
+}

Reply via email to