Skip to content

Commit

Permalink
Handle StaleDataError for ODLNotificationController
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathangreen committed Oct 21, 2024
1 parent 0e566c8 commit 1630416
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 2 deletions.
14 changes: 12 additions & 2 deletions src/palace/manager/api/controller/odl_notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from flask_babel import lazy_gettext as _
from pydantic import ValidationError
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, object_session
from sqlalchemy.orm.exc import StaleDataError

from palace.manager.api.odl.api import OPDS2WithODLApi
from palace.manager.api.problem_details import (
Expand Down Expand Up @@ -101,6 +102,15 @@ def _process_notification(self, loan: Loan | None) -> Response:
# Once we move the OPDS2WithODL scripts to celery this should be possible.
# For now we just mark the loan as expired.
if not status_doc.active:
loan.end = utc_now()
session = object_session(loan)
try:
with session.begin_nested():
loan.end = utc_now()
except StaleDataError:
# This can happen if this callback happened while we were returning this
# item. We can fetch the loan, but it's deleted by the time we go to do
# the update. This is not a problem, as we were just marking the loan as
# completed anyway so we just continue.
...

return Response(status=204)
28 changes: 28 additions & 0 deletions tests/manager/api/controller/test_odl_notify.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
from unittest.mock import PropertyMock, create_autospec, patch

import pytest
from flask import Response
from freezegun import freeze_time
from sqlalchemy.orm.exc import StaleDataError

from palace.manager.api.controller import odl_notification
from palace.manager.api.controller.odl_notification import ODLNotificationController
from palace.manager.api.odl.api import OPDS2WithODLApi
from palace.manager.api.problem_details import (
INVALID_LOAN_FOR_ODL_NOTIFICATION,
NO_ACTIVE_LOAN,
)
from palace.manager.core.problem_details import INVALID_INPUT
from palace.manager.integration.goals import Goals
from palace.manager.sqlalchemy.model.collection import Collection
from palace.manager.sqlalchemy.model.licensing import License
from palace.manager.sqlalchemy.model.patron import Loan, Patron
Expand Down Expand Up @@ -249,3 +254,26 @@ def test_notify_errors(
odl_fixture.controller.notify(
odl_fixture.patron_identifier, NON_EXISTENT_LICENSE_IDENTIFIER
)

def test__process_notification_already_deleted(
self,
odl_fixture: ODLFixture,
flask_app_fixture: FlaskAppFixture,
db: DatabaseTransactionFixture,
) -> None:
mock_loan = create_autospec(Loan)
type(mock_loan).end = PropertyMock(side_effect=StaleDataError())
mock_loan.license_pool.collection.integration_configuration.protocol = (
db.protocol_string(Goals.LICENSE_GOAL, OPDS2WithODLApi)
)
with (
flask_app_fixture.test_request_context(
"/",
method="POST",
library=odl_fixture.library,
data=odl_fixture.loan_status_document("revoked").model_dump_json(),
),
patch.object(odl_notification, "object_session"),
):
response = odl_fixture.controller._process_notification(mock_loan)
assert response.status_code == 204

0 comments on commit 1630416

Please sign in to comment.