[WIP & POC] Refactor concurrency into simpler errorgroup design - #141
[WIP & POC] Refactor concurrency into simpler errorgroup design #141tonymet wants to merge 6 commits into
Conversation
|
Amazing work @tonymet ! Luck would have it, I just started rewriting https://github.com/Ullaakut/cameradar and was going to make some fundamental changes to this library as well along the way, so I am extremely glad you beat me to it 😄 IMO progress bar support can be added back in a later PR, that is not a huge deal for me especially considering how adding that feature contributed to reducing code quality IIRC. |
Ullaakut
left a comment
There was a problem hiding this comment.
Overall lots of good things, but this highlights how much of my code is massively outdated, non-idiomatic and overall garbage quality and needs a deeper rewrite.
If you want we could join our efforts and I can join you on this PR by committing to your branch and making further changes as well for what I had planned. Let me know if that works for you, would love your feedback on my changes 👍
| if err := cmd.Start(); err != nil { | ||
| return result, warnings, err | ||
| } else if warnings, err := s.processNmapResult(s.ctx, &result, stdoutPipe, stderrPipe); err != nil { | ||
| return result, warnings, err | ||
| } else if err := cmd.Wait(); err != nil { | ||
| return result, warnings, err |
There was a problem hiding this comment.
No need for elses here, since we return in the other statements. Also personally, I am fine with inlining error checks only when the error is the only thing returned, bit of a personal preference there :p
Also I would rather add a bit more context to the errors while we're at it.
| if err := cmd.Start(); err != nil { | |
| return result, warnings, err | |
| } else if warnings, err := s.processNmapResult(s.ctx, &result, stdoutPipe, stderrPipe); err != nil { | |
| return result, warnings, err | |
| } else if err := cmd.Wait(); err != nil { | |
| return result, warnings, err | |
| if err := cmd.Start(); err != nil { | |
| return result, warnings, fmt.Errorf("starting command: %w", err) | |
| } | |
| warnings, err := s.processNmapResult(s.ctx, &result, stdoutPipe, stderrPipe) | |
| if err != nil { | |
| return result, warnings, fmt.Errorf("processing output: %w", err) | |
| } | |
| if err := cmd.Wait(); err != nil { | |
| return result, warnings, fmt.Errorf("waiting for command: %w", err) |
WDYT?
There was a problem hiding this comment.
the if / else if / else is more of a personal style thing to keep variables scoped to the if {} blocks . it can look clumsy in some of these. I defer to your style here
| } else { | ||
| err = s.processNmapResult(result, warnings, &stdout, &stderr, done, doneProgress) | ||
| return result, warnings, err | ||
| } |
There was a problem hiding this comment.
| } else { | |
| err = s.processNmapResult(result, warnings, &stdout, &stderr, done, doneProgress) | |
| return result, warnings, err | |
| } | |
| return result, warnings, nil |
If I'm reading the diff correctly, getting here means err is nil so this is clearer IMO
| if warnings, err = checkStdErr(stderr); err != nil { | ||
| return err | ||
| } else { |
There was a problem hiding this comment.
Same comment here, no need for the else since we return in the error case already.
| if warnings, err = checkStdErr(stderr); err != nil { | |
| return err | |
| } else { | |
| if warnings, err = checkStdErr(stderr); err != nil { | |
| return fmt.Errorf("stderr: %w", err) | |
| } else { |
| for _, warning := range stderrSplit { | ||
| warning = strings.Trim(warning, " ") | ||
| *warnings = append(*warnings, warning) | ||
| var warnings = make([]string, 0) |
There was a problem hiding this comment.
| var warnings = make([]string, 0) | |
| var warnings []string |
If we don't initialize it with a predetermined size, IMO it's better to just use the zero value this way
| for scanner.Scan() { | ||
| warning := scanner.Text() | ||
| warnings = append(warnings, warning) | ||
| switch { | ||
| case strings.Contains(warning, "Malloc Failed!"): | ||
| return ErrMallocFailed | ||
| return warnings, ErrMallocFailed | ||
| case strings.Contains(warning, "requires root privileges."): | ||
| return ErrRequiresRoot | ||
| // TODO: Add cases for other known errors we might want to guard. | ||
| default: | ||
| return warnings, ErrRequiresRoot |
There was a problem hiding this comment.
| for scanner.Scan() { | |
| warning := scanner.Text() | |
| warnings = append(warnings, warning) | |
| switch { | |
| case strings.Contains(warning, "Malloc Failed!"): | |
| return ErrMallocFailed | |
| return warnings, ErrMallocFailed | |
| case strings.Contains(warning, "requires root privileges."): | |
| return ErrRequiresRoot | |
| // TODO: Add cases for other known errors we might want to guard. | |
| default: | |
| return warnings, ErrRequiresRoot | |
| for scanner.Scan() { | |
| warning := scanner.Text() | |
| switch { | |
| case strings.Contains(warning, "Malloc Failed!"): | |
| return warnings, ErrMallocFailed | |
| case strings.Contains(warning, "requires root privileges."): | |
| return warnings, ErrRequiresRoot | |
| } | |
| warnings = append(warnings, warning) |
Doing it in this order avoids having the warnings also contain the error we're returning for, seems more logical to me wdyt?
|
|
||
| type testStreamer struct{} | ||
|
|
||
| var ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) |
There was a problem hiding this comment.
- Should not be a global if not absolutely necessary (in this case, it isn't)
- Since go 1.24 we have access to
t.Contextwhich should be used in any new code imo, and no need to add an extra timeout anymore, this will cleanup the components nicely at the end of the test
| dat, err := os.Open("tests/xml/scan_base.xml") | ||
| if err != nil { | ||
| panic(err) | ||
| } |
There was a problem hiding this comment.
| dat, err := os.Open("tests/xml/scan_base.xml") | |
| if err != nil { | |
| panic(err) | |
| } | |
| dat, err := os.Open("tests/xml/scan_base.xml") | |
| require.NoError(t, err) |
If you dont mind, I can make a pass on your branch to uniformize/modernize/fix all of the tests as well, might be quicker than by going through PR comments since there is a lot to change 😁
| if contentAll, err := io.ReadAll(content); err != nil { | ||
| return err | ||
| } else { | ||
| return xml.Unmarshal(contentAll, result) | ||
| } |
There was a problem hiding this comment.
| if contentAll, err := io.ReadAll(content); err != nil { | |
| return err | |
| } else { | |
| return xml.Unmarshal(contentAll, result) | |
| } | |
| b, err := io.ReadAll(content) | |
| if err != nil { | |
| return err | |
| } | |
| return xml.Unmarshal(b, result) |
|
great feedback and I love the enthusiasm. It might be best to have a chat about your goals and I can see how to best contribute changes. If you are currently planning a major overhaul, I would recommend keeping this PR as a draft and pulling these concepts into your larger work until your changes stabilize. Many of the changes in this PR are not quite ready to merge and were done just to stabilize the branch for testing with my app. the core functionality of
I recently had a similar experience with gcloud-go -- the original version had tons of goroutines, channels, workers etc to manage concurrent uploads -- and then I realized I was overthinking concurrency patterns in golang and replaced it with 80% fewer lines using errgroup. |
|
just checking in on how you'd like to proceed. if you'd like to integrate manually or you could apply your changes first and i could rebase mine on top |
|
Sorry, lots of stuff going on IRL, I have no time for open source work for now unfortunately. I'll let you decide on how you want to handle this for now 👍 Should be back in a few weeks. |
|
no worries and thanks for the updates. let's check when you get back. take care! |
hi @Ullaakut big thanks this project has been a huge help for a vulners scanner I built.
The general goal is to reduce state, channels and concurrency to make debugging the core parsing engine more intuitive.
High level changes
Benefits
Broken
[]string{nil} / []string{}I understand it's a major reworking of the core component. If it's desirable, I can restore the missing functionality into a proper PR. If it's too big a change, I'm happy to close it out.