mirror of
https://github.com/cloudnative-pg/plugin-barman-cloud.git
synced 2026-07-06 17:52: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
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
barmanRestorer "github.com/cloudnative-pg/barman-cloud/pkg/restorer"
|
||||
"google.golang.org/grpc/codes"
|
||||
"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.
|
||||
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.
|
||||
// The failure has already been logged in walRestorer.RestoreList method
|
||||
if walStatus[0].Err != nil {
|
||||
walName := walStatus[0].WalName
|
||||
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)
|
||||
}
|
||||
return classifyWALRestoreError(walStatus[0].WalName, walStatus[0].Err)
|
||||
}
|
||||
|
||||
// We skip this step if streaming connection is not available
|
||||
|
||||
Loading…
Reference in New Issue
Block a user