mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-31 09:20:50 +01:00
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:
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user