Skip to content

Commit 47f55ac

Browse files
committed
fix(sync): broadcast on-demand sync result to all waiters
Replace the shared chan error in requestStore with a shared result object so every deduped caller observes the same outcome, including early lock failures that previously delivered a nil (success) result. Signed-off-by: Marga Rudno-Rudzińska <marga.rudno@gmail.com>
1 parent 3147961 commit 47f55ac

2 files changed

Lines changed: 51 additions & 35 deletions

File tree

pkg/extensions/sync/on_demand.go

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ type lockHandle struct {
5252
owner string
5353
}
5454

55+
// syncResult is the shared outcome of a single in-flight on-demand sync.
56+
// Every caller that dedups onto the same request blocks on done and then
57+
// reads err, so all waiters observe the same result reliably, including
58+
// early failures (e.g. lock rejection) before the sync goroutine starts.
59+
type syncResult struct {
60+
done chan struct{}
61+
err error
62+
}
63+
5564
/*
5665
BaseOnDemand tracks requests that can be an image/signature/sbom.
5766
@@ -60,7 +69,7 @@ process just the first one, also keep track of all background retrying routines.
6069
*/
6170
type BaseOnDemand struct {
6271
services []Service
63-
// map[request]chan err
72+
// map[request]*syncResult for in-flight syncs; struct{}{} for background retries
6473
requestStore *sync.Map
6574
distributedLock distributedLockBackend
6675
distributedLockOwner string
@@ -220,43 +229,46 @@ func (onDemand *BaseOnDemand) runLockHeartbeat(ctx context.Context,
220229
}
221230

222231
func (onDemand *BaseOnDemand) SyncImage(ctx context.Context, repo, reference string) error {
223-
return onDemand.runWithLock(ctx, "image", repo, reference, func(syncResult chan error) {
224-
onDemand.syncImage(ctx, repo, reference, syncResult)
232+
return onDemand.runWithLock(ctx, "image", repo, reference, func(result *syncResult) {
233+
onDemand.syncImage(ctx, repo, reference, result)
225234
})
226235
}
227236

228237
func (onDemand *BaseOnDemand) SyncReferrers(ctx context.Context, repo string,
229238
subjectDigestStr string, referenceTypes []string,
230239
) error {
231-
return onDemand.runWithLock(ctx, "referrers", repo, subjectDigestStr, func(syncResult chan error) {
232-
onDemand.syncReferrers(ctx, repo, subjectDigestStr, referenceTypes, syncResult)
240+
return onDemand.runWithLock(ctx, "referrers", repo, subjectDigestStr, func(result *syncResult) {
241+
onDemand.syncReferrers(ctx, repo, subjectDigestStr, referenceTypes, result)
233242
})
234243
}
235244

236245
// runWithLock combines in-process dedup, distributed locking, and the
237246
// heartbeat goroutine. The run closure executes the actual sync and is
238-
// expected to deliver the result on syncResult and close it.
247+
// expected to set result.err and close result.done.
239248
func (onDemand *BaseOnDemand) runWithLock(ctx context.Context, kind, repo, reference string,
240-
run func(chan error),
249+
run func(*syncResult),
241250
) error {
242251
req := request{repo: repo, reference: reference}
243-
syncResult := make(chan error)
252+
result := &syncResult{done: make(chan struct{})}
244253

245-
val, loaded := onDemand.requestStore.LoadOrStore(req, syncResult)
254+
val, loaded := onDemand.requestStore.LoadOrStore(req, result)
246255
if loaded {
247256
onDemand.log.Info().Str("repo", repo).Str("reference", reference).Str("kind", kind).
248-
Msg("on-demand sync already in flight on this replica, waiting on channel")
257+
Msg("on-demand sync already in flight on this replica, waiting on result")
249258

250-
existing, _ := val.(chan error)
259+
existing, _ := val.(*syncResult)
251260

252-
return <-existing
261+
<-existing.done
262+
263+
return existing.err
253264
}
254265

255266
defer onDemand.requestStore.Delete(req)
256267

257268
handle, release, err := onDemand.acquireDistributedLock(ctx, kind, repo, reference)
258269
if err != nil {
259-
close(syncResult)
270+
result.err = err
271+
close(result.done)
260272

261273
return err
262274
}
@@ -273,15 +285,17 @@ func (onDemand *BaseOnDemand) runWithLock(ctx context.Context, kind, repo, refer
273285
go onDemand.runLockHeartbeat(heartbeatCtx, handle.key, handle.owner, syncLockHeartbeatInterval)
274286
}
275287

276-
go run(syncResult)
288+
go run(result)
289+
290+
<-result.done
277291

278-
return <-syncResult
292+
return result.err
279293
}
280294

281295
func (onDemand *BaseOnDemand) syncReferrers(ctx context.Context, repo, subjectDigestStr string,
282-
referenceTypes []string, syncResult chan error,
296+
referenceTypes []string, result *syncResult,
283297
) {
284-
defer close(syncResult)
298+
defer close(result.done)
285299

286300
var err error
287301

@@ -355,11 +369,11 @@ func (onDemand *BaseOnDemand) syncReferrers(ctx context.Context, repo, subjectDi
355369
}
356370
}
357371

358-
syncResult <- err
372+
result.err = err
359373
}
360374

361-
func (onDemand *BaseOnDemand) syncImage(ctx context.Context, repo, reference string, syncResult chan error) {
362-
defer close(syncResult)
375+
func (onDemand *BaseOnDemand) syncImage(ctx context.Context, repo, reference string, result *syncResult) {
376+
defer close(result.done)
363377

364378
var err error
365379

@@ -433,5 +447,5 @@ func (onDemand *BaseOnDemand) syncImage(ctx context.Context, repo, reference str
433447
}
434448
}
435449

436-
syncResult <- err
450+
result.err = err
437451
}

pkg/extensions/sync/sync_internal_test.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -585,22 +585,23 @@ func TestService(t *testing.T) {
585585
isBackground: false,
586586
}
587587

588-
// Create a channel that we control completely
589-
pendingChannel := make(chan error, 1)
590-
onDemand.requestStore.Store(req, pendingChannel)
588+
// Pre-populate with a shared result we control completely
589+
pendingResult := &syncResult{done: make(chan struct{})}
590+
onDemand.requestStore.Store(req, pendingResult)
591591

592-
// Start request that will wait on our channel
592+
// Start request that will wait on our result
593593
requestCompleted := make(chan error)
594594
go func() {
595595
err := onDemand.SyncImage(context.Background(), "test-guaranteed-channel-image", "guaranteed-image-tag")
596596
requestCompleted <- err
597597
}()
598598

599-
// Wait a moment for the request to reach the channel waiting code
599+
// Wait a moment for the request to reach the waiting code
600600
time.Sleep(50 * time.Millisecond)
601601

602-
// Send error through our controlled channel - this proves channel waiting worked
603-
pendingChannel <- errors.New("guaranteed channel error")
602+
// Publish the result and broadcast - this proves the waiting worked
603+
pendingResult.err = errors.New("guaranteed channel error")
604+
close(pendingResult.done)
604605

605606
// Verify the request got our controlled error
606607
err := <-requestCompleted
@@ -619,22 +620,23 @@ func TestService(t *testing.T) {
619620
isBackground: false,
620621
}
621622

622-
// Create a channel that we control completely
623-
pendingChannel := make(chan error, 1)
624-
onDemand.requestStore.Store(req, pendingChannel)
623+
// Pre-populate with a shared result we control completely
624+
pendingResult := &syncResult{done: make(chan struct{})}
625+
onDemand.requestStore.Store(req, pendingResult)
625626

626-
// Start request that will wait on our channel
627+
// Start request that will wait on our result
627628
requestCompleted := make(chan error)
628629
go func() {
629630
err := onDemand.SyncReferrers(context.Background(), "test-guaranteed-channel-referrers", "sha256:guaranteed", []string{"signature"})
630631
requestCompleted <- err
631632
}()
632633

633-
// Wait a moment for the request to reach the channel waiting code
634+
// Wait a moment for the request to reach the waiting code
634635
time.Sleep(50 * time.Millisecond)
635636

636-
// Send error through our controlled channel - this proves channel waiting worked
637-
pendingChannel <- errors.New("guaranteed referrer channel error")
637+
// Publish the result and broadcast - this proves the waiting worked
638+
pendingResult.err = errors.New("guaranteed referrer channel error")
639+
close(pendingResult.done)
638640

639641
// Verify the request got our controlled error
640642
err := <-requestCompleted

0 commit comments

Comments
 (0)