mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-31 01:10:41 +01:00
s3vectors: bound GetVectors and DeleteVectors key lists (#12238)
* s3vectors: bound GetVectors and DeleteVectors key lists Both forwarded req.Keys to the engine unbounded; AWS caps these at 500, the same window a PutVectors batch shares. * s3vectors: cap GetVectors at the AWS 100-key limit GetVectors allows at most 100 keys per request while DeleteVectors allows 500, so give vectorKeysBounds the per-call maximum instead of sharing the put-batch window. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3vectors: assert out-of-window key lists never reach the engine Record get/delete calls on the fake engine and cover both handlers rejecting empty and oversized lists with 400 ValidationException. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
a2496ad7ae
commit
72ad975803
2 files changed
+78
No files matched your search
@@ -1301,12 +1301,25 @@ func toOutputVectors(vectors []*vector_pb.Vector, returnData, returnMetadata boo
|
||||
return out
|
||||
}
|
||||
|
||||
// vectorKeysBounds enforces the AWS 1..max key list window; GetVectors is
|
||||
// capped lower than a PutVectors or DeleteVectors batch.
|
||||
func vectorKeysBounds(keys []string, max int) error {
|
||||
if len(keys) == 0 || len(keys) > max {
|
||||
return fmt.Errorf("keys must be between 1 and %d", max)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) handleGetVectors(w http.ResponseWriter, r *http.Request) {
|
||||
var req GetVectorsRequest
|
||||
if err := readJSON(r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "ValidationException", err.Error())
|
||||
return
|
||||
}
|
||||
if err := vectorKeysBounds(req.Keys, vectorsMaxGetKeys); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "ValidationException", err.Error())
|
||||
return
|
||||
}
|
||||
arns, err := s.resolveIndexARNs(req.VectorBucketName, req.VectorBucketArn, req.IndexName, req.IndexArn)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "ValidationException", err.Error())
|
||||
@@ -1409,6 +1422,7 @@ func (s *Server) handleListVectors(w http.ResponseWriter, r *http.Request) {
|
||||
// pages like a plain one does; vectorsMaxTopK is the AWS QueryVectors limit.
|
||||
const (
|
||||
vectorsDefaultPage = 500
|
||||
vectorsMaxGetKeys = 100
|
||||
vectorsMaxPutBatch = 500
|
||||
vectorsMaxPage = 1000
|
||||
vectorsMaxListPage = 500
|
||||
@@ -1469,6 +1483,10 @@ func (s *Server) handleDeleteVectors(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusBadRequest, "ValidationException", err.Error())
|
||||
return
|
||||
}
|
||||
if err := vectorKeysBounds(req.Keys, vectorsMaxPutBatch); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "ValidationException", err.Error())
|
||||
return
|
||||
}
|
||||
arns, err := s.resolveIndexARNs(req.VectorBucketName, req.VectorBucketArn, req.IndexName, req.IndexArn)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "ValidationException", err.Error())
|
||||
|
||||
@@ -27,6 +27,8 @@ type fakeVectors struct {
|
||||
rows []*vector_pb.Vector
|
||||
created []*vector_pb.CreateIndexRequest
|
||||
puts []*vector_pb.PutVectorsRequest
|
||||
gets []*vector_pb.GetVectorsRequest
|
||||
deletes []*vector_pb.DeleteVectorsRequest
|
||||
}
|
||||
|
||||
func newFakeVectors(n int) *fakeVectors {
|
||||
@@ -73,9 +75,15 @@ func (f *fakeVectors) CreateIndex(ctx context.Context, req *vector_pb.CreateInde
|
||||
}
|
||||
|
||||
func (f *fakeVectors) GetVectors(ctx context.Context, req *vector_pb.GetVectorsRequest, opts ...grpc.CallOption) (*vector_pb.GetVectorsResponse, error) {
|
||||
f.gets = append(f.gets, req)
|
||||
return &vector_pb.GetVectorsResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeVectors) DeleteVectors(ctx context.Context, req *vector_pb.DeleteVectorsRequest, opts ...grpc.CallOption) (*vector_pb.DeleteVectorsResponse, error) {
|
||||
f.deletes = append(f.deletes, req)
|
||||
return &vector_pb.DeleteVectorsResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeVectors) PutVectors(ctx context.Context, req *vector_pb.PutVectorsRequest, opts ...grpc.CallOption) (*vector_pb.PutVectorsResponse, error) {
|
||||
f.puts = append(f.puts, req)
|
||||
return &vector_pb.PutVectorsResponse{}, nil
|
||||
@@ -907,3 +915,55 @@ func TestDeleteVectorBucketPolicyRemovesLegacyDoc(t *testing.T) {
|
||||
t.Fatalf("empty shared policy should be removed: %s", doc)
|
||||
}
|
||||
}
|
||||
|
||||
func keys(n int) []string {
|
||||
out := make([]string, n)
|
||||
for i := range out {
|
||||
out[i] = "k"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestVectorKeysBounds(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
keys []string
|
||||
max int
|
||||
ok bool
|
||||
}{
|
||||
{"empty", nil, vectorsMaxGetKeys, false},
|
||||
{"one", []string{"a"}, vectorsMaxGetKeys, true},
|
||||
{"get full", keys(vectorsMaxGetKeys), vectorsMaxGetKeys, true},
|
||||
{"get over", keys(vectorsMaxGetKeys + 1), vectorsMaxGetKeys, false},
|
||||
{"delete full", keys(vectorsMaxPutBatch), vectorsMaxPutBatch, true},
|
||||
{"delete over", keys(vectorsMaxPutBatch + 1), vectorsMaxPutBatch, false},
|
||||
} {
|
||||
if err := vectorKeysBounds(tc.keys, tc.max); (err == nil) != tc.ok {
|
||||
t.Fatalf("%s: accepted=%v, want %v", tc.name, err == nil, tc.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An out-of-window key list is rejected before the engine sees it.
|
||||
func TestVectorKeysBoundsRejectsBeforeEngine(t *testing.T) {
|
||||
f := newFakeVectors(0)
|
||||
s := &Server{vectors: f}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
handler http.HandlerFunc
|
||||
payload interface{}
|
||||
}{
|
||||
{"get empty", s.handleGetVectors, &GetVectorsRequest{}},
|
||||
{"get over", s.handleGetVectors, &GetVectorsRequest{Keys: keys(vectorsMaxGetKeys + 1)}},
|
||||
{"delete empty", s.handleDeleteVectors, &DeleteVectorsRequest{}},
|
||||
{"delete over", s.handleDeleteVectors, &DeleteVectorsRequest{Keys: keys(vectorsMaxPutBatch + 1)}},
|
||||
} {
|
||||
w := postJSONAs(t, "", tc.handler, tc.payload)
|
||||
if w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), "ValidationException") {
|
||||
t.Fatalf("%s: %d %s, want 400 ValidationException", tc.name, w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
if len(f.gets)+len(f.deletes) != 0 {
|
||||
t.Fatalf("engine saw %d get/%d delete calls, want none", len(f.gets), len(f.deletes))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user