Versioning
Long-running functions inevitably change over time. Inngest enables developers to implement multiple strategies for changing long-running code over time. To manage these changes effectively, it's crucial to understand how the SDK implements determinism and executes steps.
Determinism in functions
Determinism is consistent in every Inngest language SDK. Except for language-specific idioms, all SDKs implement the same logic. In every SDK, functions in Inngest are a series of steps. Each step runs reliably, will retry on failure, and is as close to exactly-once execution as possible (excluding outbound network failures when reporting completed steps).
How the SDK works with steps
As covered in How Inngest functions are executed, each step in a function has a unique identifier, represented as a string. Each time a step is found, the SDK checks whether the step has been executed. It does this by:
- Hashing the step's unique identifier along with a counter of the number of times the step has been called. This enables steps to be used in a loop.
- Looking up the resulting hash in function run state.
- If the hash is present, the step has been executed. The SDK returns the memoized state and skips execution.
- If the hash isn't found, the SDK executes the step and returns the output to Inngest to be stored in the function run state.
After a step completes, the function execution immediately ends. The function is re-executed from the top with the updated memoized state until the function completes.
Handling determinism
The SDK handles determinism gracefully by default. The SDK keeps track of the order in which every step is executed. If new steps are added, they're executed when they're first discovered. This means that:
- The SDK always knows if functions are deterministic, even over months or years.
- New steps, or steps with changed IDs, are executed when they're discovered. If the order of step executions change, a warning is logged by default. Logging a warning allows you to comfortably extend and improve functions over time, without worrying about in-progress functions failing completely or panicking.
Change management across versions
Given the above, there are a few strategies for change management:
- Adding new steps to a function is generally safe. New steps will be executed when the functions re-run (after a step completes). Imagine a function has steps
[A, B, C]
. When you add a new stepZ
in-between the first steps, the executor will run steps[A, Z, B, C]
and log a warning. The caveat here is that you must take care to ensure that the new step can run out-of-order and doesn't reference undefined variables. Note that stepB
andC
will not automatically re-run. Instead, a warning will be logged by default. You can change logging a warning and instead permanently fail by enabling strict mode. Failing runs permanently is acceptable, and you can use Replay to bulk-replay permanent failures. - Forcing steps to re-run by changing step IDs. This changes the hash, which forces re-evaluation as the step's state is not found. Note that the SDK will log a warning by default as the order of step execution changes. If you change step
C
's ID toE
, your run's state will expect steps[A, B, C]
to run and instead will see[A, B, E]
. - For complete changes in logic, create a new function which subscribes to the same triggering event. Update the existing function's trigger to include an
if
expression to only handle events before a certain timestamp (for example:event.ts < ${EPOCH_MS}
). Then create a new function with the updated logic, the same original event trigger, and a newid
(for example:process-upload-v2
). This allows you to safely transition to the new function without losing any data. A caveat is that this creates a new function in your app and therefore the Inngest dashboard.
Conclusion
Understanding how determinism works should allow you to gracefully evolve functions over time. Consider these strategies when making changes to long-running functions to ensure that they can run successfully to completion over time.