17
17
18
18
package com .uber .cadence .internal .replay ;
19
19
20
+ import com .uber .cadence .EventType ;
20
21
import com .uber .cadence .HistoryEvent ;
21
22
import com .uber .cadence .MarkerRecordedEventAttributes ;
22
23
import com .uber .cadence .StartTimerDecisionAttributes ;
23
24
import com .uber .cadence .TimerCanceledEventAttributes ;
24
25
import com .uber .cadence .TimerFiredEventAttributes ;
26
+ import com .uber .cadence .internal .replay .DecisionContext .MutableSideEffectData ;
25
27
import com .uber .cadence .workflow .Functions .Func ;
28
+ import com .uber .cadence .workflow .Functions .Func1 ;
26
29
import java .util .HashMap ;
27
30
import java .util .Map ;
31
+ import java .util .Optional ;
28
32
import java .util .concurrent .CancellationException ;
29
33
import java .util .concurrent .TimeUnit ;
30
34
import java .util .function .BiConsumer ;
36
40
final class ClockDecisionContext {
37
41
38
42
private static final String SIDE_EFFECT_MARKER_NAME = "SideEffect" ;
43
+ private static final String MUTABLE_SIDE_EFFECT_MARKER_NAME = "MutableSideEffect" ;
39
44
40
45
private static final Logger log = LoggerFactory .getLogger (ReplayDecider .class );
41
46
@@ -53,6 +58,31 @@ public void accept(Exception reason) {
53
58
}
54
59
}
55
60
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
+
56
86
private final DecisionsHelper decisions ;
57
87
58
88
// key is startedEventId
@@ -62,7 +92,10 @@ public void accept(Exception reason) {
62
92
63
93
private boolean replaying = true ;
64
94
95
+ // Key is side effect marker eventId
65
96
private final Map <Long , byte []> sideEffectResults = new HashMap <>();
97
+ // Key is mutableSideEffect id
98
+ private final Map <String , MutableSideEffectResult > mutableSideEffectResults = new HashMap <>();
66
99
67
100
ClockDecisionContext (DecisionsHelper decisions ) {
68
101
this .decisions = decisions ;
@@ -96,7 +129,7 @@ Consumer<Exception> createTimer(long delaySeconds, Consumer<Exception> callback)
96
129
long startEventId = decisions .startTimer (timer , null );
97
130
context .setCompletionHandle ((ctx , e ) -> callback .accept (e ));
98
131
scheduledTimers .put (startEventId , context );
99
- return new ClockDecisionContext . TimerCancellationHandler (startEventId );
132
+ return new TimerCancellationHandler (startEventId );
100
133
}
101
134
102
135
void setReplaying (boolean replaying ) {
@@ -154,11 +187,87 @@ byte[] sideEffect(Func<byte[]> func) {
154
187
return result ;
155
188
}
156
189
190
+ /**
191
+ * @param id mutable side effect id
192
+ * @param func given the value from the last marker returns value to store. If result is empty
193
+ * nothing is recorded into the history.
194
+ * @return the latest value returned by func
195
+ */
196
+ 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 );
263
+ }
264
+
157
265
void handleMarkerRecorded (HistoryEvent event ) {
158
266
MarkerRecordedEventAttributes attributes = event .getMarkerRecordedEventAttributes ();
159
- if (SIDE_EFFECT_MARKER_NAME .equals (attributes .getMarkerName ())) {
267
+ String name = attributes .getMarkerName ();
268
+ if (SIDE_EFFECT_MARKER_NAME .equals (name )) {
160
269
sideEffectResults .put (event .getEventId (), attributes .getDetails ());
161
- } else {
270
+ } else if (! MUTABLE_SIDE_EFFECT_MARKER_NAME . equals ( name )) {
162
271
log .warn ("Unexpected marker: " + event );
163
272
}
164
273
}
0 commit comments