From 9a831df9e5f6ed34cd7ad764b15eb255367c77fa Mon Sep 17 00:00:00 2001 From: Guray Gurkan Date: Mon, 25 Nov 2024 00:58:53 +0300 Subject: [PATCH 1/7] retry mechanism added --- go.mod | 8 ++- go.sum | 11 ++- internal/retry/mock/mock.go | 25 +++++++ internal/retry/retry.go | 75 ++++++++++++++++++++ internal/retry/retry_test.go | 133 +++++++++++++++++++++++++++++++++++ 5 files changed, 248 insertions(+), 4 deletions(-) create mode 100644 internal/retry/mock/mock.go create mode 100644 internal/retry/retry.go create mode 100644 internal/retry/retry_test.go diff --git a/go.mod b/go.mod index a3135eec..ce9b8a4e 100644 --- a/go.mod +++ b/go.mod @@ -3,20 +3,23 @@ module github.com/spiffe/spike go 1.23.2 require ( + github.com/cenkalti/backoff/v4 v4.3.0 github.com/go-jose/go-jose/v4 v4.0.4 - github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/goexpect v0.0.0-20210430020637-ab937bf7fd6f github.com/mattn/go-sqlite3 v1.14.24 github.com/spf13/cobra v1.8.1 github.com/spiffe/go-spiffe/v2 v2.4.0 + github.com/stretchr/testify v1.9.0 golang.org/x/crypto v0.26.0 - golang.org/x/term v0.23.0 ) require ( github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/zeebo/errs v1.3.0 // indirect golang.org/x/net v0.28.0 // indirect @@ -25,4 +28,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/grpc v1.67.1 // indirect google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6b7eb683..746dabc9 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,13 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -13,8 +16,6 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= @@ -32,6 +33,10 @@ github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gm github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -98,6 +103,8 @@ google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFN google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/retry/mock/mock.go b/internal/retry/mock/mock.go new file mode 100644 index 00000000..55241683 --- /dev/null +++ b/internal/retry/mock/mock.go @@ -0,0 +1,25 @@ +// \\ SPIKE: Secure your secrets with SPIFFE. +// \\\\\ Copyright 2024-present SPIKE contributors. +// \\\\\\\ SPDX-License-Identifier: Apache-2.0 + +package mock + +import ( + "context" +) + +// MockRetrier implements Retrier for testing +type MockRetrier struct { + RetryFunc func(context.Context, func() error) error +} + +// RetryWithBackoff implements the Retrier interface +func (m *MockRetrier) RetryWithBackoff( + ctx context.Context, + operation func() error, +) error { + if m.RetryFunc != nil { + return m.RetryFunc(ctx, operation) + } + return nil +} diff --git a/internal/retry/retry.go b/internal/retry/retry.go new file mode 100644 index 00000000..946ffd62 --- /dev/null +++ b/internal/retry/retry.go @@ -0,0 +1,75 @@ +// \\ SPIKE: Secure your secrets with SPIFFE. +// \\\\\ Copyright 2024-present SPIKE contributors. +// \\\\\\\ SPDX-License-Identifier: Apache-2.0 + +package retry + +import ( + "context" + "log" + "time" + + "github.com/cenkalti/backoff/v4" +) + +// Retrier handles retry operations with backoff +type Retrier interface { + // RetryWithBackoff executes an operation with backoff + RetryWithBackoff(ctx context.Context, op func() error) error +} + +// TypedRetrier provides type-safe retry operations +type TypedRetrier[T any] struct { + retrier Retrier +} + +// NewTypedRetrier creates a new TypedRetrier with the given base Retrier +func NewTypedRetrier[T any](r Retrier) *TypedRetrier[T] { + return &TypedRetrier[T]{retrier: r} +} + +// RetryWithBackoff executes a typed operation with backoff +func (r *TypedRetrier[T]) RetryWithBackoff( + ctx context.Context, + op func() (T, error), +) (T, error) { + var result T + err := r.retrier.RetryWithBackoff(ctx, func() error { + var err error + result, err = op() + return err + }) + return result, err +} + +// ExponentialRetrier implements Retrier using exponential backoff +type ExponentialRetrier struct { + newBackOff func() backoff.BackOff +} + +// NewExponentialRetrier creates a new ExponentialRetrier with default settings +func NewExponentialRetrier() *ExponentialRetrier { + return &ExponentialRetrier{ + newBackOff: func() backoff.BackOff { + return backoff.NewExponentialBackOff() + }, + } +} + +// RetryWithBackoff implements the Retrier interface +func (r *ExponentialRetrier) RetryWithBackoff( + ctx context.Context, + operation func() error, +) error { + b := r.newBackOff() + totalDuration := time.Duration(0) + return backoff.RetryNotify( + operation, + backoff.WithContext(b, ctx), + func(err error, duration time.Duration) { + totalDuration += duration + // log the error, duration and total duration + log.Printf("Retrying operation after error: %v, duration: %v, total duration: %v", err, duration, totalDuration) + }, + ) +} diff --git a/internal/retry/retry_test.go b/internal/retry/retry_test.go new file mode 100644 index 00000000..66fdbfea --- /dev/null +++ b/internal/retry/retry_test.go @@ -0,0 +1,133 @@ +// \\ SPIKE: Secure your secrets with SPIFFE. +// \\\\\ Copyright 2024-present SPIKE contributors. +// \\\\\\\ SPDX-License-Identifier: Apache-2.0 + +package retry + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/spiffe/spike/internal/retry/mock" + "github.com/stretchr/testify/require" +) + +var errTest = errors.New("test error") + +func TestTypedRetrier(t *testing.T) { + t.Run("successful operation", func(t *testing.T) { + mockRetrier := &mock.MockRetrier{ + RetryFunc: func(_ context.Context, op func() error) error { + return op() + }, + } + + typedRetrier := NewTypedRetrier[string](mockRetrier) + result, err := typedRetrier.RetryWithBackoff( + context.Background(), + func() (string, error) { + return "success", nil + }, + ) + + require.NoError(t, err) + require.Equal(t, "success", result) + }) + + t.Run("failed operation", func(t *testing.T) { + mockRetrier := &mock.MockRetrier{ + RetryFunc: func(_ context.Context, op func() error) error { + return errTest + }, + } + + typedRetrier := NewTypedRetrier[string](mockRetrier) + result, err := typedRetrier.RetryWithBackoff( + context.Background(), + func() (string, error) { + return "", errTest + }, + ) + + require.Equal(t, "", result) + require.Equal(t, errTest, err) + }) +} + +func TestExponentialRetrier(t *testing.T) { + t.Run("succeeds immediately", func(t *testing.T) { + retrier := NewExponentialRetrier() + err := retrier.RetryWithBackoff( + context.Background(), + func() error { + return nil + }, + ) + + require.NoError(t, err) + }) + + t.Run("succeeds after retries", func(t *testing.T) { + retrier := NewExponentialRetrier() + attempts := 0 + + err := retrier.RetryWithBackoff( + context.Background(), + func() error { + attempts++ + if attempts < 3 { + return errTest + } + return nil + }, + ) + + require.NoError(t, err) + require.Equal(t, 3, attempts) + }) + + t.Run("respects context cancellation", func(t *testing.T) { + retrier := NewExponentialRetrier() + ctx, cancel := context.WithCancel(context.Background()) + attempts := 0 + + // Cancel after first attempt + go func() { + time.Sleep(10 * time.Millisecond) + cancel() + }() + + err := retrier.RetryWithBackoff( + ctx, + func() error { + attempts++ + return errTest + }, + ) + + require.ErrorIs(t, context.Canceled, err) + }) + +} + +// Example usage in documentation +func ExampleTypedRetrier() { + // Create a base retrier + baseRetrier := NewExponentialRetrier() + + // Create a typed retrier for string operations + stringRetrier := NewTypedRetrier[string](baseRetrier) + + // Use the typed retrier + result, err := stringRetrier.RetryWithBackoff( + context.Background(), + func() (string, error) { + return "success", nil + }, + ) + + _ = result // Use the result + _ = err // Handle error +} From e15b6bd643ec65a9271d8ca2e4925bef1054f4e0 Mon Sep 17 00:00:00 2001 From: Guray Gurkan Date: Mon, 25 Nov 2024 00:59:26 +0300 Subject: [PATCH 2/7] retry mechanism added --- app/nexus/internal/env/backend.go | 1 - .../internal/state/persist/credentials.go | 28 ++++++--- app/nexus/internal/state/persist/secret.go | 11 +++- app/nexus/internal/state/persist/token.go | 20 +++++-- app/spike/internal/cmd/init.go | 18 ++++-- internal/retry/retry.go | 57 +++++++++++++++++-- 6 files changed, 110 insertions(+), 25 deletions(-) diff --git a/app/nexus/internal/env/backend.go b/app/nexus/internal/env/backend.go index c012d2e1..87115349 100644 --- a/app/nexus/internal/env/backend.go +++ b/app/nexus/internal/env/backend.go @@ -40,7 +40,6 @@ func BackendStoreType() StoreType { switch strings.ToLower(st) { case string(S3): panic("SPIKE_NEXUS_BACKEND_STORE=s3 is not implemented yet") - return S3 case string(Sqlite): return Sqlite case string(Memory): diff --git a/app/nexus/internal/state/persist/credentials.go b/app/nexus/internal/state/persist/credentials.go index 6002e9a3..740ad922 100644 --- a/app/nexus/internal/state/persist/credentials.go +++ b/app/nexus/internal/state/persist/credentials.go @@ -10,6 +10,7 @@ import ( "github.com/spiffe/spike/app/nexus/internal/env" "github.com/spiffe/spike/app/nexus/internal/state/entity/data" "github.com/spiffe/spike/internal/log" + "github.com/spiffe/spike/internal/retry" ) // ReadAdminRecoveryMetadata retrieves cached admin recovery metadata from @@ -27,22 +28,27 @@ func ReadAdminRecoveryMetadata() *data.RecoveryMetadata { return nil } + retrier := retry.NewExponentialRetrier() + typedRetrier := retry.NewTypedRetrier[data.RecoveryMetadata](retrier) + ctx, cancel := context.WithTimeout( context.Background(), env.DatabaseOperationTimeout(), ) defer cancel() - cachedMetadata, err := be.LoadAdminRecoveryMetadata(ctx) + metadata, err := typedRetrier.RetryWithBackoff(ctx, func() (data.RecoveryMetadata, error) { + return be.LoadAdminRecoveryMetadata(ctx) + }) + if err != nil { - // Log error but continue - memory is source of truth log.Log().Warn("readAdminRecoveryMetadata", - "msg", "Failed to load admin recovery metadata from cache", + "msg", "Failed to load admin recovery metadata after retries", "err", err.Error(), ) return nil } - return &cachedMetadata + return &metadata } // AsyncPersistAdminRecoveryMetadata asynchronously stores admin recovery @@ -61,16 +67,20 @@ func AsyncPersistAdminRecoveryMetadata(credentials data.RecoveryMetadata) { return // No cache available } + retrier := retry.NewExponentialRetrier() + ctx, cancel := context.WithTimeout( - context.Background(), - env.DatabaseOperationTimeout(), + context.Background(), env.DatabaseOperationTimeout(), ) defer cancel() - if err := be.StoreAdminRecoveryMetadata(ctx, credentials); err != nil { - // Log error but continue - memory is source of truth + err := retrier.RetryWithBackoff(ctx, func() error { + return be.StoreAdminRecoveryMetadata(ctx, credentials) + }) + + if err != nil { log.Log().Warn("asyncPersistAdminRecoveryMetadata", - "msg", "Failed to cache admin recovery metadata", + "msg", "Failed to cache admin recovery metadata after retries", "err", err.Error(), ) } diff --git a/app/nexus/internal/state/persist/secret.go b/app/nexus/internal/state/persist/secret.go index d1663c0e..1293c502 100644 --- a/app/nexus/internal/state/persist/secret.go +++ b/app/nexus/internal/state/persist/secret.go @@ -9,6 +9,7 @@ import ( "github.com/spiffe/spike/app/nexus/internal/env" "github.com/spiffe/spike/internal/log" + "github.com/spiffe/spike/internal/retry" "github.com/spiffe/spike/pkg/store" ) @@ -35,15 +36,21 @@ func ReadSecret(path string, version int) *store.Secret { return nil } + retrier := retry.NewExponentialRetrier() + typedRetrier := retry.NewTypedRetrier[*store.Secret](retrier) + ctx, cancel := context.WithTimeout( context.Background(), env.DatabaseOperationTimeout(), ) defer cancel() - cachedSecret, err := be.LoadSecret(ctx, path) + cachedSecret, err := typedRetrier.RetryWithBackoff(ctx, func() (*store.Secret, error) { + return be.LoadSecret(ctx, path) + }) + if err != nil { log.Log().Warn("readSecret", - "msg", "Failed to load secret from cache", + "msg", "Failed to load secret from cache after retries", "path", path, "err", err.Error(), ) diff --git a/app/nexus/internal/state/persist/token.go b/app/nexus/internal/state/persist/token.go index a3afed04..3eeeec7e 100644 --- a/app/nexus/internal/state/persist/token.go +++ b/app/nexus/internal/state/persist/token.go @@ -12,6 +12,7 @@ import ( "github.com/spiffe/spike/app/nexus/internal/state/backend/memory" "github.com/spiffe/spike/app/nexus/internal/state/backend/sqlite" "github.com/spiffe/spike/internal/log" + "github.com/spiffe/spike/internal/retry" ) var ( @@ -38,12 +39,18 @@ func ReadAdminSigningToken() string { return "" } + retrier := retry.NewExponentialRetrier() + typedRetrier := retry.NewTypedRetrier[string](retrier) + ctx, cancel := context.WithTimeout( context.Background(), env.DatabaseOperationTimeout(), ) defer cancel() - cachedToken, err := be.LoadAdminSigningToken(ctx) + cachedToken, err := typedRetrier.RetryWithBackoff(ctx, func() (string, error) { + return be.LoadAdminSigningToken(ctx) + }) + if err != nil { // Log error but continue - memory is source of truth log.Log().Warn("readAdminSigningToken", @@ -74,16 +81,21 @@ func AsyncPersistAdminToken(token string) { return // No cache available } + retrier := retry.NewExponentialRetrier() + ctx, cancel := context.WithTimeout( context.Background(), env.DatabaseOperationTimeout(), ) defer cancel() - if err := be.StoreAdminToken(ctx, token); err != nil { - // Log error but continue - memory is source of truth + err := retrier.RetryWithBackoff(ctx, func() error { + return be.StoreAdminToken(ctx, token) + }) + + if err != nil { log.Log().Warn("asyncPersistAdminToken", - "msg", "Failed to cache admin token", + "msg", "Failed to cache admin token after retries", "err", err.Error(), ) } diff --git a/app/spike/internal/cmd/init.go b/app/spike/internal/cmd/init.go index 5d9081fd..05a28418 100644 --- a/app/spike/internal/cmd/init.go +++ b/app/spike/internal/cmd/init.go @@ -13,6 +13,7 @@ import ( "github.com/spiffe/spike/app/spike/internal/net/auth" "github.com/spiffe/spike/internal/config" "github.com/spiffe/spike/internal/entity/data" + "github.com/spiffe/spike/internal/retry" ) // NewInitCommand creates and returns a new cobra.Command for initializing the @@ -45,10 +46,16 @@ func NewInitCommand(source *workloadapi.X509Source) *cobra.Command { Use: "init", Short: "Initialize spike configuration", Run: func(cmd *cobra.Command, args []string) { - state, err := auth.CheckInitState(source) + retrier := retry.NewExponentialRetrier() + typedRetrier := retry.NewTypedRetrier[data.InitState](retrier) + + ctx := cmd.Context() + state, err := typedRetrier.RetryWithBackoff(ctx, func() (data.InitState, error) { + return auth.CheckInitState(source) + }) if err != nil { - fmt.Println("Failed to check init state:") + fmt.Println("Failed to check init state after retries:") fmt.Println(err.Error()) return } @@ -59,9 +66,12 @@ func NewInitCommand(source *workloadapi.X509Source) *cobra.Command { return } - err = auth.Init(source) + err = retrier.RetryWithBackoff(ctx, func() error { + return auth.Init(source) + }) + if err != nil { - fmt.Println("Failed to save admin token:") + fmt.Println("Failed to save admin token after retries:") fmt.Println(err.Error()) return } diff --git a/internal/retry/retry.go b/internal/retry/retry.go index 946ffd62..170f27a0 100644 --- a/internal/retry/retry.go +++ b/internal/retry/retry.go @@ -6,12 +6,17 @@ package retry import ( "context" - "log" "time" + "github.com/spiffe/spike/internal/log" + "github.com/cenkalti/backoff/v4" ) +const defaultInitialInterval = 500 * time.Millisecond +const defaultMaxInterval = 3 * time.Second +const defaultMaxElapsedTime = 30 * time.Second + // Retrier handles retry operations with backoff type Retrier interface { // RetryWithBackoff executes an operation with backoff @@ -47,15 +52,57 @@ type ExponentialRetrier struct { newBackOff func() backoff.BackOff } -// NewExponentialRetrier creates a new ExponentialRetrier with default settings -func NewExponentialRetrier() *ExponentialRetrier { +// ExponentialRetrierOption is a function type for configuring ExponentialRetrier +type ExponentialRetrierOption func(*backoff.ExponentialBackOff) + +// NewExponentialRetrier creates a new ExponentialRetrier with configurable settings +func NewExponentialRetrier(opts ...ExponentialRetrierOption) *ExponentialRetrier { + if len(opts) == 0 { + opts = []ExponentialRetrierOption{ + WithInitialInterval(defaultInitialInterval), + WithMaxInterval(defaultMaxInterval), + WithMaxElapsedTime(defaultMaxElapsedTime), + } + } return &ExponentialRetrier{ newBackOff: func() backoff.BackOff { - return backoff.NewExponentialBackOff() + b := backoff.NewExponentialBackOff() + for _, opt := range opts { + opt(b) + } + return b }, } } +// WithInitialInterval sets the initial interval between retries +func WithInitialInterval(d time.Duration) ExponentialRetrierOption { + return func(b *backoff.ExponentialBackOff) { + b.InitialInterval = d + } +} + +// WithMaxInterval sets the maximum interval between retries +func WithMaxInterval(d time.Duration) ExponentialRetrierOption { + return func(b *backoff.ExponentialBackOff) { + b.MaxInterval = d + } +} + +// WithMaxElapsedTime sets the maximum total time for retries +func WithMaxElapsedTime(d time.Duration) ExponentialRetrierOption { + return func(b *backoff.ExponentialBackOff) { + b.MaxElapsedTime = d + } +} + +// WithMultiplier sets the multiplier for increasing intervals +func WithMultiplier(m float64) ExponentialRetrierOption { + return func(b *backoff.ExponentialBackOff) { + b.Multiplier = m + } +} + // RetryWithBackoff implements the Retrier interface func (r *ExponentialRetrier) RetryWithBackoff( ctx context.Context, @@ -69,7 +116,7 @@ func (r *ExponentialRetrier) RetryWithBackoff( func(err error, duration time.Duration) { totalDuration += duration // log the error, duration and total duration - log.Printf("Retrying operation after error: %v, duration: %v, total duration: %v", err, duration, totalDuration) + log.Log().Debug("Retrying operation after error", "error", err.Error(), "duration", duration, "total duration", totalDuration.String()) }, ) } From dc8ac5275dc2f2db3f5a10dab56de98075458bb3 Mon Sep 17 00:00:00 2001 From: Guray Gurkan Date: Mon, 25 Nov 2024 12:02:00 +0300 Subject: [PATCH 3/7] cr fixes --- .../internal/state/persist/credentials.go | 2 +- app/nexus/internal/state/persist/secret.go | 2 +- app/nexus/internal/state/persist/token.go | 2 +- app/spike/internal/cmd/init.go | 2 +- internal/retry/retry_test.go | 133 ------------- {internal => pkg}/retry/mock/mock.go | 0 {internal => pkg}/retry/retry.go | 79 +++++--- pkg/retry/retry_test.go | 187 ++++++++++++++++++ 8 files changed, 244 insertions(+), 163 deletions(-) delete mode 100644 internal/retry/retry_test.go rename {internal => pkg}/retry/mock/mock.go (100%) rename {internal => pkg}/retry/retry.go (58%) create mode 100644 pkg/retry/retry_test.go diff --git a/app/nexus/internal/state/persist/credentials.go b/app/nexus/internal/state/persist/credentials.go index 740ad922..526c3c7e 100644 --- a/app/nexus/internal/state/persist/credentials.go +++ b/app/nexus/internal/state/persist/credentials.go @@ -10,7 +10,7 @@ import ( "github.com/spiffe/spike/app/nexus/internal/env" "github.com/spiffe/spike/app/nexus/internal/state/entity/data" "github.com/spiffe/spike/internal/log" - "github.com/spiffe/spike/internal/retry" + "github.com/spiffe/spike/pkg/retry" ) // ReadAdminRecoveryMetadata retrieves cached admin recovery metadata from diff --git a/app/nexus/internal/state/persist/secret.go b/app/nexus/internal/state/persist/secret.go index 1293c502..7ac2a58b 100644 --- a/app/nexus/internal/state/persist/secret.go +++ b/app/nexus/internal/state/persist/secret.go @@ -9,7 +9,7 @@ import ( "github.com/spiffe/spike/app/nexus/internal/env" "github.com/spiffe/spike/internal/log" - "github.com/spiffe/spike/internal/retry" + "github.com/spiffe/spike/pkg/retry" "github.com/spiffe/spike/pkg/store" ) diff --git a/app/nexus/internal/state/persist/token.go b/app/nexus/internal/state/persist/token.go index 3eeeec7e..dfc9b58d 100644 --- a/app/nexus/internal/state/persist/token.go +++ b/app/nexus/internal/state/persist/token.go @@ -12,7 +12,7 @@ import ( "github.com/spiffe/spike/app/nexus/internal/state/backend/memory" "github.com/spiffe/spike/app/nexus/internal/state/backend/sqlite" "github.com/spiffe/spike/internal/log" - "github.com/spiffe/spike/internal/retry" + "github.com/spiffe/spike/pkg/retry" ) var ( diff --git a/app/spike/internal/cmd/init.go b/app/spike/internal/cmd/init.go index 05a28418..2821fd80 100644 --- a/app/spike/internal/cmd/init.go +++ b/app/spike/internal/cmd/init.go @@ -13,7 +13,7 @@ import ( "github.com/spiffe/spike/app/spike/internal/net/auth" "github.com/spiffe/spike/internal/config" "github.com/spiffe/spike/internal/entity/data" - "github.com/spiffe/spike/internal/retry" + "github.com/spiffe/spike/pkg/retry" ) // NewInitCommand creates and returns a new cobra.Command for initializing the diff --git a/internal/retry/retry_test.go b/internal/retry/retry_test.go deleted file mode 100644 index 66fdbfea..00000000 --- a/internal/retry/retry_test.go +++ /dev/null @@ -1,133 +0,0 @@ -// \\ SPIKE: Secure your secrets with SPIFFE. -// \\\\\ Copyright 2024-present SPIKE contributors. -// \\\\\\\ SPDX-License-Identifier: Apache-2.0 - -package retry - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/spiffe/spike/internal/retry/mock" - "github.com/stretchr/testify/require" -) - -var errTest = errors.New("test error") - -func TestTypedRetrier(t *testing.T) { - t.Run("successful operation", func(t *testing.T) { - mockRetrier := &mock.MockRetrier{ - RetryFunc: func(_ context.Context, op func() error) error { - return op() - }, - } - - typedRetrier := NewTypedRetrier[string](mockRetrier) - result, err := typedRetrier.RetryWithBackoff( - context.Background(), - func() (string, error) { - return "success", nil - }, - ) - - require.NoError(t, err) - require.Equal(t, "success", result) - }) - - t.Run("failed operation", func(t *testing.T) { - mockRetrier := &mock.MockRetrier{ - RetryFunc: func(_ context.Context, op func() error) error { - return errTest - }, - } - - typedRetrier := NewTypedRetrier[string](mockRetrier) - result, err := typedRetrier.RetryWithBackoff( - context.Background(), - func() (string, error) { - return "", errTest - }, - ) - - require.Equal(t, "", result) - require.Equal(t, errTest, err) - }) -} - -func TestExponentialRetrier(t *testing.T) { - t.Run("succeeds immediately", func(t *testing.T) { - retrier := NewExponentialRetrier() - err := retrier.RetryWithBackoff( - context.Background(), - func() error { - return nil - }, - ) - - require.NoError(t, err) - }) - - t.Run("succeeds after retries", func(t *testing.T) { - retrier := NewExponentialRetrier() - attempts := 0 - - err := retrier.RetryWithBackoff( - context.Background(), - func() error { - attempts++ - if attempts < 3 { - return errTest - } - return nil - }, - ) - - require.NoError(t, err) - require.Equal(t, 3, attempts) - }) - - t.Run("respects context cancellation", func(t *testing.T) { - retrier := NewExponentialRetrier() - ctx, cancel := context.WithCancel(context.Background()) - attempts := 0 - - // Cancel after first attempt - go func() { - time.Sleep(10 * time.Millisecond) - cancel() - }() - - err := retrier.RetryWithBackoff( - ctx, - func() error { - attempts++ - return errTest - }, - ) - - require.ErrorIs(t, context.Canceled, err) - }) - -} - -// Example usage in documentation -func ExampleTypedRetrier() { - // Create a base retrier - baseRetrier := NewExponentialRetrier() - - // Create a typed retrier for string operations - stringRetrier := NewTypedRetrier[string](baseRetrier) - - // Use the typed retrier - result, err := stringRetrier.RetryWithBackoff( - context.Background(), - func() (string, error) { - return "success", nil - }, - ) - - _ = result // Use the result - _ = err // Handle error -} diff --git a/internal/retry/mock/mock.go b/pkg/retry/mock/mock.go similarity index 100% rename from internal/retry/mock/mock.go rename to pkg/retry/mock/mock.go diff --git a/internal/retry/retry.go b/pkg/retry/retry.go similarity index 58% rename from internal/retry/retry.go rename to pkg/retry/retry.go index 170f27a0..05877668 100644 --- a/internal/retry/retry.go +++ b/pkg/retry/retry.go @@ -8,14 +8,15 @@ import ( "context" "time" - "github.com/spiffe/spike/internal/log" - "github.com/cenkalti/backoff/v4" ) -const defaultInitialInterval = 500 * time.Millisecond -const defaultMaxInterval = 3 * time.Second -const defaultMaxElapsedTime = 30 * time.Second +const ( + defaultInitialInterval = 500 * time.Millisecond + defaultMaxInterval = 3 * time.Second + defaultMaxElapsedTime = 30 * time.Second + defaultMultiplier = 2.0 +) // Retrier handles retry operations with backoff type Retrier interface { @@ -47,62 +48,87 @@ func (r *TypedRetrier[T]) RetryWithBackoff( return result, err } +// NotifyFn is a callback function type for retry notifications +type NotifyFn func(err error, duration, totalDuration time.Duration) + // ExponentialRetrier implements Retrier using exponential backoff type ExponentialRetrier struct { newBackOff func() backoff.BackOff + notify NotifyFn } -// ExponentialRetrierOption is a function type for configuring ExponentialRetrier -type ExponentialRetrierOption func(*backoff.ExponentialBackOff) +// RetrierOption is a function type for configuring ExponentialRetrier +type RetrierOption func(*ExponentialRetrier) + +// BackOffOption is a function type for configuring ExponentialBackOff +type BackOffOption func(*backoff.ExponentialBackOff) // NewExponentialRetrier creates a new ExponentialRetrier with configurable settings -func NewExponentialRetrier(opts ...ExponentialRetrierOption) *ExponentialRetrier { - if len(opts) == 0 { - opts = []ExponentialRetrierOption{ - WithInitialInterval(defaultInitialInterval), - WithMaxInterval(defaultMaxInterval), - WithMaxElapsedTime(defaultMaxElapsedTime), - } - } - return &ExponentialRetrier{ +func NewExponentialRetrier(opts ...RetrierOption) *ExponentialRetrier { + b := backoff.NewExponentialBackOff() + b.InitialInterval = defaultInitialInterval + b.MaxInterval = defaultMaxInterval + b.MaxElapsedTime = defaultMaxElapsedTime + b.Multiplier = defaultMultiplier + + r := &ExponentialRetrier{ newBackOff: func() backoff.BackOff { - b := backoff.NewExponentialBackOff() - for _, opt := range opts { - opt(b) - } return b }, } + + for _, opt := range opts { + opt(r) + } + + return r +} + +// WithBackOffOptions configures the backoff settings +func WithBackOffOptions(opts ...BackOffOption) RetrierOption { + return func(r *ExponentialRetrier) { + b := r.newBackOff().(*backoff.ExponentialBackOff) + for _, opt := range opts { + opt(b) + } + } } // WithInitialInterval sets the initial interval between retries -func WithInitialInterval(d time.Duration) ExponentialRetrierOption { +func WithInitialInterval(d time.Duration) BackOffOption { return func(b *backoff.ExponentialBackOff) { b.InitialInterval = d } } // WithMaxInterval sets the maximum interval between retries -func WithMaxInterval(d time.Duration) ExponentialRetrierOption { +func WithMaxInterval(d time.Duration) BackOffOption { return func(b *backoff.ExponentialBackOff) { b.MaxInterval = d } } // WithMaxElapsedTime sets the maximum total time for retries -func WithMaxElapsedTime(d time.Duration) ExponentialRetrierOption { +func WithMaxElapsedTime(d time.Duration) BackOffOption { return func(b *backoff.ExponentialBackOff) { b.MaxElapsedTime = d } } // WithMultiplier sets the multiplier for increasing intervals -func WithMultiplier(m float64) ExponentialRetrierOption { +func WithMultiplier(m float64) BackOffOption { return func(b *backoff.ExponentialBackOff) { b.Multiplier = m } } +// WithNotify is an option to set the notification callback +func WithNotify(fn NotifyFn) RetrierOption { + return func(r *ExponentialRetrier) { + r.notify = fn + } +} + // RetryWithBackoff implements the Retrier interface func (r *ExponentialRetrier) RetryWithBackoff( ctx context.Context, @@ -115,8 +141,9 @@ func (r *ExponentialRetrier) RetryWithBackoff( backoff.WithContext(b, ctx), func(err error, duration time.Duration) { totalDuration += duration - // log the error, duration and total duration - log.Log().Debug("Retrying operation after error", "error", err.Error(), "duration", duration, "total duration", totalDuration.String()) + if r.notify != nil { + r.notify(err, duration, totalDuration) + } }, ) } diff --git a/pkg/retry/retry_test.go b/pkg/retry/retry_test.go new file mode 100644 index 00000000..a4b596f7 --- /dev/null +++ b/pkg/retry/retry_test.go @@ -0,0 +1,187 @@ +// \\ SPIKE: Secure your secrets with SPIFFE. +// \\\\\ Copyright 2024-present SPIKE contributors. +// \\\\\\\ SPDX-License-Identifier: Apache-2.0 + +package retry + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/stretchr/testify/assert" +) + +func TestExponentialRetrier_Success(t *testing.T) { + retrier := NewExponentialRetrier() + + // Operation that succeeds immediately + err := retrier.RetryWithBackoff(context.Background(), func() error { + return nil + }) + + assert.NoError(t, err) +} + +func TestExponentialRetrier_EventualSuccess(t *testing.T) { + attempts := 0 + maxAttempts := 3 + + retrier := NewExponentialRetrier( + WithBackOffOptions( + WithInitialInterval(1 * time.Millisecond), + WithMaxInterval(5 * time.Millisecond), + ), + ) + + err := retrier.RetryWithBackoff(context.Background(), func() error { + attempts++ + if attempts < maxAttempts { + return errors.New("temporary error") + } + return nil + }) + + assert.NoError(t, err) + assert.Equal(t, maxAttempts, attempts) +} + +func TestExponentialRetrier_Failure(t *testing.T) { + retrier := NewExponentialRetrier( + WithBackOffOptions( + WithMaxElapsedTime(10 * time.Millisecond), + WithInitialInterval(1 * time.Millisecond), + ), + ) + + expectedErr := errors.New("persistent error") + err := retrier.RetryWithBackoff(context.Background(), func() error { + return expectedErr + }) + + assert.Error(t, err) + assert.Equal(t, expectedErr, err) +} + +func TestExponentialRetrier_ContextCancellation(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + retrier := NewExponentialRetrier( + WithBackOffOptions( + WithInitialInterval(100 * time.Millisecond), + ), + ) + + go func() { + time.Sleep(50 * time.Millisecond) + cancel() + }() + + err := retrier.RetryWithBackoff(ctx, func() error { + return errors.New("keep retrying") + }) + + assert.Error(t, err) + assert.Equal(t, context.Canceled, err) +} + +func TestExponentialRetrier_Notification(t *testing.T) { + var notifications []time.Duration + totalDurations := make([]time.Duration, 0) + + retrier := NewExponentialRetrier( + WithNotify(func(_ error, duration, totalDuration time.Duration) { + notifications = append(notifications, duration) + totalDurations = append(totalDurations, totalDuration) + }), + WithBackOffOptions( + WithInitialInterval(1 * time.Millisecond), + WithMaxInterval(5 * time.Millisecond), + WithMaxElapsedTime(20 * time.Millisecond), + ), + ) + + attempts := 0 + _ = retrier.RetryWithBackoff(context.Background(), func() error { + attempts++ + if attempts < 3 { + return errors.New("temporary error") + } + return nil + }) + + assert.Equal(t, 2, len(notifications)) + assert.Equal(t, 2, len(totalDurations)) + + // Verify that durations are increasing + assert.Less(t, notifications[0], notifications[1]) + assert.Less(t, totalDurations[0], totalDurations[1]) +} + +func TestTypedRetrier_Success(t *testing.T) { + baseRetrier := NewExponentialRetrier() + typedRetrier := NewTypedRetrier[string](baseRetrier) + + expected := "success" + result, err := typedRetrier.RetryWithBackoff(context.Background(), func() (string, error) { + return expected, nil + }) + + assert.NoError(t, err) + assert.Equal(t, expected, result) +} + +func TestTypedRetrier_Failure(t *testing.T) { + baseRetrier := NewExponentialRetrier( + WithBackOffOptions( + WithMaxElapsedTime(10 * time.Millisecond), + WithInitialInterval(1 * time.Millisecond), + ), + ) + typedRetrier := NewTypedRetrier[int](baseRetrier) + + expectedErr := errors.New("typed error") + result, err := typedRetrier.RetryWithBackoff(context.Background(), func() (int, error) { + return 0, expectedErr + }) + + assert.Error(t, err) + assert.Equal(t, expectedErr, err) + assert.Equal(t, 0, result) +} + +func TestBackOffOptions(t *testing.T) { + initialInterval := 100 * time.Millisecond + maxInterval := 1 * time.Second + maxElapsedTime := 5 * time.Second + multiplier := 2.5 + + retrier := NewExponentialRetrier( + WithBackOffOptions( + WithInitialInterval(initialInterval), + WithMaxInterval(maxInterval), + WithMaxElapsedTime(maxElapsedTime), + WithMultiplier(multiplier), + ), + ) + + // Access the backoff configuration + b := retrier.newBackOff().(*backoff.ExponentialBackOff) + + assert.Equal(t, initialInterval, b.InitialInterval) + assert.Equal(t, maxInterval, b.MaxInterval) + assert.Equal(t, maxElapsedTime, b.MaxElapsedTime) + assert.Equal(t, multiplier, b.Multiplier) +} + +func TestDefaultSettings(t *testing.T) { + retrier := NewExponentialRetrier() + b := retrier.newBackOff().(*backoff.ExponentialBackOff) + + assert.Equal(t, defaultInitialInterval, b.InitialInterval) + assert.Equal(t, defaultMaxInterval, b.MaxInterval) + assert.Equal(t, defaultMaxElapsedTime, b.MaxElapsedTime) + assert.Equal(t, defaultMultiplier, b.Multiplier) +} \ No newline at end of file From 09ad6e2a2d77e5ba86ffc61f5954a2c6e4ffbae3 Mon Sep 17 00:00:00 2001 From: Guray Gurkan Date: Mon, 25 Nov 2024 13:08:38 +0300 Subject: [PATCH 4/7] refactor func loc --- pkg/retry/retry.go | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/pkg/retry/retry.go b/pkg/retry/retry.go index 05877668..cbd91c9e 100644 --- a/pkg/retry/retry.go +++ b/pkg/retry/retry.go @@ -84,6 +84,25 @@ func NewExponentialRetrier(opts ...RetrierOption) *ExponentialRetrier { return r } +// RetryWithBackoff implements the Retrier interface +func (r *ExponentialRetrier) RetryWithBackoff( + ctx context.Context, + operation func() error, +) error { + b := r.newBackOff() + totalDuration := time.Duration(0) + return backoff.RetryNotify( + operation, + backoff.WithContext(b, ctx), + func(err error, duration time.Duration) { + totalDuration += duration + if r.notify != nil { + r.notify(err, duration, totalDuration) + } + }, + ) +} + // WithBackOffOptions configures the backoff settings func WithBackOffOptions(opts ...BackOffOption) RetrierOption { return func(r *ExponentialRetrier) { @@ -128,22 +147,3 @@ func WithNotify(fn NotifyFn) RetrierOption { r.notify = fn } } - -// RetryWithBackoff implements the Retrier interface -func (r *ExponentialRetrier) RetryWithBackoff( - ctx context.Context, - operation func() error, -) error { - b := r.newBackOff() - totalDuration := time.Duration(0) - return backoff.RetryNotify( - operation, - backoff.WithContext(b, ctx), - func(err error, duration time.Duration) { - totalDuration += duration - if r.notify != nil { - r.notify(err, duration, totalDuration) - } - }, - ) -} From 8badf38fc032ff2c16bb74cec9c221693d096dd1 Mon Sep 17 00:00:00 2001 From: Guray Gurkan Date: Wed, 27 Nov 2024 02:42:17 +0300 Subject: [PATCH 5/7] Remove .DS_Store files and update .gitignore --- .DS_Store | Bin 8196 -> 0 bytes app/.DS_Store | Bin 6148 -> 0 bytes docs/.DS_Store | Bin 8196 -> 0 bytes internal/.DS_Store | Bin 6148 -> 0 bytes pkg/.DS_Store | Bin 6148 -> 0 bytes 5 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store delete mode 100644 app/.DS_Store delete mode 100644 docs/.DS_Store delete mode 100644 internal/.DS_Store delete mode 100644 pkg/.DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 8324a9494cbec2831aa65fcfa9559d4c9d8f95ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHM&ubGw6n>K*O|VdUlH9rny@*63dKA_YL2reEXVXN}+N8VI3LY2zA3R&|kiW#g zz&}BR-lT{2Ac)_aH{G4t-RMax_y%UaFyDLoz5TM;+1U`0*?c`25cPV!I=4yXg_fI6TK{2LBn&F0o!vhEA3qdK4t{Fe^! z{UJhUj2&iz_SS*HZUKNTOxuRf*az60$YJa-6SS!~rtBV!OEqqbVI&=V#Noi$VJ0Z) zWF(!8Q#NjgVyrsyA{$O77SvH4PzS0G@ZP;meVWjSj!OIY#%+%G*m*J>?#z-M%y!p0 zKTglye0orhnEWOp-SyrOG+6+5fIdxWkB)-32Uow}^*LYr&bxcp&qX`WohOBI9UtiS zwph1`b51D@R2z>^3dYx5ELz8>qUrK{#VQ-aEQ zTbT=V@~w_C>ty0YqY;r9W4 zMbGFEIHFmo|MougD=~f@8&C2&YT^nXeic_!R})wHe)&q6yhJsQ@oV_F$ZN>iLb=KY zdh`tkT}S6n~NQ0f?&|}LBQIegF5h69ry{+ C&JP3t diff --git a/app/.DS_Store b/app/.DS_Store deleted file mode 100644 index 44218a12898477ca123b4e1d01f37d00498408a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKJ5Iwu5S_&ivY<&B3Z(7Qk{g)F)D*b@3XT8~V~Z$KO4Cvy!38)21r0S7r{M&= z+1)t1i9?AH%t*7(nU81uBJX&KNDb%tfT&AE36wG3!_p$`XWft;XW?YU_n2M`M(1Tw zZA6Me7!U@2BLn8er<^8KP#M0z(JLB`m%gIRvhlPW!;iB8$o@#@0k!3|6ldL*c*fFvS35=S9HAW^vH1{5H12uey2C!nQ( zngWS}3jTdy@5T?P5Q5prJFDG)>@WWkTVsew)P~JPqAC%UamYAZL6_3xx;zqX%*=R* z0{OJRy|~&4I=zIFC)fdYfE{25*a3Fnw>W@jwru5$=e}O$o*iHZ{-p!*{@~$|(Kj*F zQQbPwsS*IOh-uX@o|OYc&qUwEP)Dqw37v|lQ-vupgigo2XLP=ap^iG8geg9Rxw0@7 zicnX_`8_2k;p@mfJHQSk9q=$!q)lp4pAOvm{btu*>jd@s)_$;sx$fHub^FwyHto=WS{Myzh?$nlvHX;$wYjd(BCgDHcY>JnJ9?K}V$+1h zeK<;6c}ps6#;c2napXKUS+>gI(KpKycn*uA&(Hq-lE#FVOx$?}yBpMIE3fvyLa`tWN2z5tZ2 zUsSvRVXDq%JiU(?Gqf?~S6Y`-J-uc?iC=-*(Jn|cX$!BQ%~9LT^bj%UX2j&*%Shob zgIRjqpgp+UrET}GU_!ri9MRaUYtIpD;#~HxBv0pZEx;&l>;OApIZ)8!s;vK)v)}() z89ZbM*nz+1fGE~i>ot5UQ(Gs#pRBb#92+=fle$nxm4Z%{<>U&Xe+f%hZeyWK7t*7h7V$5LcBX$ ziQUkS2%$U4e#xEB@BF0Lb&1Hd7yTAdlZYBPoOR)Ae4WkvI^QpZDBLBw0Hi}>81*9CVGEmvQShJ3Q_8jYd9ZK&lhE$4`$^6{`73I_wcoNJ#NN~evO%5 zhUbLE65w$I4p%h6ZycW^hi~=ykjLkJz#~|rD1Ugg#l$>V4%z_^C;?4sEKVo!Y{=pL zam69X4;ieFi1&Zy{aJ^la2Qh&>yUiZ=R*!JPb&`58X=3en3#v?z+BWeR{_*)w)DWE z_Nss?pbD%M;Pb(vF=ieshjMhFlPdr)fm;k?sU1Me4q)c7atII1xKyA^HU5fWTsr*L z$7LQXhc2Cr?2L8%&c@$RjIUhAN;sL!q4uhPDiABM?H)T^|4-WQ|8bH&sRF9NMk!#D zyr1{*O1`!>UXE+6MY}*_ From 984e793b1f437b7265cd210523ab7c3bfd56d92f Mon Sep 17 00:00:00 2001 From: Guray Gurkan Date: Wed, 27 Nov 2024 02:52:31 +0300 Subject: [PATCH 6/7] updated .gitignore --- .gitignore | 2 ++ private.key | 0 2 files changed, 2 insertions(+) create mode 100644 private.key diff --git a/.gitignore b/.gitignore index 38e2f1b0..4e7acb74 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,8 @@ go.work.sum .idea .code .history +*.DS_Store +*.iml # App-specific: .spike-token diff --git a/private.key b/private.key new file mode 100644 index 00000000..e69de29b From 979cbb8edf61437a4377682dc85f5fe647457584 Mon Sep 17 00:00:00 2001 From: Guray Gurkan Date: Wed, 27 Nov 2024 02:53:32 +0300 Subject: [PATCH 7/7] remove custom file --- private.key | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 private.key diff --git a/private.key b/private.key deleted file mode 100644 index e69de29b..00000000