-
Notifications
You must be signed in to change notification settings - Fork 53
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
feat: Create errors package #548
Conversation
@AndrewSisley, are you using anything as a reference? |
The description in the epic, and a vague memory of earlier conversations on this. Plus I'd like to stick to a structured-logging type style - having constant messages makes a lot of things much easier to do (for us and defra consumers) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant your reference for the pattern you're using :)
Reviewable status: 0 of 1 files reviewed, all discussions resolved (waiting on @AndrewSisley)
Ah :) Is roughly the same pattern as the logger package, which in turn is similar to other loggers (and Error[F]/Sprint[F] type funcs) |
75a9a6e
to
1554d2a
Compare
I don't think the error package should follow a logger pattern. Have a look at this for reference: https://github.com/upspin/upspin/tree/master/errors |
Is not a logger pattern. It just structures any attached variables so they don't pollute a core error message. And uses [S] as a func suffix in a similar way to [F] is used in Sprint/Error[F].
I would prefer if we would avoid defining our own error type, sticking to the out-of-the-box Go type would be much preferred IMO - should create fewer complications for any consumers of these errors. |
1554d2a
to
e1b024f
Compare
errors/errors.go
Outdated
|
||
// ErrorS creates a new Defra error, suffixing any key-value | ||
// pairs provided, and a stacktrace. | ||
func ErrorS(message string, keyvals ...KV) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really not sure call stack inclusion should be based on the error - might be better to have that configurable across defra (like log levels)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to global-config approach
88eac0a
to
5633b75
Compare
I disagree here. I think we need our own custom error type. We will still be returning just a normal |
We might have a different set of requirements in mind. What functionality do you think our errors should have? EDIT: My bad, |
I expect it to keep track of the original error (i.e. system or 3rd party package error), the error we might wrap with (custom message or internal error kind) and the stack trace. |
This does that. |
It does but it doesn't follow the standard lib error interface implementation and as far as I can tell doesn't seem like it would work with |
idea: A pattern to consider is to have multiple error interfaces that are optimized for the various error domains Defra deals with, as exemplified in https://github.com/carlmjohnson/resperr/blob/master/resperr.go |
I think this error package PR jumped the gun a bit. I think we prob need to ideate the goals/priorities/design of our defra native errors stuff, as there are a bunch of constraints (re standard I think @fredcarle should take point a bit (obviously with input from everyone) as he has the most (short of me 😃) experience with (idiomatic) Go, and this package has as much to do with Go best practices/patterns as it does with defra internal error goals. I agree, that it needs to be based on the Go errors "wrapping" semantics support Happy to take this to a disord thread, or on the original #488 issue. note: this errors initiative was spawned from a previous PR (can't remember which tbh), and this discord thread. |
Again, the current setup allows all of the requirements laid out in your comment.
It does not do this. I think you are misunderstanding things. |
Maybe we need a call where you explain how that is the case because I don't see it. Maybe it's in how you see us use it? |
Will probs get this working first - will make chatting about it easier |
d10786e
to
f08077a
Compare
Codecov Report
@@ Coverage Diff @@
## develop #548 +/- ##
===========================================
+ Coverage 58.07% 58.25% +0.18%
===========================================
Files 144 146 +2
Lines 16578 16652 +74
===========================================
+ Hits 9627 9701 +74
Misses 6043 6043
Partials 908 908
|
d09017c
to
13eb1fc
Compare
errors/error.go
Outdated
@@ -0,0 +1,85 @@ | |||
// Copyright 2022 Democratized Data Foundation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: Rename file to defraError.go
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure :)
- rename file
errors/error.go
Outdated
return e.inner | ||
} | ||
|
||
func (e *defraError) Format(f fmt.State, verb rune) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: Perhaps a few words on the possible options for formatting (i.e. %v
, %+v
, %s
and %q
) would be useful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am unsure if this func should ever be called directly, and as such saw little value in externally visible documentation (the code is brief and more accurate than any possible comment if looking at the implementation). But it is public and so I cannot really argue against a request unless someone with more golang experience in this area chimes in
- Probably add func docs here
errors/error.go
Outdated
} | ||
|
||
func (e *defraError) Is(other error) bool { | ||
switch otherTyped := other.(type) { //nolint:errorlint |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: does this work when other
is itself multiple wrapped errors?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is only shallow compares other
, so other.inner
(s) appear irrelevant
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: The stdlib Is
"reports whether any error in err's chain matches target." This divergence in how we implement the interface should be documented.
errors/errors.go
Outdated
) | ||
|
||
const InnerErrorKey string = "Inner" | ||
const StackKey string = "Stack" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: move StackKey
to the other file (error.go
), as it is the only place it is used in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do.
- move const (leave public as it is outputted via format and thus public)
/* | ||
The Go test flag `-race` messes with the stacktrace causing this function's frame to be ommited from | ||
the stacktrace, as our CI runs with the `-race` flag, these assertions need to be disabled. | ||
|
||
// Assert that the first line starts with the error message and contains this [test] function's stacktrace-line - | ||
// including file, line number, and function reference. An exact string match should not be used as the stacktrace | ||
// is machine dependent. | ||
assert.Regexp(t, fmt.Sprintf("^%s\\. Stack: .*\\/defradb\\/errors\\/errors_test\\.go:[0-9]+ \\([a-zA-Z0-9]*\\)", errorMessage), result) | ||
// Assert that the error contains this function's name, and a print-out of the generating line. | ||
assert.Regexp(t, "TestErrorFmtvWithStacktrace: err := Error\\(errorMessage\\)", result) | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thought: Why not use go's build tag to turn race off for these assertions or if that's not possible then this file? I think it was something like this:
// +build !race
But never used it before, so not a 100% if it works properly, just an idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I searched for a way of doing this and couldn't find one, will try, as it is nice if these assertions run otherwise
- Try comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I quickly looked into this and it seems highly unlikely that -race
affects compilation/would not be accessible via build tags. Build tags also can only be applied to the entire file, which would mean breaking up these tests into two files (plus the original shared), which on it's own would probably not be worth it. Leaving as it, but thanks for letting me such a thing exists in go, even if it seems somewhat error prone lol:
To distinguish build constraints from package documentation, a build constraint should be followed by a blank line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If all it takes is splitting these tests into two files and in return it would let us test these assertions, IMO is still worth it. The final decision is up to your discretion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be 3 files - shared, race, and non-race. But I think we'd need to create our own build flags for the CI to use and -race
would still fail if you don't set the custom flags. Leaving as is
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case agree with omitting.
d782bad
to
9d615e4
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Would also suggest to wait for atleast one other approval before merging.
Cheers, will leave it for a bit longer, in case anyone else chimes in, but I won't wait until Fred gets back from holiday - easier to just tweak stuff post merge than to leave this PR hanging around. |
errors/errors.go
Outdated
// New creates a new Defra error, suffixing any key-value | ||
// pairs provided. | ||
// | ||
// A stacktrace will be yielded if formating with a `+`, e.g `fmt.Sprintf("%+v", err)`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: formating
-> formatting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- typo
return e.message == otherTyped.message | ||
default: | ||
otherString := other.Error() | ||
return e.message == otherString || e.Error() == otherString || errors.Is(e.inner, other) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: because this is extra and different behavior compared above the stdlib - i.e. the stdlib isn't comparing error strings like this - documenting the Is(...)
func would be beneficial
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the standard lib comparing if not the error string? Will look into
- maybe doc?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
stdlib does essentially compare strings when dealing with errors created by errors.New
- leaving as is
|
||
assert.ErrorIs(t, err2, errors.New(errorMessage1)) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: Add a test using a chain of at least 2 defra errors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TestErrorWrap tests this
_ fmt.Formatter = (*defraError)(nil) | ||
) | ||
|
||
type defraError struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: why not implement func (e *defraError) As(target any) bool
as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What behaviour are you looking to gain by doing so?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well essentially to not change too much the existing codebase by using our errors pkg. in my understanding, when we do importing our errors
pkg instead of the errors one - everywhere we use errors.Is
breaks. we can import both libs but might as well just import one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
everywhere we use errors.Is breaks
This is incorrect as far as I can see, errors.Is
should not break, and is even used by the tests for this package - assert.ErrorIs
uses/calls errors.Is
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just to be a bit more clear: using errors.Is()
would only break when import our library as errors
because don't implement the function, so that means when we do use our errors
in the rest of the codebase we will have to import both stdlib errors
and our errors
pkg
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you mean because of the package name clash? Is easy to solve by aliasing the import. I'm really not looking to replace the errors.Is
function in this PR (and am doubtful as to whether we ever should)
} | ||
|
||
func withStackTrace(message string, keyvals ...KV) *defraError { | ||
stackBuffer := make([]uintptr, MaxStackDepth) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: which "error generation library" is it from? It still is unclear where the 4 is coming from.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This library. I'll adjust the comment.
- Improve wording
goErrors "github.com/go-errors/errors" | ||
) | ||
|
||
// todo: make this configurable: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thought: in-code todos like this seems to be a natural occurence, and I'm fine with it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We agreed to only allow todos if linked to a ticket - isn't too bad I think :)
errors/defraError.go
Outdated
|
||
// Format writes the error into the given state. | ||
// | ||
// Currently the following runes are supported: `v[+]` (+ also writes out the stacktrace), `s`, `q` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: end sentence with a period
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- .
} | ||
} | ||
|
||
// New creates a new Defra error, suffixing any key-value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: can be on one line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leaving as is - doesn't matter
errors/error.go
Outdated
} | ||
|
||
func (e *defraError) Is(other error) bool { | ||
switch otherTyped := other.(type) { //nolint:errorlint |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: The stdlib Is
"reports whether any error in err's chain matches target." This divergence in how we implement the interface should be documented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a few suggestions, question, and nitpicks before approval
return builder.String() | ||
} | ||
|
||
func (e *defraError) Is(other error) bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion(unsure): additionally implement a func Is(err, target error) bool
to facilitate porting from stdlib to use defra errors pkg.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that would potentially allow to avoid import both defra errors and stdlib errors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am doubtful as to whether that would be worth the time/effort, and I consider it out of scope for this PR. Maybe something to consider in the future though
I can't find a way to reply to this comment normally so doing it here. This is the implemented (and tested) behaviour, there should be no divergence :) |
IMO there is divergence specifically in how our implementation tests for the error message string equivalent versus the stdlib which doesn't |
The stlib does implicitly - the 'default' |
13dfa36
to
70a0333
Compare
70a0333
to
af6dc0b
Compare
* Create errors package
Relevant issue(s)
Resolves #545
Description
Creates a defra errors package that can be used by any defra code to create standardized errors and to keep any implementation specifics all in one place.
It does not hook it up to anywhere in our codebase, that can be done in a later PR.
Specify the platform(s) on which this was tested: