Skip to content

Commit d2b7a3c

Browse files
Limit Go-Git repo cache size and max in-memory file size (kptdev#1077)
* Limit repo cache size and max file size in go-git Signed-off-by: Rendre Greyling <rendre.greyling@nokia.com> * Add options to repo controller Signed-off-by: Rendre Greyling <rendre.greyling@nokia.com> * Fix lint issue Signed-off-by: Rendre Greyling <rendre.greyling@nokia.com> --------- Signed-off-by: Rendre Greyling <rendre.greyling@nokia.com>
1 parent f5192f1 commit d2b7a3c

9 files changed

Lines changed: 70 additions & 24 deletions

File tree

controllers/repositories/pkg/controllers/repository/cache.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ func (r *RepositoryReconciler) buildCacheOptions(
153153
CaBundleResolver: caBundleResolver,
154154
UserInfoProvider: userInfoProvider,
155155
RepoOperationRetryAttempts: r.RepoOperationRetryAttempts,
156+
GoGitRepoCacheSize: r.GoGitRepoCacheSize,
157+
GoGitCacheMaxFileSize: r.GoGitCacheMaxFileSize,
156158
},
157159
RepoPRChangeNotifier: cachetypes.NewNoOpRepoPRChangeNotifier(),
158160
DbPushDraftsToGit: r.PushDraftsToGit,

controllers/repositories/pkg/controllers/repository/config.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ const (
2929
defaultSyncStaleTimeout = 20 * time.Minute
3030
defaultRepoOperationRetryAttempts = 3
3131
defaultCacheDirectory = "/cache"
32+
defaultGoGitRepoCacheSize = 8 // MiB
33+
defaultGoGitCacheMaxFileSize = 1 * 1024 * 512 // bytes (512 KiB)
3234
)
3335

3436
// InitDefaults initializes default values for standalone controller
@@ -41,6 +43,8 @@ func (r *RepositoryReconciler) InitDefaults() {
4143
r.RepoOperationRetryAttempts = defaultRepoOperationRetryAttempts
4244
r.cacheType = string(cachetypes.DBCacheType)
4345
r.cacheDirectory = defaultCacheDirectory
46+
r.GoGitRepoCacheSize = defaultGoGitRepoCacheSize
47+
r.GoGitCacheMaxFileSize = defaultGoGitCacheMaxFileSize
4448
r.validateConfig()
4549
}
4650

@@ -57,6 +61,8 @@ func (r *RepositoryReconciler) BindFlags(prefix string, flags *flag.FlagSet) {
5761
flags.BoolVar(&r.useUserDefinedCaBundle, prefix+"use-user-defined-ca-bundle", false, "Enable custom CA bundle support from secrets")
5862
flags.BoolVar(&r.CreateV1Alpha2Rpkg, prefix+"create-v1alpha2-rpkg", false, "Create v1alpha2 PackageRevision resources during repository sync")
5963
flags.BoolVar(&r.PushDraftsToGit, prefix+"push-drafts-to-git", false, "Push draft and proposed branches to git when using DB cache")
64+
flags.IntVar(&r.GoGitRepoCacheSize, prefix+"gogit-repo-cache-size", defaultGoGitRepoCacheSize, "Size of the in-memory cache for git repositories when using gogit (in MiB)")
65+
flags.Int64Var(&r.GoGitCacheMaxFileSize, prefix+"gogit-cache-max-file-size", defaultGoGitCacheMaxFileSize, "Maximum file size (in bytes) that will be read into the in-memory cache for git repositories when using gogit; files larger than this will be streamed from disk")
6066
}
6167

6268
// validateConfig ensures configuration values are valid
@@ -95,7 +101,9 @@ func (r *RepositoryReconciler) LogConfig(log interface {
95101
"cacheType", r.cacheType,
96102
"cacheDirectory", r.cacheDirectory,
97103
"createV1Alpha2Rpkg", r.CreateV1Alpha2Rpkg,
98-
"pushDraftsToGit", r.PushDraftsToGit)
104+
"pushDraftsToGit", r.PushDraftsToGit,
105+
"goGitRepoCacheSize", r.GoGitRepoCacheSize,
106+
"goGitCacheMaxFileSize", r.GoGitCacheMaxFileSize)
99107

100108
if r.HealthCheckFrequency < defaultHealthCheckFrequency {
101109
log.Info("Health check frequency is lower than recommended default",

controllers/repositories/pkg/controllers/repository/repository_controller.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ type RepositoryReconciler struct {
6060
CreateV1Alpha2Rpkg bool // Create v1alpha2 PackageRevision resources during repo sync
6161
PushDraftsToGit bool // Push draft/proposed branches to git (DB cache only)
6262

63+
// GoGit cache configuration
64+
GoGitRepoCacheSize int // In-memory cache size for git repositories (MiB)
65+
GoGitCacheMaxFileSize int64 // Max file size (bytes) to read into the in-memory git cache
66+
6367
// Configuration (set via flags or defaults)
6468
cacheType string // Cache type (DB or CR)
6569
cacheDirectory string // Directory for git repository cache

pkg/cmd/server/start.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ type PorchServerOptions struct {
6868
DbMaxConnLifetime time.Duration
6969
DbPushDrafsToGit bool
7070

71+
GoGitRepoCacheSize int
72+
GoGitCacheMaxFileSize int64
73+
7174
DefaultImagePrefix string
7275
FunctionRunnerAddress string
7376
LocalStandaloneDebugging bool // Enables local standalone running/debugging of the apiserver.
@@ -331,6 +334,8 @@ func (o *PorchServerOptions) Config() (*apiserver.Config, error) {
331334
ExternalRepoOptions: externalrepotypes.ExternalRepoOptions{
332335
LocalDirectory: o.CacheDirectory,
333336
UseUserDefinedCaBundle: o.UseUserDefinedCaBundle,
337+
GoGitRepoCacheSize: o.GoGitRepoCacheSize,
338+
GoGitCacheMaxFileSize: o.GoGitCacheMaxFileSize,
334339
},
335340
RepoOperationRetryAttempts: o.RepoOperationRetryAttempts,
336341
CacheType: cachetypes.CacheType(o.CacheType),
@@ -395,6 +400,10 @@ func (o *PorchServerOptions) AddFlags(fs *pflag.FlagSet) {
395400
fs.StringVar(&o.DbCacheDataSource, "db-cache-data-source", "", "Address of the database, for example \"postgresql://user:pass@hostname:port/database\"")
396401
fs.BoolVar(&o.DbPushDrafsToGit, "db-push-drafts-to-git", false, "If true, Porch will push draft package revisions to git when using the DB cache")
397402

403+
//GoGit configuration
404+
fs.IntVar(&o.GoGitRepoCacheSize, "gogit-repo-cache-size", 8, "Size of the in-memory cache for git repositories when using gogit (in MiB)")
405+
fs.Int64Var(&o.GoGitCacheMaxFileSize, "gogit-cache-max-file-size", 1*1024*512, "Maximum file size (in bytes) that will be read into the in-memory cache for git repositories when using gogit; files larger than this will be streamed from disk to avoid memory pressure")
406+
398407
// Function runner configuration
399408
fs.StringVar(&o.DefaultImagePrefix, "default-image-prefix", runneroptions.GHCRImagePrefix, "Default prefix for unqualified function names")
400409
fs.StringVar(&o.FunctionRunnerAddress, "function-runner", "", "Address of the function runner gRPC service.")

pkg/externalrepo/git/cachedir_pool.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ var globalDirectoryPool = &directoryPool{
4141
}
4242

4343
// getOrCreateSharedRepository safely initializes or reuses a cached git directory
44-
func (p *directoryPool) getOrCreateSharedRepository(dir, reponame string) (*sharedDirectory, error) {
44+
func (p *directoryPool) getOrCreateSharedRepository(dir, reponame string, opts GitRepositoryOptions) (*sharedDirectory, error) {
4545
// Fast path: check if directory already exists
4646
if sharedDir, exists := p.directories.Load(dir); exists {
4747
p.mutex.Lock()
@@ -72,7 +72,7 @@ func (p *directoryPool) getOrCreateSharedRepository(dir, reponame string) (*shar
7272
if !os.IsNotExist(err) {
7373
return nil, err
7474
}
75-
repo, err = initEmptyRepository(dir)
75+
repo, err = initEmptyRepository(dir, opts)
7676
if err != nil {
7777
if removeErr := os.RemoveAll(dir); removeErr != nil {
7878
klog.Errorf("Failed to remove partially created directory %s: %v", dir, removeErr)
@@ -82,7 +82,7 @@ func (p *directoryPool) getOrCreateSharedRepository(dir, reponame string) (*shar
8282
} else if !fi.IsDir() {
8383
return nil, fmt.Errorf("cache location %q is not a directory", dir)
8484
} else {
85-
repo, err = openRepository(dir)
85+
repo, err = openRepository(dir, opts)
8686
if err != nil {
8787
if removeErr := os.RemoveAll(dir); removeErr != nil {
8888
klog.Errorf("Failed to open repository %s: %v (also failed to remove corrupted directory: %v)", dir, err, removeErr)

pkg/externalrepo/git/cachedir_pool_test.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import (
2525
"github.com/stretchr/testify/require"
2626
)
2727

28+
var opts = GitRepositoryOptions{}
29+
2830
func TestDirectoryPool_GetOrCreateSharedRepository(t *testing.T) {
2931
pool := &directoryPool{
3032
directories: sync.Map{},
@@ -34,13 +36,13 @@ func TestDirectoryPool_GetOrCreateSharedRepository(t *testing.T) {
3436
repoDir := filepath.Join(tempDir, "test-repo")
3537

3638
// First call should create new shared directory
37-
shared1, err := pool.getOrCreateSharedRepository(repoDir, "test-repo")
39+
shared1, err := pool.getOrCreateSharedRepository(repoDir, "test-repo", opts)
3840
require.NoError(t, err)
3941
require.NotNil(t, shared1)
4042
assert.Equal(t, 1, shared1.refCount)
4143

4244
// Second call should reuse existing directory
43-
shared2, err := pool.getOrCreateSharedRepository(repoDir, "test-repo")
45+
shared2, err := pool.getOrCreateSharedRepository(repoDir, "test-repo", opts)
4446
require.NoError(t, err)
4547
assert.Same(t, shared1, shared2)
4648
assert.Equal(t, 2, shared2.refCount)
@@ -55,11 +57,11 @@ func TestDirectoryPool_ReleaseSharedRepository(t *testing.T) {
5557
repoDir := filepath.Join(tempDir, "test-repo")
5658

5759
// Create shared repository
58-
shared, err := pool.getOrCreateSharedRepository(repoDir, "test-repo")
60+
shared, err := pool.getOrCreateSharedRepository(repoDir, "test-repo", opts)
5961
require.NoError(t, err)
6062

6163
// Add another reference
62-
_, err = pool.getOrCreateSharedRepository(repoDir, "test-repo")
64+
_, err = pool.getOrCreateSharedRepository(repoDir, "test-repo", opts)
6365
require.NoError(t, err)
6466

6567
// First release should decrement refCount
@@ -76,7 +78,7 @@ func TestDirectoryPool_ReleaseSharedRepository(t *testing.T) {
7678

7779
func TestSharedDirectory_WithLock(t *testing.T) {
7880
tempDir := t.TempDir()
79-
repo, err := initEmptyRepository(tempDir)
81+
repo, err := initEmptyRepository(tempDir, opts)
8082
require.NoError(t, err)
8183

8284
shared := &sharedDirectory{
@@ -97,7 +99,7 @@ func TestSharedDirectory_WithLock(t *testing.T) {
9799

98100
func TestSharedDirectory_WithRLock(t *testing.T) {
99101
tempDir := t.TempDir()
100-
repo, err := initEmptyRepository(tempDir)
102+
repo, err := initEmptyRepository(tempDir, opts)
101103
require.NoError(t, err)
102104

103105
shared := &sharedDirectory{
@@ -132,7 +134,7 @@ func TestDirectoryPool_ConcurrentAccess(t *testing.T) {
132134
wg.Add(1)
133135
go func(id int) {
134136
defer wg.Done()
135-
_, err := pool.getOrCreateSharedRepository(repoDir, "concurrent-repo")
137+
_, err := pool.getOrCreateSharedRepository(repoDir, "concurrent-repo", opts)
136138
assert.NoError(t, err)
137139
}(i)
138140
}
@@ -174,7 +176,7 @@ func TestDirectoryPool_InitEmptyRepositoryFailure(t *testing.T) {
174176
repoDir := filepath.Join(filePath, "subdir") // This will fail because parent is a file
175177

176178
// Should fail to create repository
177-
_, err = pool.getOrCreateSharedRepository(repoDir, "test-repo")
179+
_, err = pool.getOrCreateSharedRepository(repoDir, "test-repo", opts)
178180
assert.Error(t, err)
179181

180182
// Should not be in pool
@@ -183,7 +185,7 @@ func TestDirectoryPool_InitEmptyRepositoryFailure(t *testing.T) {
183185

184186
// Retry should work if we fix the issue
185187
os.Remove(filePath)
186-
_, err = pool.getOrCreateSharedRepository(repoDir, "test-repo")
188+
_, err = pool.getOrCreateSharedRepository(repoDir, "test-repo", opts)
187189
assert.NoError(t, err)
188190
}
189191

@@ -203,7 +205,7 @@ func TestDirectoryPool_OpenRepositoryFailure(t *testing.T) {
203205
require.NoError(t, err)
204206

205207
// Should fail to open corrupted repository
206-
_, err = pool.getOrCreateSharedRepository(repoDir, "test-repo")
208+
_, err = pool.getOrCreateSharedRepository(repoDir, "test-repo", opts)
207209
assert.Error(t, err)
208210
assert.Contains(t, err.Error(), "corrupted cache was removed")
209211

@@ -216,7 +218,7 @@ func TestDirectoryPool_OpenRepositoryFailure(t *testing.T) {
216218
assert.True(t, os.IsNotExist(err))
217219

218220
// Retry should work now that corrupted dir is removed
219-
_, err = pool.getOrCreateSharedRepository(repoDir, "test-repo")
221+
_, err = pool.getOrCreateSharedRepository(repoDir, "test-repo", opts)
220222
assert.NoError(t, err)
221223
}
222224

@@ -242,7 +244,7 @@ func TestDirectoryPool_OpenRepositoryCleanupFailure(t *testing.T) {
242244
defer os.Chmod(repoDir, 0755)
243245

244246
// Should fail to open and also fail to cleanup
245-
_, err = pool.getOrCreateSharedRepository(repoDir, "test-repo")
247+
_, err = pool.getOrCreateSharedRepository(repoDir, "test-repo", opts)
246248
assert.Error(t, err)
247249
// When cleanup fails, error message should mention checking local cache
248250
assert.Contains(t, err.Error(), "check the local git cache")
@@ -269,7 +271,7 @@ func TestDirectoryPool_PathIsFile(t *testing.T) {
269271
require.NoError(t, err)
270272

271273
// Should fail because path is a file, not a directory
272-
_, err = pool.getOrCreateSharedRepository(filePath, "test-repo")
274+
_, err = pool.getOrCreateSharedRepository(filePath, "test-repo", opts)
273275
assert.Error(t, err)
274276
assert.Contains(t, err.Error(), "not a directory")
275277

pkg/externalrepo/git/git.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ func OpenRepository(ctx context.Context, name, namespace string, spec *configapi
159159
}
160160

161161
// Get or create shared repository - let SharedDirectory handle cleanup
162-
sharedDir, err := globalDirectoryPool.getOrCreateSharedRepository(dir, name)
162+
sharedDir, err := globalDirectoryPool.getOrCreateSharedRepository(dir, name, opts)
163163
if err != nil {
164164
return nil, pkgerrors.Wrapf(err, "error cloning git repository %+v", spec.Repo)
165165
}

pkg/externalrepo/git/gogit.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import (
2727

2828
// This file contains helpers for interacting with gogit.
2929

30-
func initEmptyRepository(path string) (*git.Repository, error) {
30+
func initEmptyRepository(path string, opts GitRepositoryOptions) (*git.Repository, error) {
3131
isBare := true // Porch only uses bare repositories
3232
repo, err := git.PlainInit(path, isBare)
3333
if err != nil {
@@ -36,7 +36,9 @@ func initEmptyRepository(path string) (*git.Repository, error) {
3636
if err := initializeDefaultBranches(repo); err != nil {
3737
return nil, pkgerrors.Wrapf(err, "gogit: default branch initialize failed on repo for path %q", path)
3838
}
39-
return repo, nil
39+
// Re-open with tuned storage settings to avoid the default 96MiB in-memory LRU cache
40+
// that PlainInit creates, since this repo will be long-lived in the directory pool.
41+
return openRepository(path, opts)
4042
}
4143

4244
func initializeDefaultBranches(repo *git.Repository) error {
@@ -52,11 +54,28 @@ func initializeDefaultBranches(repo *git.Repository) error {
5254
return nil
5355
}
5456

55-
func openRepository(path string) (*git.Repository, error) {
57+
func openRepository(path string, opts GitRepositoryOptions) (*git.Repository, error) {
5658
dot := osfs.New(path)
57-
storage := filesystem.NewStorageWithOptions(dot, cache.NewObjectLRUDefault(), filesystem.Options{
58-
ExclusiveAccess: true,
59-
})
59+
60+
const (
61+
defaultRepoCacheSizeMiB = 8
62+
defaultMaxFileSize = 512 * 1024
63+
)
64+
cacheSizeMiB := opts.GoGitRepoCacheSize
65+
if cacheSizeMiB <= 0 {
66+
cacheSizeMiB = defaultRepoCacheSizeMiB
67+
}
68+
maxFileSize := opts.GoGitCacheMaxFileSize
69+
if maxFileSize <= 0 {
70+
maxFileSize = defaultMaxFileSize
71+
}
72+
objectCache := cache.NewObjectLRU(cache.FileSize(cacheSizeMiB) * cache.MiByte)
73+
74+
fileOpts := filesystem.Options{
75+
ExclusiveAccess: true,
76+
LargeObjectThreshold: maxFileSize,
77+
}
78+
storage := filesystem.NewStorageWithOptions(dot, objectCache, fileOpts)
6079
return git.Open(storage, dot)
6180
}
6281

pkg/externalrepo/types/externalrepotypes.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,6 @@ type ExternalRepoOptions struct {
3333
CaBundleResolver repository.CredentialResolver
3434
UserInfoProvider repository.UserInfoProvider
3535
RepoOperationRetryAttempts int
36+
GoGitRepoCacheSize int
37+
GoGitCacheMaxFileSize int64
3638
}

0 commit comments

Comments
 (0)