9
9
"context"
10
10
"fmt"
11
11
"io"
12
+ "math"
12
13
"net"
13
14
"os"
14
15
"os/exec"
@@ -27,7 +28,10 @@ import (
27
28
"google.golang.org/grpc/codes"
28
29
"google.golang.org/grpc/status"
29
30
31
+ linuxproc "github.com/c9s/goprocinfo/linux"
30
32
"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"
31
35
"github.com/gitpod-io/gitpod/common-go/log"
32
36
"github.com/gitpod-io/gitpod/common-go/tracing"
33
37
wsinit "github.com/gitpod-io/gitpod/content-service/pkg/initializer"
@@ -180,6 +184,9 @@ func (wbs *InWorkspaceServiceServer) Start() error {
180
184
"/iws.InWorkspaceService/Teardown" : ratelimit {
181
185
UseOnce : true ,
182
186
},
187
+ "/iws.InWorkspaceService/WorkspaceInfo" : ratelimit {
188
+ Limiter : rate .NewLimiter (rate .Every (1500 * time .Millisecond ), 4 ),
189
+ },
183
190
}
184
191
185
192
wbs .srv = grpc .NewServer (grpc .ChainUnaryInterceptor (limits .UnaryInterceptor ()))
@@ -995,6 +1002,307 @@ func (wbs *InWorkspaceServiceServer) unPrepareForUserNS() error {
995
1002
return nil
996
1003
}
997
1004
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
+
998
1306
type ratelimitingInterceptor map [string ]ratelimit
999
1307
1000
1308
type ratelimit struct {
0 commit comments