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 "errors"
7 "fmt"
8 "os"
9 "os/exec"
10 "slices"
11 "strconv"
12 "strings"
13
14 "golang.org/x/sys/unix"
15)
16
17// tierExpected is the four limit files for l, as the kernel reads them
18// back: memory.high rounded down to the page.
19func tierExpected(l Limit, oomGroup string) map[string]string {
20 page := int64(os.Getpagesize())
21 return map[string]string{
22 "memory.max": strconv.FormatInt(l.MemoryMax, 10),
23 "memory.high": strconv.FormatInt(HighOf(l.MemoryMax)/page*page, 10),
24 "memory.swap.max": strconv.FormatInt(l.MemoryMax, 10),
25 "memory.oom.group": oomGroup,
26 }
27}
28
29// pinChild pins a child cgroup under parent: beforeMkdir callback (if provided),
30// mkdir, create child struct, FcntlInt, strictOpen (dir, events, kill), strictID,
31// reservation, and population check.
32// beforeMkdir is called before mkdir (for role intent persistence; may be nil).
33// allowExisting controls whether mkdir's EEXIST is accepted: true allows reusing
34// an existing directory (generation children, role children without intent),
35// false refuses it (role children with intent require a new name).
36// On success, returns a fully pinned, reserved child.
37// On error, all cleanup is done internally; caller needs only to set up a defer
38// for cleanup of any subsequent operations (e.g. tier writes for generation children).
39func pinChild(parent *Prepared, name string, beforeMkdir func() error, allowExisting bool) (*Prepared, error) {
40 if beforeMkdir != nil {
41 if err := beforeMkdir(); err != nil {
42 return nil, err
43 }
44 }
45 if err := unix.Mkdirat(int(parent.dir.Fd()), name, 0755); err != nil {
46 if !allowExisting || !errors.Is(err, unix.EEXIST) {
47 return nil, ErrStrictRefused
48 }
49 // An existing child that took cgroup.kill is never reused (amadan
50 // #272). The caller holds the parent's mu.
51 if err := refuseKilledChild(parent, name); err != nil {
52 return nil, err
53 }
54 }
55 child := &Prepared{strictPreparedState: &strictPreparedState{parent: parent, host: parent.host, limits: map[string]*os.File{}, expected: map[string]string{}, entryName: name, start: func(cmd *exec.Cmd) error { return cmd.Start() }}}
56
57 fd, e := unix.FcntlInt(parent.dir.Fd(), unix.F_DUPFD_CLOEXEC, 0)
58 if e != nil {
59 return nil, ErrStrictUnsupported
60 }
61 child.entryParent = os.NewFile(uintptr(fd), name+"-parent")
62
63 var err error
64 child.dir, err = strictOpen(parent.dir, name, unix.O_RDONLY|unix.O_DIRECTORY)
65 if err != nil {
66 child.closeFiles()
67 return nil, ErrStrictRefused
68 }
69
70 child.identity, err = strictID(child.dir)
71 if err != nil {
72 child.closeFiles()
73 return nil, ErrStrictRefused
74 }
75
76 strictReservations.Lock()
77 if strictReservations.held[child.identity] {
78 strictReservations.Unlock()
79 child.closeFiles()
80 return nil, ErrStrictState
81 }
82 strictReservations.held[child.identity] = true
83 strictReservations.Unlock()
84
85 child.events, err = strictOpen(child.dir, "cgroup.events", unix.O_RDONLY)
86 if err != nil {
87 child.closeFiles()
88 strictReservations.Lock()
89 delete(strictReservations.held, child.identity)
90 strictReservations.Unlock()
91 return nil, ErrStrictUnsupported
92 }
93
94 child.kill, err = strictOpen(child.dir, "cgroup.kill", unix.O_WRONLY)
95 if err != nil {
96 child.closeFiles()
97 strictReservations.Lock()
98 delete(strictReservations.held, child.identity)
99 strictReservations.Unlock()
100 return nil, ErrStrictUnsupported
101 }
102
103 populated, err := strictPopulation(child.events)
104 if err != nil || populated {
105 child.closeFiles()
106 strictReservations.Lock()
107 delete(strictReservations.held, child.identity)
108 strictReservations.Unlock()
109 return nil, ErrStrictRefused
110 }
111
112 return child, nil
113}
114
115// delegateStrict delegates a generation parent's controllers to its
116// children and proves memory is among them: a generation child without it
117// could carry no limit.
118func delegateStrict(dir *os.File) error {
119 ctl, e := strictOpen(dir, "cgroup.controllers", unix.O_RDONLY)
120 if e != nil {
121 return ErrStrictUnsupported
122 }
123 have, e := strictRead(ctl)
124 ctl.Close()
125 if e != nil {
126 return ErrStrictRefused
127 }
128 want := delegatedControllers(have)
129 if !strings.Contains(want, "+memory") {
130 return ErrStrictUnsupported
131 }
132 sub, e := strictOpen(dir, "cgroup.subtree_control", unix.O_RDWR)
133 if e != nil {
134 return ErrStrictUnsupported
135 }
136 defer sub.Close()
137 if e = strictWrite(sub, want); e != nil {
138 return ErrStrictRefused
139 }
140 got, e := strictRead(sub)
141 if e != nil || !slices.Contains(strings.Fields(got), "memory") {
142 return ErrStrictRefused
143 }
144 return nil
145}
146
147// PrepareStrictParent pins host's cgroup as a generation parent for the life
148// of one activator entry (zero-downtime deploys spec §7.3): empty, the tier
149// written with memory.oom.group=0, its controllers delegated to its
150// children. It holds no process: Start refuses it. Each spawn pins and
151// reserves its own child with PrepareGen. The swap moves the parent's limit
152// with SetParentLimit, which keeps the expected-limit record in step.
153func (m *Manager) PrepareStrictParent(host string, tier Limit) (*Prepared, error) {
154 return m.prepareStrict(host, tier, true)
155}
156
157// PrepareGen pins generation n's child under a generation parent: newly
158// empty, reserved for this generation alone, with the tier's full limits
159// and memory.oom.group=1 (spec §7.3). tier is this generation's own and may
160// differ from the incumbent's when a release changes tier.
161func (p *Prepared) PrepareGen(n uint64, tier Limit) (out *Prepared, err error) {
162 if p == nil || p.strictPreparedState == nil || n == 0 || (tier != TierDefault && tier != TierChildren) {
163 return nil, ErrStrictRefused
164 }
165 p.mu.Lock()
166 defer p.mu.Unlock()
167 if !p.genParent || p.closed || p.empty || p.draining.Load() {
168 return nil, ErrStrictState
169 }
170 if err = p.identityOK(); err != nil {
171 return nil, err
172 }
173 name := GenChild(n)
174 child, err := pinChild(p, name, nil, true)
175 if err != nil {
176 return nil, err
177 }
178 defer func() {
179 if err != nil {
180 child.closeFiles()
181 strictReservations.Lock()
182 delete(strictReservations.held, child.identity)
183 strictReservations.Unlock()
184 }
185 }()
186 child.tier = tier
187 want := tierExpected(tier, "1")
188 var e error
189 for name := range want {
190 if child.limits[name], e = strictOpen(child.dir, name, unix.O_RDWR); e != nil {
191 return nil, ErrStrictUnsupported
192 }
193 }
194 for _, name := range []string{"memory.max", "memory.high", "memory.swap.max", "memory.oom.group"} {
195 if e = strictWrite(child.limits[name], want[name]); e != nil {
196 return nil, ErrStrictRefused
197 }
198 child.expected[name] = want[name]
199 }
200 if e = child.limitsOK(); e != nil {
201 return nil, e
202 }
203 if e = child.identityOK(); e != nil {
204 return nil, e
205 }
206 return child, nil
207}
208
209// SetParentLimit writes l onto a generation parent and records it as the
210// expected value in the same step (spec §7.3). A child's Start checks the
211// parent with limitsOK and must never see a limit this API did not set.
212// Each file's record moves only after its own write lands, so a partial
213// failure leaves the record matching the kernel. A limit someone else set
214// is refused, not overwritten.
215func (p *Prepared) SetParentLimit(l Limit) error {
216 if p == nil || p.strictPreparedState == nil || !ValidParentLimit(l) {
217 return ErrStrictRefused
218 }
219 p.mu.Lock()
220 defer p.mu.Unlock()
221 if !p.genParent || p.closed || p.empty || p.draining.Load() {
222 return ErrStrictState
223 }
224 if e := p.identityOK(); e != nil {
225 return e
226 }
227 if e := p.limitsOK(); e != nil {
228 return e
229 }
230 want := tierExpected(l, "0")
231 for _, name := range []string{"memory.max", "memory.high", "memory.swap.max"} {
232 if e := strictWrite(p.limits[name], want[name]); e != nil {
233 return ErrStrictRefused
234 }
235 p.expected[name] = want[name]
236 }
237 return p.limitsOK()
238}
239
240// CloseRemove is Close for a generation child, then removes the child's
241// directory through the parent descriptor pinned at PrepareGen. Generation
242// numbers are never reused, so a child left behind would stay until the
243// agent restarts. Only an empty child is closed. The handle is closed and
244// its reservation released even when the rmdir fails; that error is
245// returned, and the next agent start's sweep removes the directory.
246func (p *Prepared) CloseRemove() error {
247 if p == nil || p.strictPreparedState == nil {
248 return ErrStrictState
249 }
250 p.mu.Lock()
251 defer p.mu.Unlock()
252 p.draining.Store(true)
253 if p.closed || p.parent == nil || p.entryParent == nil || !isGenName(p.entryName) {
254 return ErrStrictState
255 }
256 if e := p.identityOK(); e != nil {
257 return e
258 }
259 if populated, e := strictPopulation(p.events); e != nil || populated {
260 return ErrStrictRefused
261 }
262 rmErr := unix.Unlinkat(int(p.entryParent.Fd()), p.entryName, unix.AT_REMOVEDIR)
263 p.closed = true
264 p.closeFiles()
265 strictReservations.Lock()
266 delete(strictReservations.held, p.identity)
267 strictReservations.Unlock()
268 if rmErr != nil {
269 return fmt.Errorf("cgroup: %s: remove %s: %w", p.host, p.entryName, rmErr)
270 }
271 return nil
272}
273