From 3e7c23efa6a7e42ddcdce460da22208234ab94bd Mon Sep 17 00:00:00 2001 From: Semyon Krestyaninov <krestyaninov@perx.ru> Date: Fri, 16 Aug 2024 19:48:05 +0000 Subject: [PATCH] =?UTF-8?q?feat(core):=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D0=B6=D0=B8=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8=20=D0=BE=D0=BA=D1=80=D1=83=D0=B6=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close #PRXS-2648 --- pkg/environments/environment.go | 12 ++++ pkg/environments/service.go | 53 +++++++++++++++ pkg/environments/service_test.go | 111 +++++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 pkg/environments/service_test.go diff --git a/pkg/environments/environment.go b/pkg/environments/environment.go index 465a4091..3e0f7bb6 100644 --- a/pkg/environments/environment.go +++ b/pkg/environments/environment.go @@ -6,6 +6,18 @@ const ( DefaultEnvironment = "master" ) +var ( + ReadAllowedStates = []State{ + StateNew, + StateReady, + } + + WriteAllowedStates = []State{ + StateNew, + StateReady, + } +) + type State int const ( diff --git a/pkg/environments/service.go b/pkg/environments/service.go index cd0a37f4..920fc043 100644 --- a/pkg/environments/service.go +++ b/pkg/environments/service.go @@ -2,6 +2,11 @@ package environments import ( "context" + "slices" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/errors" + "github.com/avast/retry-go/v4" ) // Environments @@ -18,3 +23,51 @@ type Environments interface { RemoveAlias(ctx context.Context, spaceId, envId, alias string) (err error) Migrate(ctx context.Context, spaceId, envId string, options ...*MigrateOptions) (err error) } + +const ( + RetryDelay = 1 * time.Second + RetryAttempts uint = 1200 +) + +// IsAvailable проверяет доступность окружения. +// Если окружение недоступно, возвращает ошибку ErrEnvironmentUnavailable +func IsAvailable(ctx context.Context, svc Environments, spaceID string, environmentID string) error { + env, err := svc.Get(ctx, spaceID, environmentID) + if err != nil { + return err + } + if env.StateInfo == nil || slices.Contains(WriteAllowedStates, env.StateInfo.State) { + return nil + } + return errors.WithContext(ErrEnvironmentUnavailable, "state", env.StateInfo.State) +} + +// WaitForAvailable периодически проверяет состояние окружения, РїРѕРєР° РѕРЅРѕ РЅРµ станет доступным. +// Если окружение становится доступным, функция возвращает nil. +// Если достигнуто максимальное количество попыток, функция возвращает ошибку. +// +// Через необязательные параметры РјРѕР¶РЅРѕ, например, настроить задержку между попытками (РїРѕ умолчанию равную RetryDelay) Рё максимальное количество попыток (РїРѕ умолчанию равное RetryAttempts), +// РЅРѕ опции retry.Context Рё retry.RetryIf Р±СѓРґСѓС‚ проигнорированы. +func WaitForAvailable(ctx context.Context, svc Environments, spaceID string, environmentID string, opts ...retry.Option) error { + opts = slices.Concat( + []retry.Option{ + retry.DelayType(retry.FixedDelay), + retry.Delay(RetryDelay), + retry.Attempts(RetryAttempts), + }, + opts, + // Чтобы предотвратить изменение параметров пользователем, размещаем РёС… РІ конце СЃРїРёСЃРєР°. + []retry.Option{ + retry.Context(ctx), + retry.RetryIf(func(err error) bool { + return errors.Is(err, ErrEnvironmentUnavailable) + }), + }, + ) + return retry.Do( + func() error { + return IsAvailable(ctx, svc, spaceID, environmentID) + }, + opts..., + ) +} diff --git a/pkg/environments/service_test.go b/pkg/environments/service_test.go new file mode 100644 index 00000000..9a3e6b06 --- /dev/null +++ b/pkg/environments/service_test.go @@ -0,0 +1,111 @@ +package environments + +import ( + "context" + "testing" + "time" + + "github.com/avast/retry-go/v4" + "github.com/stretchr/testify/assert" +) + +type dummyEnvironments struct { + Environments + environment *Environment + sleep time.Duration +} + +func (t *dummyEnvironments) Get(ctx context.Context, _ string, _ string) (*Environment, error) { + if t.sleep != 0 { + time.Sleep(t.sleep) + if err := ctx.Err(); err != nil { + return nil, err + } + } + return t.environment, nil +} + +func TestIsAvailable(t *testing.T) { + tests := []struct { + name string + environment *Environment + wantErr bool + }{ + { + "Environment has nil StateInfo: available", + &Environment{ID: "env-id", SpaceID: "space-id"}, + false, + }, + { + "Environment state is StateReady: available", + &Environment{ID: "env-id", SpaceID: "space-id", StateInfo: &StateInfo{State: StateReady}}, + false, + }, + { + "Environment state is StatePreparing: not available", + &Environment{ID: "env-id", SpaceID: "space-id", StateInfo: &StateInfo{State: StatePreparing}}, + true, + }, + { + "Environment state is StateError: not available", + &Environment{ID: "env-id", SpaceID: "space-id", StateInfo: &StateInfo{State: StateError}}, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + environments := &dummyEnvironments{environment: tt.environment} + if err := IsAvailable(context.Background(), environments, "space-id", "master"); (err != nil) != tt.wantErr { + t.Errorf("IsAvailable() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestWaitForAvailable(t *testing.T) { + t.Run("Success", func(t *testing.T) { + env := &Environment{ID: "env-id", SpaceID: "space-id", StateInfo: &StateInfo{State: StatePreparing}} + environments := &dummyEnvironments{environment: env} + + // Рмитируем завершение подготовки окружения через 1 секунду + go func() { + time.Sleep(1 * time.Second) + env.StateInfo = &StateInfo{State: StateReady} + }() + + err := WaitForAvailable(context.Background(), environments, "space-id", "master") + assert.NoError(t, err) + }) + t.Run("Overwritten retry options", func(t *testing.T) { + env := &Environment{ID: "env-id", SpaceID: "space-id", StateInfo: &StateInfo{State: StatePreparing}} + environments := &dummyEnvironments{environment: env} + + err := WaitForAvailable(context.Background(), environments, "space-id", "master", retry.Attempts(1)) + assert.Error(t, err, "Р—Р° РѕРґРЅСѓ попытку окружение РЅРµ стало доступным, получаем ошибку") + }) + t.Run("External context was canceled", func(t *testing.T) { + env := &Environment{ID: "env-id", SpaceID: "space-id", StateInfo: &StateInfo{State: StatePreparing}} + environments := &dummyEnvironments{environment: env} + + ctx, cancel := context.WithCancel(context.Background()) + + // Отменяем контекст через 1 секунду + go func() { + time.Sleep(1 * time.Second) + cancel() + }() + + err := WaitForAvailable(ctx, environments, "space-id", "master") + assert.ErrorIs(t, err, context.Canceled) + }) + t.Run("External context was deadline exceeded", func(t *testing.T) { + env := &Environment{ID: "env-id", SpaceID: "space-id", StateInfo: &StateInfo{State: StatePreparing}} + environments := &dummyEnvironments{environment: env} + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + err := WaitForAvailable(ctx, environments, "space-id", "master") + assert.ErrorIs(t, err, context.DeadlineExceeded) + }) +} -- GitLab