Failed async-reference alias swap rollback is not persisted across restart
Summary
When an alias used by an async_reference is swapped to an incompatible collection, Typesense returns an error and appears to roll the alias back in memory. However, the rejected alias target is persisted. After restarting Typesense, the alias points to the rejected target and JOINs using the reference break.
This was reproduced twice, including once in a fresh isolated container.
Environment
- Typesense:
31.0.rc14 - Docker image:
typesense/typesense:31.0.rc14 - Image digest:
sha256:6f63c3de844ce3c399dee23e04530f38e80a7462de621f1c8444245325320baa - Single-node Docker deployment
Reproduction
- Create
parent_v1with acodestring field. - Create alias
parent -> parent_v1. - Create
childrenwith:
{
"name": "parent_code",
"type": "string",
"reference": "parent.code",
"async_reference": true
}- Index
parent_v1document{"id":"p1","code":"ok"}and child document{"id":"c1","parent_code":"ok"}. - Create
parent_badwith only anotherstring field, so it does not contain the referencedcodefield. - Attempt to swap the alias:
PUT /aliases/parent
{"collection_name":"parent_bad"}Typesense returns HTTP 500:
{"message":"Referenced field `code` not found in the collection `parent_bad`."}- Before restart, verify that rollback appears successful:
GET /aliases/parentreturnsparent_v1.- The JOIN still returns the child and referenced parent.
- The child helper field still resolves to sequence ID
0.
- Restart Typesense and check again.
Actual behavior after restart
GET /aliases/parentnow returnsparent_bad.- The child's
parent_code_sequence_idbecomes the unresolved sentinel4294967295. - The JOIN fails with:
Referenced collection `parent` not found.Relevant startup log:
Field `code` not found in the collection `parent_bad` which is referenced in `children.parent_code`.Code pointer / possible fix direction
The risky ordering appears to be in CollectionManager::upsert_symlink():
- The candidate alias target is written to the store and published in memory.
- Only afterwards does the fallible deferred-reference resolution / target rebind run.
- The missing-field failure is detected while building the rebind plan, after the rejected target has already been persisted.
- The failure path does attempt to write the previous target back, but the restart reproduction shows that this rollback is not durable.
A possible direction is to preflight the full target-rebind plan (including referenced-field validation) before persisting the candidate alias, or otherwise delay the durable alias write until rebind succeeds. A regression test should reload/restart from the same store after a failed swap, not only assert the in-memory alias immediately after the error.
Expected behavior
If the alias update returns an error, the rollback should be durable. Both the in-memory alias and persisted alias should continue pointing to parent_v1, including after restart or snapshot restoration.
Impact
A deployment can receive an error and verify that the old alias and JOINs still work, yet a later restart silently activates the rejected target and breaks reference-based searches. This makes the rollback response misleading and turns a rejected schema deployment into a delayed outage.
Related
- #2827
- PR #2963, which added alias-target rebind validation and rollback behavior
The original successful alias-swap scenarios from #2827 work correctly in 31.0.rc14; this report is specifically about persistence after a failed swap.
Source: typesense/typesense