Skip to content

Commit a4ee8e3

Browse files
committed
[ws-daemon] Implement workspace info endpoint
1 parent aea4b38 commit a4ee8e3

File tree

5 files changed

+314
-0
lines changed

5 files changed

+314
-0
lines changed

components/ws-daemon/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ require (
4848
github.com/Microsoft/go-winio v0.5.2 // indirect
4949
github.com/Microsoft/hcsshim v0.9.2 // indirect
5050
github.com/beorn7/perks v1.0.1 // indirect
51+
github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8
5152
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
5253
github.com/cespare/xxhash/v2 v2.1.2 // indirect
5354
github.com/cilium/ebpf v0.9.0 // indirect

components/ws-daemon/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7
143143
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
144144
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
145145
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
146+
github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8 h1:SjZ2GvvOononHOpK84APFuMvxqsk3tEIaKH/z4Rpu3g=
147+
github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE=
146148
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
147149
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
148150
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=

components/ws-daemon/pkg/iws/iws.go

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"context"
1010
"fmt"
1111
"io"
12+
"math"
1213
"net"
1314
"os"
1415
"os/exec"
@@ -27,7 +28,10 @@ import (
2728
"google.golang.org/grpc/codes"
2829
"google.golang.org/grpc/status"
2930

31+
linuxproc "github.com/c9s/goprocinfo/linux"
3032
"github.com/gitpod-io/gitpod/common-go/cgroups"
33+
v1 "github.com/gitpod-io/gitpod/common-go/cgroups/v1"
34+
v2 "github.com/gitpod-io/gitpod/common-go/cgroups/v2"
3135
"github.com/gitpod-io/gitpod/common-go/log"
3236
"github.com/gitpod-io/gitpod/common-go/tracing"
3337
wsinit "github.com/gitpod-io/gitpod/content-service/pkg/initializer"
@@ -180,6 +184,9 @@ func (wbs *InWorkspaceServiceServer) Start() error {
180184
"/iws.InWorkspaceService/Teardown": ratelimit{
181185
UseOnce: true,
182186
},
187+
"/iws.InWorkspaceService/WorkspaceInfo": ratelimit{
188+
Limiter: rate.NewLimiter(rate.Every(1500*time.Millisecond), 4),
189+
},
183190
}
184191

185192
wbs.srv = grpc.NewServer(grpc.ChainUnaryInterceptor(limits.UnaryInterceptor()))
@@ -995,6 +1002,307 @@ func (wbs *InWorkspaceServiceServer) unPrepareForUserNS() error {
9951002
return nil
9961003
}
9971004

1005+
func (wbs *InWorkspaceServiceServer) WorkspaceInfo(ctx context.Context, req *api.WorkspaceInfoRequest) (*api.WorkspaceInfoResponse, error) {
1006+
log.Info("Received workspace info request")
1007+
rt := wbs.Uidmapper.Runtime
1008+
if rt == nil {
1009+
return nil, status.Errorf(codes.FailedPrecondition, "not connected to container runtime")
1010+
}
1011+
wscontainerID, err := rt.WaitForContainer(ctx, wbs.Session.InstanceID)
1012+
if err != nil {
1013+
log.WithError(err).WithFields(wbs.Session.OWI()).Error("EvacuateCGroup: cannot find workspace container")
1014+
return nil, status.Errorf(codes.NotFound, "cannot find workspace container")
1015+
}
1016+
1017+
cgroupPath, err := rt.ContainerCGroupPath(ctx, wscontainerID)
1018+
if err != nil {
1019+
log.WithError(err).WithFields(wbs.Session.OWI()).Error("EvacuateCGroup: cannot find workspace container CGroup path")
1020+
return nil, status.Errorf(codes.NotFound, "cannot find workspace container cgroup")
1021+
}
1022+
1023+
unified, err := cgroups.IsUnifiedCgroupSetup()
1024+
if err != nil {
1025+
// log error and do not expose it to the user
1026+
log.WithError(err).Error("could not determine cgroup setup")
1027+
return nil, status.Errorf(codes.FailedPrecondition, "could not determine cgroup setup")
1028+
}
1029+
1030+
resources, err := getWorkspaceResourceInfo(wbs.CGroupMountPoint, cgroupPath, unified)
1031+
if err != nil {
1032+
log.WithError(err).Error("could not get resource information")
1033+
return nil, status.Error(codes.Unknown, err.Error())
1034+
}
1035+
1036+
return &api.WorkspaceInfoResponse{
1037+
Resources: resources,
1038+
}, nil
1039+
}
1040+
1041+
func getWorkspaceResourceInfo(mountPoint, cgroupPath string, unified bool) (*api.Resources, error) {
1042+
if unified {
1043+
cpu, err := getCpuResourceInfoV2(mountPoint, cgroupPath)
1044+
if err != nil {
1045+
return nil, err
1046+
}
1047+
1048+
memory, err := getMemoryResourceInfoV2(mountPoint, cgroupPath)
1049+
if err != nil {
1050+
return nil, err
1051+
}
1052+
1053+
return &api.Resources{
1054+
Cpu: cpu,
1055+
Memory: memory,
1056+
}, nil
1057+
} else {
1058+
cpu, err := getCpuResourceInfoV1(mountPoint, cgroupPath)
1059+
if err != nil {
1060+
return nil, err
1061+
}
1062+
1063+
memory, err := getMemoryResourceInfoV1(mountPoint, cgroupPath)
1064+
if err != nil {
1065+
return nil, err
1066+
}
1067+
1068+
return &api.Resources{
1069+
Cpu: cpu,
1070+
Memory: memory,
1071+
}, nil
1072+
}
1073+
}
1074+
1075+
func getCpuResourceInfoV2(mountPoint, cgroupPath string) (*api.Cpu, error) {
1076+
cpu := v2.NewCpuControllerWithMount(mountPoint, cgroupPath)
1077+
1078+
t, err := resolveCPUStatV2(cpu)
1079+
if err != nil {
1080+
return nil, err
1081+
}
1082+
1083+
time.Sleep(time.Second)
1084+
1085+
t2, err := resolveCPUStatV2(cpu)
1086+
if err != nil {
1087+
return nil, err
1088+
}
1089+
1090+
cpuUsage := t2.usage - t.usage
1091+
totalTime := t2.uptime - t.uptime
1092+
used := cpuUsage / totalTime * 1000
1093+
1094+
quota, period, err := cpu.Max()
1095+
if err != nil {
1096+
return nil, err
1097+
}
1098+
1099+
// if no cpu limit has been specified, use the number of cores
1100+
var limit uint64
1101+
if quota == math.MaxUint64 {
1102+
cpuInfo, err := linuxproc.ReadCPUInfo("/proc/cpuinfo")
1103+
if err != nil {
1104+
return nil, err
1105+
}
1106+
1107+
limit = uint64(cpuInfo.NumCore()) * 1000
1108+
} else {
1109+
limit = quota / period * 1000
1110+
}
1111+
1112+
return &api.Cpu{
1113+
Used: int64(used),
1114+
Limit: int64(limit),
1115+
}, nil
1116+
}
1117+
1118+
func getMemoryResourceInfoV2(mountPoint, cgroupPath string) (*api.Memory, error) {
1119+
memory := v2.NewMemoryControllerWithMount(mountPoint, cgroupPath)
1120+
memoryLimit, err := memory.Max()
1121+
if err != nil {
1122+
return nil, xerrors.Errorf("could not retrieve memory max: %w", err)
1123+
}
1124+
1125+
memInfo, err := linuxproc.ReadMemInfo("/proc/meminfo")
1126+
if err != nil {
1127+
return nil, xerrors.Errorf("failed to read meminfo: %w", err)
1128+
}
1129+
1130+
// if no memory limit has been specified, use total available memory
1131+
if memoryLimit == math.MaxUint64 || memoryLimit > memInfo.MemTotal*1024 {
1132+
// total memory is specifed on kilobytes -> convert to bytes
1133+
memoryLimit = memInfo.MemTotal * 1024
1134+
}
1135+
1136+
usedMemory, err := memory.Current()
1137+
if err != nil {
1138+
return nil, xerrors.Errorf("failed to read current memory usage: %w", err)
1139+
}
1140+
1141+
stats, err := memory.Stat()
1142+
if err != nil {
1143+
return nil, xerrors.Errorf("failed to read memory stats: %w", err)
1144+
}
1145+
1146+
if stats.InactiveFileTotal > 0 {
1147+
if usedMemory < stats.InactiveFileTotal {
1148+
usedMemory = 0
1149+
} else {
1150+
usedMemory -= stats.InactiveFileTotal
1151+
}
1152+
}
1153+
1154+
return &api.Memory{
1155+
Limit: int64(memoryLimit),
1156+
Used: int64(usedMemory),
1157+
}, nil
1158+
}
1159+
1160+
func getMemoryResourceInfoV1(mountPoint, cgroupPath string) (*api.Memory, error) {
1161+
memory := v1.NewMemoryControllerWithMount(mountPoint, cgroupPath)
1162+
1163+
memoryLimit, err := memory.Limit()
1164+
if err != nil {
1165+
return nil, err
1166+
}
1167+
1168+
memInfo, err := linuxproc.ReadMemInfo("/proc/meminfo")
1169+
if err != nil {
1170+
return nil, xerrors.Errorf("failed to read meminfo: %w", err)
1171+
}
1172+
1173+
// if no memory limit has been specified, use total available memory
1174+
if memoryLimit == math.MaxUint64 || memoryLimit > memInfo.MemTotal*1024 {
1175+
// total memory is specifed on kilobytes -> convert to bytes
1176+
memoryLimit = memInfo.MemTotal * 1024
1177+
}
1178+
1179+
usedMemory, err := memory.Usage()
1180+
if err != nil {
1181+
return nil, xerrors.Errorf("failed to read memory limit: %w", err)
1182+
}
1183+
1184+
stats, err := memory.Stat()
1185+
if err != nil {
1186+
return nil, xerrors.Errorf("failed to read memory stats: %w", err)
1187+
}
1188+
1189+
if stats.InactiveFileTotal > 0 {
1190+
if usedMemory < stats.InactiveFileTotal {
1191+
usedMemory = 0
1192+
} else {
1193+
usedMemory -= stats.InactiveFileTotal
1194+
}
1195+
}
1196+
1197+
return &api.Memory{
1198+
Limit: int64(memoryLimit),
1199+
Used: int64(usedMemory),
1200+
}, nil
1201+
}
1202+
1203+
func getCpuResourceInfoV1(mountPoint, cgroupPath string) (*api.Cpu, error) {
1204+
cpu := v1.NewCpuControllerWithMount(mountPoint, cgroupPath)
1205+
1206+
t, err := resolveCPUStatV1(cpu)
1207+
if err != nil {
1208+
return nil, err
1209+
}
1210+
1211+
time.Sleep(time.Second)
1212+
1213+
t2, err := resolveCPUStatV1(cpu)
1214+
if err != nil {
1215+
return nil, err
1216+
}
1217+
1218+
cpuUsage := t2.usage - t.usage
1219+
totalTime := t2.uptime - t.uptime
1220+
used := cpuUsage / totalTime * 1000
1221+
1222+
quota, err := cpu.Quota()
1223+
if err != nil {
1224+
return nil, err
1225+
}
1226+
1227+
// if no cpu limit has been specified, use the number of cores
1228+
var limit uint64
1229+
if quota == math.MaxUint64 {
1230+
content, err := os.ReadFile(filepath.Join(mountPoint, "cpu", cgroupPath, "cpuacct.usage_percpu"))
1231+
if err != nil {
1232+
return nil, xerrors.Errorf("failed to read cpuacct.usage_percpu: %w", err)
1233+
}
1234+
limit = uint64(len(strings.Split(strings.TrimSpace(string(content)), " "))) * 1000
1235+
} else {
1236+
period, err := cpu.Period()
1237+
if err != nil {
1238+
return nil, err
1239+
}
1240+
1241+
limit = quota / period * 1000
1242+
}
1243+
1244+
return &api.Cpu{
1245+
Used: int64(used),
1246+
Limit: int64(limit),
1247+
}, nil
1248+
}
1249+
1250+
type cpuStat struct {
1251+
usage float64
1252+
uptime float64
1253+
}
1254+
1255+
func resolveCPUStatV1(cpu *v1.Cpu) (*cpuStat, error) {
1256+
usage_ns, err := cpu.Usage()
1257+
if err != nil {
1258+
return nil, xerrors.Errorf("failed to get cpu usage: %w", err)
1259+
}
1260+
1261+
// convert from nanoseconds to seconds
1262+
usage := float64(usage_ns) * 1e-9
1263+
uptime, err := readProcUptime()
1264+
if err != nil {
1265+
return nil, err
1266+
}
1267+
1268+
return &cpuStat{
1269+
usage: usage,
1270+
uptime: uptime,
1271+
}, nil
1272+
}
1273+
1274+
func resolveCPUStatV2(cpu *v2.Cpu) (*cpuStat, error) {
1275+
stats, err := cpu.Stat()
1276+
if err != nil {
1277+
return nil, xerrors.Errorf("failed to get cpu usage: %w", err)
1278+
}
1279+
1280+
usage := float64(stats.UsageTotal) * 1e-6
1281+
uptime, err := readProcUptime()
1282+
if err != nil {
1283+
return nil, err
1284+
}
1285+
1286+
return &cpuStat{
1287+
usage: usage,
1288+
uptime: uptime,
1289+
}, nil
1290+
}
1291+
1292+
func readProcUptime() (float64, error) {
1293+
content, err := os.ReadFile("/proc/uptime")
1294+
if err != nil {
1295+
return 0, xerrors.Errorf("failed to read uptime: %w", err)
1296+
}
1297+
values := strings.Split(strings.TrimSpace(string(content)), " ")
1298+
uptime, err := strconv.ParseFloat(values[0], 64)
1299+
if err != nil {
1300+
return 0, xerrors.Errorf("failed to parse uptime: %w", err)
1301+
}
1302+
1303+
return uptime, nil
1304+
}
1305+
9981306
type ratelimitingInterceptor map[string]ratelimit
9991307

10001308
type ratelimit struct {

install/installer/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ require (
6262
github.com/beorn7/perks v1.0.1 // indirect
6363
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b // indirect
6464
github.com/btcsuite/btcd v0.21.0-beta // indirect
65+
github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8 // indirect
6566
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
6667
github.com/cespare/xxhash/v2 v2.1.2 // indirect
6768
github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect

install/installer/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@ github.com/bugsnag/bugsnag-go v1.5.3 h1:yeRUT3mUE13jL1tGwvoQsKdVbAsQx9AJ+fqahKve
234234
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
235235
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
236236
github.com/bugsnag/panicwrap v1.2.0 h1:OzrKrRvXis8qEvOkfcxNcYbOd2O7xXS2nnKMEMABFQA=
237+
github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8 h1:SjZ2GvvOononHOpK84APFuMvxqsk3tEIaKH/z4Rpu3g=
238+
github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE=
237239
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
238240
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
239241
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=

0 commit comments

Comments
 (0)