Skip to content

Commit b8ace30

Browse files
authored
Add preferred version provider to worker options (#2942)
1 parent 23590a1 commit b8ace30

16 files changed

Lines changed: 911 additions & 35 deletions

temporal-sdk/src/main/java/io/temporal/internal/replay/ReplayWorkflowContextImpl.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
import io.temporal.internal.common.ProtobufTimeUtils;
1616
import io.temporal.internal.common.SdkFlag;
1717
import io.temporal.internal.statemachines.*;
18+
import io.temporal.internal.sync.WorkflowInternal;
1819
import io.temporal.internal.worker.SingleWorkerOptions;
20+
import io.temporal.worker.PreferredVersionProvider;
21+
import io.temporal.worker.PreferredVersionProviderInput;
1922
import io.temporal.workflow.Functions;
2023
import io.temporal.workflow.Functions.Func;
2124
import io.temporal.workflow.Functions.Func1;
@@ -337,7 +340,20 @@ public Integer getVersion(
337340
int minSupported,
338341
int maxSupported,
339342
Functions.Proc2<Integer, RuntimeException> callback) {
340-
return workflowStateMachines.getVersion(changeId, minSupported, maxSupported, callback);
343+
PreferredVersionProvider preferredVersionProvider = workerOptions.getPreferredVersionProvider();
344+
return workflowStateMachines.getVersion(
345+
changeId,
346+
minSupported,
347+
maxSupported,
348+
preferredVersionProvider == null
349+
? null
350+
: (min, max) ->
351+
WorkflowInternal.readOnly(
352+
() ->
353+
preferredVersionProvider.getPreferredVersion(
354+
new PreferredVersionProviderInput(
355+
WorkflowInternal.getWorkflowInfo(), changeId, min, max))),
356+
callback);
341357
}
342358

343359
@Override

temporal-sdk/src/main/java/io/temporal/internal/statemachines/VersionStateMachine.java

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
import io.temporal.api.history.v1.HistoryEvent;
1515
import io.temporal.internal.history.VersionMarkerUtils;
1616
import io.temporal.worker.NonDeterministicException;
17+
import io.temporal.worker.VersionPreference;
1718
import io.temporal.workflow.Functions;
1819
import java.util.Objects;
20+
import java.util.function.BiFunction;
1921
import javax.annotation.Nullable;
2022

2123
final class VersionStateMachine {
@@ -137,17 +139,22 @@ class InvocationStateMachine
137139
private final Functions.Func1<Integer, SearchAttributes> upsertSearchAttributeCallback;
138140
private final Functions.Proc2<Integer, RuntimeException> resultCallback;
139141

142+
@Nullable
143+
private final BiFunction<Integer, Integer, VersionPreference> preferredVersionProvider;
144+
140145
InvocationStateMachine(
141146
int minSupported,
142147
int maxSupported,
143148
boolean waitForMarkerRecordedReplaying,
144149
Functions.Func1<Integer, SearchAttributes> upsertSearchAttributeCallback,
150+
@Nullable BiFunction<Integer, Integer, VersionPreference> preferredVersionProvider,
145151
Functions.Proc2<Integer, RuntimeException> callback) {
146152
super(STATE_MACHINE_DEFINITION, VersionStateMachine.this.commandSink, stateMachineSink);
147153
this.minSupported = minSupported;
148154
this.maxSupported = maxSupported;
149155
this.waitForMarkerRecordedReplaying = waitForMarkerRecordedReplaying;
150156
this.upsertSearchAttributeCallback = upsertSearchAttributeCallback;
157+
this.preferredVersionProvider = preferredVersionProvider;
151158
this.resultCallback = Objects.requireNonNull(callback);
152159
}
153160

@@ -210,13 +217,17 @@ private void validateVersionAndThrow(boolean preloaded) {
210217
throw new IllegalStateException((preloaded ? "preloaded " : "") + " version not set");
211218
}
212219
if (versionToUse < minSupported || versionToUse > maxSupported) {
213-
throw new UnsupportedVersion.UnsupportedVersionException(
214-
String.format(
215-
"Version %d of changeId %s is not supported. Supported v is between %d and %d.",
216-
versionToUse, changeId, minSupported, maxSupported));
220+
throwUnsupportedVersion(versionToUse);
217221
}
218222
}
219223

224+
private void throwUnsupportedVersion(int unsupportedVersion) {
225+
throw new UnsupportedVersion.UnsupportedVersionException(
226+
String.format(
227+
"Version %d of changeId %s is not supported. Supported v is between %d and %d.",
228+
unsupportedVersion, changeId, minSupported, maxSupported));
229+
}
230+
220231
void notifyFromVersion(boolean preloaded) {
221232
Integer versionToUse = preloaded ? preloadedVersion : version;
222233
resultCallback.apply(versionToUse, null);
@@ -227,8 +238,8 @@ void notifyFromException(RuntimeException ex) {
227238
}
228239

229240
void notifyFromVersionExecuting() {
230-
// the only case when we don't need to validate before notification because
231-
// we just initialized the version with maxVersion
241+
// No validation needed before notification here: resolveVersionExecuting() already guarantees
242+
// the version it produces is within [minSupported, maxSupported].
232243
notifyFromVersion(false);
233244
}
234245

@@ -238,7 +249,7 @@ State createMarkerExecuting() {
238249
addCommand(StateMachineCommandUtils.RECORD_MARKER_FAKE_COMMAND);
239250
return State.SKIPPED;
240251
} else {
241-
version = maxSupported;
252+
version = resolveVersionExecuting();
242253
SearchAttributes sa = upsertSearchAttributeCallback.apply(version);
243254
writeVersionChangeSA = sa != null;
244255
RecordMarkerCommandAttributes markerAttributes =
@@ -253,6 +264,27 @@ State createMarkerExecuting() {
253264
}
254265
}
255266

267+
private int resolveVersionExecuting() {
268+
if (preferredVersionProvider == null) {
269+
return maxSupported;
270+
}
271+
VersionPreference versionPreference =
272+
preferredVersionProvider.apply(minSupported, maxSupported);
273+
if (versionPreference == null) {
274+
return maxSupported;
275+
}
276+
277+
int preferredVersion = versionPreference.getVersion();
278+
if (preferredVersion >= minSupported && preferredVersion <= maxSupported) {
279+
return preferredVersion;
280+
}
281+
if (versionPreference.isClampToSupportedRange()) {
282+
return Math.min(Math.max(preferredVersion, minSupported), maxSupported);
283+
}
284+
throwUnsupportedVersion(preferredVersion);
285+
throw new IllegalStateException("unreachable");
286+
}
287+
256288
void notifySkippedExecuting() {
257289
cancelCommand();
258290
try {
@@ -411,13 +443,15 @@ public Integer getVersion(
411443
int maxSupported,
412444
boolean waitForMarkerRecordedReplaying,
413445
Functions.Func1<Integer, SearchAttributes> upsertSearchAttributeCallback,
446+
@Nullable BiFunction<Integer, Integer, VersionPreference> preferredVersionProvider,
414447
Functions.Proc2<Integer, RuntimeException> callback) {
415448
InvocationStateMachine ism =
416449
new InvocationStateMachine(
417450
minSupported,
418451
maxSupported,
419452
waitForMarkerRecordedReplaying,
420453
upsertSearchAttributeCallback,
454+
preferredVersionProvider,
421455
callback);
422456
ism.explicitEvent(ExplicitEvent.CHECK_EXECUTION_STATE);
423457
ism.explicitEvent(ExplicitEvent.SCHEDULE);

temporal-sdk/src/main/java/io/temporal/internal/statemachines/WorkflowStateMachines.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@
3030
import io.temporal.serviceclient.Version;
3131
import io.temporal.worker.MetricsType;
3232
import io.temporal.worker.NonDeterministicException;
33+
import io.temporal.worker.VersionPreference;
3334
import io.temporal.worker.WorkflowImplementationOptions;
3435
import io.temporal.workflow.ChildWorkflowCancellationType;
3536
import io.temporal.workflow.Functions;
3637
import io.temporal.workflow.NexusOperationCancellationType;
3738
import java.nio.charset.StandardCharsets;
3839
import java.util.*;
40+
import java.util.function.BiFunction;
3941
import javax.annotation.Nonnull;
4042
import javax.annotation.Nullable;
4143
import org.slf4j.Logger;
@@ -1244,6 +1246,15 @@ public Integer getVersion(
12441246
int minSupported,
12451247
int maxSupported,
12461248
Functions.Proc2<Integer, RuntimeException> callback) {
1249+
return getVersion(changeId, minSupported, maxSupported, null, callback);
1250+
}
1251+
1252+
public Integer getVersion(
1253+
String changeId,
1254+
int minSupported,
1255+
int maxSupported,
1256+
@Nullable BiFunction<Integer, Integer, VersionPreference> preferredVersionProvider,
1257+
Functions.Proc2<Integer, RuntimeException> callback) {
12471258
VersionStateMachine stateMachine =
12481259
versions.computeIfAbsent(
12491260
changeId,
@@ -1275,6 +1286,7 @@ public Integer getVersion(
12751286
}
12761287
return sa;
12771288
},
1289+
preferredVersionProvider,
12781290
(v, e) -> {
12791291
callback.apply(v, e);
12801292
// without this getVersion call will trigger the end of WFT,

temporal-sdk/src/main/java/io/temporal/internal/sync/SyncWorkflowContext.java

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,21 +1143,26 @@ private <R> R mutableSideEffectImpl(
11431143
@Override
11441144
public int getVersion(String changeId, int minSupported, int maxSupported) {
11451145
CompletablePromise<Integer> result = Workflow.newPromise();
1146-
Integer versionToUse =
1147-
replayContext.getVersion(
1148-
changeId,
1149-
minSupported,
1150-
maxSupported,
1151-
(v, e) ->
1152-
runner.executeInWorkflowThread(
1153-
"version-callback",
1154-
() -> {
1155-
if (v != null) {
1156-
result.complete(v);
1157-
} else {
1158-
result.completeExceptionally(e);
1159-
}
1160-
}));
1146+
Integer versionToUse;
1147+
try {
1148+
versionToUse =
1149+
replayContext.getVersion(
1150+
changeId,
1151+
minSupported,
1152+
maxSupported,
1153+
(v, e) ->
1154+
runner.executeInWorkflowThread(
1155+
"version-callback",
1156+
() -> {
1157+
if (v != null) {
1158+
result.complete(v);
1159+
} else {
1160+
result.completeExceptionally(e);
1161+
}
1162+
}));
1163+
} catch (UnsupportedVersion.UnsupportedVersionException ex) {
1164+
throw new UnsupportedVersion(ex);
1165+
}
11611166
/*
11621167
* If we are replaying a workflow and encounter a getVersion call it is possible that this call did not exist
11631168
* on the original execution. If the call did not exist on the original execution then we cannot block on results

temporal-sdk/src/main/java/io/temporal/internal/sync/WorkflowInternal.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,17 @@ public static <T> T deadlockDetectorOff(Functions.Func<T> func) {
674674
}
675675
}
676676

677+
public static <T> T readOnly(Functions.Func<T> func) {
678+
SyncWorkflowContext workflowContext = getRootWorkflowContext();
679+
boolean previousReadOnly = workflowContext.isReadOnly();
680+
workflowContext.setReadOnly(true);
681+
try {
682+
return func.apply();
683+
} finally {
684+
workflowContext.setReadOnly(previousReadOnly);
685+
}
686+
}
687+
677688
public static WorkflowInfo getWorkflowInfo() {
678689
return new WorkflowInfoImpl(getRootWorkflowContext().getReplayContext());
679690
}

temporal-sdk/src/main/java/io/temporal/internal/worker/SingleWorkerOptions.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.temporal.common.converter.DataConverter;
88
import io.temporal.common.converter.GlobalDataConverter;
99
import io.temporal.common.interceptors.WorkerInterceptor;
10+
import io.temporal.worker.PreferredVersionProvider;
1011
import io.temporal.worker.WorkerDeploymentOptions;
1112
import java.time.Duration;
1213
import java.util.List;
@@ -43,6 +44,7 @@ public static final class Builder {
4344
private String workerInstanceKey;
4445
private boolean allowActivityHeartbeatDuringShutdown;
4546
private String workerControlTaskQueue;
47+
private PreferredVersionProvider preferredVersionProvider;
4648

4749
private Builder() {}
4850

@@ -70,6 +72,7 @@ private Builder(SingleWorkerOptions options) {
7072
this.workerInstanceKey = options.getWorkerInstanceKey();
7173
this.allowActivityHeartbeatDuringShutdown = options.getAllowActivityHeartbeatDuringShutdown();
7274
this.workerControlTaskQueue = options.getWorkerControlTaskQueue();
75+
this.preferredVersionProvider = options.getPreferredVersionProvider();
7376
}
7477

7578
public Builder setIdentity(String identity) {
@@ -177,6 +180,11 @@ public Builder setWorkerControlTaskQueue(String workerControlTaskQueue) {
177180
return this;
178181
}
179182

183+
public Builder setPreferredVersionProvider(PreferredVersionProvider preferredVersionProvider) {
184+
this.preferredVersionProvider = preferredVersionProvider;
185+
return this;
186+
}
187+
180188
public SingleWorkerOptions build() {
181189
PollerOptions pollerOptions = this.pollerOptions;
182190
if (pollerOptions == null) {
@@ -218,7 +226,8 @@ public SingleWorkerOptions build() {
218226
this.deploymentOptions,
219227
this.workerInstanceKey,
220228
this.allowActivityHeartbeatDuringShutdown,
221-
this.workerControlTaskQueue);
229+
this.workerControlTaskQueue,
230+
this.preferredVersionProvider);
222231
}
223232
}
224233

@@ -242,6 +251,7 @@ public SingleWorkerOptions build() {
242251
private final String workerInstanceKey;
243252
private final boolean allowActivityHeartbeatDuringShutdown;
244253
private final String workerControlTaskQueue;
254+
private final PreferredVersionProvider preferredVersionProvider;
245255

246256
private SingleWorkerOptions(
247257
String identity,
@@ -263,7 +273,8 @@ private SingleWorkerOptions(
263273
WorkerDeploymentOptions deploymentOptions,
264274
String workerInstanceKey,
265275
boolean allowActivityHeartbeatDuringShutdown,
266-
String workerControlTaskQueue) {
276+
String workerControlTaskQueue,
277+
PreferredVersionProvider preferredVersionProvider) {
267278
this.identity = identity;
268279
this.binaryChecksum = binaryChecksum;
269280
this.buildId = buildId;
@@ -284,6 +295,7 @@ private SingleWorkerOptions(
284295
this.workerInstanceKey = workerInstanceKey;
285296
this.allowActivityHeartbeatDuringShutdown = allowActivityHeartbeatDuringShutdown;
286297
this.workerControlTaskQueue = workerControlTaskQueue;
298+
this.preferredVersionProvider = preferredVersionProvider;
287299
}
288300

289301
public String getIdentity() {
@@ -377,6 +389,10 @@ public String getWorkerControlTaskQueue() {
377389
return workerControlTaskQueue;
378390
}
379391

392+
public PreferredVersionProvider getPreferredVersionProvider() {
393+
return preferredVersionProvider;
394+
}
395+
380396
public WorkerVersioningOptions getWorkerVersioningOptions() {
381397
return new WorkerVersioningOptions(
382398
this.getBuildId(), this.isUsingBuildIdForVersioning(), this.getDeploymentOptions());
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package io.temporal.worker;
2+
3+
import io.temporal.common.Experimental;
4+
import javax.annotation.Nonnull;
5+
import javax.annotation.Nullable;
6+
7+
/**
8+
* Provides the version to record for a {@link io.temporal.workflow.Workflow#getVersion(String, int,
9+
* int)} call when the version marker is created for the first time.
10+
*
11+
* <p>This provider is called only during non-replay workflow task execution, before the SDK records
12+
* a version marker for the change ID. It is not called during replay or after a version was already
13+
* memoized for the same change ID.
14+
*/
15+
@Experimental
16+
@FunctionalInterface
17+
public interface PreferredVersionProvider {
18+
19+
/**
20+
* Returns the preferred version for the supplied {@code getVersion} call, or {@code null} to use
21+
* the SDK default of {@code maxSupported}.
22+
*
23+
* <p>This method is invoked on the workflow thread. Any exception it throws propagates out of the
24+
* {@code getVersion} call and fails the current workflow task, which will be retried; a provider
25+
* that always throws will therefore block the workflow. Implementations should be deterministic
26+
* and side-effect free.
27+
*/
28+
@Nullable
29+
VersionPreference getPreferredVersion(@Nonnull PreferredVersionProviderInput input);
30+
}

0 commit comments

Comments
 (0)