diff --git a/server/internal/api/apiv1/post_init_handlers.go b/server/internal/api/apiv1/post_init_handlers.go index 59d01858..caa8491c 100644 --- a/server/internal/api/apiv1/post_init_handlers.go +++ b/server/internal/api/apiv1/post_init_handlers.go @@ -475,7 +475,11 @@ func (s *PostInitHandlers) DeleteDatabase(ctx context.Context, req *api.DeleteDa return nil, ErrDatabaseNotModifiable } - prevState := db.State + // Use the raw stored state as the guard for this transition. + prevState, err := s.dbSvc.GetStoredDatabaseState(ctx, db.DatabaseID) + if err != nil { + return nil, apiErr(err) + } err = s.dbSvc.UpdateDatabaseState(ctx, db.DatabaseID, prevState, database.DatabaseStateDeleting) if err != nil { return nil, apiErr(err) diff --git a/server/internal/database/database.go b/server/internal/database/database.go index d550fde8..51f79f89 100644 --- a/server/internal/database/database.go +++ b/server/internal/database/database.go @@ -71,13 +71,31 @@ func databaseToStored(d *Database) *StoredDatabase { } } +var degradedInstanceStates = ds.NewSet( + InstanceStateDegraded, + InstanceStateFailed, + InstanceStateUnknown, + InstanceStateStopped, +) + func storedToDatabase(d *StoredDatabase, storedSpec *StoredSpec, instances []*Instance, serviceInstances []*ServiceInstance) *Database { + state := d.State + + if state == DatabaseStateAvailable { + for _, instance := range instances { + if degradedInstanceStates.Has(instance.State) { + state = DatabaseStateDegraded + break + } + } + } + return &Database{ DatabaseID: d.DatabaseID, TenantID: d.TenantID, CreatedAt: d.CreatedAt, UpdatedAt: d.UpdatedAt, - State: d.State, + State: state, Spec: storedSpec.Spec, Instances: instances, ServiceInstances: serviceInstances, @@ -95,7 +113,6 @@ func storedToDatabases(storedDbs []*StoredDatabase, storedSpecs []*StoredSpec, a for _, instance := range allInstances { instancesByID[instance.DatabaseID] = append(instancesByID[instance.DatabaseID], instance) } - serviceInstancesByID := make(map[string][]*ServiceInstance, len(allServiceInstances)) for _, serviceInstance := range allServiceInstances { serviceInstancesByID[serviceInstance.DatabaseID] = append(serviceInstancesByID[serviceInstance.DatabaseID], serviceInstance) diff --git a/server/internal/database/database_test.go b/server/internal/database/database_test.go new file mode 100644 index 00000000..f3c6e677 --- /dev/null +++ b/server/internal/database/database_test.go @@ -0,0 +1,54 @@ +package database + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStoredToDatabaseDegradesWhenInstanceUnavailable(t *testing.T) { + tests := []struct { + name string + instanceState InstanceState + wantState DatabaseState + }{ + {"available instance keeps database available", InstanceStateAvailable, DatabaseStateAvailable}, + {"degraded instance degrades database", InstanceStateDegraded, DatabaseStateDegraded}, + {"failed instance degrades database", InstanceStateFailed, DatabaseStateDegraded}, + {"unknown instance degrades database", InstanceStateUnknown, DatabaseStateDegraded}, + {"stopped instance degrades database", InstanceStateStopped, DatabaseStateDegraded}, + {"creating instance does not degrade database", InstanceStateCreating, DatabaseStateAvailable}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stored := &StoredDatabase{ + DatabaseID: "db1", + State: DatabaseStateAvailable, + } + storedSpec := &StoredSpec{} + instances := []*Instance{ + {InstanceID: "i1", DatabaseID: "db1", State: tt.instanceState}, + } + + db := storedToDatabase(stored, storedSpec, instances, nil) + + assert.Equal(t, tt.wantState, db.State) + }) + } +} + +func TestStoredToDatabaseDoesNotDegradeNonAvailableDatabase(t *testing.T) { + stored := &StoredDatabase{ + DatabaseID: "db1", + State: DatabaseStateCreating, + } + storedSpec := &StoredSpec{} + instances := []*Instance{ + {InstanceID: "i1", DatabaseID: "db1", State: InstanceStateFailed}, + } + + db := storedToDatabase(stored, storedSpec, instances, nil) + + assert.Equal(t, DatabaseStateCreating, db.State) +}