diff --git a/mateclaw-server/src/main/java/vip/mate/channel/wecom/WeComChannelAdapter.java b/mateclaw-server/src/main/java/vip/mate/channel/wecom/WeComChannelAdapter.java index 1395bfb9..c97e796e 100644 --- a/mateclaw-server/src/main/java/vip/mate/channel/wecom/WeComChannelAdapter.java +++ b/mateclaw-server/src/main/java/vip/mate/channel/wecom/WeComChannelAdapter.java @@ -148,6 +148,19 @@ public class WeComChannelAdapter extends AbstractChannelAdapter { */ private final AtomicBoolean replyQueueAccepting = new AtomicBoolean(false); + /** + * Idle-timeout (ms) for the per-reqId worker's {@code queue.poll}. + * Default 60s in production; tests in the same package may lower + * this to the millisecond range to surface idle-close vs late-offer + * races without waiting a real minute (RFC-32 §3.0 S-3 stress). + * + *
Package-private on purpose — not exposed via getter or
+ * setter; tests assign it directly. Production code never writes
+ * to this field.
+ */
+ @SuppressWarnings("PackageVisibleField")
+ volatile long workerIdleTimeoutMs = 60_000L;
+
/**
* Per-reqId 回复队列状态。
*
@@ -426,7 +439,7 @@ public class WeComChannelAdapter extends AbstractChannelAdapter {
while (running.get() && !Thread.currentThread().isInterrupted()) {
ReplyTask task;
try {
- task = state.queue().poll(60, TimeUnit.SECONDS);
+ task = state.queue().poll(workerIdleTimeoutMs, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break; // fall through to drainStateExceptionally + return
@@ -1643,7 +1656,12 @@ public class WeComChannelAdapter extends AbstractChannelAdapter {
/**
* 发送 WebSocket 帧(fire and forget)
*/
- private void sendFrame(Map