Skip to content

Commit 238435e

Browse files
geroplona-agent
andcommitted
feat(cache): add immediate upload mode for external interruption resilience
Add LEEWAY_CACHE_UPLOAD_IMMEDIATE environment variable to enable uploading packages to remote cache immediately after each successful build, rather than waiting until the end of the entire build process. This addresses scenarios where builds are interrupted externally (CI timeouts, manual cancellation, SIGTERM/SIGKILL) before the final upload phase, causing loss of all successfully built packages. Changes: - Add LEEWAY_CACHE_UPLOAD_IMMEDIATE environment variable (default: false) - Implement immediate per-package upload in RegisterNewlyBuilt() - Track uploaded packages to prevent duplicates - Skip already-uploaded packages in final batch upload - Add uploadPackageImmediately() method with error handling - Document new environment variable in README Trade-offs: - Immediate mode: More network calls but survives interruptions - Default mode: Single batch upload, more efficient but vulnerable to interrupts Usage: export LEEWAY_CACHE_UPLOAD_IMMEDIATE=true leeway build your-package Co-authored-by: Ona <no-reply@ona.com>
1 parent 37bc783 commit 238435e

2 files changed

Lines changed: 71 additions & 12 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,7 @@ variables have an effect on leeway:
491491
For details on configuring AWS credentials see https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html
492492
- `LEEWAY_CACHE_DIR`: Location of the local build cache. The directory does not have to exist yet.
493493
- `LEEWAY_BUILD_DIR`: Working location of leeway (i.e. where the actual builds happen). This location will see heavy I/O which makes it advisable to place this on a fast SSD or in RAM.
494+
- `LEEWAY_CACHE_UPLOAD_IMMEDIATE`: When set to "true", packages are uploaded to remote cache immediately after successful build, rather than waiting until the end of the entire build. This is useful when builds may be interrupted externally (e.g., CI timeouts, manual cancellation) to ensure partial results are cached. Defaults to "false".
494495
- `LEEWAY_YARN_MUTEX`: Configures the mutex flag leeway will pass to yarn. Defaults to "network". See https://yarnpkg.com/lang/en/docs/cli/#toc-concurrency-and-mutex for possible values.
495496
- `LEEWAY_EXPERIMENTAL`: Enables exprimental features
496497

pkg/leeway/build.go

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,11 @@ type buildContext struct {
6868
mu sync.Mutex
6969
newlyBuiltPackages map[string]*Package
7070

71-
pkgLockCond *sync.Cond
72-
pkgLocks map[string]struct{}
73-
buildLimit *semaphore.Weighted
71+
pkgLockCond *sync.Cond
72+
pkgLocks map[string]struct{}
73+
buildLimit *semaphore.Weighted
74+
immediateCacheUpload bool
75+
uploadedPackages map[string]struct{} // Track packages already uploaded to avoid duplicates
7476
}
7577

7678
const (
@@ -85,6 +87,12 @@ const (
8587
// Defaults to "network".
8688
EnvvarYarnMutex = "LEEWAY_YARN_MUTEX"
8789

90+
// EnvvarImmediateCacheUpload controls whether packages are uploaded to remote cache immediately
91+
// after successful build, rather than waiting until the end of the entire build.
92+
// Set to "true" to enable immediate uploads. This is useful when builds may be interrupted
93+
// externally (e.g., CI timeouts, manual cancellation) to ensure partial results are cached.
94+
EnvvarImmediateCacheUpload = "LEEWAY_CACHE_UPLOAD_IMMEDIATE"
95+
8896
// dockerImageNamesFiles is the name of the file store in poushed Docker build artifacts
8997
// which contains the names of the Docker images we just pushed
9098
dockerImageNamesFiles = "imgnames.txt"
@@ -145,15 +153,20 @@ func newBuildContext(options buildOptions) (ctx *buildContext, err error) {
145153
return nil, xerrors.Errorf("cannot compute hash of myself: %w", err)
146154
}
147155

156+
// Check if immediate cache upload is enabled
157+
immediateCacheUpload := os.Getenv(EnvvarImmediateCacheUpload) == "true"
158+
148159
ctx = &buildContext{
149-
buildOptions: options,
150-
buildDir: buildDir,
151-
buildID: buildID,
152-
newlyBuiltPackages: make(map[string]*Package),
153-
pkgLockCond: sync.NewCond(&sync.Mutex{}),
154-
pkgLocks: make(map[string]struct{}),
155-
buildLimit: buildLimit,
156-
leewayHash: hex.EncodeToString(leewayHash.Sum(nil)),
160+
buildOptions: options,
161+
buildDir: buildDir,
162+
buildID: buildID,
163+
newlyBuiltPackages: make(map[string]*Package),
164+
pkgLockCond: sync.NewCond(&sync.Mutex{}),
165+
pkgLocks: make(map[string]struct{}),
166+
buildLimit: buildLimit,
167+
leewayHash: hex.EncodeToString(leewayHash.Sum(nil)),
168+
immediateCacheUpload: immediateCacheUpload,
169+
uploadedPackages: make(map[string]struct{}),
157170
}
158171

159172
err = os.MkdirAll(buildDir, 0755)
@@ -235,16 +248,61 @@ func (c *buildContext) RegisterNewlyBuilt(p *Package) error {
235248
c.mu.Lock()
236249
c.newlyBuiltPackages[ver] = p
237250
c.mu.Unlock()
251+
252+
// If immediate cache upload is enabled, upload this package right away
253+
if c.immediateCacheUpload && !p.Ephemeral {
254+
if err := c.uploadPackageImmediately(p); err != nil {
255+
// Log the error but don't fail the build
256+
log.WithError(err).WithField("package", p.FullName()).Warn("immediate cache upload failed")
257+
c.Reporter.CacheUploadFailed([]cache.Package{p}, err)
258+
}
259+
}
260+
261+
return nil
262+
}
263+
264+
// uploadPackageImmediately uploads a single package to remote cache immediately after build
265+
func (c *buildContext) uploadPackageImmediately(p *Package) error {
266+
ver, err := p.Version()
267+
if err != nil {
268+
return err
269+
}
270+
271+
c.mu.Lock()
272+
// Check if already uploaded to avoid duplicate uploads
273+
if _, uploaded := c.uploadedPackages[ver]; uploaded {
274+
c.mu.Unlock()
275+
return nil
276+
}
277+
c.uploadedPackages[ver] = struct{}{}
278+
c.mu.Unlock()
279+
280+
log.WithField("package", p.FullName()).Debug("uploading package to remote cache immediately")
281+
282+
pkgsToUpload := []cache.Package{p}
283+
err = c.RemoteCache.Upload(context.Background(), c.LocalCache, pkgsToUpload)
284+
if err != nil {
285+
// Remove from uploaded set on failure so it can be retried at the end
286+
c.mu.Lock()
287+
delete(c.uploadedPackages, ver)
288+
c.mu.Unlock()
289+
return err
290+
}
291+
238292
return nil
239293
}
240294

241295
func (c *buildContext) GetNewPackagesForCache() []*Package {
242296
res := make([]*Package, 0, len(c.newlyBuiltPackages))
243297
c.mu.Lock()
244-
for _, pkg := range c.newlyBuiltPackages {
298+
for ver, pkg := range c.newlyBuiltPackages {
245299
if pkg.Ephemeral {
246300
continue
247301
}
302+
// Skip packages that were already uploaded immediately
303+
if _, uploaded := c.uploadedPackages[ver]; uploaded {
304+
continue
305+
}
248306
res = append(res, pkg)
249307
}
250308
c.mu.Unlock()

0 commit comments

Comments
 (0)