Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion server/internal/api/apiv1/post_init_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,8 @@ 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 := db.RawState
err = s.dbSvc.UpdateDatabaseState(ctx, db.DatabaseID, prevState, database.DatabaseStateDeleting)
if err != nil {
return nil, apiErr(err)
Expand Down
27 changes: 25 additions & 2 deletions server/internal/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ type Database struct {
CreatedAt time.Time
UpdatedAt time.Time
State DatabaseState
// RawState is the state as persisted in storage, before any derived
// adjustments (e.g. degrading State based on instance health). Callers
// that need to perform a guarded transition against the stored state
// (e.g. a compare-and-swap) should use this instead of State.
RawState DatabaseState
Spec *Spec
Instances []*Instance
ServiceInstances []*ServiceInstance
Expand All @@ -71,13 +76,32 @@ 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,
RawState: d.State,
Spec: storedSpec.Spec,
Instances: instances,
ServiceInstances: serviceInstances,
Expand All @@ -95,7 +119,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)
Expand Down
54 changes: 54 additions & 0 deletions server/internal/database/database_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions server/internal/database/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func (s *Service) CreateDatabase(ctx context.Context, spec *Spec) (*Database, er
CreatedAt: now,
UpdatedAt: now,
State: DatabaseStateCreating,
RawState: DatabaseStateCreating,
Spec: spec,
NotCreated: true,
}
Expand Down