carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package cgroup
4
5import (
6 "context"
7 "errors"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "strconv"
12 "syscall"
13 "testing"
14 "time"
15)
16
17// genParentFixture is strictFixture turned into a generation parent: the
18// tier on its files, memory.oom.group=0, the record matching.
19func genParentFixture(t *testing.T) *Prepared {
20 t.Helper()
21 p := strictFixture(t)
22 p.genParent, p.tier = true, TierDefault
23 for name, value := range tierExpected(TierDefault, "0") {
24 if err := strictWrite(p.limits[name], value); err != nil {
25 t.Fatal(err)
26 }
27 p.expected[name] = value
28 }
29 return p
30}
31
32func makeGenFixture(t *testing.T, p *Prepared, n uint64, events string) string {
33 t.Helper()
34 d := filepath.Join(p.dir.Name(), GenChild(n))
35 if err := os.Mkdir(d, 0o700); err != nil {
36 t.Fatal(err)
37 }
38 for name, v := range map[string]string{"cgroup.events": events, "cgroup.kill": "", "memory.max": "", "memory.high": "", "memory.swap.max": "", "memory.oom.group": ""} {
39 if err := os.WriteFile(filepath.Join(d, name), []byte(v), 0o600); err != nil {
40 t.Fatal(err)
41 }
42 }
43 return d
44}
45
46func TestPrepareGenWritesTheFullTier(t *testing.T) {
47 p := genParentFixture(t)
48 d := makeGenFixture(t, p, 1, "populated 0\n")
49 child, err := p.PrepareGen(1, TierDefault)
50 if err != nil {
51 t.Fatal(err)
52 }
53 defer child.Close()
54 want := tierExpected(TierDefault, "1")
55 for name, value := range want {
56 if got := readFixture(t, filepath.Join(d, name)); got != value {
57 t.Errorf("g1/%s = %q, want %q", name, got, value)
58 }
59 }
60 if want["memory.max"] != "536870912" || want["memory.oom.group"] != "1" {
61 t.Fatalf("tierExpected = %v", want)
62 }
63}
64
65func TestPrepareGenReservesPerChild(t *testing.T) {
66 p := genParentFixture(t)
67 makeGenFixture(t, p, 1, "populated 0\n")
68 makeGenFixture(t, p, 2, "populated 0\n")
69 g1, err := p.PrepareGen(1, TierDefault)
70 if err != nil {
71 t.Fatal(err)
72 }
73 defer g1.Close()
74 if again, err := p.PrepareGen(1, TierDefault); !errors.Is(err, ErrStrictState) || again != nil {
75 t.Fatalf("second reservation of g1: %v", err)
76 }
77 g2, err := p.PrepareGen(2, TierDefault)
78 if err != nil {
79 t.Fatalf("g2 beside a live g1: %v", err)
80 }
81 defer g2.Close()
82 if _, err := p.PrepareGen(0, TierDefault); !errors.Is(err, ErrStrictRefused) {
83 t.Errorf("generation 0: %v", err)
84 }
85 if _, err := p.PrepareGen(3, Limit{MemoryMax: 64 << 20}); !errors.Is(err, ErrStrictRefused) {
86 t.Errorf("a limit that is not a tier: %v", err)
87 }
88}
89
90func TestPrepareGenAndSetParentLimitRefuseAPlainHostGroup(t *testing.T) {
91 p := strictFixture(t)
92 if _, err := p.PrepareGen(1, TierDefault); !errors.Is(err, ErrStrictState) {
93 t.Errorf("PrepareGen on a leaf host group: %v", err)
94 }
95 if err := p.SetParentLimit(TierDefault); !errors.Is(err, ErrStrictState) {
96 t.Errorf("SetParentLimit on a leaf host group: %v", err)
97 }
98}
99
100func TestGenerationParentRefusesAProcess(t *testing.T) {
101 p := genParentFixture(t)
102 if err := p.Start(exec.Command("/bin/true")); !errors.Is(err, ErrStrictState) {
103 t.Fatalf("a generation parent accepted a process: %v", err)
104 }
105}
106
107// The parent's record moves with the limit it raises, so a child's Start
108// passes limitsOK after a raise; a limit set by anyone else fails it.
109func TestSetParentLimitRecordsWhatItWrites(t *testing.T) {
110 p := genParentFixture(t)
111 makeGenFixture(t, p, 1, "populated 0\n")
112 makeGenFixture(t, p, 2, "populated 0\n")
113 child, err := p.PrepareGen(1, TierDefault)
114 if err != nil {
115 t.Fatal(err)
116 }
117 defer child.Close()
118 if err := p.SetParentLimit(OverlapLimit(TierDefault, TierDefault)); err != nil {
119 t.Fatal(err)
120 }
121 if got := readFixture(t, filepath.Join(p.dir.Name(), "memory.max")); got != "1073741824" {
122 t.Fatalf("parent memory.max = %q", got)
123 }
124 // The fixture's start answers ENOSYS: reaching it means every check passed.
125 child.start = func(*exec.Cmd) error { return syscall.ENOSYS }
126 if err := child.Start(exec.Command("/bin/true")); !errors.Is(err, ErrStrictUnsupported) {
127 t.Fatalf("child start after an API raise: %v, want it to reach the spawn", err)
128 }
129 // strictWrite does not truncate a fixture file: "9" over "1073741824"
130 // leaves "9073741824", a limit this API never set.
131 if err := strictWrite(p.limits["memory.max"], "9"); err != nil {
132 t.Fatal(err)
133 }
134 other, err := p.PrepareGen(2, TierDefault)
135 if err != nil {
136 t.Fatal(err)
137 }
138 defer other.Close()
139 if err := other.Start(exec.Command("/bin/true")); !errors.Is(err, ErrStrictRefused) {
140 t.Fatalf("child start under a limit the API did not set: %v", err)
141 }
142 if err := p.SetParentLimit(TierDefault); !errors.Is(err, ErrStrictRefused) {
143 t.Fatalf("SetParentLimit papered over a foreign limit: %v", err)
144 }
145}
146
147// Review focus 4: a release that changes tier.
148func TestSetParentLimitAcceptsATierChange(t *testing.T) {
149 p := genParentFixture(t)
150 if err := p.SetParentLimit(OverlapLimit(TierDefault, TierChildren)); err != nil {
151 t.Fatalf("512M + 1G: %v", err)
152 }
153 if got := readFixture(t, filepath.Join(p.dir.Name(), "memory.max")); got != strconv.FormatInt(1536<<20, 10) {
154 t.Fatalf("parent memory.max = %q", got)
155 }
156 if err := p.SetParentLimit(Limit{MemoryMax: 64 << 20}); !errors.Is(err, ErrStrictRefused) {
157 t.Fatalf("a limit that is no tier sum: %v", err)
158 }
159}
160
161func TestGenerationKillAndEmptyTouchesOnlyItsChild(t *testing.T) {
162 p := genParentFixture(t)
163 d1 := makeGenFixture(t, p, 1, "populated 1\n")
164 d2 := makeGenFixture(t, p, 2, "populated 0\n")
165 g1, err := p.PrepareGen(1, TierDefault)
166 if err == nil {
167 t.Fatal("PrepareGen pinned a populated group")
168 }
169 _ = g1
170 if err := os.WriteFile(filepath.Join(d1, "cgroup.events"), []byte("populated 0\n"), 0o600); err != nil {
171 t.Fatal(err)
172 }
173 if g1, err = p.PrepareGen(1, TierDefault); err != nil {
174 t.Fatal(err)
175 }
176 defer g1.Close()
177 g2, err := p.PrepareGen(2, TierDefault)
178 if err != nil {
179 t.Fatal(err)
180 }
181 ctx, cancel := context.WithTimeout(t.Context(), time.Second)
182 defer cancel()
183 if err := g2.KillAndEmpty(ctx); err != nil {
184 t.Fatal(err)
185 }
186 for dir, want := range map[string]string{d2: "1", d1: "", p.dir.Name(): ""} {
187 if got := readFixture(t, filepath.Join(dir, "cgroup.kill")); got != want {
188 t.Errorf("%s/cgroup.kill = %q, want %q", dir, got, want)
189 }
190 }
191 // CloseRemove: refused while populated; once empty it closes and frees
192 // the reservation even when rmdir fails (a fixture dir holds files).
193 if err := os.WriteFile(filepath.Join(d2, "cgroup.events"), []byte("populated 1\n"), 0o600); err != nil {
194 t.Fatal(err)
195 }
196 if err := g2.CloseRemove(); !errors.Is(err, ErrStrictRefused) {
197 t.Fatalf("CloseRemove of a populated child: %v", err)
198 }
199 if err := os.WriteFile(filepath.Join(d2, "cgroup.events"), []byte("populated 0\n"), 0o600); err != nil {
200 t.Fatal(err)
201 }
202 if err := g2.CloseRemove(); err == nil {
203 t.Fatal("rmdir of a fixture dir holding files reported success")
204 }
205 if _, err := g2.Populated(); !errors.Is(err, ErrStrictState) {
206 t.Fatalf("handle still open after CloseRemove: %v", err)
207 }
208 strictReservations.Lock()
209 held := strictReservations.held[g2.identity]
210 strictReservations.Unlock()
211 if held {
212 t.Fatal("reservation not released by CloseRemove")
213 }
214 // The directory survived its failed rmdir, but it took cgroup.kill, so
215 // it is never pinned for a spawn again (amadan #272).
216 if again, err := p.PrepareGen(2, TierDefault); !errors.Is(err, ErrStrictRefused) {
217 if again != nil {
218 again.Close()
219 }
220 t.Fatalf("re-pin of a killed generation child: %v, want ErrStrictRefused", err)
221 }
222}
223