Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(DynamicStore): retry setCredsStore on next PUT #728

Merged
merged 4 commits into from Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 34 additions & 2 deletions internal/syncutil/once.go
Expand Up @@ -15,10 +15,14 @@

package syncutil

import "context"
import (
"context"
"sync"
"sync/atomic"
)

// Once is an object that will perform exactly one action.
// Unlike sync.Once, this Once allowes the action to have return values.
// Unlike sync.Once, this Once allows the action to have return values.
type Once struct {
result interface{}
err error
Expand Down Expand Up @@ -68,3 +72,31 @@
}
}
}

// OnceOrRetry is an object that will perform exactly one success action.
type OnceOrRetry struct {
done atomic.Bool
lock sync.Mutex
}

// OnceOrRetry calls the function f if and only if Do is being called for the
// first time for this instance of Once or all previous calls to Do are failed.
func (o *OnceOrRetry) Do(f func() error) error {
// fast path
if o.done.Load() {
return nil
}

// slow path
o.lock.Lock()
defer o.lock.Unlock()

if o.done.Load() {
return nil
}

Check warning on line 96 in internal/syncutil/once.go

View check run for this annotation

Codecov / codecov/patch

internal/syncutil/once.go#L95-L96

Added lines #L95 - L96 were not covered by tests
if err := f(); err != nil {
return err
}
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved
o.done.Store(true)
return nil
}
95 changes: 95 additions & 0 deletions internal/syncutil/once_test.go
Expand Up @@ -22,6 +22,7 @@ import (
"reflect"
"strconv"
"sync"
"sync/atomic"
"testing"
"time"
)
Expand Down Expand Up @@ -191,3 +192,97 @@ func TestOnce_Do_Cancel_Panic(t *testing.T) {
t.Fatalf("Once.Do() result = %v, want %v", result, wantResult)
}
}

func TestOnceOrRetry_Do(t *testing.T) {
var once OnceOrRetry
var count atomic.Int32
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
err := once.Do(func() error {
count.Add(1)
return nil
})
if err != nil {
t.Errorf("OnceOrRetry.Do() error = %v, wantErr %v", err, nil)
}
}()
}
wg.Wait()

if got := count.Load(); got != 1 {
t.Fatal("OnceOrRetry.Do() called more than once")
}
}

func TestOnceOrRetry_Do_Fail(t *testing.T) {
var once OnceOrRetry
var wg sync.WaitGroup

// test failure
for i := 0; i < 100; i++ {
wg.Add(1)
go func(wantErr error) {
defer wg.Done()
err := once.Do(func() error {
return wantErr
})
if err != wantErr {
t.Errorf("OnceOrRetry.Do() error = %v, wantErr %v", err, wantErr)
}
}(errors.New(strconv.Itoa(i)))
}
wg.Wait()

// retry after failure
err := once.Do(func() error {
return nil
})
if err != nil {
t.Fatalf("OnceOrRetry.Do() error = %v, wantErr %v", err, nil)
}

// no retry after success
err = once.Do(func() error {
t.Fatal("OnceOrRetry.Do() called twice")
return nil
})
if err != nil {
t.Fatalf("OnceOrRetry.Do() error = %v, wantErr %v", err, nil)
}
}

func TestOnceOrRetry_Do_Panic(t *testing.T) {
var once OnceOrRetry

// test panic
func() {
defer func() {
if r := recover(); r == nil {
t.Fatal("OnceOrRetry.Do() did not panic")
}
}()
_ = once.Do(func() error {
panic("failed")
})
}()

// retry after panic
err := once.Do(func() error {
return nil
})
if err != nil {
t.Fatalf("OnceOrRetry.Do() error = %v, wantErr %v", err, nil)
}

// no retry after success
err = once.Do(func() error {
t.Fatal("OnceOrRetry.Do() called twice")
return nil
})
if err != nil {
t.Fatalf("OnceOrRetry.Do() error = %v, wantErr %v", err, nil)
}
}
12 changes: 6 additions & 6 deletions registry/remote/credentials/store.go
Expand Up @@ -25,8 +25,8 @@
"fmt"
"os"
"path/filepath"
"sync"

"oras.land/oras-go/v2/internal/syncutil"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/credentials/internal/config"
)
Expand All @@ -53,7 +53,7 @@
config *config.Config
options StoreOptions
detectedCredsStore string
setCredsStoreOnce sync.Once
setCredsStoreOnce syncutil.OnceOrRetry
}

// StoreOptions provides options for NewStore.
Expand Down Expand Up @@ -136,19 +136,19 @@
// Put saves credentials into the store for the given server address.
// Put returns ErrPlaintextPutDisabled if native store is not available and
// [StoreOptions].AllowPlaintextPut is set to false.
func (ds *DynamicStore) Put(ctx context.Context, serverAddress string, cred auth.Credential) (returnErr error) {
func (ds *DynamicStore) Put(ctx context.Context, serverAddress string, cred auth.Credential) error {
if err := ds.getStore(serverAddress).Put(ctx, serverAddress, cred); err != nil {
return err
}
// save the detected creds store back to the config file on first put
ds.setCredsStoreOnce.Do(func() {
return ds.setCredsStoreOnce.Do(func() error {
if ds.detectedCredsStore != "" {
if err := ds.config.SetCredentialsStore(ds.detectedCredsStore); err != nil {
returnErr = fmt.Errorf("failed to set credsStore: %w", err)
return fmt.Errorf("failed to set credsStore: %w", err)

Check warning on line 147 in registry/remote/credentials/store.go

View check run for this annotation

Codecov / codecov/patch

registry/remote/credentials/store.go#L147

Added line #L147 was not covered by tests
}
}
return nil
})
return returnErr
}

// Delete removes credentials from the store for the given server address.
Expand Down