fix(volume): return conflict and explicit proof for read-only upload refusals (#12236)

* fix(volume): report read-only HTTP refusals in Rust

* fix(volume): mirror HTTP write refusal proof in Go

* test(volume): cover HTTP read-only refusal parity

* test(volume): cover local refusal after remote append

* fix(volume): honor decoded replica controls before retry proof

* fix(operation): reassign proven HTTP conflict refusals

* fix(operation): stop same-target retries on HTTP conflict

* fix(volume): replace replica marker when forwarding requests

* fix(java): gate conflict reassignment on refusal proof

* fix(volume): preserve authentication query encoding in replication

* fix(operation): stop unproven conflicts in forever retries
This commit is contained in:
Chris Lu authored and GitHub committed 2026-10-29 23:33:56 -07:00
1 parent 665659097d
commit 94e9b8a49a
15 files changed
+1008 -72

No files matched your search

@@ -43,7 +43,6 @@ func TestReplicatedUploadSucceedsImmediatelyAfterAllocate(t *testing.T) {
}
client := framework.NewHTTPClient()
fid := framework.NewFileID(volumeID, 881001, 0x0B0C0D0E)
payload := []byte("replicated-upload-after-allocate")
// The master only learns about replica locations through volume-server
@@ -57,21 +56,24 @@ func TestReplicatedUploadSucceedsImmediatelyAfterAllocate(t *testing.T) {
t.Fatalf("master did not report 2 replica locations for volume %d within deadline", volumeID)
}
uploadResp := framework.UploadBytes(t, client, clusterHarness.VolumeAdminURL(0), fid, payload)
_ = framework.ReadAllAndClose(t, uploadResp)
if uploadResp.StatusCode != http.StatusCreated {
t.Fatalf("replicated upload expected 201, got %d", uploadResp.StatusCode)
}
for i, query := range []string{"", "?type=client", "?type=", "?%74ype=client", "?type=client&type=old", "?type=client&fsync=%74rue"} {
fid := framework.NewFileID(volumeID, 881001+uint64(i), 0x0B0C0D0E)
uploadResp := framework.UploadBytes(t, client, clusterHarness.VolumeAdminURL(0), fid+query, payload)
_ = framework.ReadAllAndClose(t, uploadResp)
if uploadResp.StatusCode != http.StatusCreated {
t.Fatalf("replicated upload expected 201, got %d", uploadResp.StatusCode)
}
replicaReadURL := clusterHarness.VolumeAdminURL(1) + "/" + fid
var replicaBody []byte
if !waitForHTTPStatus(t, client, replicaReadURL, http.StatusOK, 10*time.Second, func(resp *http.Response) {
replicaBody = framework.ReadAllAndClose(t, resp)
}) {
t.Fatalf("replica did not become readable within deadline")
}
if string(replicaBody) != string(payload) {
t.Fatalf("replica body mismatch: got %q want %q", string(replicaBody), string(payload))
replicaReadURL := clusterHarness.VolumeAdminURL(1) + "/" + fid
var replicaBody []byte
if !waitForHTTPStatus(t, client, replicaReadURL, http.StatusOK, 10*time.Second, func(resp *http.Response) {
replicaBody = framework.ReadAllAndClose(t, resp)
}) {
t.Fatalf("replica did not become readable within deadline")
}
if string(replicaBody) != string(payload) {
t.Fatalf("replica body mismatch: got %q want %q", string(replicaBody), string(payload))
}
}
}
@@ -1,12 +1,18 @@
package volume_server_http_test
import (
"bytes"
"context"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"github.com/seaweedfs/seaweedfs/test/volume_server/framework"
"github.com/seaweedfs/seaweedfs/test/volume_server/matrix"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
)
func TestWriteInvalidVidAndFidReturnBadRequest(t *testing.T) {
@@ -72,3 +78,99 @@ func TestWriteMalformedMultipartAndMD5Mismatch(t *testing.T) {
t.Fatalf("content-md5 mismatch response should mention Content-MD5, got %q", string(md5MismatchBody))
}
}
func TestWriteReadOnlyReportsConflictAndSafeRefusal(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
cluster := framework.StartVolumeCluster(t, matrix.P1())
conn, volume := framework.DialVolumeServer(t, cluster.VolumeGRPCAddress())
defer conn.Close()
const volumeID = uint32(99)
framework.AllocateVolume(t, volume, volumeID, "")
client := framework.NewHTTPClient()
fid := framework.NewFileID(volumeID, 773001, 0x1A2B3C4D)
payload := []byte("existing bytes must survive a refused overwrite")
response := framework.DoRequest(t, client, newUploadRequest(t, cluster.VolumeAdminURL()+"/"+fid, payload))
body := framework.ReadAllAndClose(t, response)
if response.StatusCode != http.StatusCreated {
t.Fatalf("initial upload: %d %s", response.StatusCode, body)
}
if _, err := volume.VolumeMarkReadonly(context.Background(), &volume_server_pb.VolumeMarkReadonlyRequest{VolumeId: volumeID}); err != nil {
t.Fatal(err)
}
dat := filepath.Join(cluster.BaseDir(), "volume", "99.dat")
before, err := os.Stat(dat)
if err != nil {
t.Fatal(err)
}
for _, query := range []string{"", "?fsync=true", "?fsync=%74rue", "?type=replicate", "?type=%72eplicate", "?%74ype=replicate", "multipart:first", "multipart:last"} {
t.Run(query, func(t *testing.T) {
request := newUploadRequest(t, cluster.VolumeAdminURL()+"/"+fid+query, []byte("refused replacement"))
if strings.HasPrefix(query, "multipart:") {
var multipartBody bytes.Buffer
writer := multipart.NewWriter(&multipartBody)
if query == "multipart:first" {
if err := writer.WriteField("type", "replicate"); err != nil {
t.Fatal(err)
}
}
file, err := writer.CreateFormFile("file", "data.bin")
if err != nil {
t.Fatal(err)
}
if _, err := file.Write([]byte("refused replacement")); err != nil {
t.Fatal(err)
}
if query == "multipart:last" {
if err := writer.WriteField("type", "replicate"); err != nil {
t.Fatal(err)
}
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
request, err = http.NewRequest(http.MethodPost, cluster.VolumeAdminURL()+"/"+fid, &multipartBody)
if err != nil {
t.Fatal(err)
}
request.Header.Set("Content-Type", writer.FormDataContentType())
}
response := framework.DoRequest(t, client, request)
body := framework.ReadAllAndClose(t, response)
if response.StatusCode != http.StatusConflict {
t.Fatalf("read-only upload: %d %s", response.StatusCode, body)
}
want := ""
if query == "" {
want = "refused-before-append"
}
if got := response.Header.Get("X-Seaweed-Write-Outcome"); got != want {
t.Fatalf("write outcome: %q, want %q", got, want)
}
})
}
after, err := os.Stat(dat)
if err != nil {
t.Fatal(err)
}
if after.Size() != before.Size() {
t.Fatalf("refused write grew .dat from %d to %d", before.Size(), after.Size())
}
response = framework.DoRequest(t, client, mustNewRequest(t, http.MethodGet, cluster.VolumeAdminURL()+"/"+fid))
body = framework.ReadAllAndClose(t, response)
if response.StatusCode != http.StatusOK || !bytes.Equal(body, payload) {
t.Fatalf("existing data changed: %d %q", response.StatusCode, body)
}
if _, err := volume.VolumeMarkWritable(context.Background(), &volume_server_pb.VolumeMarkWritableRequest{VolumeId: volumeID}); err != nil {
t.Fatal(err)
}
response = framework.DoRequest(t, client, newUploadRequest(t, cluster.VolumeAdminURL()+"/"+fid, []byte("writable again")))
body = framework.ReadAllAndClose(t, response)
if response.StatusCode != http.StatusCreated {
t.Fatalf("unsealed upload: %d %s", response.StatusCode, body)
}
if got := response.Header.Get("X-Seaweed-Write-Outcome"); got != "" {
t.Fatalf("successful write marked refused: %q", got)
}
}