gitee.com/mysnapcore/mysnapd@v0.1.0/snap/quota/quota_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2021 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package quota_test 21 22 import ( 23 "fmt" 24 "math" 25 "testing" 26 "time" 27 28 . "gopkg.in/check.v1" 29 30 "gitee.com/mysnapcore/mysnapd/gadget/quantity" 31 "gitee.com/mysnapcore/mysnapd/snap/quota" 32 "gitee.com/mysnapcore/mysnapd/systemd" 33 ) 34 35 // Hook up check.v1 into the "go test" runner 36 func Test(t *testing.T) { TestingT(t) } 37 38 type quotaTestSuite struct{} 39 40 var _ = Suite("aTestSuite{}) 41 42 func (ts *quotaTestSuite) TestNewGroup(c *C) { 43 44 tt := []struct { 45 name string 46 sliceFileName string 47 limits quota.Resources 48 err string 49 comment string 50 }{ 51 { 52 name: "group1", 53 limits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).WithCPUCount(1).WithCPUPercentage(100).WithCPUSet([]int{0}).WithThreadLimit(32).Build(), 54 comment: "basic happy", 55 }, 56 { 57 name: "biglimit", 58 limits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.Size(math.MaxUint64)).Build(), 59 comment: "huge limit happy", 60 }, 61 { 62 name: "zero", 63 limits: quota.NewResourcesBuilder().Build(), 64 err: `quota group must have at least one resource limit set`, 65 comment: "group with no limits", 66 }, 67 { 68 name: "group1-unsupported chars", 69 limits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build(), 70 err: `invalid quota group name: contains invalid characters.*`, 71 comment: "unsupported characters in group name", 72 }, 73 { 74 name: "group%%%", 75 limits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build(), 76 err: `invalid quota group name: contains invalid characters.*`, 77 comment: "more invalid characters in name", 78 }, 79 { 80 name: "CAPITALIZED", 81 limits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build(), 82 err: `invalid quota group name: contains invalid characters.*`, 83 comment: "capitalized letters", 84 }, 85 { 86 name: "g1", 87 limits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build(), 88 comment: "small group name", 89 }, 90 { 91 name: "name-with-dashes", 92 sliceFileName: `name\x2dwith\x2ddashes`, 93 limits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build(), 94 comment: "name with dashes", 95 }, 96 { 97 name: "", 98 limits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build(), 99 err: `invalid quota group name: must not be empty`, 100 comment: "empty group name", 101 }, 102 { 103 name: "g", 104 limits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build(), 105 err: `invalid quota group name: must be between 2 and 40 characters long.*`, 106 comment: "too small group name", 107 }, 108 { 109 name: "root", 110 limits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build(), 111 err: `group name "root" reserved`, 112 comment: "reserved root name", 113 }, 114 { 115 name: "snapd", 116 limits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build(), 117 err: `group name "snapd" reserved`, 118 comment: "reserved snapd name", 119 }, 120 { 121 name: "system", 122 limits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build(), 123 err: `group name "system" reserved`, 124 comment: "reserved system name", 125 }, 126 { 127 name: "user", 128 limits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build(), 129 err: `group name "user" reserved`, 130 comment: "reserved user name", 131 }, 132 } 133 134 for _, t := range tt { 135 comment := Commentf(t.comment) 136 grp, err := quota.NewGroup(t.name, t.limits) 137 if t.err != "" { 138 c.Assert(err, ErrorMatches, t.err, comment) 139 continue 140 } 141 c.Assert(err, IsNil, comment) 142 143 if t.sliceFileName != "" { 144 c.Assert(grp.SliceFileName(), Equals, "snap."+t.sliceFileName+".slice", comment) 145 } else { 146 c.Assert(grp.SliceFileName(), Equals, "snap."+t.name+".slice", comment) 147 } 148 } 149 } 150 151 func (ts *quotaTestSuite) TestSimpleSubGroupVerification(c *C) { 152 tt := []struct { 153 rootname string 154 rootlimits quota.Resources 155 subname string 156 sliceFileName string 157 sublimits quota.Resources 158 err string 159 comment string 160 }{ 161 { 162 rootlimits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).WithCPUCount(1).WithCPUPercentage(100).WithCPUSet([]int{0}).WithThreadLimit(32).Build(), 163 subname: "sub", 164 sublimits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).WithCPUCount(1).WithCPUPercentage(100).WithCPUSet([]int{0}).WithThreadLimit(32).Build(), 165 comment: "basic sub group with same quota as parent happy", 166 }, 167 { 168 rootlimits: quota.NewResourcesBuilder().WithMemoryLimit(2 * quantity.SizeMiB).WithCPUCount(1).WithCPUPercentage(100).WithCPUSet([]int{0}).WithThreadLimit(32).Build(), 169 subname: "sub", 170 sublimits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).WithCPUCount(1).WithCPUPercentage(50).WithCPUSet([]int{0}).WithThreadLimit(16).Build(), 171 comment: "basic sub group with smaller quota than parent happy", 172 }, 173 { 174 rootlimits: quota.NewResourcesBuilder().WithMemoryLimit(2 * quantity.SizeMiB).WithCPUCount(1).WithCPUPercentage(100).WithCPUSet([]int{0}).WithThreadLimit(32).Build(), 175 subname: "sub-with-dashes", 176 sliceFileName: `myroot-sub\x2dwith\x2ddashes`, 177 sublimits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).WithCPUCount(1).WithCPUPercentage(50).WithCPUSet([]int{0}).WithThreadLimit(16).Build(), 178 comment: "basic sub group with dashes in the name", 179 }, 180 { 181 rootname: "my-root", 182 rootlimits: quota.NewResourcesBuilder().WithMemoryLimit(2 * quantity.SizeMiB).WithCPUCount(1).WithCPUPercentage(100).WithCPUSet([]int{0}).WithThreadLimit(32).Build(), 183 subname: "sub-with-dashes", 184 sliceFileName: `my\x2droot-sub\x2dwith\x2ddashes`, 185 sublimits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).WithCPUCount(1).WithCPUPercentage(50).WithCPUSet([]int{0}).WithThreadLimit(16).Build(), 186 comment: "parent and sub group have dashes in name", 187 }, 188 { 189 rootlimits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).WithCPUCount(1).WithCPUPercentage(100).WithCPUSet([]int{0}).WithThreadLimit(32).Build(), 190 subname: "sub", 191 sublimits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB * 2).Build(), 192 err: "sub-group memory limit of 2 MiB is too large to fit inside group \"myroot\" remaining quota space 1 MiB", 193 comment: "sub group with larger memory quota than parent unhappy", 194 }, 195 { 196 rootlimits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).WithCPUCount(1).WithCPUPercentage(100).WithCPUSet([]int{0}).WithThreadLimit(32).Build(), 197 subname: "sub", 198 sublimits: quota.NewResourcesBuilder().WithCPUCount(2).WithCPUPercentage(100).Build(), 199 err: "sub-group cpu limit of 200% is too large to fit inside group \"myroot\" remaining quota space 100%", 200 comment: "sub group with larger cpu count quota than parent unhappy", 201 }, 202 { 203 rootlimits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).WithCPUCount(1).WithCPUPercentage(100).WithCPUSet([]int{0}).WithThreadLimit(32).Build(), 204 subname: "sub", 205 sublimits: quota.NewResourcesBuilder().WithCPUSet([]int{1}).Build(), 206 err: "sub-group cpu-set \\[1\\] is not a subset of group \"myroot\" cpu-set \\[0\\]", 207 comment: "sub group with different cpu allowance quota than parent unhappy", 208 }, 209 { 210 rootlimits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).WithCPUCount(1).WithCPUPercentage(100).WithCPUSet([]int{0}).WithThreadLimit(32).Build(), 211 subname: "sub", 212 sublimits: quota.NewResourcesBuilder().WithThreadLimit(64).Build(), 213 err: "sub-group thread limit of 64 is too large to fit inside group \"myroot\" remaining quota space 32", 214 comment: "sub group with larger task allowance quota than parent unhappy", 215 }, 216 { 217 rootlimits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).WithCPUCount(1).WithCPUPercentage(100).WithCPUSet([]int{0}).WithThreadLimit(32).Build(), 218 subname: "sub invalid chars", 219 sublimits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).WithCPUCount(1).WithCPUPercentage(100).WithCPUSet([]int{0}).WithThreadLimit(32).Build(), 220 err: `invalid quota group name: contains invalid characters.*`, 221 comment: "sub group with invalid name", 222 }, 223 { 224 rootlimits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).WithCPUCount(1).WithCPUPercentage(100).WithCPUSet([]int{0}).WithThreadLimit(32).Build(), 225 subname: "myroot", 226 sublimits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).WithCPUCount(1).WithCPUPercentage(100).WithCPUSet([]int{0}).WithThreadLimit(32).Build(), 227 err: `cannot use same name "myroot" for sub group as parent group`, 228 comment: "sub group with same name as parent group", 229 }, 230 { 231 rootlimits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).WithCPUCount(1).WithCPUPercentage(100).WithCPUSet([]int{0}).WithThreadLimit(32).Build(), 232 subname: "snapd", 233 sublimits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).WithCPUCount(1).WithCPUPercentage(100).WithCPUSet([]int{0}).WithThreadLimit(32).Build(), 234 err: `group name "snapd" reserved`, 235 comment: "sub group with reserved name", 236 }, 237 { 238 rootlimits: quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).WithCPUCount(1).WithCPUPercentage(100).WithCPUSet([]int{0}).WithThreadLimit(32).Build(), 239 subname: "zero", 240 sublimits: quota.NewResourcesBuilder().Build(), 241 err: `quota group must have at least one resource limit set`, 242 comment: "sub group with no limits", 243 }, 244 } 245 246 for _, t := range tt { 247 comment := Commentf(t.comment) 248 // make a root group 249 rootname := t.rootname 250 if rootname == "" { 251 rootname = "myroot" 252 } 253 rootGrp, err := quota.NewGroup(rootname, t.rootlimits) 254 c.Assert(err, IsNil, comment) 255 256 // make a sub-group under the root group 257 subGrp, err := rootGrp.NewSubGroup(t.subname, t.sublimits) 258 if t.err != "" { 259 c.Assert(err, ErrorMatches, t.err, comment) 260 continue 261 } 262 c.Assert(err, IsNil, comment) 263 264 if t.sliceFileName != "" { 265 c.Assert(subGrp.SliceFileName(), Equals, "snap."+t.sliceFileName+".slice") 266 } else { 267 c.Assert(subGrp.SliceFileName(), Equals, "snap.myroot-"+t.subname+".slice") 268 } 269 } 270 } 271 272 func (ts *quotaTestSuite) TestComplexSubGroups(c *C) { 273 rootGrp, err := quota.NewGroup("myroot", quota.NewResourcesBuilder().WithMemoryLimit(2*quantity.SizeMiB).Build()) 274 c.Assert(err, IsNil) 275 276 // try adding 2 sub-groups with total quota split exactly equally 277 sub1, err := rootGrp.NewSubGroup("sub1", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build()) 278 c.Assert(err, IsNil) 279 c.Assert(sub1.SliceFileName(), Equals, "snap.myroot-sub1.slice") 280 281 sub2, err := rootGrp.NewSubGroup("sub2", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build()) 282 c.Assert(err, IsNil) 283 c.Assert(sub2.SliceFileName(), Equals, "snap.myroot-sub2.slice") 284 285 // adding another sub-group to this group fails 286 _, err = rootGrp.NewSubGroup("sub3", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build()) 287 c.Assert(err, ErrorMatches, "sub-group memory limit of 1 MiB is too large to fit inside group \"myroot\" remaining quota space 0 B") 288 289 // we can however add a sub-group to one of the sub-groups with the exact 290 // size of the parent sub-group 291 subsub1, err := sub1.NewSubGroup("subsub1", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build()) 292 c.Assert(err, IsNil) 293 c.Assert(subsub1.SliceFileName(), Equals, "snap.myroot-sub1-subsub1.slice") 294 295 // and we can even add a sub-sub-sub-group to the sub-group 296 subsubsub1, err := subsub1.NewSubGroup("subsubsub1", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build()) 297 c.Assert(err, IsNil) 298 c.Assert(subsubsub1.SliceFileName(), Equals, "snap.myroot-sub1-subsub1-subsubsub1.slice") 299 } 300 301 func (ts *quotaTestSuite) TestGroupUnmixableSnapsSubgroups(c *C) { 302 parent, err := quota.NewGroup("parent", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build()) 303 c.Assert(err, IsNil) 304 305 // now we add a snap to the parent group 306 parent.Snaps = []string{"test-snap"} 307 308 // add a subgroup to the parent group, this should fail as the group now has snaps 309 _, err = parent.NewSubGroup("sub", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build()) 310 c.Assert(err, ErrorMatches, "cannot mix sub groups with snaps in the same group") 311 } 312 313 func (ts *quotaTestSuite) TestJournalNamespaceName(c *C) { 314 grp, err := quota.NewGroup("foo", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build()) 315 c.Assert(err, IsNil) 316 c.Check(grp.JournalNamespaceName(), Equals, "snap-foo") 317 } 318 319 func (ts *quotaTestSuite) TestJournalConfFileName(c *C) { 320 grp, err := quota.NewGroup("foo", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build()) 321 c.Assert(err, IsNil) 322 c.Check(grp.JournalConfFileName(), Equals, "journald@snap-foo.conf") 323 } 324 325 func (ts *quotaTestSuite) TestJournalServiceName(c *C) { 326 grp, err := quota.NewGroup("foo", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build()) 327 c.Assert(err, IsNil) 328 c.Check(grp.JournalServiceName(), Equals, "systemd-journald@snap-foo.service") 329 } 330 331 func (ts *quotaTestSuite) TestJournalServiceDropInDir(c *C) { 332 grp, err := quota.NewGroup("foo", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build()) 333 c.Assert(err, IsNil) 334 c.Check(grp.JournalServiceDropInDir(), Equals, "/etc/systemd/system/systemd-journald@snap-foo.service.d") 335 } 336 337 func (ts *quotaTestSuite) TestJournalServiceDropInFile(c *C) { 338 grp, err := quota.NewGroup("foo", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB).Build()) 339 c.Assert(err, IsNil) 340 c.Check(grp.JournalServiceDropInFile(), Equals, "/etc/systemd/system/systemd-journald@snap-foo.service.d/00-snap.conf") 341 } 342 343 func (ts *quotaTestSuite) TestResolveCrossReferences(c *C) { 344 tt := []struct { 345 grps map[string]*quota.Group 346 err string 347 comment string 348 }{ 349 { 350 grps: map[string]*quota.Group{ 351 "foogroup": { 352 Name: "foogroup", 353 MemoryLimit: quantity.SizeMiB, 354 }, 355 }, 356 comment: "single group", 357 }, 358 { 359 grps: map[string]*quota.Group{ 360 "foogroup": { 361 Name: "foogroup", 362 MemoryLimit: quantity.SizeMiB, 363 ParentGroup: "foogroup", 364 }, 365 }, 366 err: `group "foogroup" is invalid: group has circular parent reference to itself`, 367 comment: "parent group self-reference group", 368 }, 369 { 370 grps: map[string]*quota.Group{ 371 "foogroup": { 372 Name: "foogroup", 373 MemoryLimit: quantity.SizeMiB, 374 SubGroups: []string{"foogroup"}, 375 }, 376 }, 377 err: `group "foogroup" is invalid: group has circular sub-group reference to itself`, 378 comment: "parent group self-reference group", 379 }, 380 { 381 grps: map[string]*quota.Group{ 382 "foogroup": { 383 Name: "foogroup", 384 MemoryLimit: 0, 385 }, 386 }, 387 err: `group "foogroup" is invalid: quota group must have at least one resource limit set`, 388 comment: "invalid group", 389 }, 390 { 391 grps: map[string]*quota.Group{ 392 "foogroup": { 393 Name: "foogroup", 394 MemoryLimit: quantity.SizeMiB, 395 }, 396 "foogroup2": { 397 Name: "foogroup2", 398 MemoryLimit: quantity.SizeMiB, 399 }, 400 }, 401 comment: "multiple root groups", 402 }, 403 { 404 grps: map[string]*quota.Group{ 405 "foogroup": { 406 Name: "foogroup", 407 MemoryLimit: quantity.SizeMiB, 408 }, 409 "subgroup": { 410 Name: "subgroup", 411 MemoryLimit: quantity.SizeMiB, 412 ParentGroup: "foogroup", 413 }, 414 }, 415 err: `group "foogroup" does not reference necessary child group "subgroup"`, 416 comment: "incomplete references in parent group to child group", 417 }, 418 { 419 grps: map[string]*quota.Group{ 420 "foogroup": { 421 Name: "foogroup", 422 MemoryLimit: quantity.SizeMiB, 423 SubGroups: []string{"subgroup"}, 424 }, 425 "subgroup": { 426 Name: "subgroup", 427 MemoryLimit: quantity.SizeMiB, 428 }, 429 }, 430 err: `group "subgroup" does not reference necessary parent group "foogroup"`, 431 comment: "incomplete references in sub-group to parent group", 432 }, 433 { 434 grps: map[string]*quota.Group{ 435 "foogroup": { 436 Name: "foogroup", 437 MemoryLimit: quantity.SizeMiB, 438 SubGroups: []string{"subgroup"}, 439 }, 440 "subgroup": { 441 Name: "subgroup", 442 MemoryLimit: quantity.SizeMiB, 443 ParentGroup: "foogroup", 444 }, 445 }, 446 comment: "valid fully specified sub-group", 447 }, 448 { 449 grps: map[string]*quota.Group{ 450 "foogroup": { 451 Name: "foogroup", 452 MemoryLimit: 2 * quantity.SizeMiB, 453 SubGroups: []string{"subgroup1", "subgroup2"}, 454 }, 455 "subgroup1": { 456 Name: "subgroup1", 457 MemoryLimit: quantity.SizeMiB, 458 ParentGroup: "foogroup", 459 }, 460 "subgroup2": { 461 Name: "subgroup2", 462 MemoryLimit: quantity.SizeMiB, 463 ParentGroup: "foogroup", 464 }, 465 }, 466 comment: "multiple valid fully specified sub-groups", 467 }, 468 { 469 grps: map[string]*quota.Group{ 470 "foogroup": { 471 Name: "foogroup", 472 MemoryLimit: quantity.SizeMiB, 473 SubGroups: []string{"subgroup1"}, 474 }, 475 "subgroup1": { 476 Name: "subgroup1", 477 MemoryLimit: quantity.SizeMiB, 478 ParentGroup: "foogroup", 479 SubGroups: []string{"subgroup2"}, 480 }, 481 "subgroup2": { 482 Name: "subgroup2", 483 MemoryLimit: quantity.SizeMiB, 484 ParentGroup: "subgroup1", 485 }, 486 }, 487 comment: "deeply nested valid fully specified sub-groups", 488 }, 489 { 490 grps: map[string]*quota.Group{ 491 "foogroup": { 492 Name: "foogroup", 493 MemoryLimit: quantity.SizeMiB, 494 SubGroups: []string{"subgroup1"}, 495 }, 496 "subgroup1": { 497 Name: "subgroup1", 498 MemoryLimit: quantity.SizeMiB, 499 ParentGroup: "foogroup", 500 SubGroups: []string{"subgroup2"}, 501 }, 502 "subgroup2": { 503 Name: "subgroup2", 504 MemoryLimit: quantity.SizeMiB, 505 // missing parent reference 506 }, 507 }, 508 err: `group "subgroup2" does not reference necessary parent group "subgroup1"`, 509 comment: "deeply nested invalid fully specified sub-groups", 510 }, 511 { 512 grps: map[string]*quota.Group{ 513 "not-foogroup": { 514 Name: "foogroup", 515 MemoryLimit: quantity.SizeMiB, 516 }, 517 }, 518 err: `group has name "foogroup", but is referenced as "not-foogroup"`, 519 comment: "group misname", 520 }, 521 { 522 grps: map[string]*quota.Group{ 523 "foogroup": { 524 Name: "foogroup", 525 MemoryLimit: quantity.SizeMiB, 526 SubGroups: []string{"other-missing"}, 527 }, 528 }, 529 err: `missing group "other-missing" referenced as the sub-group of group "foogroup"`, 530 comment: "missing sub-group name", 531 }, 532 { 533 grps: map[string]*quota.Group{ 534 "foogroup": { 535 Name: "foogroup", 536 MemoryLimit: quantity.SizeMiB, 537 ParentGroup: "other-missing", 538 }, 539 }, 540 err: `missing group "other-missing" referenced as the parent of group "foogroup"`, 541 comment: "missing sub-group name", 542 }, 543 } 544 545 for _, t := range tt { 546 comment := Commentf(t.comment) 547 err := quota.ResolveCrossReferences(t.grps) 548 if t.err != "" { 549 c.Assert(err, ErrorMatches, t.err, comment) 550 } else { 551 c.Assert(err, IsNil, comment) 552 } 553 } 554 } 555 556 func (ts *quotaTestSuite) TestChangingRequirementsDoesNotBreakExistingGroups(c *C) { 557 tt := []struct { 558 grp *quota.Group 559 err string 560 comment string 561 }{ 562 // Test that an existing group with lower than 640kB limit 563 // does not break .validate(), since the requirement was increased 564 { 565 grp: "a.Group{ 566 Name: "foogroup", 567 MemoryLimit: quantity.SizeKiB * 12, 568 }, 569 comment: "group with a lower memory limit than 640kB", 570 }, 571 } 572 573 for _, t := range tt { 574 comment := Commentf(t.comment) 575 err := t.grp.ValidateGroup() 576 if t.err != "" { 577 c.Assert(err, ErrorMatches, t.err, comment) 578 } else { 579 c.Assert(err, IsNil, comment) 580 } 581 } 582 } 583 584 func (ts *quotaTestSuite) TestAddAllNecessaryGroupsAvoidsInfiniteRecursion(c *C) { 585 grp, err := quota.NewGroup("infinite-group", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 586 c.Assert(err, IsNil) 587 588 grp2, err := grp.NewSubGroup("infinite-group2", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 589 c.Assert(err, IsNil) 590 591 // create a cycle artificially to the same group 592 grp2.SetInternalSubGroups([]*quota.Group{grp2}) 593 594 // now we fail to add this to a quota set 595 qs := "a.QuotaGroupSet{} 596 err = qs.AddAllNecessaryGroups(grp) 597 c.Assert(err, ErrorMatches, "internal error: circular reference found") 598 599 // create a more difficult to detect cycle going from the child to the 600 // parent 601 grp2.SetInternalSubGroups([]*quota.Group{grp}) 602 err = qs.AddAllNecessaryGroups(grp) 603 c.Assert(err, ErrorMatches, "internal error: circular reference found") 604 605 // make a real sub-group and try one more level of indirection going back 606 // to the parent 607 grp2.SetInternalSubGroups(nil) 608 grp3, err := grp2.NewSubGroup("infinite-group3", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 609 c.Assert(err, IsNil) 610 grp3.SetInternalSubGroups([]*quota.Group{grp}) 611 612 err = qs.AddAllNecessaryGroups(grp) 613 c.Assert(err, ErrorMatches, "internal error: circular reference found") 614 } 615 616 func (ts *quotaTestSuite) TestAddAllNecessaryGroups(c *C) { 617 qs := "a.QuotaGroupSet{} 618 619 // it should initially be empty 620 c.Assert(qs.AllQuotaGroups(), HasLen, 0) 621 622 grp1, err := quota.NewGroup("myroot", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 623 c.Assert(err, IsNil) 624 625 // add the group and make sure it is in the set 626 err = qs.AddAllNecessaryGroups(grp1) 627 c.Assert(err, IsNil) 628 c.Assert(qs.AllQuotaGroups(), DeepEquals, []*quota.Group{grp1}) 629 630 // adding multiple times doesn't change the set 631 err = qs.AddAllNecessaryGroups(grp1) 632 c.Assert(err, IsNil) 633 err = qs.AddAllNecessaryGroups(grp1) 634 c.Assert(err, IsNil) 635 c.Assert(qs.AllQuotaGroups(), DeepEquals, []*quota.Group{grp1}) 636 637 // add a new group and make sure it is in the set now 638 grp2, err := quota.NewGroup("myroot2", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 639 c.Assert(err, IsNil) 640 err = qs.AddAllNecessaryGroups(grp2) 641 c.Assert(err, IsNil) 642 c.Assert(qs.AllQuotaGroups(), DeepEquals, []*quota.Group{grp1, grp2}) 643 644 // start again 645 qs = "a.QuotaGroupSet{} 646 647 // make a sub-group and add the root group - it will automatically add 648 // the sub-group without us needing to explicitly add the sub-group 649 subgrp1, err := grp1.NewSubGroup("mysub1", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 650 c.Assert(err, IsNil) 651 // add grp2 as well 652 err = qs.AddAllNecessaryGroups(grp2) 653 c.Assert(err, IsNil) 654 655 err = qs.AddAllNecessaryGroups(grp1) 656 c.Assert(err, IsNil) 657 c.Assert(qs.AllQuotaGroups(), DeepEquals, []*quota.Group{grp1, grp2, subgrp1}) 658 659 // we can explicitly add the sub-group and still have the same set too 660 err = qs.AddAllNecessaryGroups(subgrp1) 661 c.Assert(err, IsNil) 662 c.Assert(qs.AllQuotaGroups(), DeepEquals, []*quota.Group{grp1, grp2, subgrp1}) 663 664 // create a new set of group and sub-groups to add the deepest child group 665 // and add that, and notice that the root groups are also added 666 grp3, err := quota.NewGroup("myroot3", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 667 c.Assert(err, IsNil) 668 669 subgrp3, err := grp3.NewSubGroup("mysub3", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 670 c.Assert(err, IsNil) 671 672 subsubgrp3, err := subgrp3.NewSubGroup("mysubsub3", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 673 c.Assert(err, IsNil) 674 675 err = qs.AddAllNecessaryGroups(subsubgrp3) 676 c.Assert(err, IsNil) 677 c.Assert(qs.AllQuotaGroups(), DeepEquals, []*quota.Group{grp1, grp2, grp3, subgrp1, subgrp3, subsubgrp3}) 678 679 // finally create a tree with multiple branches and ensure that adding just 680 // a single deepest child will add all the other deepest children from other 681 // branches 682 grp4, err := quota.NewGroup("myroot4", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 683 c.Assert(err, IsNil) 684 685 subgrp4, err := grp4.NewSubGroup("mysub4", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB/2).Build()) 686 c.Assert(err, IsNil) 687 688 subgrp5, err := grp4.NewSubGroup("mysub5", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB/2).Build()) 689 c.Assert(err, IsNil) 690 691 // adding just subgrp5 to a quota set will automatically add the other sub 692 // group, subgrp4 693 qs2 := "a.QuotaGroupSet{} 694 err = qs2.AddAllNecessaryGroups(subgrp4) 695 c.Assert(err, IsNil) 696 c.Assert(qs2.AllQuotaGroups(), DeepEquals, []*quota.Group{grp4, subgrp4, subgrp5}) 697 } 698 699 func (ts *quotaTestSuite) TestResolveCrossReferencesLimitCheckSkipsSelf(c *C) { 700 grp1, err := quota.NewGroup("myroot", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 701 c.Assert(err, IsNil) 702 703 subgrp1, err := grp1.NewSubGroup("mysub1", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 704 c.Assert(err, IsNil) 705 706 subgrp2, err := subgrp1.NewSubGroup("mysub2", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 707 c.Assert(err, IsNil) 708 709 all := map[string]*quota.Group{ 710 "myroot": grp1, 711 "mysub1": subgrp1, 712 "mysub2": subgrp2, 713 } 714 err = quota.ResolveCrossReferences(all) 715 c.Assert(err, IsNil) 716 } 717 718 func (ts *quotaTestSuite) TestResolveCrossReferencesCircular(c *C) { 719 grp1, err := quota.NewGroup("myroot", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 720 c.Assert(err, IsNil) 721 722 subgrp1, err := grp1.NewSubGroup("mysub1", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 723 c.Assert(err, IsNil) 724 725 subgrp2, err := subgrp1.NewSubGroup("mysub2", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 726 c.Assert(err, IsNil) 727 728 all := map[string]*quota.Group{ 729 "myroot": grp1, 730 "mysub1": subgrp1, 731 "mysub2": subgrp2, 732 } 733 // try to set up circular ref 734 subgrp2.SubGroups = append(subgrp2.SubGroups, "mysub1") 735 err = quota.ResolveCrossReferences(all) 736 c.Assert(err, ErrorMatches, `.*reference necessary parent.*`) 737 } 738 739 type systemctlInactiveServiceError struct{} 740 741 func (s systemctlInactiveServiceError) Msg() []byte { return []byte("inactive") } 742 func (s systemctlInactiveServiceError) ExitCode() int { return 0 } 743 func (s systemctlInactiveServiceError) Error() string { return "inactive" } 744 745 func (ts *quotaTestSuite) TestCurrentMemoryUsage(c *C) { 746 systemctlCalls := 0 747 r := systemd.MockSystemctl(func(args ...string) ([]byte, error) { 748 systemctlCalls++ 749 switch systemctlCalls { 750 751 // inactive case, memory is 0 752 case 1: 753 // first time pretend the service is inactive 754 c.Assert(args, DeepEquals, []string{"is-active", "snap.group.slice"}) 755 return []byte("inactive"), systemctlInactiveServiceError{} 756 757 // active but no tasks, but we still return the memory usage because it 758 // can be valid on some systems to have non-zero memory usage for a 759 // group without any tasks in it, such as on hirsute, arch, fedora 33+, 760 // and debian sid 761 case 2: 762 // now pretend it is active 763 c.Assert(args, DeepEquals, []string{"is-active", "snap.group.slice"}) 764 return []byte("active"), nil 765 case 3: 766 // and the memory count can be non-zero like 767 c.Assert(args, DeepEquals, []string{"show", "--property", "MemoryCurrent", "snap.group.slice"}) 768 return []byte("MemoryCurrent=4096"), nil 769 770 case 4: 771 // now pretend it is active 772 c.Assert(args, DeepEquals, []string{"is-active", "snap.group.slice"}) 773 return []byte("active"), nil 774 case 5: 775 // and the memory count can be zero too 776 c.Assert(args, DeepEquals, []string{"show", "--property", "MemoryCurrent", "snap.group.slice"}) 777 return []byte("MemoryCurrent=0"), nil 778 779 // bug case where 16 exb is erroneous - this is left in for posterity, 780 // but we don't handle this differently, previously we had a workaround 781 // for this sort of case, but it ended up not being tenable but still 782 // test that a huge value just gets reported as-is 783 case 6: 784 // the cgroup is active, has no tasks and has 16 exb usage 785 c.Assert(args, DeepEquals, []string{"is-active", "snap.group.slice"}) 786 return []byte("active"), nil 787 case 7: 788 // since it is active, we will query the current memory usage, 789 // this time return an obviously wrong number 790 c.Assert(args, DeepEquals, []string{"show", "--property", "MemoryCurrent", "snap.group.slice"}) 791 return []byte("MemoryCurrent=18446744073709551615"), nil 792 793 default: 794 c.Errorf("too many systemctl calls (%d) (current call is %+v)", systemctlCalls, args) 795 return []byte("broken test"), fmt.Errorf("broken test") 796 } 797 }) 798 defer r() 799 800 grp1, err := quota.NewGroup("group", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 801 c.Assert(err, IsNil) 802 803 // group initially is inactive, so it has no current memory usage 804 currentMem, err := grp1.CurrentMemoryUsage() 805 c.Assert(err, IsNil) 806 c.Assert(currentMem, Equals, quantity.Size(0)) 807 808 // now with the slice mocked as active it has real usage 809 currentMem, err = grp1.CurrentMemoryUsage() 810 c.Assert(err, IsNil) 811 c.Assert(currentMem, Equals, 4*quantity.SizeKiB) 812 813 // but it can also have 0 usage 814 currentMem, err = grp1.CurrentMemoryUsage() 815 c.Assert(err, IsNil) 816 c.Assert(currentMem, Equals, quantity.Size(0)) 817 818 // and it can also be an incredibly huge value too 819 currentMem, err = grp1.CurrentMemoryUsage() 820 c.Assert(err, IsNil) 821 const sixteenExb = quantity.Size(1<<64 - 1) 822 c.Assert(currentMem, Equals, sixteenExb) 823 } 824 825 func (ts *quotaTestSuite) TestCurrentTaskUsage(c *C) { 826 systemctlCalls := 0 827 r := systemd.MockSystemctl(func(args ...string) ([]byte, error) { 828 systemctlCalls++ 829 switch systemctlCalls { 830 831 // inactive case, number of tasks must be 0 832 case 1: 833 // first time pretend the service is inactive 834 c.Assert(args, DeepEquals, []string{"is-active", "snap.group.slice"}) 835 return []byte("inactive"), systemctlInactiveServiceError{} 836 837 // active cases 838 case 2: 839 // now pretend it is active 840 c.Assert(args, DeepEquals, []string{"is-active", "snap.group.slice"}) 841 return []byte("active"), nil 842 case 3: 843 // and the task count can be non-zero like 844 c.Assert(args, DeepEquals, []string{"show", "--property", "TasksCurrent", "snap.group.slice"}) 845 return []byte("TasksCurrent=32"), nil 846 847 case 4: 848 // now pretend it is active 849 c.Assert(args, DeepEquals, []string{"is-active", "snap.group.slice"}) 850 return []byte("active"), nil 851 case 5: 852 // and no tasks are active 853 c.Assert(args, DeepEquals, []string{"show", "--property", "TasksCurrent", "snap.group.slice"}) 854 return []byte("TasksCurrent=0"), nil 855 856 default: 857 c.Errorf("unexpected number of systemctl calls (%d) (current call is %+v)", systemctlCalls, args) 858 return []byte("broken test"), fmt.Errorf("broken test") 859 } 860 }) 861 defer r() 862 863 grp1, err := quota.NewGroup("group", quota.NewResourcesBuilder().WithThreadLimit(32).Build()) 864 c.Assert(err, IsNil) 865 866 // group initially is inactive, so it has no current task usage 867 currentTasks, err := grp1.CurrentTaskUsage() 868 c.Check(err, IsNil) 869 c.Check(currentTasks, Equals, 0) 870 c.Check(systemctlCalls, Equals, 1) 871 872 // now with the slice mocked as active it has real usage 873 currentTasks, err = grp1.CurrentTaskUsage() 874 c.Check(err, IsNil) 875 c.Check(currentTasks, Equals, 32) 876 c.Check(systemctlCalls, Equals, 3) 877 878 // but it can also have 0 usage 879 currentTasks, err = grp1.CurrentTaskUsage() 880 c.Check(err, IsNil) 881 c.Check(currentTasks, Equals, 0) 882 c.Check(systemctlCalls, Equals, 5) 883 } 884 885 func (ts *quotaTestSuite) TestGetGroupQuotaAllocations(c *C) { 886 // Verify we get the correct allocations for a group with a more complex tree-structure 887 // and different quotas split out into different sub-groups. 888 // The tree we will be verifying will be like this 889 // <groot> (root group, 1GB Memory) 890 // / | \ 891 // <cpu-q0> | \ (subgroup, 2x50% Cpu Quota) 892 // / <thread-q0> \ (subgroup, 32 threads) 893 // / | <cpus-q0> (subgroup, cpu-set quota with cpus 0,1) 894 // <mem-q1> <mem-q2> \ (2 subgroups, 256MB Memory each) 895 // | | <cpus-q1> (subgroup, cpu-set quota with cpus 0) 896 // <cpu-q1> <thread-q1> (subgroups, cpu quota of 50%, thread quota of 16) 897 // | 898 // <mem-q3> (subgroup, 128MB Memory) 899 grp1, err := quota.NewGroup("groot", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 900 c.Assert(err, IsNil) 901 902 cpuq0, err := grp1.NewSubGroup("cpu-q0", quota.NewResourcesBuilder().WithCPUCount(2).WithCPUPercentage(50).Build()) 903 c.Assert(err, IsNil) 904 905 thrq0, err := grp1.NewSubGroup("thread-q0", quota.NewResourcesBuilder().WithThreadLimit(32).Build()) 906 c.Assert(err, IsNil) 907 908 cpusq0, err := grp1.NewSubGroup("cpus-q0", quota.NewResourcesBuilder().WithCPUSet([]int{0, 1}).Build()) 909 c.Assert(err, IsNil) 910 911 memq1, err := cpuq0.NewSubGroup("mem-q1", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB*256).Build()) 912 c.Assert(err, IsNil) 913 914 memq2, err := thrq0.NewSubGroup("mem-q2", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB*256).Build()) 915 c.Assert(err, IsNil) 916 917 _, err = cpusq0.NewSubGroup("cpus-q1", quota.NewResourcesBuilder().WithCPUSet([]int{0}).Build()) 918 c.Check(err, IsNil) 919 920 _, err = memq1.NewSubGroup("cpu-q1", quota.NewResourcesBuilder().WithCPUCount(1).WithCPUPercentage(50).Build()) 921 c.Check(err, IsNil) 922 923 thrq1, err := memq2.NewSubGroup("thread-q1", quota.NewResourcesBuilder().WithThreadLimit(16).Build()) 924 c.Assert(err, IsNil) 925 926 _, err = thrq1.NewSubGroup("mem-q3", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB*128).Build()) 927 c.Check(err, IsNil) 928 929 // Now we verify that the reservations made for the relevant groups are correct. The upper parent group will 930 // contained a combined overview of reserveations made. 931 allReservations := grp1.InspectInternalQuotaAllocations() 932 933 // Verify the root group 934 c.Check(allReservations["groot"], DeepEquals, "a.GroupQuotaAllocations{ 935 MemoryLimit: quantity.SizeGiB, 936 MemoryReservedByChildren: quantity.SizeMiB * 512, 937 CPUReservedByChildren: 100, 938 ThreadsReservedByChildren: 32, 939 CPUSetLimit: []int{}, 940 CPUSetReservedByChildren: []int{0, 1}, 941 }) 942 943 // Verify the subgroup cpu-q0 944 c.Check(allReservations["cpu-q0"], DeepEquals, "a.GroupQuotaAllocations{ 945 CPULimit: 100, 946 CPUReservedByChildren: 50, 947 MemoryReservedByChildren: quantity.SizeMiB * 256, 948 CPUSetLimit: []int{}, 949 }) 950 951 // Verify the subgroup thread-q0 952 c.Check(allReservations["thread-q0"], DeepEquals, "a.GroupQuotaAllocations{ 953 MemoryReservedByChildren: quantity.SizeMiB * 256, 954 ThreadsLimit: 32, 955 ThreadsReservedByChildren: 16, 956 CPUSetLimit: []int{}, 957 }) 958 959 // Verify the subgroup cpus-q0 960 c.Check(allReservations["cpus-q0"], DeepEquals, "a.GroupQuotaAllocations{ 961 CPUSetLimit: []int{0, 1}, 962 CPUSetReservedByChildren: []int{0}, 963 }) 964 965 // Verify the subgroup cpus-q1 966 c.Check(allReservations["cpus-q1"], DeepEquals, "a.GroupQuotaAllocations{ 967 CPUSetLimit: []int{0}, 968 }) 969 970 // Verify the subgroup mem-q1 971 c.Check(allReservations["mem-q1"], DeepEquals, "a.GroupQuotaAllocations{ 972 MemoryLimit: quantity.SizeMiB * 256, 973 CPUReservedByChildren: 50, 974 CPUSetLimit: []int{}, 975 }) 976 977 // Verify the subgroup mem-q2 978 c.Check(allReservations["mem-q2"], DeepEquals, "a.GroupQuotaAllocations{ 979 MemoryLimit: quantity.SizeMiB * 256, 980 MemoryReservedByChildren: quantity.SizeMiB * 128, 981 ThreadsReservedByChildren: 16, 982 CPUSetLimit: []int{}, 983 }) 984 985 // Verify the subgroup cpu-q1 986 c.Check(allReservations["cpu-q1"], DeepEquals, "a.GroupQuotaAllocations{ 987 CPULimit: 50, 988 CPUSetLimit: []int{}, 989 }) 990 991 // Verify the subgroup thread-q1 992 c.Check(allReservations["thread-q1"], DeepEquals, "a.GroupQuotaAllocations{ 993 MemoryReservedByChildren: quantity.SizeMiB * 128, 994 ThreadsLimit: 16, 995 CPUSetLimit: []int{}, 996 }) 997 998 // Verify the subgroup mem-q3 999 c.Check(allReservations["mem-q3"], DeepEquals, "a.GroupQuotaAllocations{ 1000 MemoryLimit: quantity.SizeMiB * 128, 1001 CPUSetLimit: []int{}, 1002 }) 1003 } 1004 1005 func (ts *quotaTestSuite) TestNestingOfLimitsWithExceedingParent(c *C) { 1006 grp1, err := quota.NewGroup("groot", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 1007 c.Assert(err, IsNil) 1008 1009 subgrp1, err := grp1.NewSubGroup("cpu-sub", quota.NewResourcesBuilder().WithCPUCount(2).WithCPUPercentage(50).Build()) 1010 c.Assert(err, IsNil) 1011 1012 _, err = grp1.NewSubGroup("thread-sub", quota.NewResourcesBuilder().WithThreadLimit(32).Build()) 1013 c.Check(err, IsNil) 1014 1015 _, err = grp1.NewSubGroup("cpus-sub", quota.NewResourcesBuilder().WithCPUSet([]int{0, 1}).Build()) 1016 c.Check(err, IsNil) 1017 1018 // Now we have the root with a memory limit, and three subgroups with 1019 // each with one of the remaining limits. The point of this test is to make 1020 // sure nested cases of limits that don't fit are caught and reported. So in a 1021 // sub-sub group we create a limit higher than the upper parent 1022 _, err = subgrp1.NewSubGroup("mem-sub", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB*2).Build()) 1023 c.Check(err, ErrorMatches, `sub-group memory limit of 2 GiB is too large to fit inside group \"groot\" remaining quota space 1 GiB`) 1024 } 1025 1026 func (ts *quotaTestSuite) TestNestingOfLimitsWithExceedingSiblings(c *C) { 1027 grp1, err := quota.NewGroup("groot", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 1028 c.Assert(err, IsNil) 1029 1030 subgrp1, err := grp1.NewSubGroup("cpu-sub", quota.NewResourcesBuilder().WithCPUCount(2).WithCPUPercentage(50).Build()) 1031 c.Assert(err, IsNil) 1032 1033 _, err = grp1.NewSubGroup("thread-sub", quota.NewResourcesBuilder().WithThreadLimit(32).Build()) 1034 c.Check(err, IsNil) 1035 1036 subgrp2, err := grp1.NewSubGroup("cpus-sub", quota.NewResourcesBuilder().WithCPUSet([]int{0, 1}).Build()) 1037 c.Check(err, IsNil) 1038 1039 // The point here is to catch if we, in a nested, scenario, together with our siblings 1040 // exceed one of the parent's limits. 1041 subgrp3, err := subgrp1.NewSubGroup("mem-sub1", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 1042 c.Assert(err, IsNil) 1043 1044 _, err = subgrp3.NewSubGroup("mem-sub-sub", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 1045 c.Check(err, IsNil) 1046 1047 // now we have consumed the entire memory quota set by the parent, so this should fail 1048 _, err = subgrp2.NewSubGroup("mem-sub2", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 1049 c.Check(err, ErrorMatches, `sub-group memory limit of 1 GiB is too large to fit inside group \"groot\" remaining quota space 0 B`) 1050 } 1051 1052 func (ts *quotaTestSuite) TestChangingSubgroupLimits(c *C) { 1053 grp1, err := quota.NewGroup("groot", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 1054 c.Assert(err, IsNil) 1055 1056 subgrp1, err := grp1.NewSubGroup("cpu-sub", quota.NewResourcesBuilder().WithCPUCount(2).WithCPUPercentage(50).Build()) 1057 c.Assert(err, IsNil) 1058 1059 // Create a nested subgroup with a memory limit of only half, then we try to adjust the value to another 1060 // larger value. This must succeed. 1061 memgrp, err := subgrp1.NewSubGroup("mem-sub", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB/2).Build()) 1062 c.Assert(err, IsNil) 1063 1064 // Now we change it to fill the entire quota of our upper parent 1065 err = memgrp.UpdateQuotaLimits(quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 1066 c.Check(err, IsNil) 1067 1068 // Now we try to change the limits of the subgroup to a value that is too large to fit inside the parent, 1069 // the error message should also correctly report that the remaining space is 1GiB, as it should not consider 1070 // the current memory quota of the subgroup. 1071 err = memgrp.UpdateQuotaLimits(quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB * 2).Build()) 1072 c.Check(err, ErrorMatches, `sub-group memory limit of 2 GiB is too large to fit inside group \"groot\" remaining quota space 1 GiB`) 1073 } 1074 1075 func (ts *quotaTestSuite) TestChangingParentMemoryLimits(c *C) { 1076 // The purpose here is to make sure we can't change the limits of the parent group 1077 // that would otherwise conflict with the current usage of limits by children of the 1078 // parent. 1079 grp1, err := quota.NewGroup("groot", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 1080 c.Assert(err, IsNil) 1081 1082 subgrp1, err := grp1.NewSubGroup("cpu-sub", quota.NewResourcesBuilder().WithCPUCount(2).WithCPUPercentage(50).Build()) 1083 c.Assert(err, IsNil) 1084 1085 // Create a nested subgroup with a memory limit that takes up the entire quota of the parent 1086 _, err = subgrp1.NewSubGroup("mem-sub", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 1087 c.Assert(err, IsNil) 1088 1089 // Now the test is to change the upper most parent limit so that it would be less 1090 // than the current usage, which we should not be able to do 1091 err = grp1.QuotaUpdateCheck(quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB / 2).Build()) 1092 c.Check(err, ErrorMatches, `group memory limit of 512 MiB is too small to fit current subgroup usage of 1 GiB`) 1093 } 1094 1095 func (ts *quotaTestSuite) TestChangingParentCpuPercentageLimits(c *C) { 1096 // The purpose here is to make sure we can't change the limits of the parent group 1097 // that would otherwise conflict with the current usage of limits by children of the 1098 // parent. 1099 grp1, err := quota.NewGroup("groot", quota.NewResourcesBuilder().WithCPUCount(2).WithCPUPercentage(50).Build()) 1100 c.Assert(err, IsNil) 1101 1102 subgrp1, err := grp1.NewSubGroup("mem-sub", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 1103 c.Assert(err, IsNil) 1104 1105 // Create a nested subgroup with a cpu limit that takes up the entire quota of the parent 1106 _, err = subgrp1.NewSubGroup("cpu-sub", quota.NewResourcesBuilder().WithCPUCount(2).WithCPUPercentage(50).Build()) 1107 c.Assert(err, IsNil) 1108 1109 // Now the test is to change the upper most parent limit so that it would be less 1110 // than the current usage, which we should not be able to do 1111 err = grp1.UpdateQuotaLimits(quota.NewResourcesBuilder().WithCPUCount(1).WithCPUPercentage(50).Build()) 1112 c.Check(err, ErrorMatches, `group cpu limit of 50% is less than current subgroup usage of 100%`) 1113 } 1114 1115 func (ts *quotaTestSuite) TestChangingParentCpuSetLimits(c *C) { 1116 // The purpose here is to make sure we can't change the limits of the parent group 1117 // that would otherwise conflict with the current usage of limits by children of the 1118 // parent. 1119 grp1, err := quota.NewGroup("groot", quota.NewResourcesBuilder().WithCPUSet([]int{0, 1}).Build()) 1120 c.Assert(err, IsNil) 1121 1122 subgrp1, err := grp1.NewSubGroup("cpu-sub", quota.NewResourcesBuilder().WithCPUCount(2).WithCPUPercentage(50).Build()) 1123 c.Assert(err, IsNil) 1124 1125 // Create a nested subgroup with a cpu limit that uses both of allowed cpus 1126 _, err = subgrp1.NewSubGroup("cpuset-sub", quota.NewResourcesBuilder().WithCPUSet([]int{0, 1}).Build()) 1127 c.Assert(err, IsNil) 1128 1129 // Now the test is to change the upper most parent limit so that it would be more 1130 // restrictive then the previous limit 1131 err = grp1.UpdateQuotaLimits(quota.NewResourcesBuilder().WithCPUSet([]int{0}).Build()) 1132 c.Check(err, ErrorMatches, `group cpu-set \[0\] is not a superset of current subgroup usage of \[0 1\]`) 1133 } 1134 1135 func (ts *quotaTestSuite) TestChangingParentThreadLimits(c *C) { 1136 // The purpose here is to make sure we can't change the limits of the parent group 1137 // that would otherwise conflict with the current usage of limits by children of the 1138 // parent. 1139 grp1, err := quota.NewGroup("groot", quota.NewResourcesBuilder().WithThreadLimit(32).Build()) 1140 c.Assert(err, IsNil) 1141 1142 subgrp1, err := grp1.NewSubGroup("cpu-sub", quota.NewResourcesBuilder().WithCPUCount(2).WithCPUPercentage(50).Build()) 1143 c.Assert(err, IsNil) 1144 1145 // Create a nested subgroup with a thread limit that takes up the entire quota of the parent 1146 _, err = subgrp1.NewSubGroup("thread-sub", quota.NewResourcesBuilder().WithThreadLimit(32).Build()) 1147 c.Assert(err, IsNil) 1148 1149 // Now the test is to change the upper most parent limit so that it would be less 1150 // than the current usage, which we should not be able to do 1151 err = grp1.QuotaUpdateCheck(quota.NewResourcesBuilder().WithThreadLimit(16).Build()) 1152 c.Check(err, ErrorMatches, `group thread limit of 16 is too small to fit current subgroup usage of 32`) 1153 } 1154 1155 func (ts *quotaTestSuite) TestChangingMiddleParentLimits(c *C) { 1156 // Catch any algorithmic mistakes made in regards to not catching parents 1157 // that are also children of other parents. 1158 grp1, err := quota.NewGroup("groot", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 1159 c.Assert(err, IsNil) 1160 1161 subgrp1, err := grp1.NewSubGroup("cpu-sub1", quota.NewResourcesBuilder().WithCPUCount(2).WithCPUPercentage(50).Build()) 1162 c.Assert(err, IsNil) 1163 1164 // Create a nested subgroup with a memory limit that takes up the entire quota of the upper parent 1165 subgrp2, err := subgrp1.NewSubGroup("mem-sub", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 1166 c.Assert(err, IsNil) 1167 1168 // Create a nested subgroup with a cpu limit that takes up the entire quota of the middle parent 1169 _, err = subgrp2.NewSubGroup("cpu-sub2", quota.NewResourcesBuilder().WithCPUCount(2).WithCPUPercentage(50).Build()) 1170 c.Assert(err, IsNil) 1171 1172 // Now the test is to change the middle parent limit so that it would be less 1173 // than the current usage, which we should not be able to do 1174 err = subgrp1.UpdateQuotaLimits(quota.NewResourcesBuilder().WithCPUCount(1).WithCPUPercentage(50).Build()) 1175 c.Check(err, ErrorMatches, `group cpu limit of 50% is less than current subgroup usage of 100%`) 1176 } 1177 1178 func (ts *quotaTestSuite) TestAddingNewMiddleParentMemoryLimits(c *C) { 1179 // The purpose here is to make sure we catch any new limits inserted into 1180 // the tree, which would conflict with the current usage. 1181 grp1, err := quota.NewGroup("groot", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB*2).Build()) 1182 c.Assert(err, IsNil) 1183 1184 subgrp1, err := grp1.NewSubGroup("cpu-sub1", quota.NewResourcesBuilder().WithCPUCount(2).WithCPUPercentage(50).Build()) 1185 c.Assert(err, IsNil) 1186 1187 // Create a nested subgroup with a memory limit that takes half of the quota of the upper parent 1188 subgrp2, err := subgrp1.NewSubGroup("mem-sub", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 1189 c.Assert(err, IsNil) 1190 1191 // Create a nested subgroup with a cpu limit that takes up the entire quota of the middle parent 1192 _, err = subgrp2.NewSubGroup("cpu-sub2", quota.NewResourcesBuilder().WithCPUCount(2).WithCPUPercentage(50).Build()) 1193 c.Assert(err, IsNil) 1194 1195 // Now lets inject a memory quota that is less than currently used by children 1196 err = subgrp1.UpdateQuotaLimits(quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeMiB * 512).Build()) 1197 c.Check(err, ErrorMatches, `group memory limit of 512 MiB is too small to fit current subgroup usage of 1 GiB`) 1198 1199 // Now lets inject one that is larger, that should be possible 1200 err = subgrp1.UpdateQuotaLimits(quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB * 2).Build()) 1201 c.Check(err, IsNil) 1202 } 1203 1204 func (ts *quotaTestSuite) TestAddingNewMiddleParentCpuLimits(c *C) { 1205 // The purpose here is to make sure we catch any new limits inserted into 1206 // the tree, which would conflict with the current usage. 1207 grp1, err := quota.NewGroup("groot", quota.NewResourcesBuilder().WithCPUCount(2).WithCPUPercentage(50).Build()) 1208 c.Assert(err, IsNil) 1209 1210 subgrp1, err := grp1.NewSubGroup("mem-sub1", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 1211 c.Assert(err, IsNil) 1212 1213 // Create a nested subgroup with a cpu limit that takes half of the quota of the upper parent 1214 subgrp2, err := subgrp1.NewSubGroup("cpu-sub", quota.NewResourcesBuilder().WithCPUCount(1).WithCPUPercentage(50).Build()) 1215 c.Assert(err, IsNil) 1216 1217 // Create a nested subgroup with a memory limit that takes up the entire quota of the middle parent 1218 _, err = subgrp2.NewSubGroup("mem-sub2", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 1219 c.Assert(err, IsNil) 1220 1221 // Now lets inject a cpu quota that is less than currently used by children 1222 err = subgrp1.UpdateQuotaLimits(quota.NewResourcesBuilder().WithCPUCount(1).WithCPUPercentage(25).Build()) 1223 c.Check(err, ErrorMatches, `group cpu limit of 25% is less than current subgroup usage of 50%`) 1224 1225 // Now lets inject one that is larger, that should be possible 1226 err = subgrp1.UpdateQuotaLimits(quota.NewResourcesBuilder().WithCPUCount(2).WithCPUPercentage(50).Build()) 1227 c.Check(err, IsNil) 1228 } 1229 1230 func (ts *quotaTestSuite) TestAddingNewMiddleParentCpuSetLimits(c *C) { 1231 // The purpose here is to make sure we catch any new limits inserted into 1232 // the tree, which would conflict with the current usage. 1233 grp1, err := quota.NewGroup("groot", quota.NewResourcesBuilder().WithCPUSet([]int{0, 1, 2, 3}).Build()) 1234 c.Assert(err, IsNil) 1235 1236 subgrp1, err := grp1.NewSubGroup("cpu-sub1", quota.NewResourcesBuilder().WithCPUCount(2).WithCPUPercentage(50).Build()) 1237 c.Assert(err, IsNil) 1238 1239 // Create a nested subgroup with a more restrictive cpu-set of the upper parent 1240 subgrp2, err := subgrp1.NewSubGroup("cpuset-sub", quota.NewResourcesBuilder().WithCPUSet([]int{0, 1}).Build()) 1241 c.Assert(err, IsNil) 1242 1243 // Create a nested subgroup with a cpu limit that takes up the entire quota of the middle parent 1244 _, err = subgrp2.NewSubGroup("cpu-sub2", quota.NewResourcesBuilder().WithCPUCount(2).WithCPUPercentage(50).Build()) 1245 c.Assert(err, IsNil) 1246 1247 // Now lets inject a cpu-set that does not match whats currently used by children 1248 err = subgrp1.UpdateQuotaLimits(quota.NewResourcesBuilder().WithCPUSet([]int{2, 3}).Build()) 1249 c.Check(err, ErrorMatches, `group cpu-set \[2 3\] is not a superset of current subgroup usage of \[0 1\]`) 1250 1251 // Now lets inject one that is larger, that should be possible 1252 err = subgrp1.UpdateQuotaLimits(quota.NewResourcesBuilder().WithCPUSet([]int{0, 1, 2}).Build()) 1253 c.Check(err, IsNil) 1254 } 1255 1256 func (ts *quotaTestSuite) TestAddingNewMiddleParentThreadLimits(c *C) { 1257 // The purpose here is to make sure we catch any new limits inserted into 1258 // the tree, which would conflict with the current usage. 1259 grp1, err := quota.NewGroup("groot", quota.NewResourcesBuilder().WithThreadLimit(1024).Build()) 1260 c.Assert(err, IsNil) 1261 1262 subgrp1, err := grp1.NewSubGroup("cpu-sub1", quota.NewResourcesBuilder().WithCPUCount(2).WithCPUPercentage(50).Build()) 1263 c.Assert(err, IsNil) 1264 1265 // Create a nested subgroup with a thread limit that takes half of the quota of the upper parent 1266 subgrp2, err := subgrp1.NewSubGroup("thread-sub", quota.NewResourcesBuilder().WithThreadLimit(512).Build()) 1267 c.Assert(err, IsNil) 1268 1269 // Create a nested subgroup with a cpu limit that takes up the entire quota of the middle parent 1270 _, err = subgrp2.NewSubGroup("cpu-sub2", quota.NewResourcesBuilder().WithCPUCount(2).WithCPUPercentage(50).Build()) 1271 c.Assert(err, IsNil) 1272 1273 // Now lets inject a thread quota that is less than currently used by children 1274 err = subgrp1.UpdateQuotaLimits(quota.NewResourcesBuilder().WithThreadLimit(256).Build()) 1275 c.Check(err, ErrorMatches, `group thread limit of 256 is too small to fit current subgroup usage of 512`) 1276 1277 // Now lets inject one that is larger, that should be possible 1278 err = subgrp1.UpdateQuotaLimits(quota.NewResourcesBuilder().WithThreadLimit(1024).Build()) 1279 c.Check(err, IsNil) 1280 } 1281 1282 func (ts *quotaTestSuite) TestCombinedCpuPercentageWithCpuSetLimits(c *C) { 1283 // mock the CPU count to be above 2 1284 restore := quota.MockRuntimeNumCPU(func() int { return 4 }) 1285 defer restore() 1286 1287 grp1, err := quota.NewGroup("groot", quota.NewResourcesBuilder().WithCPUSet([]int{0, 1}).Build()) 1288 c.Assert(err, IsNil) 1289 1290 // Create a subgroup of the CPU set of 0,1 with 50% allowed CPU usage. This should result in a combined 1291 // allowance of 100% 1292 subgrp1, err := grp1.NewSubGroup("cpu-sub1", quota.NewResourcesBuilder().WithCPUPercentage(50).Build()) 1293 c.Assert(err, IsNil) 1294 c.Check(subgrp1.GetCPUQuotaPercentage(), Equals, 100) 1295 1296 _, err = grp1.NewSubGroup("cpu-sub2", quota.NewResourcesBuilder().WithCPUCount(8).WithCPUPercentage(50).Build()) 1297 c.Assert(err, ErrorMatches, `sub-group cpu limit of 400% is too large to fit inside group "groot" with allowed CPU set \[0 1\]`) 1298 } 1299 1300 func (ts *quotaTestSuite) TestCombinedCpuPercentageWithLowCoreCount(c *C) { 1301 // mock the CPU count to be above 1 1302 restore := quota.MockRuntimeNumCPU(func() int { return 1 }) 1303 defer restore() 1304 1305 grp1, err := quota.NewGroup("groot", quota.NewResourcesBuilder().WithCPUSet([]int{0, 1}).Build()) 1306 c.Assert(err, IsNil) 1307 1308 subgrp1, err := grp1.NewSubGroup("cpu-sub1", quota.NewResourcesBuilder().WithCPUPercentage(50).Build()) 1309 c.Assert(err, IsNil) 1310 1311 // Even though the CPU set is set to cores 0+1, which technically means that a CPUPercentage of 50 would 1312 // be half of this, the CPU percentage is capped at at total of 100% because the number of cores on the system 1313 // is 1. 1314 c.Check(subgrp1.GetCPUQuotaPercentage(), Equals, 50) 1315 1316 subgrp2, err := grp1.NewSubGroup("cpu-sub2", quota.NewResourcesBuilder().WithCPUCount(4).WithCPUPercentage(50).Build()) 1317 c.Assert(err, IsNil) 1318 1319 // Verify that the number of cpus are now correctly reported as the one explicitly set 1320 // by the quota 1321 c.Check(subgrp2.GetCPUQuotaPercentage(), Equals, 200) 1322 } 1323 1324 func (ts *quotaTestSuite) TestJournalQuotasSetCorrectly(c *C) { 1325 grp1, err := quota.NewGroup("groot1", quota.NewResourcesBuilder().WithJournalNamespace().Build()) 1326 c.Assert(err, IsNil) 1327 c.Assert(grp1.JournalLimit, NotNil) 1328 1329 grp2, err := quota.NewGroup("groot2", quota.NewResourcesBuilder().WithJournalRate(15, time.Second).Build()) 1330 c.Assert(err, IsNil) 1331 c.Assert(grp2.JournalLimit, NotNil) 1332 c.Check(grp2.JournalLimit.RateCount, Equals, 15) 1333 c.Check(grp2.JournalLimit.RatePeriod, Equals, time.Second) 1334 1335 grp3, err := quota.NewGroup("groot3", quota.NewResourcesBuilder().WithJournalSize(quantity.SizeMiB).Build()) 1336 c.Assert(err, IsNil) 1337 c.Assert(grp3.JournalLimit, NotNil) 1338 c.Check(grp3.JournalLimit.Size, Equals, quantity.SizeMiB) 1339 } 1340 1341 func (ts *quotaTestSuite) TestJournalQuotasUpdatesCorrectly(c *C) { 1342 grp1, err := quota.NewGroup("groot1", quota.NewResourcesBuilder().WithMemoryLimit(quantity.SizeGiB).Build()) 1343 c.Assert(err, IsNil) 1344 c.Assert(grp1.JournalLimit, IsNil) 1345 1346 grp1.UpdateQuotaLimits(quota.NewResourcesBuilder().WithJournalNamespace().Build()) 1347 c.Assert(grp1.JournalLimit, NotNil) 1348 c.Check(grp1.JournalLimit.Size, Equals, quantity.Size(0)) 1349 c.Check(grp1.JournalLimit.RateCount, Equals, 0) 1350 c.Check(grp1.JournalLimit.RatePeriod, Equals, time.Duration(0)) 1351 1352 grp1.UpdateQuotaLimits(quota.NewResourcesBuilder().WithJournalRate(15, time.Microsecond*5).WithJournalSize(quantity.SizeMiB).Build()) 1353 c.Assert(grp1.JournalLimit, NotNil) 1354 c.Check(grp1.JournalLimit.Size, Equals, quantity.SizeMiB) 1355 c.Check(grp1.JournalLimit.RateCount, Equals, 15) 1356 c.Check(grp1.JournalLimit.RatePeriod, Equals, time.Microsecond*5) 1357 }