Skip to content

Deserialize effects once at the invocation boundary - #257

Open
stidsborg wants to merge 2 commits into
mainfrom
remove-stored-input-output
Open

Deserialize effects once at the invocation boundary#257
stidsborg wants to merge 2 commits into
mainfrom
remove-stored-input-output

Conversation

@stidsborg

@stidsborg stidsborg commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • EffectResults now takes a list of already-deserialized effects (DeserializedEffect): InvocationHelper.CreateEffect (now async) deserializes the fetched StoredEffects up front via a new StoredEffect.Deserialize(serializer, typeMapper) extension.
  • PendingEffectChange carries both forms: Effect — the object-form read view (always present except for deletes) — and StoredEffect — the serialized write payload, non-null only while the entry is dirty and nulled once a flush persists it.
  • The read path (TryGet — now sync — CreateOrGet, InnerCapture replay, GetStatus, EffectPrinter) is cast-only: no serializer and no type-store resolution on reads. The write path is unchanged (eager serialization at capture, EnsurePersisted before flush).
  • Deletes the unused raw-StoredEffect accessors Effect.GetStoredEffect/FlushlessSet; EffectResults.Set is reduced to the payload-less completion marker Mark needs.
  • Also removes the unused StoredInputOutput type (first commit).

Deserialization failure fails the flow

A flow whose persisted effects cannot be deserialized under the running deployment is deterministic-broken — retrying cannot succeed. ScheduleRestart therefore fails the flow (persisting the exception and notifying any parent) instead of leaving it to the watchdog's endless retry cycle:

  • Deterministic failures — the recorded result type no longer resolves (TypeLoadException) or the serializer rejects the payload — are wrapped in a new EffectDeserializationException and surfaced as FatalWorkflowException.
  • Transient failures (e.g. a failed type-store round-trip inside ResolveType) still propagate raw, keeping the flow retryable — as does a failure of the fail-status write itself.
  • The flow parks visibly as Failed with the deserialization error recorded and can be restarted explicitly after a fix/redeploy.

Semantic changes

  • Replaying an effect within the same incarnation returns the same instance (the post-EffectValue.ForSerialization materialized value) instead of a fresh deserialized copy.
  • Deserialization is eager: an undeserializable payload now fails the flow at restart preparation rather than throwing at first read inside the body.

Testing

New EffectDeserializationFailureTests cover both deterministic failure modes (corrupt payload, unknown result type) end-to-end through the watchdog restart path. All suites green: Core 559, PostgreSQL 393, MariaDB 393, SqlServer 393 — 0 failures.

🤖 Generated with Claude Code

The record had no live consumers: its only producer - the PostgreSQL
SqlGenerator's ReadStoredFunction - was itself uncalled, and ToStoredFlow
had no callers either. Delete both.
EffectResults now takes already-deserialized effects (DeserializedEffect):
InvocationHelper.CreateEffect (now async) deserializes the fetched
StoredEffects up front, and PendingEffectChange carries the object-form
read view alongside the serialized write payload (the latter only while
dirty). The read path - TryGet (now sync), CreateOrGet and InnerCapture
replay - is cast-only: no serializer or type-store resolution on reads.

Also deletes the unused raw StoredEffect accessors on Effect
(GetStoredEffect/FlushlessSet) and reduces EffectResults.Set to the
payload-less completion marker Mark needs.
@stidsborg stidsborg changed the title Deserialisation Deserialize effects once at the invocation boundary Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant