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 'NoneType' object has no attribute 'computed' error (PP-1961) #2184

Merged
merged 5 commits into from
Nov 22, 2024

Conversation

jonathangreen
Copy link
Member

@jonathangreen jonathangreen commented Nov 21, 2024

Description

There are two parts to this fix:

  1. Trying a bit harder to get a work for the licensepools we are working with.
  2. Tightening up some of our type hints and None handling, so that we log appropriate errors if we do get a None coming back.
  • I tried to make sure to add log statements for all of these so that we can figure out whats happening from the logs, if we see these happening in production.

Motivation and Context

This fixes an error we were seeing in production , where we couldn't generate a single item feed for a work. I was actually able to find an item this was happening to and reproduce it, which let me get a fix in place.

Stack trace from production:

Traceback (most recent call last):
  File "/var/www/circulation/env/lib/python3.10/site-packages/flask/app.py", line 880, in full_dispatch_request
    rv = self.dispatch_request()
  File "/var/www/circulation/env/lib/python3.10/site-packages/flask/app.py", line 865, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)  # type: ignore[no-any-return]
  File "/var/www/circulation/src/palace/manager/api/routes.py", line 120, in decorated
    return f(*args, **kwargs)
  File "/var/www/circulation/src/palace/manager/api/routes.py", line 70, in decorated
    return f(*args, **kwargs)
  File "/var/www/circulation/src/palace/manager/api/routes.py", line 93, in wrapped_function
    resp = make_response(f(*args, **kwargs))
  File "/var/www/circulation/src/palace/manager/core/app_server.py", line 87, in decorated
    v = f(*args, **kwargs)
  File "/var/www/circulation/src/palace/manager/core/app_server.py", line 163, in compressor
    return f(*args, **kwargs)
  File "/var/www/circulation/src/palace/manager/api/routes.py", line 510, in permalink
    return app.manager.work_controller.permalink(identifier_type, identifier)
  File "/var/www/circulation/src/palace/manager/api/controller/work.py", line 135, in permalink
    return OPDSAcquisitionFeed.entry_as_response(
  File "/var/www/circulation/src/palace/manager/feed/opds.py", line 88, in entry_as_response
    if not entry.computed:
AttributeError: 'NoneType' object has no attribute 'computed'

JIRA:

How Has This Been Tested?

  • Tested with item that was causing issue locally
  • Running tests in CI

Checklist

  • I have updated the documentation accordingly.
  • All new and existing tests passed.

@jonathangreen jonathangreen added the bug Something isn't working label Nov 21, 2024
work: Work = pools[0].work
# We know there is at least one LicensePool. Find the first one with
# a work set on it.
work: Work | None = first_or_default([lp.work for lp in pools if lp.work])
Copy link
Member Author

Choose a reason for hiding this comment

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

This is where the problem we were seeing in production stems from. pools[0].work was None, and that's where the trouble began. In that case there was another license pool that had a work set, so we try to find the first LP with a work set instead of just using the first one.

I also added some handling for the case where there is no work, since this case is still possible.

def permalink(self, identifier_type, identifier):
def permalink(
self, identifier_type: str, identifier: str
) -> OPDSEntryResponse | ProblemDetail:
Copy link
Member Author

Choose a reason for hiding this comment

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

Added some type hinting here so mypy checks this function, which would have given us an indication this could have been a problem.

@@ -558,15 +557,13 @@ def single_entry_loans_feed(
if not annotator:
annotator = LibraryLoanAndHoldAnnotator(circulation, None, library)

log = logging.getLogger(cls.__name__)

Copy link
Member Author

Choose a reason for hiding this comment

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

The logging changes in this file are only tangentially related. I just cleaned up some of the logging here to use LoggerMixin while I was at it, so its easier to see where these messages are coming from in the future.

@@ -608,7 +605,7 @@ def single_entry_loans_feed(
@classmethod
def single_entry(
cls,
work: Work | Edition | None,
work: Work | Edition,
Copy link
Member Author

Choose a reason for hiding this comment

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

I looked at everyone calling this function to make sure they are not (or updated them) passing None here.

@jonathangreen jonathangreen requested a review from a team November 21, 2024 19:52
Copy link

codecov bot commented Nov 21, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 91.03%. Comparing base (1d1b4eb) to head (2af3db1).
Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2184      +/-   ##
==========================================
+ Coverage   91.01%   91.03%   +0.01%     
==========================================
  Files         361      361              
  Lines       41205    41198       -7     
  Branches     8849     8846       -3     
==========================================
  Hits        37504    37504              
+ Misses       2425     2420       -5     
+ Partials     1276     1274       -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.


🚨 Try these New Features:

@jonathangreen jonathangreen changed the title Fix 'NoneType' object has no attribute 'computed' error (PP-1860) Fix 'NoneType' object has no attribute 'computed' error (PP-1961) Nov 22, 2024
Copy link
Contributor

@tdilauro tdilauro left a comment

Choose a reason for hiding this comment

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

Looks good! A couple small comments below.

Comment on lines 382 to 385
else:
content = etree.tostring(feed)
return Response(
response=content,
status=200,
content_type=OPDSFeed.ACQUISITION_FEED_TYPE,
)
raise PalaceValueError(
"Unexpected return type from OPDSAcquisitionFeed.single_entry_loans_feed"
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: We can remove the else and pop the throw out one level, since it's guarded by other two cases.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed. I just completely removed the if statement and raise since it should never get hit anyway.


# There's no reason to present a book that has no active license pool.
if not identifier:
logging.warning("%r HAS NO IDENTIFIER", work)
return None
raise PalaceValueError("Work has no identifier")
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we not want the work info in this log message? Or are we getting that somehow?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good call. I updated the exception to add the repr to the message:

PalaceValueError(f"Work has no associated identifier: {work!r}")

)
return cls.error_message(
identifier,
403,
"I know about this work but can offer no way of fulfilling it.",
)
except Exception as e:
logging.error("Exception generating OPDS entry for %r", work, exc_info=e)
return None
Copy link
Member Author

Choose a reason for hiding this comment

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

Drop this overly broad except. If we are getting random exceptions, we should handle them as they come up. Looking back at our logs, I don't see this being triggered recently, so it should be fine to drop.

with services_fixture.wired():
feed = OPDSAcquisitionFeed.active_loans_for(
None, patron, LibraryAnnotator(None, None, db.default_library())
)
Copy link
Member Author

Choose a reason for hiding this comment

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

This change is needed because without a wired service container we get an exception (and have been for awhile) that was being swallowed by the except clause I removed.

with (
admin_librarian_fixture.request_context_with_library_and_admin("/"),
services_fixture.wired(),
):
Copy link
Member Author

Choose a reason for hiding this comment

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

This change is needed because without a wired service container we get an exception (and have been for awhile) that was being swallowed by the except clause I removed.

@jonathangreen jonathangreen merged commit 7b3a977 into main Nov 22, 2024
21 checks passed
@jonathangreen jonathangreen deleted the bugfix/nonetype-computed branch November 22, 2024 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants