Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.golangci.yml3
-rw-r--r--STYLE.md24
-rw-r--r--cmd/gitaly-hooks/hooks_test.go3
-rw-r--r--internal/gitaly/service/blob/get_blob.go8
-rw-r--r--internal/gitaly/service/blob/get_blobs.go10
-rw-r--r--internal/gitaly/service/commit/raw_blame.go2
-rw-r--r--internal/gitaly/service/commit/tree_entry.go8
-rw-r--r--internal/gitaly/service/diff/commit.go8
-rw-r--r--internal/gitaly/service/diff/commit_test.go4
-rw-r--r--internal/gitaly/service/diff/find_changed_paths.go2
-rw-r--r--internal/gitaly/service/diff/numstat.go4
-rw-r--r--internal/gitaly/service/diff/numstat_test.go8
-rw-r--r--internal/gitaly/service/diff/raw.go2
-rw-r--r--internal/gitaly/service/smarthttp/receive_pack.go4
-rw-r--r--internal/gitaly/service/smarthttp/upload_pack.go6
-rw-r--r--internal/gitaly/service/smarthttp/upload_pack_test.go2
-rw-r--r--internal/gitaly/service/ssh/upload_pack.go2
-rw-r--r--internal/grpc/middleware/limithandler/rate_limiter.go2
-rw-r--r--internal/structerr/error.go18
-rw-r--r--tools/golangci-lint/gitaly/lint.go4
-rw-r--r--tools/golangci-lint/gitaly/testdata/src/unavailable_code/unavailable_code_test.go21
-rw-r--r--tools/golangci-lint/gitaly/unavailable_code.go48
-rw-r--r--tools/golangci-lint/gitaly/unavailable_code_test.go22
23 files changed, 176 insertions, 39 deletions
diff --git a/.golangci.yml b/.golangci.yml
index c41e1df5d..aaca0828f 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -144,6 +144,9 @@ linters-settings:
included-functions:
- fmt.Errorf
- gitlab.com/gitlab-org/gitaly/v16/internal/structerr.*
+ unavailable_code:
+ included-functions:
+ - gitlab.com/gitlab-org/gitaly/v16/internal/structerr.NewUnavailable
issues:
exclude-use-default: false
diff --git a/STYLE.md b/STYLE.md
index 81132e77c..bbd390894 100644
--- a/STYLE.md
+++ b/STYLE.md
@@ -163,6 +163,30 @@ message FrobnicateError {
}
```
+### Unavailable code
+
+The Unavailable status code is reserved for cases that clients are encouraged to retry. The most suitable use cases for
+this status code are in interceptors or network-related components such as load-balancing. The official documentation
+differentiates the usage of status codes as the following:
+
+> (a) Use UNAVAILABLE if the client can retry just the failing call.
+> (b) Use ABORTED if the client should retry at a higher level
+> (c) Use FAILED_PRECONDITION if the client should not retry until the
+> system state has been explicitly fixed
+
+In the past we've had multiple places in the source code where an error from sending a streaming message was captured
+and wrapped in an `Unavailable` code. This status code is often not correct because it can raise other less common
+errors, such as buffer overflow (`ResourceExhausted`), max message size exceeded (`ResourceExhausted`), or encoding
+failure (`Internal`). It's more appropriate for the handler to propagate the error up as an `Aborted` status code.
+
+Another common misused pattern is wrapping the spawned process exit code. In many cases, if Gitaly can intercept the
+exit code or/and error from stderr, it must have a precise error code (`InvalidArgument`, `NotFound`, `Internal`).
+However, Git processes may exit with 128 status code and un-parseable stderr. We can intercept it as an operation was
+rejected because the system is not in a state where it can be executed (resource inaccessible, invalid refs, etc.).
+For these situations, `FailedPrecondition` is the most suitable choice.
+
+Thus, gRPC handlers should avoid using `Unavailable` status code.
+
## Logging
### Use context-based logging
diff --git a/cmd/gitaly-hooks/hooks_test.go b/cmd/gitaly-hooks/hooks_test.go
index 88b15e6a1..9ceb43681 100644
--- a/cmd/gitaly-hooks/hooks_test.go
+++ b/cmd/gitaly-hooks/hooks_test.go
@@ -786,7 +786,8 @@ remote: error resource exhausted, please try again later
},
{
name: "other error - status code is hidden",
- err: structerr.NewUnavailable("server is not available"),
+ //nolint:gitaly-linters
+ err: structerr.NewUnavailable("server is not available"),
expectedStderr: `
remote: error executing git hook
`,
diff --git a/internal/gitaly/service/blob/get_blob.go b/internal/gitaly/service/blob/get_blob.go
index bde49365a..ea4d294d7 100644
--- a/internal/gitaly/service/blob/get_blob.go
+++ b/internal/gitaly/service/blob/get_blob.go
@@ -31,7 +31,7 @@ func (s *server) GetBlob(in *gitalypb.GetBlobRequest, stream gitalypb.BlobServic
if err != nil {
if catfile.IsNotFound(err) {
if err := stream.Send(&gitalypb.GetBlobResponse{}); err != nil {
- return structerr.NewUnavailable("sending empty response: %w", err)
+ return structerr.NewAborted("sending empty response: %w", err)
}
return nil
}
@@ -40,7 +40,7 @@ func (s *server) GetBlob(in *gitalypb.GetBlobRequest, stream gitalypb.BlobServic
if blob.Type != "blob" {
if err := stream.Send(&gitalypb.GetBlobResponse{}); err != nil {
- return structerr.NewUnavailable("sending empty response: %w", err)
+ return structerr.NewAborted("sending empty response: %w", err)
}
return nil
@@ -57,7 +57,7 @@ func (s *server) GetBlob(in *gitalypb.GetBlobRequest, stream gitalypb.BlobServic
if readLimit == 0 {
if err := stream.Send(firstMessage); err != nil {
- return structerr.NewUnavailable("sending empty blob: %w", err)
+ return structerr.NewAborted("sending empty blob: %w", err)
}
return nil
@@ -75,7 +75,7 @@ func (s *server) GetBlob(in *gitalypb.GetBlobRequest, stream gitalypb.BlobServic
_, err = io.CopyN(sw, blob, readLimit)
if err != nil {
- return structerr.NewUnavailable("send: %w", err)
+ return structerr.NewAborted("send: %w", err)
}
return nil
diff --git a/internal/gitaly/service/blob/get_blobs.go b/internal/gitaly/service/blob/get_blobs.go
index e27b54ae8..bb6042370 100644
--- a/internal/gitaly/service/blob/get_blobs.go
+++ b/internal/gitaly/service/blob/get_blobs.go
@@ -46,7 +46,7 @@ func sendGetBlobsResponse(
if treeEntry == nil || len(treeEntry.Oid) == 0 {
if err := stream.Send(response); err != nil {
- return structerr.NewUnavailable("send: %w", err)
+ return structerr.NewAborted("send: %w", err)
}
continue
@@ -60,7 +60,7 @@ func sendGetBlobsResponse(
response.Type = gitalypb.ObjectType_COMMIT
if err := stream.Send(response); err != nil {
- return structerr.NewUnavailable("send: %w", err)
+ return structerr.NewAborted("send: %w", err)
}
continue
@@ -82,7 +82,7 @@ func sendGetBlobsResponse(
if response.Type != gitalypb.ObjectType_BLOB {
if err := stream.Send(response); err != nil {
- return structerr.NewUnavailable("send: %w", err)
+ return structerr.NewAborted("send: %w", err)
}
continue
}
@@ -115,7 +115,7 @@ func sendBlobTreeEntry(
// blobObj.
if readLimit == 0 {
if err := stream.Send(response); err != nil {
- return structerr.NewUnavailable("send: %w", err)
+ return structerr.NewAborted("send: %w", err)
}
return nil
}
@@ -147,7 +147,7 @@ func sendBlobTreeEntry(
_, err = io.CopyN(sw, blobObj, readLimit)
if err != nil {
- return structerr.NewUnavailable("send: %w", err)
+ return structerr.NewAborted("send: %w", err)
}
return nil
diff --git a/internal/gitaly/service/commit/raw_blame.go b/internal/gitaly/service/commit/raw_blame.go
index 366e243fd..aeb74bd32 100644
--- a/internal/gitaly/service/commit/raw_blame.go
+++ b/internal/gitaly/service/commit/raw_blame.go
@@ -46,7 +46,7 @@ func (s *server) RawBlame(in *gitalypb.RawBlameRequest, stream gitalypb.CommitSe
_, err = io.Copy(sw, cmd)
if err != nil {
- return structerr.NewUnavailable("send: %w", err)
+ return structerr.NewAborted("send: %w", err)
}
if err := cmd.Wait(); err != nil {
diff --git a/internal/gitaly/service/commit/tree_entry.go b/internal/gitaly/service/commit/tree_entry.go
index 881bedcfc..b1cf25ba1 100644
--- a/internal/gitaly/service/commit/tree_entry.go
+++ b/internal/gitaly/service/commit/tree_entry.go
@@ -38,7 +38,7 @@ func sendTreeEntry(
Oid: treeEntry.Oid,
}
if err := stream.Send(response); err != nil {
- return structerr.NewUnavailable("send: %w", err)
+ return structerr.NewAborted("send: %w", err)
}
return nil
@@ -58,7 +58,7 @@ func sendTreeEntry(
}
if err := stream.Send(response); err != nil {
- return structerr.NewUnavailable("sending response: %w", err)
+ return structerr.NewAborted("sending response: %w", err)
}
return nil
@@ -97,7 +97,7 @@ func sendTreeEntry(
}
if dataLength == 0 {
if err := stream.Send(response); err != nil {
- return structerr.NewUnavailable("sending response: %w", err)
+ return structerr.NewAborted("sending response: %w", err)
}
return nil
@@ -115,7 +115,7 @@ func sendTreeEntry(
response.Data = p
if err := stream.Send(response); err != nil {
- return structerr.NewUnavailable("send: %w", err)
+ return structerr.NewAborted("send: %w", err)
}
// Use a new response so we don't send other fields (Size, ...) over and over
diff --git a/internal/gitaly/service/diff/commit.go b/internal/gitaly/service/diff/commit.go
index 35c5aa4fc..961c9f165 100644
--- a/internal/gitaly/service/diff/commit.go
+++ b/internal/gitaly/service/diff/commit.go
@@ -109,7 +109,7 @@ func (s *server) CommitDiff(in *gitalypb.CommitDiffRequest, stream gitalypb.Diff
response.EndOfPatch = true
if err := stream.Send(response); err != nil {
- return structerr.NewUnavailable("send: %w", err)
+ return structerr.NewAborted("send: %w", err)
}
} else {
patch := diff.Patch
@@ -125,7 +125,7 @@ func (s *server) CommitDiff(in *gitalypb.CommitDiffRequest, stream gitalypb.Diff
}
if err := stream.Send(response); err != nil {
- return structerr.NewUnavailable("send: %w", err)
+ return structerr.NewAborted("send: %w", err)
}
// Use a new response so we don't send other fields (FromPath, ...) over and over
@@ -177,7 +177,7 @@ func (s *server) CommitDelta(in *gitalypb.CommitDeltaRequest, stream gitalypb.Di
}
if err := stream.Send(&gitalypb.CommitDeltaResponse{Deltas: batch}); err != nil {
- return structerr.NewUnavailable("send: %w", err)
+ return structerr.NewAborted("send: %w", err)
}
return nil
@@ -249,7 +249,7 @@ func (s *server) eachDiff(ctx context.Context, repo *gitalypb.Repository, subCmd
}
if err := cmd.Wait(); err != nil {
- return structerr.NewUnavailable("%w", err)
+ return structerr.NewFailedPrecondition("%w", err)
}
return nil
diff --git a/internal/gitaly/service/diff/commit_test.go b/internal/gitaly/service/diff/commit_test.go
index aa23953d7..dc9bf8f79 100644
--- a/internal/gitaly/service/diff/commit_test.go
+++ b/internal/gitaly/service/diff/commit_test.go
@@ -1353,7 +1353,7 @@ func TestFailedCommitDiffRequestWithNonExistentCommit(t *testing.T) {
require.NoError(t, err)
err = drainCommitDiffResponse(c)
- testhelper.RequireGrpcCode(t, err, codes.Unavailable)
+ testhelper.RequireGrpcCode(t, err, codes.FailedPrecondition)
}
func TestSuccessfulCommitDeltaRequest(t *testing.T) {
@@ -1562,7 +1562,7 @@ func TestFailedCommitDeltaRequestWithNonExistentCommit(t *testing.T) {
require.NoError(t, err)
err = drainCommitDeltaResponse(c)
- testhelper.RequireGrpcCode(t, err, codes.Unavailable)
+ testhelper.RequireGrpcCode(t, err, codes.FailedPrecondition)
}
func drainCommitDiffResponse(c gitalypb.DiffService_CommitDiffClient) error {
diff --git a/internal/gitaly/service/diff/find_changed_paths.go b/internal/gitaly/service/diff/find_changed_paths.go
index 9e3e6ada2..440fa82df 100644
--- a/internal/gitaly/service/diff/find_changed_paths.go
+++ b/internal/gitaly/service/diff/find_changed_paths.go
@@ -90,7 +90,7 @@ func (s *server) FindChangedPaths(in *gitalypb.FindChangedPathsRequest, stream g
}
if err := cmd.Wait(); err != nil {
- return structerr.NewUnavailable("cmd wait err: %w", err)
+ return structerr.NewFailedPrecondition("cmd wait err: %w", err)
}
return diffChunker.Flush()
diff --git a/internal/gitaly/service/diff/numstat.go b/internal/gitaly/service/diff/numstat.go
index e0fbbaff5..d4bf2dae9 100644
--- a/internal/gitaly/service/diff/numstat.go
+++ b/internal/gitaly/service/diff/numstat.go
@@ -57,7 +57,7 @@ func (s *server) DiffStats(in *gitalypb.DiffStatsRequest, stream gitalypb.DiffSe
}
if err := cmd.Wait(); err != nil {
- return structerr.NewUnavailable("%w", err)
+ return structerr.NewFailedPrecondition("%w", err)
}
return sendStats(batch, stream)
@@ -69,7 +69,7 @@ func sendStats(batch []*gitalypb.DiffStats, stream gitalypb.DiffService_DiffStat
}
if err := stream.Send(&gitalypb.DiffStatsResponse{Stats: batch}); err != nil {
- return structerr.NewUnavailable("send: %w", err)
+ return structerr.NewAborted("send: %w", err)
}
return nil
diff --git a/internal/gitaly/service/diff/numstat_test.go b/internal/gitaly/service/diff/numstat_test.go
index bd95e6559..38f55b576 100644
--- a/internal/gitaly/service/diff/numstat_test.go
+++ b/internal/gitaly/service/diff/numstat_test.go
@@ -188,28 +188,28 @@ func TestFailedDiffStatsRequest(t *testing.T) {
repo: repo,
leftCommitID: "invalidinvalidinvalid",
rightCommitID: "8a0f2ee90d940bfb0ba1e14e8214b0649056e4ab",
- expectedErr: status.Error(codes.Unavailable, "exit status 128"),
+ expectedErr: status.Error(codes.FailedPrecondition, "exit status 128"),
},
{
desc: "invalid right commit",
repo: repo,
leftCommitID: "e4003da16c1c2c3fc4567700121b17bf8e591c6c",
rightCommitID: "invalidinvalidinvalid",
- expectedErr: status.Error(codes.Unavailable, "exit status 128"),
+ expectedErr: status.Error(codes.FailedPrecondition, "exit status 128"),
},
{
desc: "left commit not found",
repo: repo,
leftCommitID: "a4003da16c1c2c3fc4567700121b17bf8e591c6c",
rightCommitID: "8a0f2ee90d940bfb0ba1e14e8214b0649056e4ab",
- expectedErr: status.Error(codes.Unavailable, "exit status 128"),
+ expectedErr: status.Error(codes.FailedPrecondition, "exit status 128"),
},
{
desc: "right commit not found",
repo: repo,
leftCommitID: "e4003da16c1c2c3fc4567700121b17bf8e591c6c",
rightCommitID: "a4003da16c1c2c3fc4567700121b17bf8e591c6c",
- expectedErr: status.Error(codes.Unavailable, "exit status 128"),
+ expectedErr: status.Error(codes.FailedPrecondition, "exit status 128"),
},
}
diff --git a/internal/gitaly/service/diff/raw.go b/internal/gitaly/service/diff/raw.go
index c6927b531..0882f0302 100644
--- a/internal/gitaly/service/diff/raw.go
+++ b/internal/gitaly/service/diff/raw.go
@@ -53,7 +53,7 @@ func sendRawOutput(ctx context.Context, gitCmdFactory git.CommandFactory, repo *
}
if _, err := io.Copy(sender, cmd); err != nil {
- return structerr.NewUnavailable("send: %w", err)
+ return structerr.NewAborted("send: %w", err)
}
return cmd.Wait()
diff --git a/internal/gitaly/service/smarthttp/receive_pack.go b/internal/gitaly/service/smarthttp/receive_pack.go
index 7b6a21f63..d440bc8db 100644
--- a/internal/gitaly/service/smarthttp/receive_pack.go
+++ b/internal/gitaly/service/smarthttp/receive_pack.go
@@ -63,11 +63,11 @@ func (s *server) PostReceivePack(stream gitalypb.SmartHTTPService_PostReceivePac
git.WithConfig(config...),
)
if err != nil {
- return structerr.NewUnavailable("spawning receive-pack: %w", err)
+ return structerr.NewFailedPrecondition("spawning receive-pack: %w", err)
}
if err := cmd.Wait(); err != nil {
- return structerr.NewUnavailable("waiting for receive-pack: %w", err)
+ return structerr.NewFailedPrecondition("waiting for receive-pack: %w", err)
}
// In cases where all reference updates are rejected by git-receive-pack(1), we would end up
diff --git a/internal/gitaly/service/smarthttp/upload_pack.go b/internal/gitaly/service/smarthttp/upload_pack.go
index 5f004c9dd..589e8456e 100644
--- a/internal/gitaly/service/smarthttp/upload_pack.go
+++ b/internal/gitaly/service/smarthttp/upload_pack.go
@@ -119,13 +119,13 @@ func (s *server) runUploadPack(ctx context.Context, req *gitalypb.PostUploadPack
Args: []string{repoPath},
}, commandOpts...)
if err != nil {
- return nil, structerr.NewUnavailable("spawning upload-pack: %w", err)
+ return nil, structerr.NewFailedPrecondition("spawning upload-pack: %w", err)
}
// Use a custom buffer size to minimize the number of system calls.
respBytes, err := io.CopyBuffer(stdout, cmd, make([]byte, 64*1024))
if err != nil {
- return nil, structerr.NewUnavailable("copying stdout from upload-pack: %w", err)
+ return nil, structerr.NewFailedPrecondition("copying stdout from upload-pack: %w", err)
}
if err := cmd.Wait(); err != nil {
@@ -137,7 +137,7 @@ func (s *server) runUploadPack(ctx context.Context, req *gitalypb.PostUploadPack
return stats, nil
}
- return nil, structerr.NewUnavailable("waiting for upload-pack: %w", err)
+ return nil, structerr.NewFailedPrecondition("waiting for upload-pack: %w", err)
}
ctxlogrus.Extract(ctx).WithField("request_sha", fmt.Sprintf("%x", h.Sum(nil))).WithField("response_bytes", respBytes).Info("request details")
diff --git a/internal/gitaly/service/smarthttp/upload_pack_test.go b/internal/gitaly/service/smarthttp/upload_pack_test.go
index f3ca4f7d1..368d515b4 100644
--- a/internal/gitaly/service/smarthttp/upload_pack_test.go
+++ b/internal/gitaly/service/smarthttp/upload_pack_test.go
@@ -157,7 +157,7 @@ func testServerPostUploadPackGitConfigOptions(t *testing.T, ctx context.Context,
},
}
response, err := makeRequest(t, ctx, cfg.SocketPath, cfg.Auth.Token, rpcRequest, bytes.NewReader(requestBody.Bytes()))
- testhelper.RequireGrpcError(t, structerr.NewUnavailable("running upload-pack: waiting for upload-pack: exit status 128"), err)
+ testhelper.RequireGrpcError(t, structerr.NewFailedPrecondition("running upload-pack: waiting for upload-pack: exit status 128"), err)
// The failure message proves that upload-pack failed because of
// GitConfigOptions, and that proves that passing GitConfigOptions works.
diff --git a/internal/gitaly/service/ssh/upload_pack.go b/internal/gitaly/service/ssh/upload_pack.go
index aac712b91..c5815536a 100644
--- a/internal/gitaly/service/ssh/upload_pack.go
+++ b/internal/gitaly/service/ssh/upload_pack.go
@@ -206,7 +206,7 @@ func (rf *largeBufferReaderFrom) ReadFrom(r io.Reader) (int64, error) {
func (s *server) SSHUploadPackWithSidechannel(ctx context.Context, req *gitalypb.SSHUploadPackWithSidechannelRequest) (*gitalypb.SSHUploadPackWithSidechannelResponse, error) {
conn, err := sidechannel.OpenSidechannel(ctx)
if err != nil {
- return nil, structerr.NewUnavailable("opennig sidechannel: %w", err)
+ return nil, structerr.NewAborted("opennig sidechannel: %w", err)
}
defer conn.Close()
diff --git a/internal/grpc/middleware/limithandler/rate_limiter.go b/internal/grpc/middleware/limithandler/rate_limiter.go
index be09a1f77..577e68b1d 100644
--- a/internal/grpc/middleware/limithandler/rate_limiter.go
+++ b/internal/grpc/middleware/limithandler/rate_limiter.go
@@ -51,7 +51,7 @@ func (r *RateLimiter) Limit(ctx context.Context, lockKey string, f LimitedFunc)
// of traffic.
r.requestsDroppedMetric.Inc()
- return nil, structerr.NewUnavailable("%w", ErrRateLimit).WithDetail(
+ return nil, structerr.NewResourceExhausted("%w", ErrRateLimit).WithDetail(
&gitalypb.LimitError{
ErrorMessage: ErrRateLimit.Error(),
RetryAfter: durationpb.New(0),
diff --git a/internal/structerr/error.go b/internal/structerr/error.go
index 541bec49e..184b0da90 100644
--- a/internal/structerr/error.go
+++ b/internal/structerr/error.go
@@ -177,8 +177,22 @@ func NewResourceExhausted(format string, a ...any) Error {
return newError(codes.ResourceExhausted, format, a...)
}
-// NewUnavailable constructs a new error code with the Unavailable error code. Please refer to New
-// for further details.
+// NewUnavailable constructs a new error code with the Unavailable error code. Please refer to New for further details.
+// Please note that the Unavailable status code is a signal telling clients to retry automatically. This auto-retry
+// mechanism is handled at the library layer, without client consensus. Typically, it is used for the situations where
+// the gRPC is not available or is not responding. Here are some discrete examples:
+//
+// - Server downtime: The server hosting the gRPC service is down for maintenance or has crashed.
+// - Network issues: Connectivity problems between the client and server, like network congestion or a broken connection,
+// can cause the service to appear unavailable.
+// - Load balancing failure: In a distributed system, the load balancer may be unable to route the client's request to a
+// healthy instance of the gRPC service. This can happen if all instances are down or if the load balancer is
+// misconfigured.
+// - TLS/SSL handshake failure: If there's a problem during the TLS/SSL handshake between the client and the server, the
+// connection may fail, leading to an UNAVAILABLE status code.
+//
+// Thus, this status code should be used by interceptors or network-related components. gRPC handlers should use another
+// status code instead.
func NewUnavailable(format string, a ...any) Error {
return newError(codes.Unavailable, format, a...)
}
diff --git a/tools/golangci-lint/gitaly/lint.go b/tools/golangci-lint/gitaly/lint.go
index 1d76a9400..6840b15ac 100644
--- a/tools/golangci-lint/gitaly/lint.go
+++ b/tools/golangci-lint/gitaly/lint.go
@@ -23,6 +23,10 @@ func (p *analyzerPlugin) GetAnalyzers() []*analysis.Analyzer {
"included-functions",
),
}),
+ newUnavailableCodeAnalyzer(&unavailableCodeAnalyzerSettings{IncludedFunctions: p.configStringSlicesAt(
+ unavailableCodeAnalyzerName,
+ "included-functions",
+ )}),
}
}
diff --git a/tools/golangci-lint/gitaly/testdata/src/unavailable_code/unavailable_code_test.go b/tools/golangci-lint/gitaly/testdata/src/unavailable_code/unavailable_code_test.go
new file mode 100644
index 000000000..5493e3f7a
--- /dev/null
+++ b/tools/golangci-lint/gitaly/testdata/src/unavailable_code/unavailable_code_test.go
@@ -0,0 +1,21 @@
+package unavailable_code
+
+import (
+ "fmt"
+)
+
+func NewUnavailable(msg string) error {
+ return fmt.Errorf("unavailable: %s", msg)
+}
+
+func NewAborted(msg string) error {
+ return fmt.Errorf("aborted: %s", msg)
+}
+
+func errorWrapOkay() {
+ _ = NewAborted("hello world")
+}
+
+func errorWrapNotOkay() {
+ _ = NewUnavailable("hello world") // please avoid using the Unavailable status code: https://gitlab.com/gitlab-org/gitaly/-/blob/master/STYLE.md?plain=0#unavailable-code
+}
diff --git a/tools/golangci-lint/gitaly/unavailable_code.go b/tools/golangci-lint/gitaly/unavailable_code.go
new file mode 100644
index 000000000..9546b1871
--- /dev/null
+++ b/tools/golangci-lint/gitaly/unavailable_code.go
@@ -0,0 +1,48 @@
+package main
+
+import (
+ "go/ast"
+
+ "golang.org/x/tools/go/analysis"
+)
+
+const unavailableCodeAnalyzerName = "unavailable_code"
+
+type unavailableCodeAnalyzerSettings struct {
+ IncludedFunctions []string `mapstructure:"included-functions"`
+}
+
+// newErrorWrapAnalyzer warns if Unavailable status code is used. Unavailable status code is reserved to signal server's
+// unavailability. It should be used by some specific components. gRPC handlers should typically avoid this type of
+// error.
+// For more information:
+// https://gitlab.com/gitlab-org/gitaly/-/blob/master/STYLE.md?plain=0#unavailable-code
+func newUnavailableCodeAnalyzer(settings *unavailableCodeAnalyzerSettings) *analysis.Analyzer {
+ return &analysis.Analyzer{
+ Name: unavailableCodeAnalyzerName,
+ Doc: `discourage the usage of Unavailable status code`,
+ Run: runUnavailableCodeAnalyzer(settings.IncludedFunctions),
+ }
+}
+
+func runUnavailableCodeAnalyzer(rules []string) func(*analysis.Pass) (interface{}, error) {
+ return func(pass *analysis.Pass) (interface{}, error) {
+ matcher := NewMatcher(pass)
+ for _, file := range pass.Files {
+ ast.Inspect(file, func(n ast.Node) bool {
+ if call, ok := n.(*ast.CallExpr); ok {
+ if matcher.MatchFunction(call, rules) {
+ pass.Report(analysis.Diagnostic{
+ Pos: call.Pos(),
+ End: call.End(),
+ Message: "please avoid using the Unavailable status code: https://gitlab.com/gitlab-org/gitaly/-/blob/master/STYLE.md?plain=0#unavailable-code",
+ SuggestedFixes: nil,
+ })
+ }
+ }
+ return true
+ })
+ }
+ return nil, nil
+ }
+}
diff --git a/tools/golangci-lint/gitaly/unavailable_code_test.go b/tools/golangci-lint/gitaly/unavailable_code_test.go
new file mode 100644
index 000000000..39d97e788
--- /dev/null
+++ b/tools/golangci-lint/gitaly/unavailable_code_test.go
@@ -0,0 +1,22 @@
+package main
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "golang.org/x/tools/go/analysis/analysistest"
+)
+
+func TestUnavailableCode(t *testing.T) {
+ wd, err := os.Getwd()
+ if err != nil {
+ t.Fatalf("Failed to get wd: %s", err)
+ }
+
+ testdata := filepath.Join(wd, "testdata")
+ analyzer := newUnavailableCodeAnalyzer(&unavailableCodeAnalyzerSettings{IncludedFunctions: []string{
+ "unavailable_code.NewUnavailable",
+ }})
+ analysistest.Run(t, testdata, analyzer, "unavailable_code")
+}