Programming & Compilers

Runtime code replacement: calls, state and version compatibility

Runtime code replacement must coordinate active calls, retained callbacks and live state migration. Trace compatibility, old-code retirement and rollback.

By Republeq Editorial · 10 min read ·
Runtime code replacement illustrated through new and active calls, retained callbacks, alternative execution policies, guarded live state migration, old-code retirement and rollback limits.

Installing a new function leaves several questions open. A call may already be executing the old function. A callback may still point to it. An object may contain a value whose meaning changed in the replacement. runtime code replacement therefore needs a rule for admitting execution to the new version and a rule for handling the state that execution receives.

Use a stipulated service with two versions of an account module. Version A stores balances as integer cents. Version B stores an amount together with an explicit unit tag. Both versions have an update function, and the service retains accounts between requests. This example makes a compatibility problem visible without relying on a particular deployment tool or claiming that every running program can be updated safely.

Build a graph of version-qualified execution

Before replacement, the service's public update entry points to update_A. One request has an active frame inside update_A. A scheduled callback retains a reference to fee_A, and an account object carries state in format A. The frame also has a return address into version A code. These are separate relationships: changing the public entry affects the first relationship, while the other relationships can remain intact.

Label each code node by its version. Label each state object by the format its authoritative reader understands. Draw the edges for new calls, currently active frames, retained callbacks, and object access. The graph should show where a call obtains its executable target and where that target obtains its state. A version label on a release file cannot answer either question by itself.

Assume that replacement redirects the public update entry to update_B. New requests now enter B. The request already executing A continues from its existing program counter, and its return address still leads into A. If that request calls a helper through an entry that now selects B, the call chain becomes A to B to A. Whether the chain is valid depends on the helper's input and output contract.

The bytecode-verification guide follows stack and local-slot categories through reachable instructions, checking whether incoming states meet each instruction's contract. That is relevant background for execution version consistency: an executable target must accept the state supplied to it. Its verification example concerns a fixed program, so it does not establish that redirecting a live entry preserves an old caller's value meanings or return contract.

Choose an execution version consistency policy

One policy lets existing calls finish entirely on A and sends new calls to B. For this to work, the old calls need access to the A code and the compatible state they still require. Their helper calls may need to remain tied to A as well. Otherwise, an apparently preserved top-level activation can cross into B halfway through its work.

Another policy permits switching only at explicit boundaries. A long-running loop might check for an update between messages, finish its current message, transform its state, and enter the B loop. The boundary supplies a location where the old activation has reached an agreed state. The runtime must define which references survive the switch and how the next activation is selected.

A third policy waits for quiescence. It stops admitting relevant new work and waits until no affected operation remains active. The transition can wait for all affected access to end, provided admission is closed for every operation that could recreate it. A stuck operation can prevent the boundary from being reached. Callbacks or references outside that boundary still need separate treatment.

execution version consistency means satisfying the selected policy across the actual call and state graph. It can permit carefully specified coexistence. Requiring every frame to have the same version label is one possible policy, but compatibility sometimes allows old and new frames to interact. The policy must say which interactions are valid and provide checks that can distinguish them from accidental mixing.

Trace a mixed-version interface failure

Suppose update_A computes a new balance of 12500 cents and passes the integer to a helper named persist. Replacement has redirected persist to persist_B. The B helper expects a record with fields amount and unit, not a bare integer. An explicit type check can reject the call. If a less explicit interface treats the integer as a different unit, the same call can produce a wrong stored meaning instead.

Keeping the parameter count unchanged does not preserve this contract. Compatibility includes the interpretation of values, as well as their representation. The return path also matters. If a B helper returns a tagged amount and an A caller assumes an integer, a failure can occur after the helper completed correctly according to its own version.

Several policies could prevent this particular mismatch. An A caller could remain bound to persist_A until its request completes. A compatibility adapter could convert the A argument into an admitted B record and translate the result back. Alternatively, replacement could wait until no A request can reach persist. Each approach has a different boundary to verify. An adapter must convert the meaning correctly; preserving an old helper must preserve its required state access.

A small callback experiment can record the retained target, redirect the public entry, and then inspect the target invoked by the callback. The Lenovo IdeaPad 15.6-inch FHD Touch laptop is a computing-workspace example for such an exercise. Its listing specifies a Core i5-1334U processor, 16GB DDR4 memory and 512GB PCIe storage. Whether the selected runtime permits the experiment depends on that runtime's call rules, not those laptop specifications.

Live state migration needs an authoritative format

For the stipulated account object, define the A state as balance_cents = 12500. Define the B state as amount = 12500 and unit = "cent". The migration preserves the amount's meaning while making its unit explicit. A precondition is that the source value really is an integer number of cents. Treating an already converted value as an A value could cause a second conversion or attach an incorrect unit.

A migration protocol needs a way to know which format is authoritative. For example, the object can have a format marker that readers check, with access serialized during conversion. Prepare the B representation, validate it, and change the authoritative marker only at the boundary specified by the protocol. Readers must not interpret a partially constructed B representation as a complete object.

That description stipulates the access discipline; it does not prove that any particular runtime supplies it. If an A frame retains direct access to the object while migration proceeds, the protocol has another writer or reader to account for. It may need to wait for that access to end, maintain a compatible view, or restrict which operations can run concurrently. Merely locking the migration function is insufficient when other access ignores that lock.

live state migration also covers objects that have not yet reached the conversion boundary. An eager policy converts all affected objects before B is admitted. A lazy policy converts an object when B first accesses it. With lazy conversion, B must recognize the old format and perform or request an admissible transition. The system may therefore contain both A-format and B-format objects for a time, even after new calls select B.

In either policy, failures require an explicit object status. A conversion that fails validation can leave the A state authoritative and the prepared B state unused. A conversion that has already committed cannot be described as untouched A state. Recording where each object stands prevents a later reader from guessing its format from the installed code version.

A retained callback can bypass the redirected entry

Return to the scheduled callback that retains fee_A. The scheduler can invoke that reference directly after the public update entry selects B. The reference is an edge outside the entry point that was redirected. If the callback now receives a B-format account object, it encounters the same kind of compatibility problem as an old active frame.

A runtime can define callbacks to resolve a current target by name at invocation time, or it can let them retain a particular executable reference. These policies have different consequences. With late resolution, the argument contract still needs checking against the selected target. With a retained target, the old code and its expected state may need to survive until the callback is replaced, cancelled, or completed.

The audit must include callbacks held by components outside the immediate module. A timer, event registry, or another object may own the reference. Updating one module's internal list does not establish that those external references have disappeared. The retirement decision needs evidence about the reference owners and their invocation rules.

The register-allocation guide follows logical values through machine registers and spill slots, showing how control-flow paths determine which values must survive. Compare that preservation problem with live state migration. Allocation keeps the same program's required values available; the replacement example must additionally convert an object into the new version's admitted format. A successful storage assignment supplies no proof that this format conversion is correct.

What a documented runtime example establishes

Erlang's code-loading documentation describes module code as current or old, with both variants able to execute concurrently. Fully qualified function calls select current code. Processes can remain in old code, and a documented loop example switches by making a fully qualified call at a chosen point. This is a concrete instance of version selection through a call boundary.

The same documentation describes consequences when another module version is loaded and old code is purged, including termination of processes lingering in that code. Those rules belong to Erlang's documented model. They should not be generalized into a claim that every runtime supports exactly two versions or applies the same purge behavior.

OTP release handling also describes advanced updates involving state changes. The release handler suspends relevant processes, requests code changes, and resumes them; behavior callbacks handle the specified change. Such facilities provide a defined mechanism for arranging a transition. Application-specific conversion logic must still preserve the meaning of its own state and meet the mechanism's requirements.

Retirement and rollback follow the surviving graph

Old code can be retired safely under the stipulated policy when no active frame still needs to execute or return into it, no retained reference can invoke it, and no admitted future path depends on it. Compatible state access must also be settled. Installing B supplies no direct evidence that these conditions hold. A frame count alone also misses an inactive callback that can create a new A activation later.

Consider an interrupted transition after B has accepted one request and committed a B-format account. Redirecting the entry back to A does not convert that account back. If A cannot read its format, the reversal creates another incompatibility. A rollback plan needs either a valid reverse migration, a compatible A reader, or a recovery policy that preserves the new state until a version able to interpret it can run.

The ASUS Blue 14 FHD laptop provides another computing-workspace example; the catalogue identifies it as a laptop with a 14-inch FHD display. A transition table prepared on that workspace should record call admission, object conversion and old-code retirement independently. Recovery behavior comes from the running program and its saved transition record, rather than the device used to inspect them.

Rollback can also encounter effects outside the replaceable state. If the B request sent an acknowledgement or performed an irreversible external action, restoring an earlier object image cannot erase what another participant observed. The protocol must specify how that committed effect is reconciled. A saved code artifact gives a way to reload code; it does not by itself reverse all actions of that code.

A replacement record identifies the call policy, each affected object's authoritative format and the surviving executable references. Record the point at which admission changed and the conversions that committed before an interruption. If a callback still retains A, keep its required code and compatible state available or prevent it from running through an established cancellation rule. If an object has committed to B, recovery must preserve a reader that understands that state or perform a verified reverse conversion.

Runtime code replacement, state and version compatibility