Upgrading a Live Mesh¶
Order of operations, version-skew guarantees, schema migrations, and in-flight job safety for upgrading a mesh that is already serving traffic. For local development you can restart freely; this page is about upgrading a running deployment without dropping in-flight work.
Upgrade order for the Helm charts: 3.3.x → 3.4.x → 3.5.0
Do not upgrade the mcp-mesh-core chart from 3.3.x straight to 3.5.0. Chart 3.5.0 changes namespaceCreate to default false, which drops the release's Namespace object from the rendered manifest — and Helm deletes a resource that leaves the manifest, cascading to every agent, Service, Secret and PVC in the namespace. What makes that removal safe is helm.sh/resource-policy: keep on the live Namespace, and chart 3.4.0 is the first version that puts it there.
- On chart 3.4.x already: nothing to do — the annotation is on the live namespace, so 3.5.0 removes the object without deleting anything.
- On chart 3.3.x or older: upgrade to the latest 3.4.x first, verify with
kubectl get ns <ns> -o jsonpath='{.metadata.annotations}', then upgrade to 3.5.0. To jump in one step instead, pin--set namespaceCreate=truefor that upgrade, verify the annotation, and drop the pin on a second upgrade.
--reuse-values does not protect you: it replays the values you supplied, and a release that simply took the old default has nothing to replay. Details in Namespace handling.
Source migration required for 3.4.0 (Java and TypeScript)
3.4.0 aligns every dependency-injection site on positional binding. Java @MeshRoute / @MeshA2A and TypeScript mesh.route / mesh.a2a.mount changed how declared dependencies reach handler parameters. Python is unchanged, and Java @MeshTool and TypeScript addTool were already positional — but @MeshInject is now honoured on @MeshTool parameters, where it was previously ignored, so a value that disagrees with its parameter's position fails at boot. Nothing on the wire changed, so no coordinated rollout is needed — but handlers need a source edit. Do not filter by dependency count: every TypeScript handler still taking a capability-keyed object converts whatever it declares, and a Java handler with one dependency is exempt only when its @MeshInject already names that capability, or carries none. See Migrating to positional DI.
Recommended order¶
Upgrade the registry first, then the agents.
The registry runs its schema migration at startup (see Schema migrations), so bringing it up first means the new columns exist before any newer-SDK agent tries to use them. The compatibility contract (see Version skew) holds in both directions, so agents can trail the registry by a version with no hard failure — but registry-first is the order with the fewest moving parts: migrate once, then roll agents at your own pace.
Version skew¶
A mixed-version mesh always converges to the older side's semantics — there is no hard failure when registry and SDK versions differ. This is the compatibility contract:
- Newer registry, older-SDK agents. Older agents post epoch-less job deltas. The registry validates these owner-only, with no fencing — the legacy path. Event reads without identity parameters are served anonymously (unchanged). When an agent sends no identity headers, the registry's identity accessors read null and it falls back to legacy handling.
- Older registry, newer-SDK agents. The newer SDK's epoch and identity headers are unknown fields to an older registry and are ignored — so the same legacy, owner-only behavior results.
In both directions the behavior degrades to the pre-fencing legacy path by design. The only thing you lose in a skewed mesh is claim-epoch fencing (dual-ownership protection) — which is exactly why an upgrade should drain in-flight jobs rather than rely on fencing across the restart (see In-flight job safety).
Schema migrations¶
The registry runs ent automigrate at startup — it reconciles the database schema against the compiled models on every boot. New releases add columns; automigrate applies them automatically. There is no manual migration step.
Forward upgrade (new registry against an existing database) is purely additive — new columns are created, existing data is untouched.
Rollback caveat
Automigrate runs with drop-column and drop-index enabled. If you downgrade the registry binary, its older schema no longer declares the newer columns, so the startup migration will drop them. The older binary operates correctly afterward (it never referenced those columns), but the drop is destructive — any state held in the newer columns (for example the claim-epoch/lease bookkeeping for in-flight jobs) is lost and does not come back if you later re-upgrade. Treat a registry downgrade as forward-only-safe: fine for the running version, not a non-destructive rollback.
In-flight job safety¶
Job rows persist across a registry restart, but leases cannot renew while the registry is down. During downtime:
- job completions retry against the unreachable registry,
- event-gated (
input_required) jobs freeze rather than drain — their gates stall because consumer answers cannot be posted, - lease clocks keep advancing, so a lease can expire across the outage window.
On restart the orphan/expired-lease reclaim sweep races the owner's first renewing poll: whichever lands first wins. If the sweep reclaims first, a newer-SDK owner's next delta carries a stale epoch and is fenced (claim_superseded) — safe. But an older, unfenced SDK that is re-claimed by the same instance can produce dual ownership (double execution), because epoch-less deltas get owner-only validation with no fencing.
Therefore: drain before a live upgrade rather than pulling the registry out from under running jobs.
# 1. Pause new claims; block until every running job releases its owner
meshctl registry drain --wait
# 2. Upgrade / restart the registry (running jobs have finished; queue is safe)
# 3. Resume normal dispatch — queued jobs become claimable again (FIFO)
meshctl registry resume
While draining, new claims are paused (queued jobs stay queued — no attempt is burned), running jobs keep renewing their leases and complete normally, and submissions are still accepted for after resume. drain --wait returns once live_claims reaches zero, and aborts with an error if the registry stops draining mid-wait (a concurrent resume or restart) instead of falsely reporting the window is safe.
Note
An event-gated job parked in input_required counts as a live claim and holds the drain open until it is answered or completes. Answer or cancel such jobs before draining if you need a bounded window.
Multi-replica (HA) deployments
Drain state is per-replica and in-memory — it is not shared across replicas and a registry restart clears it. In an HA topology a load balancer may route each meshctl registry command to a different replica, so registry status can flap and a single registry drain pauses only the replica that served the request. Before an HA upgrade, drain every replica by pointing --registry-url at each replica's address directly.
Separate admin port
If the registry runs a dedicated admin port (MCP_MESH_ADMIN_PORT), the /admin/drain endpoints live only on that port. Pass the admin address: meshctl registry drain --registry-url http://<host>:<admin-port>.
Run meshctl man registry for the full drain command reference (status, --wait-timeout, --poll-interval); the Registry concept page covers the registry's role in the mesh.
Helm mechanics¶
For Kubernetes deployments, upgrade in place with helm upgrade — not by uninstalling and reinstalling.
# Preserve the existing release's env/values across the upgrade
helm upgrade <release> <chart> --reuse-values
# Verify the effective values before and after
helm get values <release>
--reuse-valuescarries forward the environment configuration set at install time so an upgrade does not silently reset it. Confirm withhelm get valuesthat the values you expect are still present.- Do not use
helm uninstallas an upgrade mechanism. To change the core,helm upgradethe existing release — uninstalling and reinstalling discards the release's history and values along with the running workloads, for no benefit. helm uninstallis still a legitimate, explicit teardown. It removes the core workloads (registry, PostgreSQL, Redis, and any observability components), takes the registry offline with them, and leaves theNamespacein place (see the next bullet). The registry's contents are derived state — agents re-register on their next heartbeat, so the registry repopulates itself and the cost is a transient topology gap, not data loss. Agents keep serving while it is down: resolved dependencies are never cleared on a registry disconnect (a deliberate resilience invariant). What pauses is topology detection — discovering new capabilities, re-resolving changed ones, and looking up an agent a client has not already resolved. What is not derived is application data: if your own workloads used the bundled PostgreSQL or Redis for their own storage, that data is yours to protect before you tear anything down.helm uninstallno longer reclaims Grafana's data volume. The Grafana PVC picks up a"helm.sh/resource-policy": keepannotation on upgrade, so Helm skips it on uninstall —grafana.persistence.enableddefaults totrue, and the dashboards, annotations, users, and API keys ingrafana.dbare not derived from anything the mesh can replay. Previously that volume was deleted along with the release. A reinstall under the same release name adopts the existing claim, data intact; reclaim the storage deliberately withkubectl delete pvc <release>-mcp-mesh-grafana-pvc -n <ns>. This lands on a plainhelm upgradewith no pod restart — the claim never leaves the rendered manifest, so it is a metadata-only patch.- Tempo's PVC is deleted by this upgrade, and that is intended.
mcp-mesh-tempo.tempo.persistence.enablednow defaults tofalse, so the claim leaves the rendered manifest and Helm reclaims it on the nexthelm upgrade— Tempo comes back on anemptyDir. What is lost is the volume and up toretention(default1h) of buffered traces: Tempo's volume is a rolling buffer under active retention, not durable storage, and a live install measured 3.1MB in it after 25 hours of uptime. Nothing else on the release is affected and ingestion resumes normally. The default changed because theReadWriteOnceclaim it required is a real rollout constraint on a single-replica Deployment — that is what deadlocked the chart on multi-node clusters — paid for minutes of traces. To keep the volume, pin the old value in the same upgrade:--set mcp-mesh-tempo.tempo.persistence.enabled=true; setting it afterward provisions a new, empty claim rather than recovering the reclaimed one. This is the deliberate opposite of Grafana's treatment above — Grafana's volume holds state you authored and nothing can replay. - The core chart no longer renders a
Namespace.namespaceCreatenow defaults tofalse:helm install --create-namespace,kubectl create namespace, or Argo CD'sCreateNamespace=truecreates the namespace, and the release does not own it. The old default could not be installed at all on Helm 3, or against any pre-created namespace on any client — Helm writes the release secret into the-nnamespace before applying the manifest, so a chart-templatedNamespacecan only re-declare a namespace that already exists, which Helm's ownership check rejects. Read the upgrade-order warning at the top of this page before upgrading an existing release, and see Namespace handling for the--take-ownershippath if you want the release to own its namespace deliberately. helm.sh/resource-policyis reserved incommonAnnotations. When the chart does render aNamespace, itskeepis the only thing standing betweenhelm uninstalland everything in that namespace, and acommonAnnotationsvalue would override it on last-wins. So the chart fails the render when the key is set to anything butkeep— remove it fromcommonAnnotations, or set it tokeep, before upgrading. This check runs whether or notnamespaceCreateis on.- What the namespace deletion actually looks like, if you skip the ordering above. The upgrade reports
STATUS: deployedand exit 0 while deleting the namespace and everything in it. Whenglobal.namespacematches the namespace passed to-n, it is also unrecoverable: Helm keeps the release secret in the-nnamespace, which is the one being deleted. A retry while the namespace drains fails with... is forbidden: ... because it is being terminated, and once it is gonehelm historyreportsrelease: not found. Helm readshelm.sh/resource-policyoff the live object, which is why the annotation has to have landed on a previous upgrade — the chart cannot check for it at render time, becauselookupreturns nothing underhelm template, the way Argo CD and every other GitOps renderer evaluates it.