Skip to content

Commit 1defe40

Browse files
authored
implement getVersion (#166)
* implement getVersion * review comments * support the case where getVersion call is removed during event replay * use data converter instead of lambda for encoding * update class name * turn off debug in test * fix getversions get removed before another version marker decision * address review comments * add comments on addDecision, addAllMissingVersionMarker and nextDecisionEventId * fix getOptionalDecisionEvent
1 parent 089ae4a commit 1defe40

16 files changed

+685
-194
lines changed

src/main/java/com/uber/cadence/internal/replay/ActivityDecisionContext.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,6 @@ Consumer<Exception> scheduleActivityTask(
9797
attributes.setScheduleToStartTimeoutSeconds(
9898
(int) parameters.getScheduleToStartTimeoutSeconds());
9999
attributes.setStartToCloseTimeoutSeconds((int) parameters.getStartToCloseTimeoutSeconds());
100-
//
101-
// attributes.setTaskPriority(InternalUtils.taskPriorityToString(parameters.getTaskPriority()));
102-
String activityId = parameters.getActivityId();
103-
if (activityId == null) {
104-
activityId = String.valueOf(decisions.getNextId());
105-
}
106-
attributes.setActivityId(activityId);
107100

108101
String taskList = parameters.getTaskList();
109102
if (taskList != null && !taskList.isEmpty()) {
@@ -144,6 +137,11 @@ void handleActivityTaskCompleted(HistoryEvent event) {
144137
byte[] result = attributes.getResult();
145138
BiConsumer<byte[], Exception> completionHandle = scheduled.getCompletionCallback();
146139
completionHandle.accept(result, null);
140+
} else {
141+
throw new NonDeterminisicWorkflowError(
142+
"Trying to complete activity event "
143+
+ attributes.getScheduledEventId()
144+
+ " that is not in scheduledActivities");
147145
}
148146
}
149147
}

src/main/java/com/uber/cadence/internal/replay/ClockDecisionContext.java

Lines changed: 53 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@
1717

1818
package com.uber.cadence.internal.replay;
1919

20-
import com.uber.cadence.EventType;
2120
import com.uber.cadence.HistoryEvent;
2221
import com.uber.cadence.MarkerRecordedEventAttributes;
2322
import com.uber.cadence.StartTimerDecisionAttributes;
2423
import com.uber.cadence.TimerCanceledEventAttributes;
2524
import com.uber.cadence.TimerFiredEventAttributes;
26-
import com.uber.cadence.internal.replay.DecisionContext.MutableSideEffectData;
25+
import com.uber.cadence.converter.DataConverter;
26+
import com.uber.cadence.internal.replay.MarkerHandler.MarkerData;
27+
import com.uber.cadence.internal.sync.WorkflowInternal;
2728
import com.uber.cadence.workflow.Functions.Func;
2829
import com.uber.cadence.workflow.Functions.Func1;
2930
import java.util.HashMap;
@@ -33,6 +34,7 @@
3334
import java.util.concurrent.TimeUnit;
3435
import java.util.function.BiConsumer;
3536
import java.util.function.Consumer;
37+
import java.util.function.Predicate;
3638
import org.slf4j.Logger;
3739
import org.slf4j.LoggerFactory;
3840

@@ -41,6 +43,7 @@ final class ClockDecisionContext {
4143

4244
private static final String SIDE_EFFECT_MARKER_NAME = "SideEffect";
4345
private static final String MUTABLE_SIDE_EFFECT_MARKER_NAME = "MutableSideEffect";
46+
public static final String VERSION_MARKER_NAME = "Version";
4447

4548
private static final Logger log = LoggerFactory.getLogger(ReplayDecider.class);
4649

@@ -58,31 +61,6 @@ public void accept(Exception reason) {
5861
}
5962
}
6063

61-
private static final class MutableSideEffectResult {
62-
63-
private final byte[] data;
64-
65-
/**
66-
* Count of how many times the mutableSideEffect was called since the last marker recorded. It
67-
* is used to ensure that an updated value is returned after the same exact number of times
68-
* during a replay.
69-
*/
70-
private int accessCount;
71-
72-
private MutableSideEffectResult(byte[] data) {
73-
this.data = data;
74-
}
75-
76-
public byte[] getData() {
77-
accessCount++;
78-
return data;
79-
}
80-
81-
public int getAccessCount() {
82-
return accessCount;
83-
}
84-
}
85-
8664
private final DecisionsHelper decisions;
8765

8866
// key is startedEventId
@@ -94,11 +72,15 @@ public int getAccessCount() {
9472

9573
// Key is side effect marker eventId
9674
private final Map<Long, byte[]> sideEffectResults = new HashMap<>();
97-
// Key is mutableSideEffect id
98-
private final Map<String, MutableSideEffectResult> mutableSideEffectResults = new HashMap<>();
75+
76+
private final MarkerHandler mutableSideEffectHandler;
77+
private final MarkerHandler versionHandler;
9978

10079
ClockDecisionContext(DecisionsHelper decisions) {
10180
this.decisions = decisions;
81+
mutableSideEffectHandler =
82+
new MarkerHandler(decisions, MUTABLE_SIDE_EFFECT_MARKER_NAME, () -> replaying);
83+
versionHandler = new MarkerHandler(decisions, VERSION_MARKER_NAME, () -> replaying);
10284
}
10385

10486
long currentTimeMillis() {
@@ -125,8 +107,7 @@ Consumer<Exception> createTimer(long delaySeconds, Consumer<Exception> callback)
125107
final OpenRequestInfo<?, Long> context = new OpenRequestInfo<>(firingTime);
126108
final StartTimerDecisionAttributes timer = new StartTimerDecisionAttributes();
127109
timer.setStartToFireTimeoutSeconds(delaySeconds);
128-
timer.setTimerId(String.valueOf(decisions.getNextId()));
129-
long startEventId = decisions.startTimer(timer, null);
110+
long startEventId = decisions.startTimer(timer);
130111
context.setCompletionHandle((ctx, e) -> callback.accept(e));
131112
scheduledTimers.put(startEventId, context);
132113
return new TimerCancellationHandler(startEventId);
@@ -167,6 +148,7 @@ private void timerCancelled(long startEventId, Exception reason) {
167148
}
168149

169150
byte[] sideEffect(Func<byte[]> func) {
151+
decisions.addAllMissingVersionMarker(false, Optional.empty());
170152
long sideEffectEventId = decisions.getNextDecisionEventId();
171153
byte[] result;
172154
if (replaying) {
@@ -194,81 +176,54 @@ byte[] sideEffect(Func<byte[]> func) {
194176
* @return the latest value returned by func
195177
*/
196178
Optional<byte[]> mutableSideEffect(
197-
String id,
198-
Func1<MutableSideEffectData, byte[]> markerDataSerializer,
199-
Func1<byte[], MutableSideEffectData> markerDataDeserializer,
200-
Func1<Optional<byte[]>, Optional<byte[]>> func) {
201-
MutableSideEffectResult result = mutableSideEffectResults.get(id);
202-
Optional<byte[]> stored;
203-
if (result == null) {
204-
stored = Optional.empty();
205-
} else {
206-
stored = Optional.of(result.getData());
207-
}
208-
long eventId = decisions.getNextDecisionEventId();
209-
int accessCount = result == null ? 0 : result.getAccessCount();
210-
if (replaying) {
211-
212-
Optional<byte[]> data =
213-
getSideEffectDataFromHistory(eventId, id, accessCount, markerDataDeserializer);
214-
if (data.isPresent()) {
215-
// Need to insert marker to ensure that eventId is incremented
216-
recordMutableSideEffectMarker(id, eventId, data.get(), accessCount, markerDataSerializer);
217-
return data;
218-
}
219-
return stored;
220-
}
221-
Optional<byte[]> toStore = func.apply(stored);
222-
if (toStore.isPresent()) {
223-
byte[] data = toStore.get();
224-
recordMutableSideEffectMarker(id, eventId, data, accessCount, markerDataSerializer);
225-
return toStore;
226-
}
227-
return stored;
228-
}
229-
230-
private Optional<byte[]> getSideEffectDataFromHistory(
231-
long eventId,
232-
String mutableSideEffectId,
233-
int expectedAcccessCount,
234-
Func1<byte[], MutableSideEffectData> markerDataDeserializer) {
235-
HistoryEvent event = decisions.getDecisionEvent(eventId);
236-
if (event.getEventType() != EventType.MarkerRecorded) {
237-
return Optional.empty();
238-
}
239-
MarkerRecordedEventAttributes attributes = event.getMarkerRecordedEventAttributes();
240-
String name = attributes.getMarkerName();
241-
if (!MUTABLE_SIDE_EFFECT_MARKER_NAME.equals(name)) {
242-
return Optional.empty();
243-
}
244-
MutableSideEffectData markerData = markerDataDeserializer.apply(attributes.getDetails());
245-
// access count is used to not return data from the marker before the recorded number of calls
246-
if (!mutableSideEffectId.equals(markerData.getId())
247-
|| markerData.getAccessCount() > expectedAcccessCount) {
248-
return Optional.empty();
249-
}
250-
return Optional.of(markerData.getData());
251-
}
252-
253-
private void recordMutableSideEffectMarker(
254-
String id,
255-
long eventId,
256-
byte[] data,
257-
int accessCount,
258-
Func1<MutableSideEffectData, byte[]> markerDataSerializer) {
259-
MutableSideEffectData dataObject = new MutableSideEffectData(id, eventId, data, accessCount);
260-
byte[] details = markerDataSerializer.apply(dataObject);
261-
mutableSideEffectResults.put(id, new MutableSideEffectResult(data));
262-
decisions.recordMarker(MUTABLE_SIDE_EFFECT_MARKER_NAME, details);
179+
String id, DataConverter converter, Func1<Optional<byte[]>, Optional<byte[]>> func) {
180+
decisions.addAllMissingVersionMarker(false, Optional.empty());
181+
return mutableSideEffectHandler.handle(id, converter, func);
263182
}
264183

265184
void handleMarkerRecorded(HistoryEvent event) {
266185
MarkerRecordedEventAttributes attributes = event.getMarkerRecordedEventAttributes();
267186
String name = attributes.getMarkerName();
268187
if (SIDE_EFFECT_MARKER_NAME.equals(name)) {
269188
sideEffectResults.put(event.getEventId(), attributes.getDetails());
270-
} else if (!MUTABLE_SIDE_EFFECT_MARKER_NAME.equals(name)) {
189+
} else if (!MUTABLE_SIDE_EFFECT_MARKER_NAME.equals(name) && !VERSION_MARKER_NAME.equals(name)) {
271190
log.warn("Unexpected marker: " + event);
272191
}
273192
}
193+
194+
int getVersion(String changeId, DataConverter converter, int minSupported, int maxSupported) {
195+
Predicate<byte[]> changeIdEquals =
196+
(bytesInEvent) -> {
197+
MarkerData markerData = converter.fromData(bytesInEvent, MarkerData.class);
198+
return markerData.getId().equals(changeId);
199+
};
200+
decisions.addAllMissingVersionMarker(true, Optional.of(changeIdEquals));
201+
202+
Optional<byte[]> result =
203+
versionHandler.handle(
204+
changeId,
205+
converter,
206+
(stored) -> {
207+
if (stored.isPresent()) {
208+
return Optional.empty();
209+
}
210+
return Optional.of(converter.toData(maxSupported));
211+
});
212+
213+
if (!result.isPresent()) {
214+
return WorkflowInternal.DEFAULT_VERSION;
215+
}
216+
int version = converter.fromData(result.get(), Integer.class);
217+
validateVersion(changeId, version, minSupported, maxSupported);
218+
return version;
219+
}
220+
221+
private void validateVersion(String changeID, int version, int minSupported, int maxSupported) {
222+
if (version < minSupported || version > maxSupported) {
223+
throw new Error(
224+
String.format(
225+
"Version %d of changeID %s is not supported. Supported version is between %d and %d.",
226+
version, changeID, minSupported, maxSupported));
227+
}
228+
}
274229
}

src/main/java/com/uber/cadence/internal/replay/DecisionContext.java

Lines changed: 19 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import com.uber.cadence.WorkflowExecution;
2121
import com.uber.cadence.WorkflowType;
22+
import com.uber.cadence.converter.DataConverter;
2223
import com.uber.cadence.workflow.Functions.Func;
2324
import com.uber.cadence.workflow.Functions.Func1;
2425
import com.uber.cadence.workflow.Promise;
@@ -34,37 +35,6 @@
3435
*/
3536
public interface DecisionContext extends ReplayAware {
3637

37-
final class MutableSideEffectData {
38-
39-
private final String id;
40-
private final long eventId;
41-
private final byte[] data;
42-
private final int accessCount;
43-
44-
public MutableSideEffectData(String id, long eventId, byte[] data, int accessCount) {
45-
this.id = id;
46-
this.eventId = eventId;
47-
this.data = data;
48-
this.accessCount = accessCount;
49-
}
50-
51-
public String getId() {
52-
return id;
53-
}
54-
55-
public long getEventId() {
56-
return eventId;
57-
}
58-
59-
public byte[] getData() {
60-
return data;
61-
}
62-
63-
public int getAccessCount() {
64-
return accessCount;
65-
}
66-
}
67-
6838
WorkflowExecution getWorkflowExecution();
6939

7040
// TODO: Add to Cadence
@@ -127,14 +97,8 @@ Consumer<Exception> signalWorkflowExecution(
12797

12898
void continueAsNewOnCompletion(ContinueAsNewWorkflowExecutionParameters parameters);
12999

130-
/** Deterministic unique Id generator */
131-
String generateUniqueId();
132-
133100
Optional<byte[]> mutableSideEffect(
134-
String id,
135-
Func1<MutableSideEffectData, byte[]> markerDataSerializer,
136-
Func1<byte[], MutableSideEffectData> markerDataDeserializer,
137-
Func1<Optional<byte[]>, Optional<byte[]>> func);
101+
String id, DataConverter dataConverter, Func1<Optional<byte[]>, Optional<byte[]>> func);
138102

139103
/**
140104
* @return time of the {@link com.uber.cadence.PollForDecisionTaskResponse} start event of the
@@ -166,6 +130,23 @@ Optional<byte[]> mutableSideEffect(
166130
*/
167131
byte[] sideEffect(Func<byte[]> func);
168132

133+
/**
134+
* GetVersion is used to safely perform backwards incompatible changes to workflow definitions. It
135+
* is not allowed to update workflow code while there are workflows running as it is going to
136+
* break determinism. The solution is to have both old code that is used to replay existing
137+
* workflows as well as the new one that is used when it is executed for the first time.
138+
* GetVersion returns maxSupported version when executed for the first time. This version is
139+
* recorded into the workflow history as a marker event. Even if maxSupported version is changed
140+
* the version that was recorded is returned on replay. DefaultVersion constant contains version
141+
* of code that wasn't versioned before.
142+
*
143+
* @param changeID identifier of a particular change
144+
* @param minSupported min version supported for the change
145+
* @param maxSupported max version supported for the change
146+
* @return version
147+
*/
148+
int getVersion(String changeID, DataConverter dataConverter, int minSupported, int maxSupported);
149+
169150
/** @return scope to be used for metrics reporting. */
170151
Scope getMetricsScope();
171152

src/main/java/com/uber/cadence/internal/replay/DecisionContextImpl.java

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.uber.cadence.WorkflowExecution;
2424
import com.uber.cadence.WorkflowExecutionStartedEventAttributes;
2525
import com.uber.cadence.WorkflowType;
26+
import com.uber.cadence.converter.DataConverter;
2627
import com.uber.cadence.internal.metrics.ReplayAwareScope;
2728
import com.uber.cadence.workflow.Functions.Func;
2829
import com.uber.cadence.workflow.Functions.Func1;
@@ -172,11 +173,6 @@ public void continueAsNewOnCompletion(ContinueAsNewWorkflowExecutionParameters p
172173
workflowClient.continueAsNewOnCompletion(parameters);
173174
}
174175

175-
@Override
176-
public String generateUniqueId() {
177-
return workflowClient.generateUniqueId();
178-
}
179-
180176
void setReplayCurrentTimeMilliseconds(long replayCurrentTimeMilliseconds) {
181177
if (replayCurrentTimeMilliseconds < workflowClock.currentTimeMillis()) {
182178
throw new IllegalArgumentException("workflow clock moved back");
@@ -201,11 +197,14 @@ public byte[] sideEffect(Func<byte[]> func) {
201197

202198
@Override
203199
public Optional<byte[]> mutableSideEffect(
204-
String id,
205-
Func1<MutableSideEffectData, byte[]> markerDataSerializer,
206-
Func1<byte[], MutableSideEffectData> markerDataDeserializer,
207-
Func1<Optional<byte[]>, Optional<byte[]>> func) {
208-
return workflowClock.mutableSideEffect(id, markerDataSerializer, markerDataDeserializer, func);
200+
String id, DataConverter converter, Func1<Optional<byte[]>, Optional<byte[]>> func) {
201+
return workflowClock.mutableSideEffect(id, converter, func);
202+
}
203+
204+
@Override
205+
public int getVersion(
206+
String changeID, DataConverter converter, int minSupported, int maxSupported) {
207+
return workflowClock.getVersion(changeID, converter, minSupported, maxSupported);
209208
}
210209

211210
@Override

0 commit comments

Comments
 (0)