Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unified error reporting in internal/vfs package #15

Merged
merged 3 commits into from
Jul 26, 2018
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
116 changes: 116 additions & 0 deletions internal/vfs/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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.

package vfs

import (
"github.com/elastic/go-txfile/internal/strbld"
)

// Error is the common error type used by vfs implementations.
type Error struct {
op string
kind error
path string
err error
}

// Kind encodes an error code for use by applications.
// Implementations of vfs must unify errors, using the Error type and the error
// codes defined here.
type Kind int

//go:generate stringer -type=Kind -linecomment=true
//go:generate beatsfmt -w kind_string.go
const (
ErrOSOther Kind = iota // unknown OS error
ErrPermission // permission denied
ErrExist // file already exists
ErrNotExist // file does not exist
ErrClosed // file already closed
ErrNoSpace // no space or quota exhausted
ErrFDLimit // process file desciptor limit reached
ErrResolvePath // cannot resolve path
ErrIO // read/write IO error
ErrNotSupported // operation not supported
ErrLockFailed // file lock failed
ErrUnlockFailed // file unlock failed

endOfErrKind // unknown error kind
)

// Error returns the error codes descriptive text.
func (k Kind) Error() string {
if k < endOfErrKind {
return k.String()
}
return "unknown"
}

// Err creates a new Error. All fields are optional.
func Err(op string, kind Kind, path string, err error) *Error {
return &Error{op: op, kind: kind, path: path, err: err}
}

// Op reports the failed operation.
func (e *Error) Op() string { return e.op }

// Kind returns the error code for use by the applications error handling code.
func (e *Error) Kind() error { return e.kind }

// Path returns the path of the file an operation failed for.
func (e *Error) Path() string { return e.path }

// Cause returns the causing error, is there is one. Returns nil if the error
// is the root cause of an error.
func (e *Error) Cause() error { return e.err }

// Errors returns the error cause as a list. The Errors method is avaialble for
// compatiblity with other error packages and libraries consuming errors (e.g. zap or multierr).
func (e *Error) Errors() []error {
if e.err == nil {
return nil
}
return []error{e.err}
}

// Error builds the error message of the underlying error.
func (e *Error) Error() string {
buf := &strbld.Builder{}
putStr(buf, e.op)
putStr(buf, e.path)
putErr(buf, e.kind)
putErr(buf, e.err)

if buf.Len() == 0 {
return "no error"
}
return buf.String()
}

func putStr(b *strbld.Builder, s string) {
if s != "" {
b.Pad(": ")
b.WriteString(s)
}
}

func putErr(b *strbld.Builder, err error) {
if err != nil {
putStr(b, err.Error())
}
}
59 changes: 59 additions & 0 deletions internal/vfs/error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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.

package vfs

import (
"errors"
"os"
"testing"

"github.com/stretchr/testify/assert"
)

func TestErrorFmt(t *testing.T) {
type testCase struct {
err *Error
expected string
}

cases := map[string]testCase{
"with op+kind only": testCase{
err: Err("pkg/op", ErrPermission, "", nil),
expected: "pkg/op: permission denied",
},
"with path": testCase{
err: Err("pkg/op", ErrNotExist, "/path/to/file", nil),
expected: "pkg/op: /path/to/file: file does not exist",
},
"nested os error": testCase{
err: Err("pkg/op", ErrPermission, "path/to/file", &os.PathError{
Op: "stat",
Path: "path/to/file",
Err: errors.New("operation not permitted"),
}),
expected: "pkg/op: path/to/file: permission denied: stat path/to/file: operation not permitted",
},
}

for name, test := range cases {
test := test
t.Run(name, func(t *testing.T) {
assert.Equal(t, test.expected, test.err.Error())
})
}
}
33 changes: 33 additions & 0 deletions internal/vfs/kind_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 73 additions & 0 deletions internal/vfs/osfs/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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.

package osfs

import (
"os"

"github.com/elastic/go-txfile/internal/vfs"
)

// errKind tries to find an appropriate error kind for an os error.
// If there is no checking predicate in the os package, sysErrKind is used to
// map OS specific error codes to error kinds, such that a common and unified
// set of error codes will be available to users of vfs.
func errKind(err error) vfs.Kind {
if os.IsPermission(err) {
return vfs.ErrPermission
}
if os.IsExist(err) {
return vfs.ErrExist
}
if os.IsNotExist(err) {
return vfs.ErrExist
}

switch err {
case os.ErrClosed:
return vfs.ErrClosed
default:
return sysErrKind(err)
}
}

// normalizeSysError returns the underlying error or nil, if the underlying
// error indicates it is no error.
func normalizeSysError(err error) error {
err = underlyingError(err)
if err == nil || err == errno0 {
return nil
}
return err
}

func underlyingError(in error) error {
switch err := in.(type) {
case *os.PathError:
return err.Err

case *os.LinkError:
return err.Err

case *os.SyscallError:
return err.Err

default:
return err
}
}
52 changes: 52 additions & 0 deletions internal/vfs/osfs/error_posix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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.

// +build !windows

package osfs

import (
"syscall"

"github.com/elastic/go-txfile/internal/vfs"
)

// sysErrKind maps POSIX error codes to vfs related error codes.
func sysErrKind(err error) vfs.Kind {
err = underlyingError(err)
switch err {
case syscall.EDQUOT, syscall.ENOSPC, syscall.ENFILE:
return vfs.ErrNoSpace

case syscall.EMFILE:
return vfs.ErrFDLimit

case syscall.ENOTDIR:
return vfs.ErrResolvePath

case syscall.ENOTSUP:
return vfs.ErrNotSupported

case syscall.EIO:
return vfs.ErrIO

case syscall.EDEADLK:
return vfs.ErrLockFailed
}

return vfs.ErrOSOther
}
53 changes: 53 additions & 0 deletions internal/vfs/osfs/error_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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.

package osfs

import (
"syscall"

"github.com/elastic/go-txfile/internal/vfs"
)

const (
ERROR_DISK_FULL syscall.Errno = 62
ERROR_DISK_QUOTA_EXCEEDED syscall.Errno = 1295
ERROR_TOO_MANY_OPEN_FILES syscall.Errno = 4
ERROR_LOCK_FAILED syscall.Errno = 167
ERROR_CANT_RESOLVE_FILENAME syscall.Errno = 1921
)

// sysErrKind maps Windows error codes to vfs related error codes.
func sysErrKind(err error) vfs.Kind {
switch underlyingError(err) {

case ERROR_DISK_FULL, ERROR_DISK_QUOTA_EXCEEDED:
return vfs.ErrNoSpace

case ERROR_TOO_MANY_OPEN_FILES:
return vfs.ErrFDLimit

case ERROR_LOCK_FAILED:
return vfs.ErrLockFailed

case ERROR_CANT_RESOLVE_FILENAME:
return vfs.ErrResolvePath

default:
return vfs.ErrOSOther
}
}
Loading