diff --git a/internal/datastore/common/errors.go b/internal/datastore/common/errors.go index 99faf7af51..81537bab84 100644 --- a/internal/datastore/common/errors.go +++ b/internal/datastore/common/errors.go @@ -44,6 +44,31 @@ func NewSerializationError(err error) error { return SerializationError{err} } +// ReadOnlyTransactionError is returned when an otherwise read-write +// transaction fails on writes with an error indicating that the datastore +// is currently in a read-only mode. +type ReadOnlyTransactionError struct { + error +} + +func (err ReadOnlyTransactionError) GRPCStatus() *status.Status { + return spiceerrors.WithCodeAndDetails( + err, + codes.Aborted, + spiceerrors.ForReason( + v1.ErrorReason_ERROR_REASON_SERVICE_READ_ONLY, + map[string]string{}, + ), + ) +} + +// NewReadOnlyTransactionError creates a new ReadOnlyTransactionError. +func NewReadOnlyTransactionError(err error) error { + return ReadOnlyTransactionError{ + fmt.Errorf("could not perform write operation, as the datastore is currently in read-only mode: %w. This may indicate that the datastore has been put into maintenance mode", err), + } +} + // CreateRelationshipExistsError is an error returned when attempting to CREATE an already-existing // relationship. type CreateRelationshipExistsError struct { diff --git a/internal/datastore/postgres/common/errors.go b/internal/datastore/postgres/common/errors.go index f4464fc78c..e71b7c8eb3 100644 --- a/internal/datastore/postgres/common/errors.go +++ b/internal/datastore/postgres/common/errors.go @@ -16,6 +16,7 @@ const ( pgUniqueConstraintViolation = "23505" pgSerializationFailure = "40001" pgTransactionAborted = "25P02" + pgReadOnlyTransaction = "25006" ) var ( @@ -30,6 +31,13 @@ func IsConstraintFailureError(err error) bool { return errors.As(err, &pgerr) && pgerr.Code == pgUniqueConstraintViolation } +// IsReadOnlyTransactionError returns true if the error is a Postgres error indicating a read-only +// transaction. +func IsReadOnlyTransactionError(err error) bool { + var pgerr *pgconn.PgError + return errors.As(err, &pgerr) && pgerr.Code == pgReadOnlyTransaction +} + // ConvertToWriteConstraintError converts the given Postgres error into a CreateRelationshipExistsError // if applicable. If not applicable, returns nils. func ConvertToWriteConstraintError(livingTupleConstraints []string, err error) error { diff --git a/internal/datastore/postgres/postgres.go b/internal/datastore/postgres/postgres.go index c1a48c6619..cd73907fb0 100644 --- a/internal/datastore/postgres/postgres.go +++ b/internal/datastore/postgres/postgres.go @@ -584,6 +584,10 @@ func wrapError(err error) error { return common.NewSerializationError(err) } + if pgxcommon.IsReadOnlyTransactionError(err) { + return common.NewReadOnlyTransactionError(err) + } + // hack: pgx asyncClose usually happens after cancellation, // but the reason for it being closed is not propagated // and all we get is attempting to perform an operation