ocket8888 opened a new pull request #6665:
URL: https://github.com/apache/trafficcontrol/pull/6665


   This PR adds a utility function to 
`github.com/apache/trafficcontrol/lib/go-util` that allows wrapping errors such 
that they may be compared to error constants using `errors.Is` without paying 
the cost of the expensive reflection used by the standard library's 
`fmt.Errorf` function. The idea is to use this function to preserve the 
identity of an underlying error when you want to add context to an error - e.g. 
prepending "querying cdns: " to an error message encountered when querying the 
`public.cdns` database table - in situations where the performance cost of 
using `fmt.Errorf` is too high and instead we are currently using `errors.New` 
with string concatenation - e.g. `errors.New("querying cdns: " + err.Error())`.
   
   This PR has a few benchmarks for the different ways an error can be 
constructed from an existing one. The common case of making a database query 
for some Traffic Ops object (e.g. a CDN in this case) was chosen, such that the 
error
   message when printed reads
   
   > querying cdns: sql: no rows in result set
   
   The benchmarks cover the new `WrapError` function as well as the two methods 
most commonly used today: errors.New("context: " + err.Error())` and  
`fmt.Errorf("context: %w", err)`.
   
   There are two different sets of benchmarks, a simpler set that just 
repeatedly constructs a single, simple error many times like stated above, and 
a more intensive test that recursively wraps a single error many times.
   
   <details><summary>Testing Platform Information</summary>
   
   ```
   goos: linux
   goarch: amd64
   pkg: github.com/apache/trafficcontrol/lib/go-util
   cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
   ```
   
   </details>
   
   The simpler benchmarks as-written simply perform the operation necessary to 
construct the error using the prescribed method and throw the resulting value 
away. Under those conditions it appears that the utility provided here is 
between 100 and 300 times faster than the currently favored `errors.New("doing 
something: "+err.Error)` method.
   
   ```
   BenchmarkErrorStringConcatenation-12    30479540       61.17 ns/op    48 
B/op  1 allocs/op
   BenchmarkErrorWrapping-12             1000000000        0.2495 ns/op   0 
B/op  0 allocs/op
   BenchmarkErrorf-12                       6151614      200.4 ns/op     80 
B/op  2 allocs/op
   ```
   
   That's the most impressive set of results I could get, which is why I kept 
that implementation. But I tried it a couple different ways. The other ways are 
less impressive, but in none of the benchmark implementations was 
error-wrapping using this utility
   slower than `errors.New` with string concatenation.
   
   <details><summary>Storing the returned value (overwriting each 
time)</summary>
   
   Using the code:
   ```go
   func BenchmarkErrorStringConcatenation(b *testing.B) {
        var err error
        for i := 0; i < b.N; i++ {
                err = errors.New("querying for cdns: " + sql.ErrNoRows.Error())
        }
        b.StopTimer()
        b.Logf("finished benchmark string concatenating '%s'", err)
   }
   
   func BenchmarkErrorWrapping(b *testing.B) {
        var err error
        for i := 0; i < b.N; i++ {
                err = WrapError("querying for cdns", sql.ErrNoRows)
        }
        b.StopTimer()
        b.Logf("finished benchmark wrapping '%s'", err)
   }
   
   func BenchmarkErrorf(b *testing.B) {
        var err error
        for i := 0; i < b.N; i++ {
                err = fmt.Errorf("querying for cdns: %w", sql.ErrNoRows)
        }
        b.StopTimer()
        b.Logf("finished benchmark fmt.Errorf-ing '%s'", err)
   }
   ```
   
   I got these results:
   ```
   BenchmarkErrorStringConcatenation-12  18886911    78.70 ns/op   64 B/op   2 
allocs/op
   BenchmarkErrorWrapping-12             39583196    39.77 ns/op   32 B/op   1 
allocs/op
   BenchmarkErrorf-12                     5614521   213.5 ns/op    80 B/op   2 
allocs/op
   ```
   
   Logging output has been omitted. Logging was only necessary because 
otherwise the value of `err` is never read, which causes compilation to fail. 
But since the timer is stopped before logging, it shouldn't be affecting 
results.
   
   </details>
   
   <details><summary>Storing the returned value (keeping every one in a 
slice)</summary>
   
   Using the code:
   
   ```go
   func BenchmarkErrorStringConcatenation(b *testing.B) {
        errs := make([]error, 0, b.N)
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
                errs = append(errs, errors.New("querying for cdns: 
"+sql.ErrNoRows.Error()))
        }
   }
   
   func BenchmarkErrorWrapping(b *testing.B) {
        errs := make([]error, 0, b.N)
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
                errs = append(errs, WrapError("querying for cdns", 
sql.ErrNoRows))
        }
   }
   
   func BenchmarkErrorf(b *testing.B) {
        errs := make([]error, 0, b.N)
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
                errs = append(errs, fmt.Errorf("querying for cdns: %w", 
sql.ErrNoRows))
        }
   }
   ```
   
   I got the results:
   ```
   BenchmarkErrorStringConcatenation-12         13846206                75.36 
ns/op           64 B/op          2 allocs/op
   BenchmarkErrorWrapping-12                    33168076                32.72 
ns/op           32 B/op          1 allocs/op
   BenchmarkErrorf-12                            6001684               194.0 
ns/op            80 B/op          2 allocs/op
   ```
   
   </details>
   
   I repeated each test a few times, and I'm satisfied they're consistent at 
least within order of magnitude. I never saw a situation where wrapping either 
used more memory or was slower than string concatenation.
   
   The more complex benchmarks are even more impressive than the best result 
from the simpler benchmarks. The performance difference between the new 
function and the string concatenation method widens as the number of of times 
an error is
   wrapped in a context grows. Doing multiple wrapping on a single error a 
great many times is more than 1000 times faster than constructing the same 
error message by repeatedly concatenating strings:
   
   ```
   BenchmarkNestedWraps-12                      17224899                72.80 
ns/op           32 B/op          1 allocs/op
   BenchmarkNestedStringConcatenations-12         111364            110801 
ns/op         1062059 B/op          2 allocs/op
   BenchmarkNestedErrorf-12                        22525            108500 
ns/op          430623 B/op          4 allocs/op
   ```
   
   The general case for our errors in ATC is that they are wrapped usually no 
less than once and no more than 5 times, at a guess. Probably 2 or 3 is most 
common. I think, given the results, under those conditions we shouldn't expect 
such a
   drastic difference - and in all likelihood those operations aren't 
performance bottlenecks anyway. Still, it doesn't seem like there's any 
situation where constructing an error through this kind of wrapping would be 
better accomplished through string concatenation, and since this method doesn't 
destroy the identity of the underlying error it seems best to use it instead in 
scenarios where performance needs preclude the use of `fmt.Errorf`. In cases 
where performance isn't an issue, `fmt.Errorf` should probably still be used, 
IMO, to avoid doing things like `util.WrapError(fmt.Sprintf(...), err)`.
   <hr/>
   
   ## Which Traffic Control components are affected by this PR?
   - None - function is currently unused. Overhauls should be in separate PRs, 
if at all.
   
   ## What is the best way to verify this PR?
   Make sure the provided test passes.
   
   ## PR submission checklist
   - [x] This PR has tests
   - [x] This PR has documentation
   - [ ] This PR has a CHANGELOG.md entry
   - [x] This PR **DOES NOT FIX A SERIOUS SECURITY VULNERABILITY**


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to