-
Notifications
You must be signed in to change notification settings - Fork 42
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
Conversation
client/securedrop_client/storage.py
Outdated
""" | ||
Handle SQLAlchemy exceptions and return relevant information to controller. | ||
""" | ||
|
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.
@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.
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 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.
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 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.)
e378b79
to
66d8605
Compare
(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) |
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.
See inline comments
client/securedrop_client/storage.py
Outdated
""" | ||
Handle SQLAlchemy exceptions and return relevant information to controller. | ||
""" | ||
|
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 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
|
||
except storage.SDDatabaseError as e: | ||
# This shouldn't happen, it's been downloaded. | ||
logger.error("Failed to find file uuid in database") |
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.
Let's have this explain your comment? e.g. "Failed to find file that was just downloaded in the database."
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.
Thank you! Addressed in 138643b
6326b43
to
138643b
Compare
I agree and not sure why I didn't look at it that way before - addressed in 138643b, thank you :) |
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.
138643b
to
a9f0590
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.
Thanks, it looks great now. I just made a slight adjustment to your commit message to break the long lines.
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
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:
If these changes add or remove files other than client code, the AppArmor profile may need to be updated. Please check as applicable:
If these changes modify the database schema, you should include a database migration. Please check as applicable:
main
and confirmed that the migration is self-contained and applies cleanlymain
and would like the reviewer to do so