enhance: make shared tool cache safe at any capacity (#1401)

Co-authored-by: bircni <[email protected]>
This commit is contained in:
bircni authored and silverwind committed 2026-11-10 14:25:50 +00:00
1 parent 84c0fcd1fa
commit a021d3139d
9 files changed
+121 -11

No files matched your search

+5
View File
@@ -79,6 +79,7 @@ type Runner struct {
runningTasks sync.Map
runningCount atomic.Int64
lastIdleCleanupUnixNano atomic.Int64
slots slotAllocator
now func() time.Time
healthCheckLast time.Time
healthCheckReady bool
@@ -451,6 +452,9 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
}
}()
slot := r.slots.acquire()
defer r.slots.release(slot)
workflowCallInputs := restoreCalledWorkflowContext(task)
for _, line := range r.setupLines(task) {
reporter.Logf("%s", line)
@@ -623,6 +627,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
ValidVolumes: r.cfg.Container.ValidVolumes,
ContainerVolumes: r.cfg.Container.Volumes,
SharedToolCache: r.cfg.Runner.ToolCacheMode == config.ToolCacheModeShared,
ToolCacheSlot: toolCacheSlot(r.cfg.Runner.ToolCacheMode, slot),
InsecureSkipTLS: r.cfg.Runner.Insecure,
ClientCertFile: r.cfg.Runner.ClientCertFile,
ClientKeyFile: r.cfg.Runner.ClientKeyFile,
+44
View File
@@ -0,0 +1,44 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package run
import (
"slices"
"strconv"
"sync"
"gitea.com/gitea/runner/internal/pkg/config"
)
// slotAllocator hands out the lowest free slot, so slot numbers stay below capacity.
type slotAllocator struct {
mu sync.Mutex
used []bool
}
func (a *slotAllocator) acquire() int {
a.mu.Lock()
defer a.mu.Unlock()
slot := slices.Index(a.used, false)
if slot < 0 {
slot = len(a.used)
a.used = append(a.used, false)
}
a.used[slot] = true
return slot
}
func (a *slotAllocator) release(slot int) {
a.mu.Lock()
defer a.mu.Unlock()
a.used[slot] = false
}
// toolCacheSlot leaves slot 0 unnamed, so it keeps the cache a runner had before slots existed.
func toolCacheSlot(mode string, slot int) string {
if mode != config.ToolCacheModeShared || slot == 0 {
return ""
}
return strconv.Itoa(slot)
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package run
import (
"testing"
"gitea.com/gitea/runner/internal/pkg/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestToolCacheSlotReusesLowestFree(t *testing.T) {
var slots slotAllocator
require.Equal(t, 0, slots.acquire())
require.Equal(t, 1, slots.acquire())
require.Equal(t, 2, slots.acquire())
slots.release(1)
assert.Equal(t, 1, slots.acquire())
assert.Equal(t, 3, slots.acquire())
assert.Equal(t, "3", toolCacheSlot(config.ToolCacheModeShared, 3))
assert.Empty(t, toolCacheSlot(config.ToolCacheModeShared, 0))
assert.Empty(t, toolCacheSlot(config.ToolCacheModeNone, 3))
}
+2 -2
View File
@@ -132,8 +132,8 @@ runner:
# What to mount at RUNNER_TOOL_CACHE (/opt/hostedtoolcache), where setup actions install tools:
# none: nothing. A docker job sees what its image ships there, a host job an empty dir, and
# either way what it installs is gone when the job ends.
# shared: one tool cache every job reuses. Two jobs writing the same tool version at once
# corrupt it, so use it only with capacity 1.
# shared: one tool cache per capacity slot, reused by later jobs. It hides the tools later job
# images ship, so use images without them.
#tool_cache_mode: none
# Optional executable on the host, run once after each task's built-in cleanup
# (post-steps, container teardown, bind-workdir removal). Additive only.
+1 -5
View File
@@ -193,7 +193,7 @@ type Container struct {
Sweep bool `yaml:"sweep"` // Sweep removes the containers, networks and volumes a job creates through its Docker proxy when the job ends.
}
// Values of Runner.ToolCacheMode: the runner mounts no tool cache, or one that every job reuses.
// Values of Runner.ToolCacheMode: the runner mounts no tool cache, or one per capacity slot that its jobs reuse.
const (
ToolCacheModeNone = "none"
ToolCacheModeShared = "shared"
@@ -454,10 +454,6 @@ func LoadDefault(file string) (*Config, error) {
}
// Validate and fix invalid config combinations to prevent confusing behavior.
if cfg.Runner.ToolCacheMode == ToolCacheModeShared && cfg.Runner.Capacity > 1 {
logging.Warnf("runner.tool_cache_mode %q with capacity %d: two jobs writing the same tool version at once corrupt it",
ToolCacheModeShared, cfg.Runner.Capacity)
}
if cfg.Runner.FetchTimeout > RequestTimeout {
logging.Warnf("fetch_timeout (%v) exceeds the RPC timeout (%v), capping it", cfg.Runner.FetchTimeout, RequestTimeout)
cfg.Runner.FetchTimeout = RequestTimeout