File: freezer.go

package info (click to toggle)
golang-github-opencontainers-cgroups 0.0.4-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 844 kB
  • sloc: makefile: 2
file content (140 lines) | stat: -rw-r--r-- 4,269 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
package fs2

import (
	"bufio"
	"errors"
	"fmt"
	"os"
	"strings"
	"time"

	"golang.org/x/sys/unix"

	"github.com/opencontainers/cgroups"
)

func setFreezer(dirPath string, state cgroups.FreezerState) error {
	var stateStr string
	switch state {
	case cgroups.Undefined:
		return nil
	case cgroups.Frozen:
		stateStr = "1"
	case cgroups.Thawed:
		stateStr = "0"
	default:
		return fmt.Errorf("invalid freezer state %q requested", state)
	}

	fd, err := cgroups.OpenFile(dirPath, "cgroup.freeze", unix.O_RDWR)
	if err != nil {
		// We can ignore this request as long as the user didn't ask us to
		// freeze the container (since without the freezer cgroup, that's a
		// no-op).
		if state != cgroups.Frozen {
			return nil
		}
		return fmt.Errorf("freezer not supported: %w", err)
	}
	defer fd.Close()

	if _, err := fd.WriteString(stateStr); err != nil {
		return err
	}
	// Confirm that the cgroup did actually change states.
	if actualState, err := readFreezer(dirPath, fd); err != nil {
		return err
	} else if actualState != state {
		return fmt.Errorf(`expected "cgroup.freeze" to be in state %q but was in %q`, state, actualState)
	}
	return nil
}

func getFreezer(dirPath string) (cgroups.FreezerState, error) {
	fd, err := cgroups.OpenFile(dirPath, "cgroup.freeze", unix.O_RDONLY)
	if err != nil {
		// If the kernel is too old, then we just treat the freezer as
		// being in an "undefined" state and ignore the error.
		return cgroups.Undefined, ignoreNotExistOrNoDeviceError(err)
	}
	defer fd.Close()

	return readFreezer(dirPath, fd)
}

func readFreezer(dirPath string, fd *os.File) (cgroups.FreezerState, error) {
	if _, err := fd.Seek(0, 0); err != nil {
		// If the cgroup path is deleted at this point, then we just treat the freezer as
		// being in an "undefined" state and ignore the error.
		return cgroups.Undefined, ignoreNotExistOrNoDeviceError(err)
	}
	state := make([]byte, 2)
	if _, err := fd.Read(state); err != nil {
		// If the cgroup path is deleted at this point, then we just treat the freezer as
		// being in an "undefined" state and ignore the error.
		return cgroups.Undefined, ignoreNotExistOrNoDeviceError(err)
	}
	switch string(state) {
	case "0\n":
		return cgroups.Thawed, nil
	case "1\n":
		return waitFrozen(dirPath)
	default:
		return cgroups.Undefined, fmt.Errorf(`unknown "cgroup.freeze" state: %q`, state)
	}
}

// ignoreNotExistOrNoDeviceError checks if the error is either a "not exist" error
// or a "no device" error, and returns nil in those cases. Otherwise, it returns the error.
func ignoreNotExistOrNoDeviceError(err error) error {
	// We can safely ignore the error in the following two common situations:
	// 1. The cgroup path does not exist at the time of opening(eg: the kernel is too old)
	//    — indicated by os.IsNotExist.
	// 2. The cgroup path is deleted during the seek/read operation — indicated by
	//    errors.Is(err, unix.ENODEV).
	// These conditions are expected and do not require special handling.
	if os.IsNotExist(err) || errors.Is(err, unix.ENODEV) {
		return nil
	}
	return err
}

// waitFrozen polls cgroup.events until it sees "frozen 1" in it.
func waitFrozen(dirPath string) (cgroups.FreezerState, error) {
	fd, err := cgroups.OpenFile(dirPath, "cgroup.events", unix.O_RDONLY)
	if err != nil {
		return cgroups.Undefined, err
	}
	defer fd.Close()

	// XXX: Simple wait/read/retry is used here. An implementation
	// based on poll(2) or inotify(7) is possible, but it makes the code
	// much more complicated. Maybe address this later.
	const (
		// Perform maxIter with waitTime in between iterations.
		waitTime = 10 * time.Millisecond
		maxIter  = 1000
	)
	scanner := bufio.NewScanner(fd)
	for i := 0; scanner.Scan(); {
		if i == maxIter {
			return cgroups.Undefined, fmt.Errorf("timeout of %s reached waiting for the cgroup to freeze", waitTime*maxIter)
		}
		if val, ok := strings.CutPrefix(scanner.Text(), "frozen "); ok {
			if val[0] == '1' {
				return cgroups.Frozen, nil
			}

			i++
			// wait, then re-read
			time.Sleep(waitTime)
			_, err := fd.Seek(0, 0)
			if err != nil {
				return cgroups.Undefined, err
			}
		}
	}
	// Should only reach here either on read error,
	// or if the file does not contain "frozen " line.
	return cgroups.Undefined, scanner.Err()
}