diff options
author | Quang-Minh Nguyen <qmnguyen@gitlab.com> | 2023-11-22 19:09:34 +0300 |
---|---|---|
committer | Quang-Minh Nguyen <qmnguyen@gitlab.com> | 2023-11-22 19:09:34 +0300 |
commit | aab593720ebff0d3022ccf2e420760850062de8a (patch) | |
tree | bcc948a7b2dc02ae8b03746b8f91622f3688f6f8 | |
parent | 6835085898eb8d3881ee98c476a7bfc2981f0067 (diff) | |
parent | b17bce5fcee8f38578568a7232e978b39633bb04 (diff) |
Merge branch 'qmnguyen0711/cgroup-allow-spawning-command-use-up-to-m-cgroups' into 'master'
cgroup: Allow a repository to use up to M repository cgroups instead of one
See merge request https://gitlab.com/gitlab-org/gitaly/-/merge_requests/6537
Merged-by: Quang-Minh Nguyen <qmnguyen@gitlab.com>
Approved-by: Christian Couder <chriscool@tuxfamily.org>
Reviewed-by: Will Chandler <wchandler@gitlab.com>
Reviewed-by: Quang-Minh Nguyen <qmnguyen@gitlab.com>
-rw-r--r-- | internal/cgroups/handler_linux_test.go | 105 | ||||
-rw-r--r-- | internal/cgroups/manager_linux.go | 29 | ||||
-rw-r--r-- | internal/cgroups/manager_linux_test.go | 83 | ||||
-rw-r--r-- | internal/cgroups/testhelper_test.go | 8 | ||||
-rw-r--r-- | internal/gitaly/config/cgroups/cgroups.go | 21 | ||||
-rw-r--r-- | internal/gitaly/config/cgroups/cgroups_test.go | 31 | ||||
-rw-r--r-- | internal/gitaly/config/config.go | 4 | ||||
-rw-r--r-- | internal/gitaly/config/config_test.go | 87 |
8 files changed, 281 insertions, 87 deletions
diff --git a/internal/cgroups/handler_linux_test.go b/internal/cgroups/handler_linux_test.go index cb2a701af..bce5c5f09 100644 --- a/internal/cgroups/handler_linux_test.go +++ b/internal/cgroups/handler_linux_test.go @@ -241,8 +241,7 @@ func TestRepoCgroups(t *testing.T) { // is creating repository directories in the correct location. requireShards(t, version, mock, manager, pid) - groupID := calcGroupID(cmdArgs, cfg.Repositories.Count) - + groupID := uint(0) // Fixed - generated from the command mock.setupMockCgroupFiles(t, manager, []uint{groupID}) require.False(t, manager.Ready()) @@ -286,33 +285,29 @@ func TestRepoCgroups(t *testing.T) { func TestAddCommand(t *testing.T) { for _, version := range []int{1, 2} { t.Run("cgroups-v"+strconv.Itoa(version), func(t *testing.T) { - mock := newMock(t, version) - - config := defaultCgroupsConfig() - config.Repositories.Count = 10 - config.Repositories.MemoryBytes = 1024 - config.Repositories.CPUShares = 16 - config.HierarchyRoot = "gitaly" - config.Mountpoint = mock.rootPath() - pid := 1 - manager1 := mock.newCgroupManager(config, testhelper.SharedLogger(t), pid) - mock.setupMockCgroupFiles(t, manager1, []uint{}) - require.NoError(t, manager1.Setup()) - ctx := testhelper.Context(t) - cmd2 := exec.CommandContext(ctx, cmdArgs[0], cmdArgs[1:]...) - require.NoError(t, cmd2.Run()) - - groupID := calcGroupID(cmd2.Args, config.Repositories.Count) + t.Run("without overridden key", func(t *testing.T) { + mock := newMock(t, version) + config := defaultCgroupsConfig() + config.Repositories.Count = 10 + config.Repositories.MemoryBytes = 1024 + config.Repositories.CPUShares = 16 + config.HierarchyRoot = "gitaly" + config.Mountpoint = mock.rootPath() + + cmd := exec.CommandContext(ctx, cmdArgs[0], cmdArgs[1:]...) + require.NoError(t, cmd.Run()) - manager2 := mock.newCgroupManager(config, testhelper.SharedLogger(t), pid) + manager := mock.newCgroupManager(config, testhelper.SharedLogger(t), pid) + mock.setupMockCgroupFiles(t, manager, []uint{}) + require.NoError(t, manager.Setup()) - t.Run("without overridden key", func(t *testing.T) { - _, err := manager2.AddCommand(cmd2) + groupID := uint(8) // Fixed - generated from the command + _, err := manager.AddCommand(cmd) require.NoError(t, err) - requireShards(t, version, mock, manager2, pid, groupID) + requireShards(t, version, mock, manager, pid, groupID) for _, path := range mock.repoPaths(pid, groupID) { procsPath := filepath.Join(path, "cgroup.procs") @@ -321,27 +316,79 @@ func TestAddCommand(t *testing.T) { cmdPid, err := strconv.Atoi(string(content)) require.NoError(t, err) - require.Equal(t, cmd2.Process.Pid, cmdPid) + require.Equal(t, cmd.Process.Pid, cmdPid) } }) t.Run("with overridden key", func(t *testing.T) { - overriddenGroupID := calcGroupID([]string{"foobar"}, config.Repositories.Count) + mock := newMock(t, version) + config := defaultCgroupsConfig() + config.Repositories.Count = 10 + config.Repositories.MemoryBytes = 1024 + config.Repositories.CPUShares = 16 + config.HierarchyRoot = "gitaly" + config.Mountpoint = mock.rootPath() + + cmd := exec.CommandContext(ctx, cmdArgs[0], cmdArgs[1:]...) + require.NoError(t, cmd.Run()) + + manager := mock.newCgroupManager(config, testhelper.SharedLogger(t), pid) + mock.setupMockCgroupFiles(t, manager, []uint{}) + require.NoError(t, manager.Setup()) - _, err := manager2.AddCommand(cmd2, WithCgroupKey("foobar")) + groupID := uint(9) // Fixed - generated from the key + _, err := manager.AddCommand(cmd, WithCgroupKey("foobar")) require.NoError(t, err) - requireShards(t, version, mock, manager2, pid, groupID, overriddenGroupID) + requireShards(t, version, mock, manager, pid, groupID) - for _, path := range mock.repoPaths(pid, overriddenGroupID) { + for _, path := range mock.repoPaths(pid, groupID) { procsPath := filepath.Join(path, "cgroup.procs") content := readCgroupFile(t, procsPath) cmdPid, err := strconv.Atoi(string(content)) require.NoError(t, err) - require.Equal(t, cmd2.Process.Pid, cmdPid) + require.Equal(t, cmd.Process.Pid, cmdPid) } }) + + t.Run("when AllocationSet is set", func(t *testing.T) { + mock := newMock(t, version) + config := defaultCgroupsConfig() + config.Repositories.Count = 10 + config.Repositories.MaxCgroupsPerRepo = 3 + config.Repositories.MemoryBytes = 1024 + config.Repositories.CPUShares = 16 + config.HierarchyRoot = "gitaly" + config.Mountpoint = mock.rootPath() + + manager := mock.newCgroupManager(config, testhelper.SharedLogger(t), pid) + mock.setupMockCgroupFiles(t, manager, []uint{}) + require.NoError(t, manager.Setup()) + + rand := newMockRand(t, 3) + manager.rand = rand + + groupIDs := []uint{8, 9, 0} + for i := 0; i < 3; i++ { + cmd := exec.CommandContext(ctx, cmdArgs[0], cmdArgs[1:]...) + require.NoError(t, cmd.Run()) + _, err := manager.AddCommand(cmd) + require.NoError(t, err) + + for _, path := range mock.repoPaths(pid, groupIDs[i]) { + procsPath := filepath.Join(path, "cgroup.procs") + content := readCgroupFile(t, procsPath) + + cmdPid, err := strconv.Atoi(string(content)) + require.NoError(t, err) + + require.Equal(t, cmd.Process.Pid, cmdPid) + } + } + + requireShards(t, version, mock, manager, pid, groupIDs...) + }) }) } } diff --git a/internal/cgroups/manager_linux.go b/internal/cgroups/manager_linux.go index 0ee67acc2..516b63fbc 100644 --- a/internal/cgroups/manager_linux.go +++ b/internal/cgroups/manager_linux.go @@ -7,6 +7,7 @@ import ( "fmt" "hash/crc32" "io" + "math/rand" "os" "os/exec" "path/filepath" @@ -14,6 +15,7 @@ import ( "sync" "sync/atomic" "syscall" + "time" cgrps "github.com/containerd/cgroups/v3" "github.com/opencontainers/runtime-spec/specs-go" @@ -72,6 +74,12 @@ func (s *cgroupStatus) getLock(cgroupPath string) *cgroupLock { return cgLock } +// randomizer is the interface of the Go random number generator. +type randomizer interface { + // Intn returns a random integer in the range [0,n). + Intn(n int) int +} + // CGroupManager is a manager class that implements specific methods related to cgroups type CGroupManager struct { cfg cgroupscfg.Config @@ -79,8 +87,8 @@ type CGroupManager struct { enabled bool repoRes *specs.LinuxResources status *cgroupStatus - handler cgroupHandler + rand randomizer } func newCgroupManager(cfg cgroupscfg.Config, logger log.Logger, pid int) *CGroupManager { @@ -106,6 +114,7 @@ func newCgroupManagerWithMode(cfg cgroupscfg.Config, logger log.Logger, pid int, handler: handler, repoRes: configRepositoryResources(cfg), status: newCgroupStatus(cfg, handler.repoPath), + rand: rand.New(rand.NewSource(time.Now().UnixNano())), } } @@ -199,13 +208,21 @@ func (cgm *CGroupManager) cgroupPathForCommand(cmd *exec.Cmd, opts []AddCommandO if key == "" { key = strings.Join(cmd.Args, "/") } + groupID := cgm.calcGroupID(cgm.rand, key, cgm.cfg.Repositories.Count, cgm.cfg.Repositories.MaxCgroupsPerRepo) + return cgm.handler.repoPath(int(groupID)) +} - checksum := crc32.ChecksumIEEE( - []byte(key), - ) +func (cgm *CGroupManager) calcGroupID(rand randomizer, key string, count uint, allocationCount uint) uint { + checksum := crc32.ChecksumIEEE([]byte(key)) - groupID := uint(checksum) % cgm.cfg.Repositories.Count - return cgm.handler.repoPath(int(groupID)) + // Pick a starting point + groupID := uint(checksum) % count + if allocationCount <= 1 { + return groupID + } + + // Shift random distance [0, allocation) from the starting point. Wrap-around if needed. + return (groupID + uint(rand.Intn(int(allocationCount)))) % count } // Describe is used to generate description information for each CGroupManager prometheus metric diff --git a/internal/cgroups/manager_linux_test.go b/internal/cgroups/manager_linux_test.go index 3c9878169..30068edc4 100644 --- a/internal/cgroups/manager_linux_test.go +++ b/internal/cgroups/manager_linux_test.go @@ -14,25 +14,47 @@ import ( "gitlab.com/gitlab-org/gitaly/v16/internal/testhelper" ) +// mockRand is a mock for random number generator that implements randomizer interface of cgroups package. +type mockRand struct { + t *testing.T + max int + n int +} + +func newMockRand(t *testing.T, max int) *mockRand { + return &mockRand{t: t, max: max, n: -1} +} + +// Intn returns 0 to inputMax-1 in turn then back to 0. It also verifies if the input argument matched the initialized max. +func (r *mockRand) Intn(inputMax int) int { + require.Equalf(r.t, r.max, inputMax, "unexpected rand.Intn() argument, expected %d, actual %d", r.max, inputMax) + + r.n++ + if r.n >= r.max { + r.n = 0 + } + return r.n +} + func TestCloneIntoCgroup(t *testing.T) { mountPoint := t.TempDir() hierarchyRoot := filepath.Join(mountPoint, "gitaly") // Create the files we expect the manager to open. require.NoError(t, os.MkdirAll(filepath.Join(hierarchyRoot, "gitaly-1"), fs.ModePerm)) - require.NoError(t, os.MkdirAll(filepath.Join(hierarchyRoot, "gitaly-1", "repos-3"), fs.ModePerm)) - require.NoError(t, os.MkdirAll(filepath.Join(hierarchyRoot, "gitaly-1", "repos-5"), fs.ModePerm)) + for i := 0; i < 10; i++ { + require.NoError(t, os.MkdirAll(filepath.Join(hierarchyRoot, "gitaly-1", fmt.Sprintf("repos-%d", i)), fs.ModePerm)) + } require.NoError(t, os.WriteFile(filepath.Join(hierarchyRoot, "gitaly-1", "cgroup.subtree_control"), nil, fs.ModePerm)) - mgr := NewManager(cgroups.Config{ - Mountpoint: mountPoint, - HierarchyRoot: "gitaly", - Repositories: cgroups.Repositories{ - Count: 10, - }, - }, testhelper.NewLogger(t), 1) - t.Run("command args used as key", func(t *testing.T) { + mgr := NewManager(cgroups.Config{ + Mountpoint: mountPoint, + HierarchyRoot: "gitaly", + Repositories: cgroups.Repositories{ + Count: 10, + }, + }, testhelper.NewLogger(t), 1) for _, tc := range []struct { desc string existingSysProcAttr *syscall.SysProcAttr @@ -74,6 +96,14 @@ func TestCloneIntoCgroup(t *testing.T) { }) t.Run("key provided", func(t *testing.T) { + mgr := NewManager(cgroups.Config{ + Mountpoint: mountPoint, + HierarchyRoot: "gitaly", + Repositories: cgroups.Repositories{ + Count: 10, + }, + }, testhelper.NewLogger(t), 1) + commandWithKey := exec.Command("command", "arg") pathWithKey, closeWithKey, err := mgr.CloneIntoCgroup(commandWithKey, WithCgroupKey("some-key")) require.NoError(t, err) @@ -93,6 +123,39 @@ func TestCloneIntoCgroup(t *testing.T) { require.NotEqual(t, pathWithKey, pathWithoutKey, "commands should be placed in different groups") require.NotEqual(t, commandWithKey.SysProcAttr.CgroupFD, commandWithoutKey.SysProcAttr.CgroupFD) }) + + t.Run("AllocationCount is set", func(t *testing.T) { + mgr := NewManager(cgroups.Config{ + Mountpoint: mountPoint, + HierarchyRoot: "gitaly", + Repositories: cgroups.Repositories{ + Count: 10, + MaxCgroupsPerRepo: 3, + }, + }, testhelper.NewLogger(t), 1) + + assertSpawnedCommand := func(key string, groupID string) { + commandWithKey := exec.Command("command", "arg") + pathWithKey, closeWithKey, err := mgr.CloneIntoCgroup(commandWithKey, WithCgroupKey(key)) + require.NoError(t, err) + defer testhelper.MustClose(t, closeWithKey) + require.Equal(t, filepath.Join(hierarchyRoot, fmt.Sprintf("gitaly-1/%s", groupID)), pathWithKey) + require.True(t, commandWithKey.SysProcAttr.UseCgroupFD) + require.NotEqual(t, 0, commandWithKey.SysProcAttr.UseCgroupFD) + } + + mgr.(*CGroupManager).rand = newMockRand(t, 3) + // Starting point is 3 (similar to the above test) + assertSpawnedCommand("some-key", "repos-3") + assertSpawnedCommand("some-key", "repos-4") + assertSpawnedCommand("some-key", "repos-5") + + mgr.(*CGroupManager).rand = newMockRand(t, 3) + // Starting point is 8 + assertSpawnedCommand("key-1", "repos-8") + assertSpawnedCommand("key-1", "repos-9") + assertSpawnedCommand("key-1", "repos-0") // Wrap-around + }) } func TestNewCgroupStatus(t *testing.T) { diff --git a/internal/cgroups/testhelper_test.go b/internal/cgroups/testhelper_test.go index 25b7e008e..33f0b6b30 100644 --- a/internal/cgroups/testhelper_test.go +++ b/internal/cgroups/testhelper_test.go @@ -3,8 +3,6 @@ package cgroups import ( - "hash/crc32" - "strings" "testing" "gitlab.com/gitlab-org/gitaly/v16/internal/testhelper" @@ -16,9 +14,3 @@ var cmdArgs = []string{"ls", "-hal", "."} func TestMain(m *testing.M) { testhelper.Run(m) } - -// calcGroupID calculates the repository cgroup ID for the key provided. -func calcGroupID(key []string, ct uint) uint { - checksum := crc32.ChecksumIEEE([]byte(strings.Join(key, "/"))) - return uint(checksum) % ct -} diff --git a/internal/gitaly/config/cgroups/cgroups.go b/internal/gitaly/config/cgroups/cgroups.go index ff19650f9..b1a437dc5 100644 --- a/internal/gitaly/config/cgroups/cgroups.go +++ b/internal/gitaly/config/cgroups/cgroups.go @@ -61,6 +61,26 @@ type Repositories struct { // Count is the number of cgroups that will be created for repository-level isolation // of git commands. Count uint `toml:"count"` + // MaxCgroupsPerRepo specifies the maximum number of cgroups to which a single repository can allocate its + // processes. + // By default, a repository can spawn processes in at most one cgroups. If the number of repositories + // is more than the number of cgroups (likely), multiple repositories share the same one. This model works very + // well if the repositories under the management of Cgroup are equivalent in size or traffic. If a node has some + // enormous repositories (mono-repo, for example), the scoping cgroups become excessively large comparing to the + // rest. This imbalance situation might force the operators to lift the repository-level cgroups. As a result, + // the isolation effect is not effective. + // This config is designed to balance resource usage between cgroups, mitigate competition for resources + // within a single cgroup, and enhance memory usage efficiency and isolation. The value can be adjusted based on + // the specific workload and number of repository cgroups on the node. + // A Git process uses its target repository's relative path as the hash key to find the corresponding cgroup. It + // is allocated randomly to any of the consequent MaxCgroupsPerRepo cgroups. It wraps around if needed. + // repo-X + // ┌───────┐ + // □ □ □ □ □ □ □ ■ ■ ■ ■ ■ ■ ■ ■ □ □ □ □ + // └────────┘ + // repo-Y + // The default value is "1". + MaxCgroupsPerRepo uint `toml:"max_cgroups_per_repo"` // MemoryBytes is the memory limit for each cgroup. 0 implies no memory limit. MemoryBytes int64 `toml:"memory_bytes"` // CPUShares are the shares of CPU that each cgroup is allowed to utilize. A value of 1024 @@ -77,6 +97,7 @@ type Repositories struct { // Validate runs validation on all fields and compose all found errors. func (r *Repositories) Validate(memBytes int64, cpuShares uint64, cpuQuotaUs int64) error { return cfgerror.New(). + Append(cfgerror.InRange(0, r.Count, r.MaxCgroupsPerRepo, cfgerror.InRangeOptIncludeMin, cfgerror.InRangeOptIncludeMax), "max_cgroups_per_repo"). Append(cfgerror.InRange(0, memBytes, r.MemoryBytes, cfgerror.InRangeOptIncludeMin, cfgerror.InRangeOptIncludeMax), "memory_bytes"). Append(cfgerror.InRange(0, cpuShares, r.CPUShares, cfgerror.InRangeOptIncludeMin, cfgerror.InRangeOptIncludeMax), "cpu_shares"). Append(cfgerror.InRange(0, cpuQuotaUs, r.CPUQuotaUs, cfgerror.InRangeOptIncludeMin, cfgerror.InRangeOptIncludeMax), "cpu_quota_us"). diff --git a/internal/gitaly/config/cgroups/cgroups_test.go b/internal/gitaly/config/cgroups/cgroups_test.go index 979a027b4..5527415ea 100644 --- a/internal/gitaly/config/cgroups/cgroups_test.go +++ b/internal/gitaly/config/cgroups/cgroups_test.go @@ -168,9 +168,21 @@ func TestRepositories_Validate(t *testing.T) { { name: "valid", repositories: Repositories{ - Count: 2, - MemoryBytes: 1024, - CPUShares: 16, + Count: 2, + MaxCgroupsPerRepo: 2, + MemoryBytes: 1024, + CPUShares: 16, + }, + memBytes: 2048, + cpuShares: 32, + }, + { + name: "valid", + repositories: Repositories{ + Count: 2, + MaxCgroupsPerRepo: 1, + MemoryBytes: 1024, + CPUShares: 16, }, memBytes: 2048, cpuShares: 32, @@ -178,16 +190,21 @@ func TestRepositories_Validate(t *testing.T) { { name: "invalid", repositories: Repositories{ - Count: 2, - MemoryBytes: 1024, - CPUShares: 16, - CPUQuotaUs: 32000, + Count: 2, + MaxCgroupsPerRepo: 3, + MemoryBytes: 1024, + CPUShares: 16, + CPUQuotaUs: 32000, }, memBytes: 256, cpuShares: 2, cpuQuotaUs: 16000, expectedErr: cfgerror.ValidationErrors{ cfgerror.NewValidationError( + fmt.Errorf("%w: 3 out of [0, 2]", cfgerror.ErrNotInRange), + "max_cgroups_per_repo", + ), + cfgerror.NewValidationError( fmt.Errorf("%w: 1024 out of [0, 256]", cfgerror.ErrNotInRange), "memory_bytes", ), diff --git a/internal/gitaly/config/config.go b/internal/gitaly/config/config.go index edf978996..49280cd3d 100644 --- a/internal/gitaly/config/config.go +++ b/internal/gitaly/config/config.go @@ -801,6 +801,10 @@ func (cfg *Cfg) SetDefaults() error { cfg.Cgroups.FallbackToOldVersion() + if cfg.Cgroups.Repositories.Count != 0 && cfg.Cgroups.Repositories.MaxCgroupsPerRepo == 0 { + cfg.Cgroups.Repositories.MaxCgroupsPerRepo = 1 + } + if cfg.Backup.Layout == "" { cfg.Backup.Layout = "pointer" } diff --git a/internal/gitaly/config/config_test.go b/internal/gitaly/config/config_test.go index 5e7d45689..417047951 100644 --- a/internal/gitaly/config/config_test.go +++ b/internal/gitaly/config/config_test.go @@ -1246,9 +1246,10 @@ func TestValidateCgroups(t *testing.T) { Shares: 512, }, Repositories: cgroups.Repositories{ - Count: 10, - MemoryBytes: 1024, - CPUShares: 512, + Count: 10, + MaxCgroupsPerRepo: 1, + MemoryBytes: 1024, + CPUShares: 512, }, }, }, @@ -1264,7 +1265,8 @@ func TestValidateCgroups(t *testing.T) { Mountpoint: "/sys/fs/cgroup", HierarchyRoot: "baz", Repositories: cgroups.Repositories{ - Count: 10, + Count: 10, + MaxCgroupsPerRepo: 1, }, }, }, @@ -1279,7 +1281,8 @@ func TestValidateCgroups(t *testing.T) { Mountpoint: "/sys/fs/cgroup", HierarchyRoot: "gitaly", Repositories: cgroups.Repositories{ - Count: 10, + Count: 10, + MaxCgroupsPerRepo: 1, }, }, }, @@ -1308,8 +1311,9 @@ func TestValidateCgroups(t *testing.T) { Shares: 0, }, Repositories: cgroups.Repositories{ - Count: 10, - MemoryBytes: 1024, + Count: 10, + MaxCgroupsPerRepo: 1, + MemoryBytes: 1024, }, }, }, @@ -1339,8 +1343,9 @@ func TestValidateCgroups(t *testing.T) { Shares: 512, }, Repositories: cgroups.Repositories{ - Count: 10, - CPUShares: 512, + Count: 10, + MaxCgroupsPerRepo: 1, + CPUShares: 512, }, }, }, @@ -1385,10 +1390,11 @@ func TestValidateCgroups(t *testing.T) { Mountpoint: "/sys/fs/cgroup", HierarchyRoot: "gitaly", Repositories: cgroups.Repositories{ - Count: 10, - MemoryBytes: 1024, - CPUShares: 512, - CPUQuotaUs: 500, + Count: 10, + MaxCgroupsPerRepo: 1, + MemoryBytes: 1024, + CPUShares: 512, + CPUQuotaUs: 500, }, }, }, @@ -1427,9 +1433,10 @@ func TestValidateCgroups(t *testing.T) { Mountpoint: "/sys/fs/cgroup", HierarchyRoot: "gitaly", Repositories: cgroups.Repositories{ - Count: 10, - CPUShares: 512, - CPUQuotaUs: 500, + Count: 10, + MaxCgroupsPerRepo: 1, + CPUShares: 512, + CPUQuotaUs: 500, }, }, }, @@ -1454,10 +1461,11 @@ func TestValidateCgroups(t *testing.T) { CPUShares: 1024, CPUQuotaUs: 800, Repositories: cgroups.Repositories{ - Count: 10, - MemoryBytes: 2147483648, - CPUShares: 128, - CPUQuotaUs: 500, + Count: 10, + MaxCgroupsPerRepo: 1, + MemoryBytes: 2147483648, + CPUShares: 128, + CPUQuotaUs: 500, }, }, validateErr: errors.New("cgroups.repositories: memory limit cannot exceed parent"), @@ -1477,8 +1485,9 @@ func TestValidateCgroups(t *testing.T) { HierarchyRoot: "gitaly", CPUShares: 128, Repositories: cgroups.Repositories{ - Count: 10, - CPUShares: 512, + Count: 10, + MaxCgroupsPerRepo: 1, + CPUShares: 512, }, }, validateErr: errors.New("cgroups.repositories: cpu shares cannot exceed parent"), @@ -1498,8 +1507,9 @@ func TestValidateCgroups(t *testing.T) { HierarchyRoot: "gitaly", CPUQuotaUs: 225, Repositories: cgroups.Repositories{ - Count: 10, - CPUQuotaUs: 500, + Count: 10, + MaxCgroupsPerRepo: 1, + CPUQuotaUs: 500, }, }, validateErr: errors.New("cgroups.repositories: cpu quota cannot exceed parent"), @@ -1520,9 +1530,32 @@ func TestValidateCgroups(t *testing.T) { HierarchyRoot: "gitaly", MetricsEnabled: true, Repositories: cgroups.Repositories{ - Count: 10, - MemoryBytes: 1024, - CPUShares: 512, + Count: 10, + MaxCgroupsPerRepo: 1, + MemoryBytes: 1024, + CPUShares: 512, + }, + }, + }, + { + name: "max_cgroups_per_repo enabled", + rawCfg: `[cgroups] + mountpoint = "/sys/fs/cgroup" + hierarchy_root = "gitaly" + [cgroups.repositories] + count = 10 + max_cgroups_per_repo = 5 + memory_bytes = 1024 + cpu_shares = 512 + `, + expect: cgroups.Config{ + Mountpoint: "/sys/fs/cgroup", + HierarchyRoot: "gitaly", + Repositories: cgroups.Repositories{ + Count: 10, + MaxCgroupsPerRepo: 5, + MemoryBytes: 1024, + CPUShares: 512, }, }, }, |