Skip to content

Commit fa7da2b

Browse files
committed
feat(engine): make fallback task poll interval configurable (default 20s)
The fallback task-polling interval (used when an SSE stream ends before a terminal state) was hardcoded at 250ms, causing ~1000 getTask polls and log spam during a 138-second task execution observed in field logs. Add taskPollIntervalMillis to WorkflowEngineClientConfig (default 20000ms = 20 seconds). Polling only triggers when a streaming response is interrupted before the task reaches a terminal state (e.g. a gateway closes the SSE channel mid-stream); while SSE is alive (heartbeats still arriving), polling does not start. Tests that exercise the polling path use 100ms for fast feedback. Signed-off-by: Zhoujie628 <zhoujie628@users.noreply.github.com>
1 parent 714da3c commit fa7da2b

7 files changed

Lines changed: 123 additions & 30 deletions

File tree

‎docs/en/API_REFERENCE.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ Builder-based configuration for the workflow engine client.
150150
| `crlPath` | `String` | null | X.509 CRL for HTTP/JSON-RPC; the default gRPC runtime rejects this unsupported combination |
151151
| `sendTimeoutSeconds` | `long` | `600` | SSE stream timeout (10 min default) |
152152
| `notificationAckTimeoutSeconds` | `long` | `300` | Wait for the first Notification-T ACK/event (five minutes by default) |
153+
| `taskPollIntervalMillis` | `long` | `20000` | Fallback task polling interval when an SSE stream ends before a terminal state (e.g. a gateway closes the channel mid-stream); 20 seconds by default. While SSE is alive (heartbeats still arriving), polling does not start |
153154
| `sendExecutorCoreSize` | `int` | `4` | Send executor core threads |
154155
| `sendExecutorMaxSize` | `int` | `16` | Send executor maximum threads |
155156
| `sendExecutorQueueCapacity` | `int` | `256` | Bounded send executor queue capacity |

‎docs/zh/API_REFERENCE.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ handle 再开始 I/O,监听器直接收到 handle 和完整 ReceivedMessage。
137137
| `crlPath` | `String` | null | HTTP/JSON-RPC 的 X.509 CRL 路径;默认 gRPC runtime 暂不支持并会拒绝启动 |
138138
| `sendTimeoutSeconds` | `long` | `600` | SSE 流超时(默认 10 分钟) |
139139
| `notificationAckTimeoutSeconds` | `long` | `300` | Notification-T 首个 ACK/事件等待时间(默认 5 分钟) |
140+
| `taskPollIntervalMillis` | `long` | `20000`| SSE 流中途断开(如网关空闲超时掐流)后的兜底任务轮询间隔(默认 20 秒)。SSE 流存活期间(心跳仍在推送时)不会启动轮询 |
140141
| `sendExecutorCoreSize` | `int` | `4` | 发送线程池核心线程数 |
141142
| `sendExecutorMaxSize` | `int` | `16` | 发送线程池最大线程数 |
142143
| `sendExecutorQueueCapacity` | `int` | `256` | 发送线程池有界队列容量 |

‎samples/src/test/java/dev/openan/workflow/engine/examples/server/EmbeddedA2AServerTest.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ void setUp() throws Exception {
171171
null,
172172
WorkflowEngineClientConfig.builder().sslVerify(false).build());
173173

174-
client = new DefaultWorkflowEngineClient(transport);
174+
client = new DefaultWorkflowEngineClient(transport, WorkflowEngineClientConfig.builder().taskPollIntervalMillis(100).build());
175175
client.setControlPoint(new WorkbenchControlPoint(sdkEnvPath));
176176
}
177177

‎workflow-engine/src/main/java/dev/openan/workflow/engine/client/DefaultWorkflowEngineClient.java‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public class DefaultWorkflowEngineClient implements WorkflowEngineClient, AutoCl
5757
private final A2ATransport transport;
5858
private final int maxNegotiationExchanges;
5959
private final boolean closeTransportOnClose;
60+
private final long taskPollIntervalMillis;
6061
private final AtomicBoolean closed = new AtomicBoolean();
6162
private final Set<Invocation> invocations = java.util.concurrent.ConcurrentHashMap.newKeySet();
6263
private final ScheduledExecutorService timeoutScheduler =
@@ -76,15 +77,27 @@ public DefaultWorkflowEngineClient(A2ATransport transport) {
7677

7778
/** Uses the configured resource exchange budget, not an SDK protocol round counter. */
7879
public DefaultWorkflowEngineClient(A2ATransport transport, WorkflowEngineClientConfig config) {
79-
this(transport, config.getMaxNegotiationExchanges(), false);
80+
this(transport, config.getMaxNegotiationExchanges(), false, config.getTaskPollIntervalMillis());
8081
}
8182

8283
private DefaultWorkflowEngineClient(A2ATransport transport, int maxExchanges, boolean owning) {
84+
this(
85+
transport,
86+
maxExchanges,
87+
owning,
88+
WorkflowEngineClientConfig.DEFAULT_TASK_POLL_INTERVAL_MILLIS);
89+
}
90+
91+
private DefaultWorkflowEngineClient(
92+
A2ATransport transport, int maxExchanges, boolean owning, long taskPollIntervalMillis) {
8393
this.transport = Objects.requireNonNull(transport, "transport");
8494
if (maxExchanges < 1)
8595
throw new IllegalArgumentException("Negotiation exchange budget must be positive");
96+
if (taskPollIntervalMillis < 100)
97+
throw new IllegalArgumentException("Task poll interval must be at least 100ms");
8698
this.maxNegotiationExchanges = maxExchanges;
8799
this.closeTransportOnClose = owning;
100+
this.taskPollIntervalMillis = taskPollIntervalMillis;
88101
}
89102

90103
/** Creates a facade owning the supplied transport. */
@@ -95,7 +108,8 @@ public static DefaultWorkflowEngineClient owning(A2ATransport transport) {
95108
/** Creates an owning facade with explicit resource settings. */
96109
public static DefaultWorkflowEngineClient owning(
97110
A2ATransport transport, WorkflowEngineClientConfig config) {
98-
return new DefaultWorkflowEngineClient(transport, config.getMaxNegotiationExchanges(), true);
111+
return new DefaultWorkflowEngineClient(
112+
transport, config.getMaxNegotiationExchanges(), true, config.getTaskPollIntervalMillis());
99113
}
100114

101115
private static ReceivedMessage negotiationResponse(SendMessageResult result) {
@@ -580,7 +594,7 @@ private CompletableFuture<Void> delayBeforeTaskPoll(Invocation invocation) {
580594
pending.completion.complete(null);
581595
}
582596
},
583-
250,
597+
taskPollIntervalMillis,
584598
TimeUnit.MILLISECONDS);
585599
if (pending.completion.isDone()) pending.scheduled.cancel(false);
586600
return pending.completion;

‎workflow-engine/src/main/java/dev/openan/workflow/engine/client/WorkflowEngineClientConfig.java‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ public class WorkflowEngineClientConfig {
3838
/** Default wait for the first Notification-T acknowledgement: five minutes. */
3939
public static final long DEFAULT_NOTIFICATION_ACK_TIMEOUT_SECONDS = 300;
4040

41+
/**
42+
* Default interval for fallback task polling (when an SSE stream ends before a terminal state):
43+
* 20 seconds. This polling only triggers when a streaming response is interrupted before the task
44+
* reaches a terminal state (e.g. a gateway closes the SSE channel mid-stream). While SSE
45+
* is alive (heartbeats still arriving), polling does not start; it only begins after the stream
46+
* returns a non-terminal state. The 20-second interval avoids hammering the remote endpoint
47+
* during long-running agent executions.
48+
*/
49+
public static final long DEFAULT_TASK_POLL_INTERVAL_MILLIS = 20_000;
50+
4151
private final boolean sslVerify;
4252
private final String caCertsPath;
4353
private final String clientCertPath;
@@ -55,6 +65,7 @@ public class WorkflowEngineClientConfig {
5565
private final Map<String, Map<String, Map<String, Object>>> credentialsConfig;
5666
private final int maxNegotiationExchanges;
5767
private final String preferredProtocol;
68+
private final long taskPollIntervalMillis;
5869

5970
private WorkflowEngineClientConfig(Builder b) {
6071
this.sslVerify = b.sslVerify;
@@ -75,6 +86,7 @@ private WorkflowEngineClientConfig(Builder b) {
7586
b.credentialsConfig != null ? copyCredentials(b.credentialsConfig) : null;
7687
this.maxNegotiationExchanges = b.maxNegotiationExchanges;
7788
this.preferredProtocol = b.preferredProtocol;
89+
this.taskPollIntervalMillis = b.taskPollIntervalMillis;
7890
}
7991

8092
private static Map<String, Map<String, Map<String, Object>>> copyCredentials(
@@ -136,6 +148,7 @@ public static class Builder {
136148
private Map<String, Map<String, Map<String, Object>>> credentialsConfig = null;
137149
private int maxNegotiationExchanges = 3;
138150
private String preferredProtocol = null;
151+
private long taskPollIntervalMillis = DEFAULT_TASK_POLL_INTERVAL_MILLIS;
139152

140153
public Builder sslVerify(boolean v) {
141154
this.sslVerify = v;
@@ -223,6 +236,15 @@ public Builder preferredProtocol(String v) {
223236
return this;
224237
}
225238

239+
/**
240+
* Interval (milliseconds) for fallback task polling when the SSE stream ends before a terminal
241+
* state. Defaults to {@value #DEFAULT_TASK_POLL_INTERVAL_MILLIS}ms (20 seconds).
242+
*/
243+
public Builder taskPollIntervalMillis(long v) {
244+
this.taskPollIntervalMillis = v;
245+
return this;
246+
}
247+
226248
public WorkflowEngineClientConfig build() {
227249
if (sendTimeoutSeconds <= 0 || notificationAckTimeoutSeconds <= 0) {
228250
throw new IllegalArgumentException("Timeouts must be positive");

‎workflow-engine/src/test/java/dev/openan/workflow/engine/client/A2ATransportHeaderTest.java‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,8 @@ public ListTasksResult listTasks(
279279
.build();
280280

281281
try (A2ATransport transport = new A2ATransport(List.of(card), runtime, config)) {
282-
new DefaultWorkflowEngineClient(transport)
282+
new DefaultWorkflowEngineClient(
283+
transport, WorkflowEngineClientConfig.builder().taskPollIntervalMillis(100).build())
283284
.listTasks(card.name(), ListTasksParams.builder().pageSize(10).build())
284285
.join();
285286
}
@@ -353,7 +354,9 @@ void borrowedClientDoesNotCloseCallerOwnedTransport() throws Exception {
353354
new CountingRuntime(closeCalls),
354355
WorkflowEngineClientConfig.builder().build());
355356

356-
new DefaultWorkflowEngineClient(transport).close();
357+
new DefaultWorkflowEngineClient(
358+
transport, WorkflowEngineClientConfig.builder().taskPollIntervalMillis(100).build())
359+
.close();
357360
assertEquals(0, closeCalls.get());
358361

359362
transport.close();
@@ -393,7 +396,10 @@ void finalStructuredContentNeedsNoContentSdk() throws Exception {
393396
List.of(new org.a2aproject.sdk.spec.DataPart(Map.of("any", List.of(1, 2)))),
394397
Map.of("contextId", "business-only"),
395398
java.util.Set.of("urn:optional:custom"));
396-
new DefaultWorkflowEngineClient(transport).sendTask("Test Agent", content).join();
399+
new DefaultWorkflowEngineClient(
400+
transport, WorkflowEngineClientConfig.builder().taskPollIntervalMillis(100).build())
401+
.sendTask("Test Agent", content)
402+
.join();
397403
assertEquals(content.parts(), captured.get().message().parts());
398404
assertEquals(content.metadata(), captured.get().message().metadata());
399405
assertFalse("business-only".equals(captured.get().message().contextId()));

0 commit comments

Comments
 (0)