From efefcff7b2838faa13ec6aa5420b551b259b0330 Mon Sep 17 00:00:00 2001 From: Jason Stiebs Date: Wed, 15 Jul 2026 13:25:23 -0500 Subject: [PATCH] Expose required mirror partial-write ambiguity MirrorStore writes the authoritative backend before its required mirror. If the mirror rejects that write, the adapter returns a plain mirror_failed error even though the authoritative value and ETag have already changed. A caller retrying with its previous ETag then conflicts, but the error contains no indication that anything committed.\n\nAdd a deterministic integration test that records the original ETag, rejects the mirror write, proves that a retry with the old ETag conflicts, and fails because the ordinary error hid the committed authoritative value. The test is intentionally red pending a decision on the contract.\n\nPossible fixes are to return typed partial success with the authoritative result and new ETag, durably queue and repair the mirror before reporting success, compensate/roll back the authoritative write when possible, or narrow the meaning of required mode so callers do not interpret this as an atomic failure. --- test/mirror_backend_integration_test.exs | 40 +++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/test/mirror_backend_integration_test.exs b/test/mirror_backend_integration_test.exs index 7993222..dc8e3f3 100644 --- a/test/mirror_backend_integration_test.exs +++ b/test/mirror_backend_integration_test.exs @@ -28,7 +28,8 @@ defmodule DurableServer.MirrorBackendIntegrationTest do do: StorageBackend.list_all_objects_stream(delegate, prefix, opts) @impl true - def put_object(_state, _key, _data, _opts), do: {:error, :promotion_write_rejected} + def put_object(state, _key, _data, _opts), + do: {:error, Map.get(state, :reason, :promotion_write_rejected)} @impl true def delete_object(%{delegate: delegate}, key), do: StorageBackend.delete_object(delegate, key) @@ -161,6 +162,43 @@ defmodule DurableServer.MirrorBackendIntegrationTest do StorageBackend.get_object(read_only_mirror, key) end + test "required mirror failure does not hide a committed authoritative write", %{ + primary: primary, + secondary: secondary + } do + key = "mirror/required_partial_write" + + assert {:ok, %{etag: original_etag}} = + StorageBackend.put_object(primary, key, "value-v1") + + assert {:ok, _obj} = StorageBackend.put_object(secondary, key, "value-v1") + + rejecting_secondary = + StorageBackend.new(RejectingPutBackend, %{ + delegate: secondary, + reason: :mirror_write_rejected + }) + + required_mirror = mirror_backend(primary, rejecting_secondary) + + assert {:error, {:mirror_failed, :mirror_write_rejected}} = + StorageBackend.put_object(required_mirror, key, "value-v2", + etag: original_etag, + max_retries: 0 + ) + + assert {:ok, %{body: authoritative_body}} = StorageBackend.get_object(primary, key) + + assert {:error, :conflict} = + StorageBackend.put_object(primary, key, "retry-with-old-etag", + etag: original_etag, + max_retries: 0 + ) + + assert authoritative_body == "value-v1", + "ordinary mirror failure hid committed authoritative value #{inspect(authoritative_body)}" + end + test "mirror writes propagate put and delete to secondary", %{ primary: primary, secondary: secondary,