Skip to content
Snippets Groups Projects
Commit 3e7c23ef authored by Semyon Krestyaninov's avatar Semyon Krestyaninov :dog2: Committed by Pavel Antonov
Browse files

feat(core): Добавлена функция для ожидания готовности окружения

Close #PRXS-2648
parent dcf5e073
No related branches found
No related tags found
No related merge requests found
......@@ -6,6 +6,18 @@ const (
DefaultEnvironment = "master"
)
var (
ReadAllowedStates = []State{
StateNew,
StateReady,
}
WriteAllowedStates = []State{
StateNew,
StateReady,
}
)
type State int
const (
......
......@@ -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...,
)
}
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)
})
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment