Skip to content

Commit e3802b7

Browse files
authored
Document a significant caveat to SideEffect funcs (#1399)
An unfortunate edge-case-use was discovered recently, and SideEffect does not have adequate protections to prevent breaking workflows at the moment. For now, just document it. A true fix will require failing calls to blocking or history-recording funcs while the callback runs, as they cannot be used safely. Future versions of this API should remove the context arg, to stop implying it can be used.
1 parent fabd87b commit e3802b7

File tree

2 files changed

+56
-87
lines changed

2 files changed

+56
-87
lines changed

internal/workflow.go

Lines changed: 2 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1522,45 +1522,7 @@ func (b EncodedValue) HasValue() bool {
15221522
return b.value != nil
15231523
}
15241524

1525-
// SideEffect executes the provided function once, records its result into the workflow history. The recorded result on
1526-
// history will be returned without executing the provided function during replay. This guarantees the deterministic
1527-
// requirement for workflow as the exact same result will be returned in replay.
1528-
// Common use case is to run some short non-deterministic code in workflow, like getting random number or new UUID.
1529-
// The only way to fail SideEffect is to panic which causes decision task failure. The decision task after timeout is
1530-
// rescheduled and re-executed giving SideEffect another chance to succeed.
1531-
//
1532-
// Caution: do not use SideEffect to modify closures. Always retrieve result from SideEffect's encoded return value.
1533-
// For example this code is BROKEN:
1534-
//
1535-
// // Bad example:
1536-
// var random int
1537-
// workflow.SideEffect(func(ctx workflow.Context) interface{} {
1538-
// random = rand.Intn(100)
1539-
// return nil
1540-
// })
1541-
// // random will always be 0 in replay, thus this code is non-deterministic
1542-
// if random < 50 {
1543-
// ....
1544-
// } else {
1545-
// ....
1546-
// }
1547-
//
1548-
// On replay the provided function is not executed, the random will always be 0, and the workflow could takes a
1549-
// different path breaking the determinism.
1550-
//
1551-
// Here is the correct way to use SideEffect:
1552-
//
1553-
// // Good example:
1554-
// encodedRandom := SideEffect(func(ctx workflow.Context) interface{} {
1555-
// return rand.Intn(100)
1556-
// })
1557-
// var random int
1558-
// encodedRandom.Get(&random)
1559-
// if random < 50 {
1560-
// ....
1561-
// } else {
1562-
// ....
1563-
// }
1525+
// SideEffect docs are in the public API to prevent duplication: [go.uber.org/cadence/workflow.SideEffect]
15641526
func SideEffect(ctx Context, f func(ctx Context) interface{}) Value {
15651527
i := getWorkflowInterceptor(ctx)
15661528
return i.SideEffect(ctx, f)
@@ -1584,22 +1546,7 @@ func (wc *workflowEnvironmentInterceptor) SideEffect(ctx Context, f func(ctx Con
15841546
return encoded
15851547
}
15861548

1587-
// MutableSideEffect executes the provided function once, then it looks up the history for the value with the given id.
1588-
// If there is no existing value, then it records the function result as a value with the given id on history;
1589-
// otherwise, it compares whether the existing value from history has changed from the new function result by calling the
1590-
// provided equals function. If they are equal, it returns the value without recording a new one in history;
1591-
//
1592-
// otherwise, it records the new value with the same id on history.
1593-
//
1594-
// Caution: do not use MutableSideEffect to modify closures. Always retrieve result from MutableSideEffect's encoded
1595-
// return value.
1596-
//
1597-
// The difference between MutableSideEffect() and SideEffect() is that every new SideEffect() call in non-replay will
1598-
// result in a new marker being recorded on history. However, MutableSideEffect() only records a new marker if the value
1599-
// changed. During replay, MutableSideEffect() will not execute the function again, but it will return the exact same
1600-
// value as it was returning during the non-replay run.
1601-
//
1602-
// One good use case of MutableSideEffect() is to access dynamically changing config without breaking determinism.
1549+
// MutableSideEffect docs are in the public API to prevent duplication: [go.uber.org/cadence/workflow.MutableSideEffect]
16031550
func MutableSideEffect(ctx Context, id string, f func(ctx Context) interface{}, equals func(a, b interface{}) bool) Value {
16041551
i := getWorkflowInterceptor(ctx)
16051552
return i.MutableSideEffect(ctx, id, f, equals)

workflow/workflow.go

Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -265,65 +265,87 @@ func GetSignalChannel(ctx Context, signalName string) Channel {
265265
return internal.GetSignalChannel(ctx, signalName)
266266
}
267267

268-
// SideEffect executes the provided function once, records its result into the workflow history. The recorded result on
269-
// history will be returned without executing the provided function during replay. This guarantees the deterministic
270-
// requirement for workflow as the exact same result will be returned in replay.
271-
// Common use case is to run some short non-deterministic code in workflow, like getting random number or new UUID.
272-
// The only way to fail SideEffect is to panic which causes decision task failure. The decision task after timeout is
273-
// rescheduled and re-executed giving SideEffect another chance to succeed.
274-
//
275-
// Caution: do not use SideEffect to modify closures. Always retrieve result from SideEffect's encoded return value.
276-
// For example this code is BROKEN:
268+
// SideEffect executes the provided callback, and records its result into the workflow history.
269+
// During replay the recorded result will be returned instead, so the callback can do non-deterministic
270+
// things (such as reading files or getting random numbers) without breaking determinism during replay.
271+
//
272+
// As there is no error return, the callback's code is assumed to be reliable.
273+
// If you cannot retrieve the value for some reason, panicking and failing the decision task
274+
// will cause it to be retried, possibly succeeding on another machine.
275+
// For better error handling, use ExecuteLocalActivity instead.
276+
//
277+
// Caution: the callback MUST NOT call any blocking or history-creating APIs,
278+
// e.g. workflow.Sleep or ExecuteActivity or calling .Get on any future.
279+
// This will (potentially) work during the initial recording of history, but will
280+
// fail when replaying because the data is not available when the call occurs
281+
// (it becomes available later).
282+
//
283+
// In other words, in general: use this *only* for code that does not need the workflow.Context.
284+
// Incorrect context-using calls are not currently prevented in tests or during execution,
285+
// but they should be eventually.
286+
//
287+
// // Bad example: this will work until a replay occurs,
288+
// // but then the workflow will fail to replay with a non-deterministic error.
289+
// var out string
290+
// err := workflow.SideEffect(func(ctx workflow.Context) interface{} {
291+
// var signal data
292+
// err := workflow.GetSignalChannel(ctx, "signal").Receive(ctx, &signal)
293+
// _ = err // ignore err for the example
294+
// return signal
295+
// }).Get(&out)
296+
// workflow.GetLogger(ctx).Info(out)
297+
//
298+
// Caution: do not use SideEffect to modify values outside the callback.
299+
// Always retrieve result from SideEffect's encoded return value.
300+
// For example this code will break during replay:
277301
//
278302
// // Bad example:
279303
// var random int
280304
// workflow.SideEffect(func(ctx workflow.Context) interface{} {
281-
// random = rand.Intn(100)
282-
// return nil
305+
// random = rand.Intn(100) // this only occurs when recording history, not during replay
306+
// return nil
283307
// })
284308
// // random will always be 0 in replay, thus this code is non-deterministic
285309
// if random < 50 {
286-
// ....
310+
// ....
287311
// } else {
288-
// ....
312+
// ....
289313
// }
290314
//
291-
// On replay the provided function is not executed, the random will always be 0, and the workflow could takes a
292-
// different path breaking the determinism.
315+
// On replay the provided function is not executed, the `random` var will always be 0,
316+
// and the workflow could take a different path and break determinism.
293317
//
294-
// Here is the correct way to use SideEffect:
318+
// The only safe way to use SideEffect is to read from the returned encoded.Value:
295319
//
296320
// // Good example:
297321
// encodedRandom := SideEffect(func(ctx workflow.Context) interface{} {
298-
// return rand.Intn(100)
322+
// return rand.Intn(100)
299323
// })
300324
// var random int
301325
// encodedRandom.Get(&random)
302326
// if random < 50 {
303-
// ....
327+
// ....
304328
// } else {
305-
// ....
329+
// ....
306330
// }
307331
func SideEffect(ctx Context, f func(ctx Context) interface{}) encoded.Value {
308332
return internal.SideEffect(ctx, f)
309333
}
310334

311-
// MutableSideEffect executes the provided function once, then it looks up the history for the value with the given id.
312-
// If there is no existing value, then it records the function result as a value with the given id on history;
313-
// otherwise, it compares whether the existing value from history has changed from the new function result by calling the
314-
// provided equals function. If they are equal, it returns the value without recording a new one in history;
315-
//
316-
// otherwise, it records the new value with the same id on history.
335+
// MutableSideEffect is similar to SideEffect, but it only records changed values per ID instead of every call.
336+
// Changes are detected by calling the provided "equals" func - return true to mark a result
337+
// as the same as before, and not record the new value. The first call per ID is always "changed" and will always be recorded.
317338
//
318-
// Caution: do not use MutableSideEffect to modify closures. Always retrieve result from MutableSideEffect's encoded
319-
// return value.
339+
// This is intended for things you want to *check* frequently, but which *change* only occasionally, as non-changing
340+
// results will not add anything to your history. This helps keep those workflows under its size/count limits, and
341+
// can help keep replays more efficient than when using SideEffect or ExecuteLocalActivity (due to not storing duplicated data).
320342
//
321-
// The difference between MutableSideEffect() and SideEffect() is that every new SideEffect() call in non-replay will
322-
// result in a new marker being recorded on history. However, MutableSideEffect() only records a new marker if the value
323-
// changed. During replay, MutableSideEffect() will not execute the function again, but it will return the exact same
324-
// value as it was returning during the non-replay run.
343+
// Caution: the callback MUST NOT block or call any history-modifying events, or replays will be broken.
344+
// See SideEffect docs for more details.
325345
//
326-
// One good use case of MutableSideEffect() is to access dynamically changing config without breaking determinism.
346+
// Caution: do not use MutableSideEffect to modify closures.
347+
// Always retrieve result from MutableSideEffect's encoded return value.
348+
// See SideEffect docs for more details.
327349
func MutableSideEffect(ctx Context, id string, f func(ctx Context) interface{}, equals func(a, b interface{}) bool) encoded.Value {
328350
return internal.MutableSideEffect(ctx, id, f, equals)
329351
}

0 commit comments

Comments
 (0)