mirror of
https://github.com/cloudnative-pg/plugin-barman-cloud.git
synced 2026-07-07 02:02:21 +02:00
test(common): cover WAL restore error classification
Extract the barman-restorer error -> gRPC code switch from
restoreFromBarmanObjectStore into a pure helper,
classifyWALRestoreError, so it can be exercised in isolation
without the surrounding k8s client / configuration scaffolding.
Add ginkgo specs that check:
- each barman sentinel maps to the expected gRPC status code
(ErrConnectivity and ErrGeneric both -> Unavailable, since
barman uses exit 4 for some retryable conditions too),
- an unclassified error falls through to codes.Internal,
- classification still works through multiple fmt.Errorf wraps,
- the switch matches by errors.Is identity rather than message
substring (so a NotFound whose message happens to mention
"connectivity" still maps to NotFound).
internal/cnpgi/common had no tests before; this introduces the
suite scaffolding alongside the new specs.
Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
This commit is contained in:
parent
f9e3eaf49b
commit
2a2e09504b
@ -20,10 +20,38 @@ SPDX-License-Identifier: Apache-2.0
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
barmanRestorer "github.com/cloudnative-pg/barman-cloud/pkg/restorer"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// classifyWALRestoreError maps an error returned by the WAL
|
||||||
|
// restorer to a gRPC-coded error so the caller can tell terminal
|
||||||
|
// failures apart from transient ones via the status code.
|
||||||
|
func classifyWALRestoreError(walName string, walErr error) error {
|
||||||
|
switch {
|
||||||
|
case errors.Is(walErr, barmanRestorer.ErrWALNotFound):
|
||||||
|
return newWALNotFoundError(walName)
|
||||||
|
case errors.Is(walErr, barmanRestorer.ErrInvalidWalName):
|
||||||
|
// A malformed WAL name will never become valid on retry.
|
||||||
|
return newInvalidWALNameError(walName, walErr)
|
||||||
|
case errors.Is(walErr, barmanRestorer.ErrConnectivity),
|
||||||
|
errors.Is(walErr, barmanRestorer.ErrGeneric):
|
||||||
|
// barman-cloud exit codes 2 (connectivity) and 4
|
||||||
|
// (generic) both surface conditions that are retryable
|
||||||
|
// in practice — barman uses the "generic" bucket for
|
||||||
|
// some connection-class failures too, not just exit 2.
|
||||||
|
return newUnavailableError(walName, walErr)
|
||||||
|
default:
|
||||||
|
// Unrecognized exit codes and unexpected failures (e.g.
|
||||||
|
// the barman-cloud command could not be executed). No
|
||||||
|
// positive signal that retry would help.
|
||||||
|
return newInternalWALRestoreError(walName, walErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ErrEndOfWALStreamReached is returned when end of WAL is detected in the cloud archive.
|
// ErrEndOfWALStreamReached is returned when end of WAL is detected in the cloud archive.
|
||||||
var ErrEndOfWALStreamReached = status.Errorf(codes.OutOfRange, "end of WAL reached")
|
var ErrEndOfWALStreamReached = status.Errorf(codes.OutOfRange, "end of WAL reached")
|
||||||
|
|
||||||
|
|||||||
86
internal/cnpgi/common/errors_test.go
Normal file
86
internal/cnpgi/common/errors_test.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
Copyright © contributors to CloudNativePG, established as
|
||||||
|
CloudNativePG a Series of LF Projects, LLC.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
barmanRestorer "github.com/cloudnative-pg/barman-cloud/pkg/restorer"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("classifyWALRestoreError", func() {
|
||||||
|
const walName = "000000010000000000000001"
|
||||||
|
|
||||||
|
DescribeTable(
|
||||||
|
"maps barman restorer sentinels to gRPC status codes",
|
||||||
|
func(walErr error, expectedCode codes.Code) {
|
||||||
|
got := classifyWALRestoreError(walName, walErr)
|
||||||
|
Expect(got).To(HaveOccurred())
|
||||||
|
|
||||||
|
st, ok := status.FromError(got)
|
||||||
|
Expect(ok).To(BeTrue(), "returned error must carry a gRPC status")
|
||||||
|
Expect(st.Code()).To(Equal(expectedCode))
|
||||||
|
Expect(st.Message()).To(ContainSubstring(walName))
|
||||||
|
},
|
||||||
|
Entry("ErrWALNotFound -> NotFound",
|
||||||
|
fmt.Errorf("object storage or file not found: %w", barmanRestorer.ErrWALNotFound),
|
||||||
|
codes.NotFound),
|
||||||
|
Entry("ErrInvalidWalName -> InvalidArgument",
|
||||||
|
fmt.Errorf("invalid name for a WAL file: %w", barmanRestorer.ErrInvalidWalName),
|
||||||
|
codes.InvalidArgument),
|
||||||
|
Entry("ErrConnectivity -> Unavailable",
|
||||||
|
fmt.Errorf("connectivity failure, retrying: %w", barmanRestorer.ErrConnectivity),
|
||||||
|
codes.Unavailable),
|
||||||
|
Entry("ErrGeneric -> Unavailable (barman uses exit 4 for some retryable cases too)",
|
||||||
|
fmt.Errorf("generic error: %w", barmanRestorer.ErrGeneric),
|
||||||
|
codes.Unavailable),
|
||||||
|
Entry("unknown error -> Internal",
|
||||||
|
errors.New("something we did not classify"),
|
||||||
|
codes.Internal),
|
||||||
|
)
|
||||||
|
|
||||||
|
It("matches the sentinel even through several wrapping layers", func() {
|
||||||
|
// The plugin wraps barman errors via fmt.Errorf("...: %w", ...);
|
||||||
|
// classification must keep working if more wraps appear above.
|
||||||
|
inner := fmt.Errorf("connectivity failure, retrying: %w", barmanRestorer.ErrConnectivity)
|
||||||
|
wrapped := fmt.Errorf("while restoring WAL %s: %w", walName, inner)
|
||||||
|
|
||||||
|
got := classifyWALRestoreError(walName, wrapped)
|
||||||
|
st, ok := status.FromError(got)
|
||||||
|
Expect(ok).To(BeTrue())
|
||||||
|
Expect(st.Code()).To(Equal(codes.Unavailable))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("treats ErrWALNotFound as terminal even when the error chain mentions other sentinels in its message", func() {
|
||||||
|
// Defensive: if the underlying error stringifies to something
|
||||||
|
// resembling another sentinel's message, the switch must still
|
||||||
|
// match by identity (errors.Is), not by substring.
|
||||||
|
walErr := fmt.Errorf("not found, looks like a connectivity failure: %w", barmanRestorer.ErrWALNotFound)
|
||||||
|
|
||||||
|
got := classifyWALRestoreError(walName, walErr)
|
||||||
|
st, _ := status.FromError(got)
|
||||||
|
Expect(st.Code()).To(Equal(codes.NotFound))
|
||||||
|
})
|
||||||
|
})
|
||||||
32
internal/cnpgi/common/suite_test.go
Normal file
32
internal/cnpgi/common/suite_test.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
Copyright © contributors to CloudNativePG, established as
|
||||||
|
CloudNativePG a Series of LF Projects, LLC.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCommon(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "Common Suite")
|
||||||
|
}
|
||||||
@ -365,29 +365,7 @@ func (w WALServiceImplementation) restoreFromBarmanObjectStore(
|
|||||||
// is the one that PostgreSQL has requested to restore.
|
// is the one that PostgreSQL has requested to restore.
|
||||||
// The failure has already been logged in walRestorer.RestoreList method
|
// The failure has already been logged in walRestorer.RestoreList method
|
||||||
if walStatus[0].Err != nil {
|
if walStatus[0].Err != nil {
|
||||||
walName := walStatus[0].WalName
|
return classifyWALRestoreError(walStatus[0].WalName, walStatus[0].Err)
|
||||||
walErr := walStatus[0].Err
|
|
||||||
switch {
|
|
||||||
case errors.Is(walErr, barmanRestorer.ErrWALNotFound):
|
|
||||||
return newWALNotFoundError(walName)
|
|
||||||
case errors.Is(walErr, barmanRestorer.ErrInvalidWalName):
|
|
||||||
// A malformed WAL name will never become valid on retry.
|
|
||||||
return newInvalidWALNameError(walName, walErr)
|
|
||||||
case errors.Is(walErr, barmanRestorer.ErrConnectivity),
|
|
||||||
errors.Is(walErr, barmanRestorer.ErrGeneric):
|
|
||||||
// barman-cloud exit codes 2 (connectivity) and 4
|
|
||||||
// (generic) both surface conditions that are retryable
|
|
||||||
// in practice — barman uses the "generic" bucket for
|
|
||||||
// some connection-class failures too, not just exit 2.
|
|
||||||
// Emit codes.Unavailable so the caller retries.
|
|
||||||
return newUnavailableError(walName, walErr)
|
|
||||||
default:
|
|
||||||
// Unrecognized exit codes and unexpected failures
|
|
||||||
// (e.g. the barman-cloud command could not be
|
|
||||||
// executed). No positive signal that retry would
|
|
||||||
// help; emit codes.Internal.
|
|
||||||
return newInternalWALRestoreError(walName, walErr)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We skip this step if streaming connection is not available
|
// We skip this step if streaming connection is not available
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user