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

[fix] - Propagate Async File Handling Errors #3403

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

ahrav
Copy link
Collaborator

@ahrav ahrav commented Oct 13, 2024

Description:

This PR updates error reporting during file processing. Previously, each Handler spawned a goroutine to handle file processing and returned a channel for the caller to collect results. However, this method only returned processed data and failed to propagate errors due to the file processing happening in separate goroutines. As a result, errors were simply logged, and critical errors were returned within the goroutine, leaving the calling function unaware of any issues. This became especially problematic when HandleFile was called via handleBinary, as the reader passed to HandleFile is a pipe. If the caller isn’t informed of an error, it can't properly consume the reader, potentially leaving it open and exhausting resources.

To resolve this, the PR introduces the DataOrErr struct, allowing handlers to return both critical and non-critical errors to the caller. This ensures better resource management and prevents resource leaks.

Checklist:

  • Tests passing (make test-community)?
  • Lint passing (make lint this requires golangci-lint)?

@ahrav ahrav force-pushed the bug-fix-potential-resource-leaks branch from 8268de0 to 264298f Compare October 13, 2024 03:00
// It takes an io.Reader and checks if it supports seeking.
// If the reader supports seeking, it is stored in the seeker field.
func NewBufferedReaderSeeker(r io.Reader) *BufferedReadSeeker {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename.

@@ -82,9 +82,6 @@ func NewBufferedReaderSeeker(r io.Reader) *BufferedReadSeeker {
)

seeker = asSeeker(r)
if seeker == nil {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was removed to avoid checking out a buffer eagerly. We already lazily check out a buffer when needed.

@rgmz
Copy link
Contributor

rgmz commented Oct 13, 2024

🐇 🕳️

@ahrav ahrav marked this pull request as ready for review October 14, 2024 02:19
@ahrav ahrav requested review from a team as code owners October 14, 2024 02:19
Base automatically changed from fix-file-descriptor-exhaustion to main October 15, 2024 19:11
}

go func() {
ctx, cancel := logContext.WithTimeout(ctx, maxTimeout)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you remove this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought I left a comment, but it was probably on the closed PR. This was removed because the context timeout is set at the call site, so setting it here has no effect since it inherits the context from HandleFile.
here

err = fmt.Errorf("panic occurred: %v", r)
panicErr = fmt.Errorf("panic occurred: %v", r)
}
ctx.Logger().Error(panicErr, "Panic occurred when attempting to open archive")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this error in particular loggable?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't be. Logging should be handled while consuming from the dataOrErrChan at the call-site. Will remove thanks.

var (
ErrEmptyReader = errors.New("reader is empty")

// ErrCriticalProcessing indicates a critical error that should halt processing.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Critical" is one of those words that unfortunately doesn't really convey any information to someone who doesn't already know the domain. Should these errors halt processing of the source? The file? Something else?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I agree. I'll update it. 👍


// If an error occurs during MIME type detection, it is important we close the BufferedReaderSeeker
// to release any resources it holds (checked out buffers or temp file).
var err error
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

err gets shadowed later in this function. If that's on purpose, I find it very confusing, and would understand much better if you used a different variable name.

// handler to manage file extraction or processing.
//
// The function will return nil (success) in the following cases:
// - If the reader is empty (ErrEmptyReader)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it return nil? Or ErrEmptyReader?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should return nil if the error from newFileReader is ErrEmptyReader. Do think think it should return ErrEmptyReader instead?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that makes sense, I was just confused because within the context the comment it wasn't clear that an empty reader was signaled (elsewhere) by ErrEmptyReader

}

go func() {
ctx, cancel := logContext.WithTimeout(ctx, maxTimeout)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question as above: why the removal?

}
}()

var rpm *rpmutils.Rpm
rpm, err = rpmutils.ReadRpm(input)
if err != nil {
ctx.Logger().Error(err, "error reading RPM")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these not real errors?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand why you thought I removed them. 😢 In the defer func(), we call .measureLatencyAndHandleErrors, which uses the shadowed err. This eventually gets logged when we consume from dataOrErrChan. I can avoid shadowing by explicitly sending err to the channel.

@ahrav ahrav mentioned this pull request Oct 18, 2024
2 tasks
@ahrav ahrav requested review from a team and rosecodym October 20, 2024 02:49
Copy link
Collaborator

@rosecodym rosecodym left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what looks like three separable PRs here:

  • Moving around some metric observations
  • Switching to DataOrErr
  • Surfacing a new archive extraction error

Could they be done separately?

// handler to manage file extraction or processing.
//
// The function will return nil (success) in the following cases:
// - If the reader is empty (ErrEmptyReader)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that makes sense, I was just confused because within the context the comment it wasn't clear that an empty reader was signaled (elsewhere) by ErrEmptyReader

@ahrav
Copy link
Collaborator Author

ahrav commented Oct 22, 2024

I see what looks like three separable PRs here:

  • Moving around some metric observations
  • Switching to DataOrErr
  • Surfacing a new archive extraction error

Could they be done separately?

Yep yep, will do. 👍

Comment on lines +152 to +161
// DataOrErr represents a result that can either contain data or an error.
// The Data field holds the byte slice of data, and the Err field holds any error that occurred.
// This structure is used to handle asynchronous file processing where each chunk of data
// or potential error needs to be communicated back to the caller. It allows for
// efficient streaming of file contents while also providing a way to propagate errors
// that may occur during the file handling process.
type DataOrErr struct {
Data []byte
Err error
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticing that DataOrErr pretty much describes Rust's Result enum. There are some useful methods if you wanted to follow their pattern.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦀 🦀 🦀

Copy link
Collaborator

@rosecodym rosecodym Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, if we do go down this route in a more organized way it'd be good to look at prior art. Rust's version of this is probably the most most usable by people other than functional programming weirdos. Kotlin has a third-party version that references the concept's monadic roots and references some prior art like the F# version and the inscrutable Haskell dorks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants