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

Improve error-handling for deleted files, messages, and replies #2231

Merged
merged 1 commit into from
Oct 24, 2024

Conversation

rocodes
Copy link
Contributor

@rocodes rocodes commented Sep 17, 2024

Status

Ready for review

Description

Fixes #2217 by ensuring that database queries expecting exactly one File, Message, or Reply are error-handled. Note: This is a deliberately limited-scope fix; a broader discussion about database error-handling improvements will happen in #2222

Test Plan

  • Visual Review
  • CI
  • Basic functionality testing (login, sync, download files and messages, delete conversation)
  • Reproduce Handle sqlalchemy error + app crash when a source that has ongoing download job is deleted  #2217 and ensure no app crash:
  • Enable debug logs on the client. Submit a large file for a source (or a normal size file but use toxiproxy so that your download speed is limited).
  • Begin downloading the file, then quicky, before the download can complete, delete the source.
  • The app does not crash
  • There is a warning in the logic.py logs about the deleted file ("get_file for uuid not found in database")
  • The debug logs display the file uuid and stacktrace

Checklist

If these changes modify code paths involving cryptography, the opening of files in VMs or network (via the RPC service) traffic, Qubes testing in the staging environment is required. For fine tuning of the graphical user interface, testing in any environment in Qubes is required. Please check as applicable:

  • I have tested these changes in the appropriate Qubes environment
  • I do not have an appropriate Qubes OS workstation set up (the reviewer will need to test these changes)
  • These changes should not need testing in Qubes

If these changes add or remove files other than client code, the AppArmor profile may need to be updated. Please check as applicable:

  • I have updated the AppArmor profile
  • No update to the AppArmor profile is required for these changes
  • I don't know and would appreciate guidance

If these changes modify the database schema, you should include a database migration. Please check as applicable:

  • I have written a migration and upgraded a test database based on main and confirmed that the migration is self-contained and applies cleanly
  • I have written a migration but have not upgraded a test database based on main and would like the reviewer to do so
  • I need help writing a database migration
  • No database schema changes are needed

"""
Handle SQLAlchemy exceptions and return relevant information to controller.
"""

Copy link
Contributor Author

@rocodes rocodes Sep 17, 2024

Choose a reason for hiding this comment

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

@legoktm : I know we talked about not adding another Exception class, and using the existing ones. However, I feel like there's a case to be made here for a generic exception class that wraps SQLAlchemy exceptions, for 2 reasons:

1: far too many parts of the client are aware of sqlalchemy exceptions, including downloads.py, crypto.py, etc (see #2222); for errorhandling consistency and to avoid uncaught exceptions leading to app crashes, I would prefer storage.py to anticipate all sqlalchemy errors and raise something db-agnostic to its callers; 2: I don't think we have a great answer in the code already (ie it's not a DownloadException etc).

For the purposes of this PR, I don't catch all SQLAlchemy exceptions and raise this one, but I'm proposing that for #2222. I also think it will be useful in that PR to be able to distinguish between "an 'anticipatable' sqlalchemyexception in our case (ie NoResultFound when searching for one() record would be anticipated under the sync/delete race condition) vs an unexpected SQLAlchemy exception.

I would welcome your thoughts though if you think there is a different/preferable way of doing this.

Copy link
Member

@legoktm legoktm Sep 20, 2024

Choose a reason for hiding this comment

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

I think the problem is that we're losing information here, we're turning a very specific NoResultFound error into a generic SDDatabaseError, which (in the future) could be any error, like db corruption or something else. And those types of different underlying errors should probably be handled differently; i.e. instead of having log messages like "likely deleted record", we should just know it absolutely is a deleted record and not something else.

IMO instead of using exceptions, I think we'd benefit from making the functions like:

def get_file(session: Session, uuid: str) -> File | None:
    try:
        return session.query(File).filter_by(uuid=uuid).one()
    except NoResultFound as e:
        return None

which is basically what you did in logic.get_file() :)

I think this addresses your concerns as the caller isn't aware of SQLAlchemy errors, it just knows there's a possibility that get_file won't return a file. The main advantage is now that it's encoded in the type system, mypy can now flag all the places this condition isn't being checked instead of us needing to manually remember to check the exception.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This all makes sense, and I was probably chickening out a bit because returning File | None, Message | None etc would require a lot more client code changes, but is the better approach.

Zooming out a bit though, even if we don't do it in this case (since in this case we can create defined behaviour instead), I do still think it's worth encapsulating SQLAlchemy errors so that non db classes don't have to be aware of them; right now gui classes try-catch sqlalchemy errors and this feels messy and/or prone to problems. Are you open to that in a later PR, if the precondition is "make sure that we do the most with types that we can first"/make sure we don't lose error fidelity?

(The "probably a deletion" issue is going to be approximately true regardless of whether it's done at the return type level or the error level, because there could be other things that would lead to the missing entry other than a remote deletion, but you're right I don't have to be so cagey in the messages.)

@rocodes rocodes force-pushed the 2217-error-handling-deleted-file branch 2 times, most recently from e378b79 to 66d8605 Compare September 17, 2024 13:19
@rocodes rocodes added this to the 0.14.0 milestone Sep 17, 2024
@rocodes
Copy link
Contributor Author

rocodes commented Sep 17, 2024

(Adding this to the 0.14.0 milestone even though it isn't strictly in the "multi-delete" category because it's a bugfix, does related to delete actions, and will be released with 0.14.0)

@rocodes rocodes marked this pull request as ready for review September 17, 2024 13:51
@rocodes rocodes requested a review from a team as a code owner September 17, 2024 13:51
@legoktm legoktm self-assigned this Sep 20, 2024
Copy link
Member

@legoktm legoktm left a comment

Choose a reason for hiding this comment

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

See inline comments

"""
Handle SQLAlchemy exceptions and return relevant information to controller.
"""

Copy link
Member

@legoktm legoktm Sep 20, 2024

Choose a reason for hiding this comment

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

I think the problem is that we're losing information here, we're turning a very specific NoResultFound error into a generic SDDatabaseError, which (in the future) could be any error, like db corruption or something else. And those types of different underlying errors should probably be handled differently; i.e. instead of having log messages like "likely deleted record", we should just know it absolutely is a deleted record and not something else.

IMO instead of using exceptions, I think we'd benefit from making the functions like:

def get_file(session: Session, uuid: str) -> File | None:
    try:
        return session.query(File).filter_by(uuid=uuid).one()
    except NoResultFound as e:
        return None

which is basically what you did in logic.get_file() :)

I think this addresses your concerns as the caller isn't aware of SQLAlchemy errors, it just knows there's a possibility that get_file won't return a file. The main advantage is now that it's encoded in the type system, mypy can now flag all the places this condition isn't being checked instead of us needing to manually remember to check the exception.

client/securedrop_client/logic.py Outdated Show resolved Hide resolved

except storage.SDDatabaseError as e:
# This shouldn't happen, it's been downloaded.
logger.error("Failed to find file uuid in database")
Copy link
Member

Choose a reason for hiding this comment

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

Let's have this explain your comment? e.g. "Failed to find file that was just downloaded in the database."

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you! Addressed in 138643b

client/securedrop_client/gui/widgets.py Outdated Show resolved Hide resolved
client/securedrop_client/storage.py Outdated Show resolved Hide resolved
client/securedrop_client/logic.py Show resolved Hide resolved
client/securedrop_client/logic.py Outdated Show resolved Hide resolved
@rocodes rocodes force-pushed the 2217-error-handling-deleted-file branch 4 times, most recently from 6326b43 to 138643b Compare October 24, 2024 18:02
@rocodes
Copy link
Contributor Author

rocodes commented Oct 24, 2024

IMO instead of using exceptions, I think we'd benefit from making the functions like [...]

I agree and not sure why I didn't look at it that way before - addressed in 138643b, thank you :)

@rocodes rocodes requested review from legoktm and a team October 24, 2024 18:07
Refactor FileWidget to accept a File instead of a file uuid, avoiding
potentially-null database query during widget construction.

Add tests for deleted db record condition.

Fixes #2217.
@legoktm legoktm force-pushed the 2217-error-handling-deleted-file branch from 138643b to a9f0590 Compare October 24, 2024 22:40
Copy link
Member

@legoktm legoktm left a comment

Choose a reason for hiding this comment

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

Thanks, it looks great now. I just made a slight adjustment to your commit message to break the long lines.

@legoktm legoktm enabled auto-merge October 24, 2024 22:41
@legoktm legoktm added this pull request to the merge queue Oct 24, 2024
Merged via the queue into main with commit edde247 Oct 24, 2024
58 checks passed
@legoktm legoktm deleted the 2217-error-handling-deleted-file branch October 24, 2024 23:19
@rocodes rocodes mentioned this pull request Nov 6, 2024
34 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

Handle sqlalchemy error + app crash when a source that has ongoing download job is deleted
2 participants