diff --git a/core/repomanager/repo_manager.go b/core/repomanager/repo_manager.go index 2bd5f60..337439b 100644 --- a/core/repomanager/repo_manager.go +++ b/core/repomanager/repo_manager.go @@ -86,7 +86,10 @@ type Params struct { // appCtx is the application-lifetime context. Cancel it when the process is // shutting down (e.g. wire it to SIGTERM/SIGINT in main) to abort any // background goroutines the manager spawns. -func NewRepoManager(appCtx context.Context, p Params) RepoManager { +func NewRepoManager(appCtx context.Context, p Params) (RepoManager, error) { + if p.PoolSize <= 0 { + return nil, fmt.Errorf("pool size must be > 0, got %d", p.PoolSize) + } return &repoManager{ git: p.Git, repoManagerClonePath: p.RepoManagerClonePath, @@ -95,7 +98,7 @@ func NewRepoManager(appCtx context.Context, p Params) RepoManager { poolSize: p.PoolSize, pools: make(map[string]*workerPool), appCtx: appCtx, - } + }, nil } func (r *repoManager) poolFor(repo string) *workerPool { @@ -105,10 +108,6 @@ func (r *repoManager) poolFor(repo string) *workerPool { if pool, ok := r.pools[repo]; ok { return pool } - // default to 1 worker workspace per repo - if r.poolSize <= 0 { - r.poolSize = 1 - } pool := &workerPool{ originDir: filepath.Join(r.repoManagerClonePath, repo), avail: make(chan *workerSlot, r.poolSize), diff --git a/core/repomanager/repo_manager_test.go b/core/repomanager/repo_manager_test.go index 1d0b951..dcda796 100644 --- a/core/repomanager/repo_manager_test.go +++ b/core/repomanager/repo_manager_test.go @@ -30,6 +30,22 @@ import ( "go.uber.org/zap" ) +func newTestRepoManager(t *testing.T, appCtx context.Context, p Params) RepoManager { + t.Helper() + rm, err := NewRepoManager(appCtx, p) + require.NoError(t, err) + return rm +} + +func TestNewRepoManager_InvalidPoolSize(t *testing.T) { + t.Parallel() + _, err := NewRepoManager(context.Background(), Params{ + Logger: zap.NewNop().Sugar(), + PoolSize: 0, + }) + require.Error(t, err, "expected setting invalid poolsize to error out") +} + func TestLease_ClonesOriginAndCreatesWorker(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) @@ -43,7 +59,7 @@ func TestLease_ClonesOriginAndCreatesWorker(t *testing.T) { g.EXPECT().Clone(gomock.Any(), remote, originDir, "-c", "gc.auto=0").Return(nil) g.EXPECT().Clone(gomock.Any(), originDir, workerDir, "--local", "-c", "gc.auto=0").Return(nil) - rm := NewRepoManager(context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 1}) + rm := newTestRepoManager(t, context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 1}) ws, err := rm.Lease(context.Background(), tangopb.BuildDescription{Remote: remote}) require.NoError(t, err) assert.Equal(t, workerDir, ws.Path()) @@ -65,7 +81,7 @@ func TestLease_SkipsOriginClone_WhenExists(t *testing.T) { // Only worker clone expected g.EXPECT().Clone(gomock.Any(), originDir, workerDir, "--local", "-c", "gc.auto=0").Return(nil) - rm := NewRepoManager(context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 1}) + rm := newTestRepoManager(t, context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 1}) ws, err := rm.Lease(context.Background(), tangopb.BuildDescription{Remote: remote}) require.NoError(t, err) assert.Equal(t, workerDir, ws.Path()) @@ -86,7 +102,7 @@ func TestLease_ReusesWorker_AfterRelease(t *testing.T) { g.EXPECT().Clone(gomock.Any(), remote, originDir, "-c", "gc.auto=0").Return(nil) g.EXPECT().Clone(gomock.Any(), originDir, workerDir, "--local", "-c", "gc.auto=0").Return(nil) - rm := NewRepoManager(context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 1}) + rm := newTestRepoManager(t, context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 1}) ctx := context.Background() ws1, err := rm.Lease(ctx, tangopb.BuildDescription{Remote: remote}) @@ -115,7 +131,7 @@ func TestLease_CreatesMultipleWorkers(t *testing.T) { g.EXPECT().Clone(gomock.Any(), originDir, dir, "--local", "-c", "gc.auto=0").Return(nil) } - rm := NewRepoManager(context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 2}) + rm := newTestRepoManager(t, context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 2}) ctx := context.Background() ws1, err := rm.Lease(ctx, tangopb.BuildDescription{Remote: remote}) @@ -141,7 +157,7 @@ func TestLease_BlocksUntilReturn(t *testing.T) { g.EXPECT().Clone(gomock.Any(), remote, originDir, "-c", "gc.auto=0").Return(nil) g.EXPECT().Clone(gomock.Any(), originDir, workerDir, "--local", "-c", "gc.auto=0").Return(nil) - rm := NewRepoManager(context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 1}) + rm := newTestRepoManager(t, context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 1}) ctx := context.Background() ws1, err := rm.Lease(ctx, tangopb.BuildDescription{Remote: remote}) @@ -187,7 +203,7 @@ func TestLease_CtxCanceled(t *testing.T) { g.EXPECT().Clone(gomock.Any(), remote, originDir, "-c", "gc.auto=0").Return(nil) g.EXPECT().Clone(gomock.Any(), originDir, workerDir, "--local", "-c", "gc.auto=0").Return(nil) - rm := NewRepoManager(context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 1}) + rm := newTestRepoManager(t, context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 1}) ws1, err := rm.Lease(context.Background(), tangopb.BuildDescription{Remote: remote}) require.NoError(t, err) @@ -209,7 +225,7 @@ func TestLease_OriginCloneFails(t *testing.T) { remote := "git@github.com:org/repo" g.EXPECT().Clone(gomock.Any(), remote, filepath.Join(root, "org/repo"), "-c", "gc.auto=0").Return(assert.AnError) - rm := NewRepoManager(context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 1}) + rm := newTestRepoManager(t, context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 1}) _, err := rm.Lease(context.Background(), tangopb.BuildDescription{Remote: remote}) require.Error(t, err) assert.Contains(t, err.Error(), "clone origin") @@ -228,7 +244,7 @@ func TestLease_WorkerCloneFails(t *testing.T) { g.EXPECT().Clone(gomock.Any(), remote, originDir, "-c", "gc.auto=0").Return(nil) g.EXPECT().Clone(gomock.Any(), originDir, workerDir, "--local", "-c", "gc.auto=0").Return(assert.AnError) - rm := NewRepoManager(context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 1}) + rm := newTestRepoManager(t, context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 1}) _, err := rm.Lease(context.Background(), tangopb.BuildDescription{Remote: remote}) require.Error(t, err) assert.Contains(t, err.Error(), "create worker") @@ -247,7 +263,7 @@ func TestLease_DiscoversExistingWorker(t *testing.T) { require.NoError(t, os.MkdirAll(filepath.Join(root, ".workers", "org/repo", "worker-1", ".git"), 0o755)) // No Clone calls — everything already exists - rm := NewRepoManager(context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 1}) + rm := newTestRepoManager(t, context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 1}) ws, err := rm.Lease(context.Background(), tangopb.BuildDescription{Remote: remote}) require.NoError(t, err) assert.Contains(t, ws.Path(), "worker-1") @@ -273,7 +289,7 @@ func TestLease_DifferentRepos_IndependentPools(t *testing.T) { g.EXPECT().Clone(gomock.Any(), remote2, origin2, "-c", "gc.auto=0").Return(nil) g.EXPECT().Clone(gomock.Any(), origin2, worker2, "--local", "-c", "gc.auto=0").Return(nil) - rm := NewRepoManager(context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 1}) + rm := newTestRepoManager(t, context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 1}) ctx := context.Background() // Both repos can be leased concurrently even with pool size 1 @@ -306,7 +322,7 @@ func TestLease_WorkerCloneFails_SlotReturnedToPool(t *testing.T) { g.EXPECT().Clone(gomock.Any(), originDir, workerDir, "--local", "-c", "gc.auto=0").Return(nil), ) - rm := NewRepoManager(context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 1}) + rm := newTestRepoManager(t, context.Background(), Params{Git: g, Logger: zap.NewNop().Sugar(), RepoManagerClonePath: root, WorkerRootPath: filepath.Join(root, ".workers"), PoolSize: 1}) ctx := context.Background() // First attempt fails diff --git a/example/main.go b/example/main.go index 06d080d..05a3036 100644 --- a/example/main.go +++ b/example/main.go @@ -82,13 +82,16 @@ func run() error { } defer os.RemoveAll(workerRootPath) - rm := repomanager.NewRepoManager(appCtx, repomanager.Params{ + rm, err := repomanager.NewRepoManager(appCtx, repomanager.Params{ Git: git.New(repoManagerClonePath), Logger: logger, RepoManagerClonePath: repoManagerClonePath, WorkerRootPath: workerRootPath, PoolSize: cfg.Service.WorkerPoolSize, }) + if err != nil { + return fmt.Errorf("failed to create repo manager: %w", err) + } orch, err := orchestrator.NewNativeOrchestrator(appCtx, orchestrator.Params{ Storage: store, RepoManager: rm,