feat(cube-bench): add host-mount flag for create requests#651
Conversation
Review: PR #651Well-structured change. The Findings1. runner.go:49 -- Silently discarded Fix: check the error from 2. runner.go:51-55 -- API response body in error messages Error includes up to 200 chars of API body, which could leak sensitive data into exported reports. 3. runner.go:82 -- Correct fix. Consider also draining: Test coverage gaps
Positive
|
| if cfg.Metadata != "" { | ||
| metaMap := cfg.metadataMap | ||
| if metaMap == nil { | ||
| var err error | ||
| metaMap, err = normalizeMetadata(cfg.Metadata) | ||
| if err != nil { | ||
| r.Err = fmt.Sprintf("metadata: %v", err) | ||
| return r | ||
| } |
There was a problem hiding this comment.
Dead fallback code on benchmark hot path. parseConfig() (main.go:151-162) already guarantees that when cfg.Metadata != "", cfg.metadataMap is populated — or the program exits with os.Exit(1). The nil-check and re-normalization on lines 32–39 are unreachable in production, yet they run on every warmup + measured iteration.
Recommendation: remove lines 32–39 and simplify to:
if cfg.Metadata != "" {
reqBody["metadata"] = cfg.metadataMap
}| server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| if r.Method != http.MethodPost || r.URL.Path != "/sandboxes" { | ||
| t.Fatalf("request = %s %s", r.Method, r.URL.Path) | ||
| } | ||
| if auth := r.Header.Get("Authorization"); auth != "Bearer test-key" { | ||
| t.Fatalf("Authorization=%q, want Bearer test-key", auth) | ||
| } | ||
| if err := json.NewDecoder(r.Body).Decode(&got); err != nil { | ||
| t.Fatalf("decode body: %v", err) | ||
| } |
There was a problem hiding this comment.
t.Fatalf inside httptest handler can hang the test. The handler runs on the server goroutine, not the test goroutine. If t.Fatal/t.Fatalf fires here (lines 21, 24, 27), runtime.Goexit() terminates only the handler goroutine — no HTTP response is written, and the test goroutine blocks forever in client.Do(req). The test times out silently (10 min) instead of failing cleanly.
Recommendation: use t.Errorf in the handler and write a non-2xx status:
if r.Method != http.MethodPost || r.URL.Path != "/sandboxes" {
t.Errorf("request = %s %s", r.Method, r.URL.Path)
w.WriteHeader(http.StatusBadRequest)
return
}| } | ||
| reqBody["metadata"] = metaMap | ||
| } | ||
| body, _ := json.Marshal(reqBody) |
There was a problem hiding this comment.
json.Marshal error silently discarded (pre-existing pattern). While json.Marshal on the current types never fails, a future change to the body structure could make this fail silently. Consider returning the error as defense-in-depth.
| if cfg.Metadata != "" { | ||
| metaMap := cfg.metadataMap | ||
| if metaMap == nil { | ||
| var err error | ||
| metaMap, err = normalizeMetadata(cfg.Metadata) | ||
| if err != nil { | ||
| r.Err = fmt.Sprintf("metadata: %v", err) | ||
| return r | ||
| } | ||
| } | ||
| reqBody["metadata"] = metaMap | ||
| } |
There was a problem hiding this comment.
Dead fallback code on hot path. parseConfig() (main.go:151-162) already guarantees that when cfg.Metadata != "", cfg.metadataMap is populated — or the program has called os.Exit(1). The nil-check and re-normalization on lines 32–39 are unreachable in production.
This matters because benchOne is on the benchmark hot path (every warmup + measured iteration). The dead branch adds a map lookup + conditional branch to every call for no benefit.
Recommendation: remove lines 32–40 and simplify to:
if cfg.Metadata != "" {
reqBody["metadata"] = cfg.metadataMap
}| if r.Method != http.MethodPost || r.URL.Path != "/sandboxes" { | ||
| t.Fatalf("request = %s %s", r.Method, r.URL.Path) |
There was a problem hiding this comment.
t.Fatalf inside httptest handler goroutine can hang the test. The handler runs on the server goroutine, not the test goroutine. If t.Fatal/t.Fatalf fires here, it calls runtime.Goexit() on the handler goroutine only — no HTTP response is written, and the test's main goroutine blocks forever in client.Do(req). The test times out silently (typically 10 min) instead of failing cleanly.
This applies to all t.Fatalf calls inside the handler (lines 21, 24, 27).
Recommendation: use t.Errorf instead and send a non-2xx status so benchOne returns an error result that can be asserted on the main goroutine:
if r.Method != http.MethodPost || r.URL.Path != "/sandboxes" {
t.Errorf("request = %s %s", r.Method, r.URL.Path)
w.WriteHeader(http.StatusBadRequest)
return
}| } | ||
| reqBody["metadata"] = metaMap | ||
| } | ||
| body, _ := json.Marshal(reqBody) |
There was a problem hiding this comment.
Silent json.Marshal error discard (pre-existing pattern). While json.Marshal on the current types is safe, a future change to the body structure could make this fail silently. Consider checking and returning the error instead as defense-in-depth.
a2b5df9 to
aaf7b83
Compare
|
|
||
| func TestBenchOneSendsMetadataAsStringMap(t *testing.T) { | ||
| rawMetadata := `{"scenario":"integration-execution","host-mount":[{"hostPath":"/tmp/data","mountPath":"/mnt/data","readOnly":false}]}` | ||
| metaMap, err := normalizeMetadata(rawMetadata) |
There was a problem hiding this comment.
Integration test bypasses prepareMetadata — the test calls normalizeMetadata directly (line 13) and manually constructs the Config fields. This means the prepareMetadata wrapper — which adds the empty-object rejection (len(metadata) == 0 check) and the --metadata is not valid JSON error wrapping — is not exercised end-to-end.
Consider calling prepareMetadata(rawMetadata) instead of normalizeMetadata(rawMetadata) so the test validates the same path parseConfig uses. This would catch any future divergence between the two functions.
| func buildCreateRequestBody(template string, metadata map[string]string) ([]byte, error) { | ||
| reqBody := map[string]interface{}{"templateID": template} | ||
| if len(metadata) > 0 { | ||
| reqBody["metadata"] = metadata | ||
| } | ||
| return json.Marshal(reqBody) | ||
| } |
There was a problem hiding this comment.
Prefer a concrete struct over map[string]interface{} — using any here means the compiler can't help catch structural errors. A struct with json:"omitempty" would be more idiomatic Go:
type createRequest struct {
TemplateID string `json:"templateID"`
Metadata map[string]string `json:"metadata,omitempty"`
}This also guarantees that when metadata is empty the key is absent from the JSON output rather than present with a null value — which is the current intent (line 93-94) but is enforced only via conditional logic rather than the type system.
| @@ -27,11 +27,9 @@ func benchOne(client *http.Client, cfg *Config, seq int) IterResult { | |||
| apiURL := cfg.APIURL | |||
| headers := map[string]string{"Authorization": "Bearer " + cfg.APIKey} | |||
There was a problem hiding this comment.
Per-iteration headers map allocation could be cached — this map is heap-allocated on every single benchmark iteration (the hot path). The pre-computation philosophy already applied to cfg.requestBody could be extended here: pre-build the headers in parseConfig() and store them in Config. The savings per-iteration are small, but for a latency-measurement tool, reducing GC pressure at high concurrency is worthwhile.
|
Thanks for the PR. Since we won't have many options similar to host-mount for benchmarks, could you just add host-mount as a standalone parameter? |
Add a --host-mount JSON array flag to cube-bench and include the compacted host-mount payload in sandbox create requests. The benchmark helper now focuses on the concrete host-mount scenario, while preserving the original host-mount JSON for config display and exported reports. Signed-off-by: zhengyilei <zheng_yilei@qq.com>
aaf7b83 to
d26ba7f
Compare
|
@ls-ggg Makes sense. I narrowed this PR to a dedicated |
|
LGTM |
Motivation
examples/cube-benchcurrently benchmarks the default raw sandbox create path. That makes it hard to measure create-time overhead forhost-mount, which is the concrete create-time variation we want to compare.CubeAPI already accepts
host-mountthroughmetadata["host-mount"]as a JSON string.cube-benchshould be able to send that same create-time payload so host-mount sandbox setups can be benchmarked directly.What Changed
This PR adds a
--host-mount '<json-array>'flag tocube-benchand includes the provided host-mount payload in sandbox create requests.Implementation highlights:
examples/cube-bench/main.goadds the new flag, validates a non-empty host-mount JSON array once during config parsing, preserves the original JSON for config display and exported reports, and caches the compactedhost-mountstring plus request body for request-time reuseexamples/cube-bench/runner.gosends the cached request body and headers on sandbox create requests, so benchmark throughput is not distorted by re-building host-mount metadata on every requestexamples/cube-bench/main_test.gocovers valid host-mount arrays, invalid JSON, non-array input, empty input, and empty-array rejectionexamples/cube-bench/README.mddocuments the new flag and a host-mount exampleThis keeps the benchmark helper focused on the current benchmark need without changing the backend metadata contract.
Validation
go test ./...passed inexamples/cube-benchmetadata["host-mount"]as a string valuecube-benchbinary using a--host-mount '[...]'payload