• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

temporalio / sdk-java / #347

31 Jul 2026 06:59PM UTC coverage: 68.148% (-0.03%) from 68.181%
#347

push

github

web-flow
NEXUS-485: Support Workflow Update as a Nexus Operation (#2945)

* NEXUS-485: Support Workflow Update as a Nexus Operation

* address comments, change signatures to newer

* address comments 2: add all workflow exec overloads

* address comments: log failed

7166 of 12554 branches covered (57.08%)

Branch coverage included in aggregate %.

118 of 296 new or added lines in 11 files covered. (39.86%)

28 existing lines in 7 files now uncovered.

29722 of 41575 relevant lines covered (71.49%)

0.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.57
/temporal-sdk/src/main/java/io/temporal/internal/client/RootWorkflowClientInvoker.java
1
package io.temporal.internal.client;
2

3
import static io.temporal.api.workflowservice.v1.ExecuteMultiOperationResponse.Response.ResponseCase.START_WORKFLOW;
4
import static io.temporal.api.workflowservice.v1.ExecuteMultiOperationResponse.Response.ResponseCase.UPDATE_WORKFLOW;
5
import static io.temporal.internal.common.HeaderUtils.intoPayloadMap;
6
import static io.temporal.internal.common.WorkflowExecutionUtils.makeUserMetaData;
7

8
import com.google.common.collect.Iterators;
9
import io.grpc.Deadline;
10
import io.grpc.Status;
11
import io.grpc.StatusRuntimeException;
12
import io.temporal.api.common.v1.*;
13
import io.temporal.api.enums.v1.UpdateWorkflowExecutionLifecycleStage;
14
import io.temporal.api.enums.v1.WorkflowExecutionStatus;
15
import io.temporal.api.errordetails.v1.MultiOperationExecutionFailure;
16
import io.temporal.api.failure.v1.MultiOperationExecutionAborted;
17
import io.temporal.api.query.v1.WorkflowQuery;
18
import io.temporal.api.sdk.v1.UserMetadata;
19
import io.temporal.api.update.v1.*;
20
import io.temporal.api.workflowservice.v1.*;
21
import io.temporal.client.*;
22
import io.temporal.common.converter.DataConverter;
23
import io.temporal.common.interceptors.WorkflowClientCallsInterceptor;
24
import io.temporal.internal.client.external.GenericWorkflowClient;
25
import io.temporal.internal.common.HeaderUtils;
26
import io.temporal.internal.common.InternalUtils;
27
import io.temporal.internal.nexus.CurrentNexusOperationContext;
28
import io.temporal.internal.nexus.InternalNexusOperationContext;
29
import io.temporal.internal.nexus.NexusOperationMetadata;
30
import io.temporal.internal.nexus.OperationTokenUtil;
31
import io.temporal.internal.worker.WorkerVersioningProtoUtils;
32
import io.temporal.payload.context.WorkflowSerializationContext;
33
import io.temporal.serviceclient.StatusUtils;
34
import io.temporal.worker.WorkflowTaskDispatchHandle;
35
import java.lang.reflect.Type;
36
import java.util.*;
37
import java.util.concurrent.CompletableFuture;
38
import java.util.concurrent.TimeUnit;
39
import java.util.concurrent.TimeoutException;
40
import java.util.stream.StreamSupport;
41
import javax.annotation.Nullable;
42
import org.slf4j.Logger;
43
import org.slf4j.LoggerFactory;
44

45
public class RootWorkflowClientInvoker implements WorkflowClientCallsInterceptor {
46
  private static final Logger log = LoggerFactory.getLogger(RootWorkflowClientInvoker.class);
1✔
47
  private static final long POLL_UPDATE_TIMEOUT_S = 60L;
48

49
  private final GenericWorkflowClient genericClient;
50
  private final WorkflowClientOptions clientOptions;
51
  private final EagerWorkflowTaskDispatcher eagerWorkflowTaskDispatcher;
52
  private final WorkflowClientRequestFactory requestsHelper;
53

54
  public RootWorkflowClientInvoker(
55
      GenericWorkflowClient genericClient,
56
      WorkflowClientOptions clientOptions,
57
      WorkerFactoryRegistry workerFactoryRegistry) {
1✔
58
    this.genericClient = genericClient;
1✔
59
    this.clientOptions = clientOptions;
1✔
60
    this.eagerWorkflowTaskDispatcher = new EagerWorkflowTaskDispatcher(workerFactoryRegistry);
1✔
61
    this.requestsHelper = new WorkflowClientRequestFactory(clientOptions);
1✔
62
  }
1✔
63

64
  @Override
65
  public WorkflowStartOutput start(WorkflowStartInput input) {
66
    DataConverter dataConverterWithWorkflowContext =
1✔
67
        clientOptions
68
            .getDataConverter()
1✔
69
            .withContext(
1✔
70
                new WorkflowSerializationContext(
71
                    clientOptions.getNamespace(), input.getWorkflowId()));
1✔
72

73
    StartWorkflowExecutionRequest.Builder startRequest =
1✔
74
        toStartRequest(dataConverterWithWorkflowContext, input);
1✔
75

76
    try (@Nullable WorkflowTaskDispatchHandle eagerDispatchHandle = obtainDispatchHandle(input)) {
1✔
77
      boolean requestEagerExecution = eagerDispatchHandle != null;
1✔
78
      startRequest.setRequestEagerExecution(requestEagerExecution);
1✔
79
      if (requestEagerExecution && eagerDispatchHandle.getDeploymentOptions() != null) {
1✔
80
        startRequest.setEagerWorkerDeploymentOptions(
1✔
81
            WorkerVersioningProtoUtils.deploymentOptionsToProto(
1✔
82
                eagerDispatchHandle.getDeploymentOptions()));
1✔
83
      }
84
      StartWorkflowExecutionResponse response = genericClient.start(startRequest.build());
1✔
85
      WorkflowExecution execution =
86
          WorkflowExecution.newBuilder()
1✔
87
              .setRunId(response.getRunId())
1✔
88
              .setWorkflowId(startRequest.getWorkflowId())
1✔
89
              .build();
1✔
90
      @Nullable
91
      PollWorkflowTaskQueueResponse eagerWorkflowTask =
92
          requestEagerExecution && response.hasEagerWorkflowTask()
1!
93
              ? response.getEagerWorkflowTask()
1✔
94
              : null;
1✔
95
      if (eagerWorkflowTask != null) {
1✔
96
        try {
97
          eagerDispatchHandle.dispatch(eagerWorkflowTask);
1✔
98
        } catch (Exception e) {
×
99
          // Any exception here is not expected, and it's a bug.
100
          // But we don't allow any exception from the dispatching to disrupt the control flow here,
101
          // the Client needs to get the execution back to matter what.
102
          // Inability to dispatch a WFT creates a latency issue, but it's not a failure of the
103
          // start itself
104
          log.error(
×
105
              "[BUG] Eager Workflow Task was received from the Server, but failed to be dispatched on the local worker",
106
              e);
107
        }
1✔
108
      }
109
      // If this start is being issued from inside a Nexus operation handler, stash only the
110
      // forward operation->workflow link from the start response so NexusStartWorkflowHelper can
111
      // attach it to the WorkflowExecutionStarted event. Unlike signal/signalWithStart, start
112
      // deliberately does NOT add a response link here: the operation->workflow relationship is
113
      // already captured by the forward link, so re-adding response.getLink() as a response link
114
      // would duplicate it on the caller's history event. Do not "restore symmetry" by calling
115
      // addResponseLink here.
116
      if (CurrentNexusOperationContext.isNexusContext()) {
1✔
117
        CurrentNexusOperationContext.get().setStartWorkflowResponseLink(response.getLink());
1✔
118
      }
119
      return new WorkflowStartOutput(execution);
1✔
120
    }
121
  }
122

123
  @Override
124
  public WorkflowSignalOutput signal(WorkflowSignalInput input) {
125
    SignalWorkflowExecutionRequest.Builder request =
126
        SignalWorkflowExecutionRequest.newBuilder()
1✔
127
            .setSignalName(input.getSignalName())
1✔
128
            .setWorkflowExecution(input.getWorkflowExecution())
1✔
129
            .setIdentity(clientOptions.getIdentity())
1✔
130
            .setNamespace(clientOptions.getNamespace())
1✔
131
            .setRequestId(UUID.randomUUID().toString())
1✔
132
            .setHeader(HeaderUtils.toHeaderGrpc(input.getHeader(), null));
1✔
133

134
    // If this signal is being issued from inside a Nexus operation handler, forward the inbound
135
    // Nexus task links so the SignalWorkflowExecution history event links back to the caller.
136
    boolean inNexusContext = CurrentNexusOperationContext.isNexusContext();
1✔
137
    if (inNexusContext) {
1✔
138
      request.addAllLinks(CurrentNexusOperationContext.get().getRequestLinks());
1✔
139
    }
140

141
    DataConverter dataConverterWitSignalContext =
1✔
142
        clientOptions
143
            .getDataConverter()
1✔
144
            .withContext(
1✔
145
                new WorkflowSerializationContext(
146
                    clientOptions.getNamespace(), input.getWorkflowExecution().getWorkflowId()));
1✔
147

148
    Optional<Payloads> inputArgs = dataConverterWitSignalContext.toPayloads(input.getArguments());
1✔
149
    inputArgs.ifPresent(request::setInput);
1✔
150
    SignalWorkflowExecutionResponse response = genericClient.signal(request.build());
1✔
151
    // Server >=1.31 with EnableCHASMSignalBacklinks returns a response link pointing at the signal
152
    // event; older servers leave it unset. Propagate when present.
153
    if (inNexusContext && response.hasLink()) {
1✔
154
      CurrentNexusOperationContext.get().addResponseLink(response.getLink());
1✔
155
    }
156
    return new WorkflowSignalOutput();
1✔
157
  }
158

159
  @Override
160
  public WorkflowSignalWithStartOutput signalWithStart(WorkflowSignalWithStartInput input) {
161
    WorkflowStartInput workflowStartInput = input.getWorkflowStartInput();
1✔
162

163
    DataConverter dataConverterWithWorkflowContext =
1✔
164
        clientOptions
165
            .getDataConverter()
1✔
166
            .withContext(
1✔
167
                new WorkflowSerializationContext(
168
                    clientOptions.getNamespace(), workflowStartInput.getWorkflowId()));
1✔
169
    StartWorkflowExecutionRequestOrBuilder startRequest =
1✔
170
        toStartRequest(dataConverterWithWorkflowContext, workflowStartInput);
1✔
171

172
    Optional<Payloads> signalInput =
1✔
173
        dataConverterWithWorkflowContext.toPayloads(input.getSignalArguments());
1✔
174
    SignalWithStartWorkflowExecutionRequest.Builder requestBuilder =
1✔
175
        requestsHelper.newSignalWithStartWorkflowExecutionRequest(
1✔
176
            startRequest, input.getSignalName(), signalInput.orElse(null));
1✔
177
    // If this signalWithStart is being issued from inside a Nexus operation handler, forward
178
    // the inbound Nexus task links so both the WorkflowExecutionStarted and
179
    // WorkflowExecutionSignaled events on the callee link back to the caller.
180
    boolean inNexusContext = CurrentNexusOperationContext.isNexusContext();
1✔
181
    if (inNexusContext) {
1✔
182
      requestBuilder.addAllLinks(CurrentNexusOperationContext.get().getRequestLinks());
1✔
183
    }
184
    SignalWithStartWorkflowExecutionRequest request = requestBuilder.build();
1✔
185
    SignalWithStartWorkflowExecutionResponse response = genericClient.signalWithStart(request);
1✔
186
    WorkflowExecution execution =
187
        WorkflowExecution.newBuilder()
1✔
188
            .setRunId(response.getRunId())
1✔
189
            .setWorkflowId(request.getWorkflowId())
1✔
190
            .build();
1✔
191
    // Server >=1.31 with EnableCHASMSignalBacklinks returns a response link pointing at the signal
192
    // event; older servers leave it unset. Propagate when present.
193
    if (inNexusContext && response.hasSignalLink()) {
1!
194
      CurrentNexusOperationContext.get().addResponseLink(response.getSignalLink());
1✔
195
    }
196
    // TODO currently SignalWithStartWorkflowExecutionResponse doesn't have eagerWorkflowTask.
197
    //  We should wire it when it's implemented server-side.
198
    return new WorkflowSignalWithStartOutput(new WorkflowStartOutput(execution));
1✔
199
  }
200

201
  @Override
202
  public <R> WorkflowUpdateWithStartOutput<R> updateWithStart(
203
      WorkflowUpdateWithStartInput<R> input) {
204

205
    WorkflowStartInput startInput = input.getWorkflowStartInput();
1✔
206
    DataConverter dataConverterWithWorkflowContext =
1✔
207
        clientOptions
208
            .getDataConverter()
1✔
209
            .withContext(
1✔
210
                new WorkflowSerializationContext(
211
                    clientOptions.getNamespace(), startInput.getWorkflowId()));
1✔
212

213
    ExecuteMultiOperationRequest request =
214
        ExecuteMultiOperationRequest.newBuilder()
1✔
215
            .setNamespace(clientOptions.getNamespace())
1✔
216
            .addOperations(
1✔
217
                0,
218
                ExecuteMultiOperationRequest.Operation.newBuilder()
1✔
219
                    .setStartWorkflow(toStartRequest(dataConverterWithWorkflowContext, startInput))
1✔
220
                    .build())
1✔
221
            .addOperations(
1✔
222
                1,
223
                ExecuteMultiOperationRequest.Operation.newBuilder()
1✔
224
                    .setUpdateWorkflow(
1✔
225
                        toUpdateWorkflowExecutionRequest(
1✔
226
                            input.getStartUpdateInput(), dataConverterWithWorkflowContext)))
1✔
227
            .build();
1✔
228

229
    ExecuteMultiOperationResponse response;
230
    StartWorkflowExecutionResponse startResponse;
231
    UpdateWorkflowExecutionResponse updateResponse;
232

233
    do {
234
      try {
235
        Deadline pollTimeoutDeadline = Deadline.after(POLL_UPDATE_TIMEOUT_S, TimeUnit.SECONDS);
1✔
236
        response = genericClient.executeMultiOperation(request, pollTimeoutDeadline);
1✔
237

238
        if (response.getResponsesCount() != request.getOperationsCount()) {
1!
239
          throw new RuntimeException(
×
240
              "Server sent back an invalid response: received "
241
                  + response.getResponsesCount()
×
242
                  + " instead of "
243
                  + request.getOperationsCount()
×
244
                  + " operation responses");
245
        }
246

247
        ExecuteMultiOperationResponse.Response firstResponse = response.getResponses(0);
1✔
248
        if (firstResponse.getResponseCase() != START_WORKFLOW) {
1!
249
          throw new RuntimeException(
×
250
              "Server sent back an invalid response type for StartWorkflow response");
251
        }
252
        startResponse = firstResponse.getStartWorkflow();
1✔
253

254
        ExecuteMultiOperationResponse.Response secondResponse = response.getResponses(1);
1✔
255
        if (secondResponse.getResponseCase() != UPDATE_WORKFLOW) {
1!
256
          throw new RuntimeException(
×
257
              "Server sent back an invalid response type for UpdateWorkflow response");
258
        }
259
        updateResponse = secondResponse.getUpdateWorkflow();
1✔
260
      } catch (StatusRuntimeException e) {
1✔
261
        if (e.getStatus().getCode() == Status.Code.DEADLINE_EXCEEDED
1!
262
            || e.getStatus().getCode() == Status.Code.CANCELLED) {
1!
263
          throw new WorkflowUpdateTimeoutOrCancelledException(
×
264
              input.getStartUpdateInput().getWorkflowExecution(),
×
265
              input.getStartUpdateInput().getUpdateName(),
×
266
              input.getStartUpdateInput().getUpdateId(),
×
267
              e);
268
        }
269

270
        MultiOperationExecutionFailure failure =
1✔
271
            StatusUtils.getFailure(e, MultiOperationExecutionFailure.class);
1✔
272
        if (failure == null) {
1!
273
          throw e;
×
274
        }
275

276
        if (failure.getStatusesCount() != request.getOperationsCount()) {
1!
277
          throw new RuntimeException(
×
278
              "Server sent back an invalid error response: received "
279
                  + failure.getStatusesCount()
×
280
                  + " instead of "
281
                  + request.getOperationsCount()
×
282
                  + " operation errors");
283
        }
284

285
        MultiOperationExecutionFailure.OperationStatus startStatus = failure.getStatuses(0);
1✔
286
        if (startStatus.getCode() != Status.Code.OK.value()
1✔
287
            && (startStatus.getDetailsCount() == 0
1✔
288
                || !startStatus.getDetails(0).is(MultiOperationExecutionAborted.class))) {
1!
289
          throw Status.fromCodeValue(startStatus.getCode())
1✔
290
              .withDescription(startStatus.getMessage())
1✔
291
              .asRuntimeException();
1✔
292
        }
293

294
        MultiOperationExecutionFailure.OperationStatus updateStatus = failure.getStatuses(1);
1✔
295
        if (updateStatus.getCode() != Status.Code.OK.value()
1!
296
            && (updateStatus.getDetailsCount() == 0
1!
297
                || !updateStatus.getDetails(0).is(MultiOperationExecutionAborted.class))) {
×
298
          throw Status.fromCodeValue(updateStatus.getCode())
1✔
299
              .withDescription(updateStatus.getMessage())
1✔
300
              .asRuntimeException();
1✔
301
        }
302

303
        throw e; // no detailed failure was found
×
304
      }
1✔
305
    } while (updateNotYetDurable(input.getStartUpdateInput(), updateResponse));
1✔
306

307
    WorkflowUpdateHandle updateHandle =
1✔
308
        toUpdateHandle(
1✔
309
            input.getStartUpdateInput(), updateResponse, dataConverterWithWorkflowContext);
1✔
310

311
    WorkflowExecution execution =
312
        WorkflowExecution.newBuilder()
1✔
313
            .setRunId(startResponse.getRunId())
1✔
314
            .setWorkflowId(
1✔
315
                toStartRequest(dataConverterWithWorkflowContext, startInput)
1✔
316
                    .build()
1✔
317
                    .getWorkflowId())
1✔
318
            .build();
1✔
319
    return new WorkflowUpdateWithStartOutput<>(new WorkflowStartOutput(execution), updateHandle);
1✔
320
  }
321

322
  private StartWorkflowExecutionRequest.Builder toStartRequest(
323
      DataConverter dataConverterWithWorkflowContext, WorkflowStartInput workflowStartInput) {
324
    Optional<Payloads> workflowInput =
1✔
325
        dataConverterWithWorkflowContext.toPayloads(workflowStartInput.getArguments());
1✔
326

327
    @Nullable
328
    Memo memo =
329
        (workflowStartInput.getOptions().getMemo() != null)
1✔
330
            ? Memo.newBuilder()
1✔
331
                .putAllFields(
1✔
332
                    intoPayloadMap(
1✔
333
                        dataConverterWithWorkflowContext,
334
                        workflowStartInput.getOptions().getMemo()))
1✔
335
                .build()
1✔
336
            : null;
1✔
337

338
    @Nullable
339
    UserMetadata userMetadata =
1✔
340
        makeUserMetaData(
1✔
341
            workflowStartInput.getOptions().getStaticSummary(),
1✔
342
            workflowStartInput.getOptions().getStaticDetails(),
1✔
343
            dataConverterWithWorkflowContext);
344

345
    return requestsHelper.newStartWorkflowExecutionRequest(
1✔
346
        workflowStartInput.getWorkflowId(),
1✔
347
        workflowStartInput.getWorkflowType(),
1✔
348
        workflowStartInput.getHeader(),
1✔
349
        workflowStartInput.getOptions(),
1✔
350
        workflowInput.orElse(null),
1✔
351
        memo,
352
        userMetadata);
353
  }
354

355
  @Override
356
  public <R> GetResultOutput<R> getResult(GetResultInput<R> input) throws TimeoutException {
357
    DataConverter dataConverterWithWorkflowContext =
1✔
358
        clientOptions
359
            .getDataConverter()
1✔
360
            .withContext(
1✔
361
                new WorkflowSerializationContext(
362
                    clientOptions.getNamespace(), input.getWorkflowExecution().getWorkflowId()));
1✔
363
    Optional<Payloads> resultValue =
1✔
364
        WorkflowClientLongPollHelper.getWorkflowExecutionResult(
1✔
365
            genericClient,
366
            requestsHelper,
367
            input.getWorkflowExecution(),
1✔
368
            input.getWorkflowType(),
1✔
369
            dataConverterWithWorkflowContext,
370
            input.getTimeout(),
1✔
371
            input.getTimeoutUnit());
1✔
372
    return new GetResultOutput<>(
1✔
373
        convertResultPayloads(
1✔
374
            resultValue,
375
            input.getResultClass(),
1✔
376
            input.getResultType(),
1✔
377
            dataConverterWithWorkflowContext));
378
  }
379

380
  @Override
381
  public <R> GetResultAsyncOutput<R> getResultAsync(GetResultInput<R> input) {
382
    DataConverter dataConverterWithWorkflowContext =
1✔
383
        clientOptions
384
            .getDataConverter()
1✔
385
            .withContext(
1✔
386
                new WorkflowSerializationContext(
387
                    clientOptions.getNamespace(), input.getWorkflowExecution().getWorkflowId()));
1✔
388
    CompletableFuture<Optional<Payloads>> resultValue =
1✔
389
        WorkflowClientLongPollAsyncHelper.getWorkflowExecutionResultAsync(
1✔
390
            genericClient,
391
            requestsHelper,
392
            input.getWorkflowExecution(),
1✔
393
            input.getWorkflowType(),
1✔
394
            input.getTimeout(),
1✔
395
            input.getTimeoutUnit(),
1✔
396
            dataConverterWithWorkflowContext);
397
    return new GetResultAsyncOutput<>(
1✔
398
        resultValue.thenApply(
1✔
399
            payloads ->
400
                convertResultPayloads(
1✔
401
                    payloads,
402
                    input.getResultClass(),
1✔
403
                    input.getResultType(),
1✔
404
                    dataConverterWithWorkflowContext)));
405
  }
406

407
  @Override
408
  public <R> QueryOutput<R> query(QueryInput<R> input) {
409
    WorkflowQuery.Builder query =
410
        WorkflowQuery.newBuilder()
1✔
411
            .setQueryType(input.getQueryType())
1✔
412
            .setHeader(HeaderUtils.toHeaderGrpc(input.getHeader(), null));
1✔
413
    DataConverter dataConverterWithWorkflowContext =
1✔
414
        clientOptions
415
            .getDataConverter()
1✔
416
            .withContext(
1✔
417
                new WorkflowSerializationContext(
418
                    clientOptions.getNamespace(), input.getWorkflowExecution().getWorkflowId()));
1✔
419

420
    Optional<Payloads> inputArgs =
1✔
421
        dataConverterWithWorkflowContext.toPayloads(input.getArguments());
1✔
422
    inputArgs.ifPresent(query::setQueryArgs);
1✔
423
    QueryWorkflowRequest request =
424
        QueryWorkflowRequest.newBuilder()
1✔
425
            .setNamespace(clientOptions.getNamespace())
1✔
426
            .setExecution(
1✔
427
                WorkflowExecution.newBuilder()
1✔
428
                    .setWorkflowId(input.getWorkflowExecution().getWorkflowId())
1✔
429
                    .setRunId(input.getWorkflowExecution().getRunId()))
1✔
430
            .setQuery(query)
1✔
431
            .setQueryRejectCondition(clientOptions.getQueryRejectCondition())
1✔
432
            .build();
1✔
433

434
    QueryWorkflowResponse result;
435
    result = genericClient.query(request);
1✔
436

437
    boolean queryRejected = result.hasQueryRejected();
1✔
438
    WorkflowExecutionStatus rejectStatus =
439
        queryRejected ? result.getQueryRejected().getStatus() : null;
1✔
440
    Optional<Payloads> queryResult =
441
        result.hasQueryResult() ? Optional.of(result.getQueryResult()) : Optional.empty();
1✔
442
    R resultValue =
1✔
443
        convertResultPayloads(
1✔
444
            queryResult,
445
            input.getResultClass(),
1✔
446
            input.getResultType(),
1✔
447
            dataConverterWithWorkflowContext);
448
    return new QueryOutput<>(rejectStatus, resultValue);
1✔
449
  }
450

451
  @Override
452
  public <R> WorkflowUpdateHandle<R> startUpdate(StartUpdateInput<R> input) {
453
    DataConverter dataConverterWithWorkflowContext =
1✔
454
        clientOptions
455
            .getDataConverter()
1✔
456
            .withContext(
1✔
457
                new WorkflowSerializationContext(
458
                    clientOptions.getNamespace(), input.getWorkflowExecution().getWorkflowId()));
1✔
459

460
    UpdateWorkflowExecutionRequest updateRequest =
1✔
461
        toUpdateWorkflowExecutionRequest(input, dataConverterWithWorkflowContext);
1✔
462

463
    // Re-attempt the update until it is at least accepted, or passes the lifecycle stage specified
464
    // by the user.
465
    UpdateWorkflowExecutionResponse result;
466
    do {
467
      Deadline pollTimeoutDeadline = Deadline.after(POLL_UPDATE_TIMEOUT_S, TimeUnit.SECONDS);
1✔
468
      try {
469
        result = genericClient.update(updateRequest, pollTimeoutDeadline);
1✔
470
      } catch (StatusRuntimeException e) {
1✔
471
        if (e.getStatus().getCode() == Status.Code.DEADLINE_EXCEEDED
1✔
472
            || e.getStatus().getCode() == Status.Code.CANCELLED) {
1!
473
          throw new WorkflowUpdateTimeoutOrCancelledException(
1✔
474
              input.getWorkflowExecution(), input.getUpdateName(), input.getUpdateId(), e);
1✔
475
        }
476
        throw e;
1✔
477
      }
1✔
478
    } while (updateNotYetDurable(input, result));
1!
479

480
    // If triggered by a Nexus Operation, set necessary fields- link, result
481
    if (CurrentNexusOperationContext.isNexusContext()) {
1✔
482
      NexusOperationMetadata nexusOperationMetadata =
483
          CurrentNexusOperationContext.get().getNexusOperationMetadata();
1✔
484
      if (nexusOperationMetadata != null) {
1✔
485
        if (result.hasLink()) {
1!
486
          // add forward links for caller->handler
NEW
487
          CurrentNexusOperationContext.get().addResponseLink(result.getLink());
×
488
        }
489
        nexusOperationMetadata.operationCompleted = result.hasOutcome();
1✔
490
      }
491
    }
492

493
    return toUpdateHandle(input, result, dataConverterWithWorkflowContext);
1✔
494
  }
495

496
  private <R> boolean updateNotYetDurable(
497
      StartUpdateInput<R> input, UpdateWorkflowExecutionResponse result) {
498
    return result.getStage().getNumber() < input.getWaitPolicy().getLifecycleStage().getNumber()
1✔
499
        && result.getStage().getNumber()
1✔
500
            < UpdateWorkflowExecutionLifecycleStage
501
                .UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_ACCEPTED
502
                .getNumber();
1✔
503
  }
504

505
  private <R> UpdateWorkflowExecutionRequest toUpdateWorkflowExecutionRequest(
506
      StartUpdateInput<R> input, DataConverter dataConverterWithWorkflowContext) {
507
    Optional<Payloads> inputArgs =
1✔
508
        dataConverterWithWorkflowContext.toPayloads(input.getArguments());
1✔
509
    Input.Builder updateInput =
510
        Input.newBuilder()
1✔
511
            .setHeader(HeaderUtils.toHeaderGrpc(input.getHeader(), null))
1✔
512
            .setName(input.getUpdateName());
1✔
513
    inputArgs.ifPresent(updateInput::setArgs);
1✔
514

515
    Request.Builder requestBuilder =
516
        Request.newBuilder()
1✔
517
            .setMeta(
1✔
518
                Meta.newBuilder()
1✔
519
                    .setUpdateId(input.getUpdateId())
1✔
520
                    .setIdentity(clientOptions.getIdentity()))
1✔
521
            .setInput(updateInput);
1✔
522

523
    // If this update is being issued via TemporalNexusClientImpl.startWorkflowUpdate,
524
    // set the fields the server needs to deliver the Nexus completion callback
525
    if (CurrentNexusOperationContext.isNexusContext()) {
1✔
526
      InternalNexusOperationContext nexusContext = CurrentNexusOperationContext.get();
1✔
527
      // already in a Nexus operation context, dont need to check nexusContext again
528
      NexusOperationMetadata nexusOperationMetadata = nexusContext.getNexusOperationMetadata();
1✔
529
      if (nexusOperationMetadata != null) {
1✔
530
        try {
531
          nexusOperationMetadata.operationToken =
1✔
532
              OperationTokenUtil.generateWorkflowUpdateOperationToken(
1✔
533
                  clientOptions.getNamespace(),
1✔
534
                  input.getWorkflowExecution().getWorkflowId(),
1✔
535
                  input.getWorkflowExecution().getRunId(),
1✔
536
                  input.getUpdateId());
1✔
NEW
537
        } catch (Exception e) {
×
NEW
538
          throw new IllegalStateException("failed to generate update operation token", e);
×
539
        }
1✔
540
        List<Link> requestLinks = nexusContext.getRequestLinks();
1✔
541
        requestBuilder
1✔
542
            .setRequestId(nexusOperationMetadata.requestId)
1✔
543
            .addCompletionCallbacks(
1✔
544
                InternalUtils.buildNexusCallback(
1✔
545
                    nexusOperationMetadata.callbackHeaders,
546
                    nexusOperationMetadata.callbackUrl,
547
                    nexusOperationMetadata.operationToken,
548
                    requestLinks))
549
            .addAllLinks(requestLinks);
1✔
550
      }
551
      // If no NexusOperationMetadata was found, but there is a NexusContext, then the
552
      // update was likely trigger via Operation handler directly
553
    }
554

555
    Request request = requestBuilder.build();
1✔
556

557
    return UpdateWorkflowExecutionRequest.newBuilder()
1✔
558
        .setNamespace(clientOptions.getNamespace())
1✔
559
        .setWaitPolicy(input.getWaitPolicy())
1✔
560
        .setWorkflowExecution(
1✔
561
            WorkflowExecution.newBuilder()
1✔
562
                .setWorkflowId(input.getWorkflowExecution().getWorkflowId())
1✔
563
                .setRunId(input.getWorkflowExecution().getRunId()))
1✔
564
        .setFirstExecutionRunId(input.getFirstExecutionRunId())
1✔
565
        .setRequest(request)
1✔
566
        .build();
1✔
567
  }
568

569
  private <R> WorkflowUpdateHandle<R> toUpdateHandle(
570
      StartUpdateInput<R> input,
571
      UpdateWorkflowExecutionResponse result,
572
      DataConverter dataConverterWithWorkflowContext) {
573
    if (result.hasOutcome()) {
1✔
574
      switch (result.getOutcome().getValueCase()) {
1!
575
        case SUCCESS:
576
          Optional<Payloads> updateResult = Optional.of(result.getOutcome().getSuccess());
1✔
577
          R resultValue =
1✔
578
              convertResultPayloads(
1✔
579
                  updateResult,
580
                  input.getResultClass(),
1✔
581
                  input.getResultType(),
1✔
582
                  dataConverterWithWorkflowContext);
583
          return new CompletedWorkflowUpdateHandleImpl<>(
1✔
584
              result.getUpdateRef().getUpdateId(),
1✔
585
              result.getUpdateRef().getWorkflowExecution(),
1✔
586
              resultValue);
587
        case FAILURE:
588
          return new CompletedWorkflowUpdateHandleImpl<>(
1✔
589
              result.getUpdateRef().getUpdateId(),
1✔
590
              result.getUpdateRef().getWorkflowExecution(),
1✔
591
              new WorkflowUpdateException(
592
                  result.getUpdateRef().getWorkflowExecution(),
1✔
593
                  result.getUpdateRef().getUpdateId(),
1✔
594
                  input.getUpdateName(),
1✔
595
                  dataConverterWithWorkflowContext.failureToException(
1✔
596
                      result.getOutcome().getFailure())));
1✔
597
        default:
598
          throw new RuntimeException(
×
599
              "Received unexpected outcome from update request: "
600
                  + result.getOutcome().getValueCase());
×
601
      }
602
    } else {
603
      LazyWorkflowUpdateHandleImpl<R> handle =
1✔
604
          new LazyWorkflowUpdateHandleImpl<>(
605
              this,
606
              input.getWorkflowType().orElse(null),
1✔
607
              input.getUpdateName(),
1✔
608
              result.getUpdateRef().getUpdateId(),
1✔
609
              result.getUpdateRef().getWorkflowExecution(),
1✔
610
              input.getResultClass(),
1✔
611
              input.getResultType());
1✔
612
      UpdateWorkflowExecutionLifecycleStage waitForStage =
1✔
613
          input.getWaitPolicy().getLifecycleStage();
1✔
614
      if (waitForStage == WorkflowUpdateStage.COMPLETED.getProto()) {
1✔
615
        // Don't return the handle until completed, since that's what's been asked for
616
        handle.waitCompleted();
1✔
617
      }
618
      return handle;
1✔
619
    }
620
  }
621

622
  @Override
623
  public <R> PollWorkflowUpdateOutput<R> pollWorkflowUpdate(PollWorkflowUpdateInput<R> input) {
624
    DataConverter dataConverterWithWorkflowContext =
1✔
625
        clientOptions
626
            .getDataConverter()
1✔
627
            .withContext(
1✔
628
                new WorkflowSerializationContext(
629
                    clientOptions.getNamespace(), input.getWorkflowExecution().getWorkflowId()));
1✔
630

631
    UpdateRef update =
632
        UpdateRef.newBuilder()
1✔
633
            .setWorkflowExecution(input.getWorkflowExecution())
1✔
634
            .setUpdateId(input.getUpdateId())
1✔
635
            .build();
1✔
636

637
    WaitPolicy waitPolicy =
638
        WaitPolicy.newBuilder()
1✔
639
            .setLifecycleStage(
1✔
640
                UpdateWorkflowExecutionLifecycleStage
641
                    .UPDATE_WORKFLOW_EXECUTION_LIFECYCLE_STAGE_COMPLETED)
642
            .build();
1✔
643

644
    PollWorkflowExecutionUpdateRequest pollUpdateRequest =
645
        PollWorkflowExecutionUpdateRequest.newBuilder()
1✔
646
            .setNamespace(clientOptions.getNamespace())
1✔
647
            .setIdentity(clientOptions.getIdentity())
1✔
648
            .setUpdateRef(update)
1✔
649
            .setWaitPolicy(waitPolicy)
1✔
650
            .build();
1✔
651

652
    CompletableFuture<PollWorkflowExecutionUpdateResponse> future = new CompletableFuture<>();
1✔
653

654
    Deadline pollTimeoutDeadline = Deadline.after(input.getTimeout(), input.getTimeoutUnit());
1✔
655
    pollWorkflowUpdateHelper(future, pollUpdateRequest, pollTimeoutDeadline);
1✔
656
    return new PollWorkflowUpdateOutput<>(
1✔
657
        future.thenApply(
1✔
658
            (result) -> {
659
              if (result.hasOutcome()) {
1!
660
                switch (result.getOutcome().getValueCase()) {
1!
661
                  case SUCCESS:
662
                    Optional<Payloads> updateResult = Optional.of(result.getOutcome().getSuccess());
1✔
663
                    return convertResultPayloads(
1✔
664
                        updateResult,
665
                        input.getResultClass(),
1✔
666
                        input.getResultType(),
1✔
667
                        dataConverterWithWorkflowContext);
668
                  case FAILURE:
669
                    throw new WorkflowUpdateException(
1✔
670
                        input.getWorkflowExecution(),
1✔
671
                        input.getUpdateId(),
1✔
672
                        input.getUpdateName(),
1✔
673
                        dataConverterWithWorkflowContext.failureToException(
1✔
674
                            result.getOutcome().getFailure()));
1✔
675
                  default:
676
                    throw new RuntimeException(
×
677
                        "Received unexpected outcome from poll update request: "
678
                            + result.getOutcome().getValueCase());
×
679
                }
680
              }
681
              throw new RuntimeException("Received no outcome from server");
×
682
            }));
683
  }
684

685
  private void pollWorkflowUpdateHelper(
686
      CompletableFuture<PollWorkflowExecutionUpdateResponse> resultCF,
687
      PollWorkflowExecutionUpdateRequest request,
688
      Deadline deadline) {
689
    genericClient
1✔
690
        .pollUpdateAsync(request, deadline)
1✔
691
        .whenComplete(
1✔
692
            (r, e) -> {
693
              if (e == null && !r.hasOutcome()) {
1✔
694
                pollWorkflowUpdateHelper(resultCF, request, deadline);
1✔
695
                return;
1✔
696
              }
697
              if ((e instanceof StatusRuntimeException
1✔
698
                      && (((StatusRuntimeException) e).getStatus().getCode()
1✔
699
                              == Status.Code.DEADLINE_EXCEEDED
700
                          || ((StatusRuntimeException) e).getStatus().getCode()
1!
701
                              == Status.Code.CANCELLED))
702
                  || deadline.isExpired()) {
1!
703
                resultCF.completeExceptionally(
1✔
704
                    new WorkflowUpdateTimeoutOrCancelledException(
705
                        request.getUpdateRef().getWorkflowExecution(),
1✔
706
                        request.getUpdateRef().getUpdateId(),
1✔
707
                        "",
708
                        e));
709
              } else if (e != null) {
1✔
710
                resultCF.completeExceptionally(e);
1✔
711
              } else {
712
                resultCF.complete(r);
1✔
713
              }
714
            });
1✔
715
  }
1✔
716

717
  @Override
718
  public CancelOutput cancel(CancelInput input) {
719
    RequestCancelWorkflowExecutionRequest.Builder request =
720
        RequestCancelWorkflowExecutionRequest.newBuilder()
1✔
721
            .setRequestId(UUID.randomUUID().toString())
1✔
722
            .setWorkflowExecution(input.getWorkflowExecution())
1✔
723
            .setNamespace(clientOptions.getNamespace())
1✔
724
            .setIdentity(clientOptions.getIdentity());
1✔
725
    if (input.getReason() != null) {
1✔
726
      request.setReason(input.getReason());
1✔
727
    }
728
    if (input.getFirstExecutionRunId() != null) {
1✔
729
      request.setFirstExecutionRunId(input.getFirstExecutionRunId());
1✔
730
    }
731
    genericClient.requestCancel(request.build());
1✔
732
    return new CancelOutput();
1✔
733
  }
734

735
  @Override
736
  public TerminateOutput terminate(TerminateInput input) {
737
    TerminateWorkflowExecutionRequest.Builder request =
738
        TerminateWorkflowExecutionRequest.newBuilder()
1✔
739
            .setNamespace(clientOptions.getNamespace())
1✔
740
            .setIdentity(clientOptions.getIdentity())
1✔
741
            .setWorkflowExecution(input.getWorkflowExecution());
1✔
742
    if (input.getReason() != null) {
1✔
743
      request.setReason(input.getReason());
1✔
744
    }
745
    if (input.getFirstExecutionRunId() != null) {
1✔
746
      request.setFirstExecutionRunId(input.getFirstExecutionRunId());
1✔
747
    }
748
    DataConverter dataConverterWithWorkflowContext =
1✔
749
        clientOptions
750
            .getDataConverter()
1✔
751
            .withContext(
1✔
752
                new WorkflowSerializationContext(
753
                    clientOptions.getNamespace(), input.getWorkflowExecution().getWorkflowId()));
1✔
754
    Optional<Payloads> payloads = dataConverterWithWorkflowContext.toPayloads(input.getDetails());
1✔
755
    payloads.ifPresent(request::setDetails);
1✔
756
    genericClient.terminate(request.build());
1✔
757
    return new TerminateOutput();
1✔
758
  }
759

760
  @Override
761
  public DescribeWorkflowOutput describe(DescribeWorkflowInput input) {
762
    DescribeWorkflowExecutionResponse response =
1✔
763
        genericClient.describeWorkflowExecution(
1✔
764
            DescribeWorkflowExecutionRequest.newBuilder()
1✔
765
                .setNamespace(clientOptions.getNamespace())
1✔
766
                .setExecution(input.getWorkflowExecution())
1✔
767
                .build());
1✔
768

769
    DataConverter dataConverterWithWorkflowContext =
1✔
770
        clientOptions
771
            .getDataConverter()
1✔
772
            .withContext(
1✔
773
                new WorkflowSerializationContext(
774
                    clientOptions.getNamespace(), input.getWorkflowExecution().getWorkflowId()));
1✔
775

776
    return new DescribeWorkflowOutput(
1✔
777
        new WorkflowExecutionDescription(response, dataConverterWithWorkflowContext));
778
  }
779

780
  @Override
781
  public CountWorkflowOutput countWorkflows(CountWorkflowsInput input) {
782
    CountWorkflowExecutionsRequest.Builder req =
783
        CountWorkflowExecutionsRequest.newBuilder().setNamespace(clientOptions.getNamespace());
×
784
    if (input.getQuery() != null) {
×
785
      req.setQuery(input.getQuery());
×
786
    }
787
    CountWorkflowExecutionsResponse resp = genericClient.countWorkflowExecutions(req.build());
×
788
    return new CountWorkflowOutput(new WorkflowExecutionCount(resp));
×
789
  }
790

791
  @Override
792
  public ListWorkflowExecutionsOutput listWorkflowExecutions(ListWorkflowExecutionsInput input) {
793
    ListWorkflowExecutionIterator iterator =
×
794
        new ListWorkflowExecutionIterator(
795
            input.getQuery(), clientOptions.getNamespace(), input.getPageSize(), genericClient);
×
796
    iterator.init();
×
797
    Iterator<WorkflowExecutionMetadata> wrappedIterator =
×
798
        Iterators.transform(
×
799
            iterator,
800
            info -> new WorkflowExecutionMetadata(info, clientOptions.getDataConverter()));
×
801

802
    // IMMUTABLE here means that "interference" (in Java Streams terms) to this spliterator is
803
    // impossible
804
    //  TODO We don't add DISTINCT to be safe. It's not explicitly stated if Temporal Server list
805
    // API
806
    // guarantees absence of duplicates
807
    final int CHARACTERISTICS = Spliterator.ORDERED | Spliterator.NONNULL | Spliterator.IMMUTABLE;
×
808

809
    return new ListWorkflowExecutionsOutput(
×
810
        StreamSupport.stream(
×
811
            Spliterators.spliteratorUnknownSize(wrappedIterator, CHARACTERISTICS), false));
×
812
  }
813

814
  private static <R> R convertResultPayloads(
815
      Optional<Payloads> resultValue,
816
      Class<R> resultClass,
817
      Type resultType,
818
      DataConverter dataConverter) {
819
    return dataConverter.fromPayloads(0, resultValue, resultClass, resultType);
1✔
820
  }
821

822
  /**
823
   * @return a handle to dispatch the eager workflow task. {@code null} if an eager execution is
824
   *     disabled through {@link io.temporal.client.WorkflowOptions} or the worker
825
   *     <ul>
826
   *       <li>is activity only worker
827
   *       <li>not started, shutdown or paused
828
   *       <li>doesn't have an executor slot available
829
   *     </ul>
830
   */
831
  @Nullable
832
  private WorkflowTaskDispatchHandle obtainDispatchHandle(WorkflowStartInput input) {
833
    if (input.getOptions().isDisableEagerExecution()) {
1✔
834
      return null;
1✔
835
    }
836
    return eagerWorkflowTaskDispatcher.tryGetLocalDispatchHandler(input);
1✔
837
  }
838
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc