-
Notifications
You must be signed in to change notification settings - Fork 1
Add subject_disk tmpfs mounts and disk_pressure_correctness cases #48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -105,6 +105,15 @@ type TestCase struct { | |
| // buckets. Mutually exclusive with AWS. See MinioConfig in cloud.go. | ||
| Minio *MinioConfig `yaml:"minio"` | ||
|
|
||
| // SubjectDisk, when set, mounts a size-limited tmpfs at Path inside the | ||
| // subject container, giving the subject a small dedicated "disk" for its | ||
| // storage/buffer directory without a specially built image. Lets a case | ||
| // exercise disk-full and disk-backpressure behavior: fill the volume with | ||
| // more data than it can hold and observe whether the subject crashes, | ||
| // drops, or backpressures. Used by the disk_pressure_correctness type but | ||
| // honored on the singular subject service for any type. | ||
| SubjectDisk *SubjectDiskConfig `yaml:"subject_disk"` | ||
|
|
||
| // Requires lists subject capabilities every subject in this case must | ||
| // declare (Subject.Capabilities); the runner fails fast on subjects | ||
| // lacking one instead of starting a run that silently produces zero | ||
|
|
@@ -1032,13 +1041,31 @@ func (tc *TestCase) IsKafkaType() bool { | |
| return strings.HasPrefix(tc.Type, "kafka_") | ||
| } | ||
|
|
||
| // subjectDiskSizeRe accepts the size strings the orchestrator's | ||
| // parseByteSize understands: plain bytes ("1048576"), plain bytes with a | ||
| // "b" suffix ("64b"), or a k/m/g unit with an optional trailing "b" | ||
| // ("64m", "64mb"), case-insensitive. Kept in lockstep with parseByteSize | ||
| // so a case that passes Validate can never fail at compose-render time. | ||
| var subjectDiskSizeRe = regexp.MustCompile(`(?i)^[0-9]+([kmg]b?|b)?$`) | ||
|
|
||
| // Validate runs structural checks that don't depend on runtime state. | ||
| // Returns an error for cases where the singular and plural forms are both | ||
| // set (ambiguous) or where required IDs on plural entries are missing. | ||
| func (tc *TestCase) Validate() error { | ||
| if len(tc.Generators) > 0 && (tc.Generator.Mode != "" || tc.Generator.Target != "") { | ||
| return fmt.Errorf("case %q: both `generator:` and `generators:` are set — pick one", tc.Name) | ||
| } | ||
| if tc.SubjectDisk != nil { | ||
| if tc.SubjectDisk.Path == "" || tc.SubjectDisk.Size == "" { | ||
| return fmt.Errorf("case %q: subject_disk requires both `path` and `size`", tc.Name) | ||
| } | ||
| if !strings.HasPrefix(tc.SubjectDisk.Path, "/") { | ||
| return fmt.Errorf("case %q: subject_disk.path must be absolute, got %q", tc.Name, tc.SubjectDisk.Path) | ||
| } | ||
| if !subjectDiskSizeRe.MatchString(tc.SubjectDisk.Size) { | ||
| return fmt.Errorf("case %q: subject_disk.size %q is not a valid tmpfs size (e.g. 64m, 1g, 1048576)", tc.Name, tc.SubjectDisk.Size) | ||
| } | ||
| } | ||
|
Comment on lines
+1058
to
+1068
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Unlike Consider adding, alongside the existing per-type checks (e.g. the if tc.Type == "disk_pressure_correctness" {
if tc.SubjectDisk == nil {
return fmt.Errorf("case %q: type %q requires a `subject_disk:` block", tc.Name, tc.Type)
}
if len(tc.AllGenerators()) == 0 {
return fmt.Errorf("case %q: type %q requires a generator", tc.Name, tc.Type)
}
}Also worth guarding: 🤖 Prompt for AI Agents |
||
| if len(tc.Receivers) > 0 && (tc.Receiver.Mode != "" || tc.Receiver.Listen != "") { | ||
| return fmt.Errorf("case %q: both `receiver:` and `receivers:` are set — pick one", tc.Name) | ||
| } | ||
|
|
@@ -2412,6 +2439,17 @@ func validateSampleFile(caseName, sampleFile string) error { | |
| return nil | ||
| } | ||
|
|
||
| // SubjectDiskConfig mounts a size-limited tmpfs inside the subject container | ||
| // (see TestCase.SubjectDisk). Path is the in-container mount point — for | ||
| // vmetric that's /opt/vmetric/storage, the root of its NATS JetStream | ||
| // StoreDir and queue/WAL files. Size is a docker-compose tmpfs size string | ||
| // ("64m", "1g", or plain bytes). The mount is created mode 01777 so | ||
| // non-root subject images can write to it. | ||
| type SubjectDiskConfig struct { | ||
| Path string `yaml:"path"` | ||
| Size string `yaml:"size"` | ||
| } | ||
|
|
||
| type GeneratorConfig struct { | ||
| // ID is only meaningful in the plural `generators:` form. It names the | ||
| // docker-compose service (`generator-<id>`) and, in sequenced/correctness | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| package config | ||
|
|
||
| import ( | ||
| "strings" | ||
| "testing" | ||
| ) | ||
|
|
||
| // subjectDiskCase returns a minimal valid case with the given | ||
| // subject_disk block, so each test tweaks exactly one dimension. | ||
| func subjectDiskCase(disk *SubjectDiskConfig) *TestCase { | ||
| return &TestCase{ | ||
| Name: "disk-validate", | ||
| Type: "disk_pressure_correctness", | ||
| SubjectDisk: disk, | ||
| } | ||
| } | ||
|
|
||
| func TestValidateSubjectDisk(t *testing.T) { | ||
| valid := []SubjectDiskConfig{ | ||
| {Path: "/opt/vmetric/storage", Size: "64m"}, | ||
| {Path: "/data", Size: "128MB"}, | ||
| {Path: "/data", Size: "1g"}, | ||
| {Path: "/data", Size: "512kb"}, | ||
| {Path: "/data", Size: "1048576"}, | ||
| {Path: "/data", Size: "64b"}, | ||
| } | ||
| for _, d := range valid { | ||
| if err := subjectDiskCase(&d).Validate(); err != nil { | ||
| t.Errorf("subject_disk %+v: unexpected error: %v", d, err) | ||
| } | ||
| } | ||
|
|
||
| invalid := []struct { | ||
| disk SubjectDiskConfig | ||
| wantSub string | ||
| }{ | ||
| {SubjectDiskConfig{Path: "", Size: "64m"}, "requires both"}, | ||
| {SubjectDiskConfig{Path: "/data", Size: ""}, "requires both"}, | ||
| {SubjectDiskConfig{Path: "data", Size: "64m"}, "must be absolute"}, | ||
| {SubjectDiskConfig{Path: "/data", Size: "sixty-four"}, "not a valid tmpfs size"}, | ||
| {SubjectDiskConfig{Path: "/data", Size: "1.5g"}, "not a valid tmpfs size"}, | ||
| {SubjectDiskConfig{Path: "/data", Size: "64bb"}, "not a valid tmpfs size"}, | ||
| {SubjectDiskConfig{Path: "/data", Size: "-5m"}, "not a valid tmpfs size"}, | ||
| {SubjectDiskConfig{Path: "/data", Size: "64x"}, "not a valid tmpfs size"}, | ||
| } | ||
| for _, tt := range invalid { | ||
| err := subjectDiskCase(&tt.disk).Validate() | ||
| if err == nil { | ||
| t.Errorf("subject_disk %+v: expected error containing %q, got nil", tt.disk, tt.wantSub) | ||
| continue | ||
| } | ||
| if !strings.Contains(err.Error(), tt.wantSub) { | ||
| t.Errorf("subject_disk %+v: error %q does not contain %q", tt.disk, err, tt.wantSub) | ||
| } | ||
| } | ||
|
|
||
| // No subject_disk block — nothing to validate. | ||
| if err := subjectDiskCase(nil).Validate(); err != nil { | ||
| t.Errorf("nil subject_disk: unexpected error: %v", err) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
subjectDiskSizeReaccepts sizes thatparseByteSizerejects — breaks the documented lockstep guarantee.The regex
^[0-9]+([kmg]b?|b)?$matches an all-zero numeral ("0","0m","0b","0kb", …), butorchestrator.parseByteSizeexplicitly rejects non-positive sizes (if n <= 0 { return error }). A case withsubject_disk: {path: /data, size: "0"}passesValidate()at load time but fails later inwriteCompose, contradicting the comment directly above the regex: "a case that passes Validate can never fail at compose-render time."🐛 Proposed fix — require at least one non-zero digit
A more robust fix is to have this package expose (or directly reuse) the same byte-size parser
writeComposecalls, so the two implementations can't drift again. Either way, please add"0"/"0m"/"0b"toTestValidateSubjectDisk's invalid cases once fixed — the current test suite doesn't cover this edge.Also applies to: 1058-1068
🤖 Prompt for AI Agents