-
Notifications
You must be signed in to change notification settings - Fork 7
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
[PP-1358] add new events #2174
[PP-1358] add new events #2174
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2174 +/- ##
==========================================
+ Coverage 91.09% 91.10% +0.01%
==========================================
Files 363 363
Lines 41201 41260 +59
Branches 8827 8834 +7
==========================================
+ Hits 37531 37591 +60
Misses 2406 2406
+ Partials 1264 1263 -1 ☔ View full report in Codecov by Sentry. |
4020690
to
a1700c8
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.
I'm a bit concerned about the performance impacts of this change. I'd like to understand that a bit better before this one goes in.
alembic/versions/20241119_58b0ae7f5b67_make_loan_patron_id_and_hold_patron_id_.py
Outdated
Show resolved
Hide resolved
69cd3e4
to
8db8da1
Compare
@dbernstein looks like mypy is failing on this one |
df35c58
to
547869f
Compare
This one has is ready to go. I fixed the mypy issues a couple of bugs, and reset the migration target. |
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.
A couple more things to look at here @dbernstein. I'd like to get the race condition when deleting holds sorted out, and make sure we're not going to run into issues referencing a closed session before this gets merged.
@@ -0,0 +1,43 @@ | |||
"""Make Loan.patron_id and Hold.patron_id non-nullable. | |||
|
|||
Revision ID: 58b0ae7f5b67 |
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.
Sorry @dbernstein, I meant to ping you on this PR. This migration can just go away as part of this PR, since the migration in #2197 covers it.
|
||
# a separate query is required to get around the | ||
# "FOR UPDATE cannot be applied to the nullable side of an outer join" issue when trying to use with_for_update | ||
# on the Hold object. |
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.
Minor: If that is the case, I think its better to just drop the lock query (lines 28 - 46) all together.
@@ -31,11 +75,12 @@ def remove_expired_holds_for_collection(db: Session, collection_id: int) -> int: | |||
.execution_options(synchronize_session="fetch") | |||
) | |||
result = db.execute(query) |
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'm still uncomfortable with the race condition that can happen here, since the delete and the event are two separate queries. If a hold expires between the select on line 54 and the delete query on line 67 we will never log an event for it.
It would be better to do this all in one query, either by iterating over the expired holds and running session.delete
on them or doing something like:
query = (
delete(Hold)
.where(
Hold.id.in_(h.id for h in expired_holds)
)
)
result = db.execute(query)
that way we are sure the set of objects that we log events for, and the set that we delete from the database are the same.
# We need the type ignores here because result doesn't always have | ||
# a rowcount, but the sqlalchemy docs swear it will in the case of | ||
# a delete statement. | ||
# https://docs.sqlalchemy.org/en/20/tutorial/data_update.html#getting-affected-row-count-from-update-delete | ||
return result.rowcount # type: ignore[attr-defined,no-any-return] | ||
return result.rowcount, expired_hold_events # type: ignore |
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.
Minor: We can probably simplify the return here to expired_hold_events
, since len(expired_hold_events)
gets us result.rowcount
|
||
# publish events only after successful commit | ||
for event in events: | ||
analytics.collect_event(**event) |
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 looks unsafe to me, since events
contains references to library
and patron
which are bound to the transactions session, but after exiting the transactions context manager the session is closed. Are you sure this will work?
I think this is likely going to result in session errors, that may not appear when running in tests, since we never actually close the session in tests, but we do when running the real application.
@@ -211,6 +288,10 @@ def recalculate_hold_queue_collection( | |||
f"{updated} holds out of date." | |||
) | |||
|
|||
# fire events after successful database update | |||
for event in events: | |||
analytics.collect_event(**event) |
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've got the same concern here about objects being referenced after their session is closed.
converted to hold.
Also use with_for_update to lock expired holds before deletion Also ensure that transaction is closed before collecting analytics events.
547869f
to
5c942dd
Compare
@jonathangreen : Hopefully I've addressed your concerns here. I wanted to keep the event collection outside the transaction as you initially suggested to limit the duration of any row level locks in the database. To accomplish this requirement I am starting a new session and reattaching the objects from the closed session. I also removed the migration. |
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 for working through the changes on this one. Looks good to me!
Description
This update captures five new circulation events:
I have a couple of questions/comments to inform the review:
We are capturing hold expirations where the CM is and is not the source of truth. However for loans I believe we are only handling expired loans where the CM is not source of truth. Did I miss something here? Or should we be doing something for loan expirations where CM is not the source of truth?
When we convert holds to loans I am both capturing the conversion event as well as recording a "checkout". Because it is a checkout event that happens to be a conversion. I think that's what we want but others may disagree.
I put a note in the code, but I'll call it out again here: there appears to be a case where a loan can be converted to a hold. I'm not sure what the scenario is, but I've captured it is a distinct event in addition to the placing of the hold.
I ran into some mypy issues with the collect_event method. That led me to observe that all loans and holds have patrons and the intention seems to be that all patrons must have loans and holds but the database does not enforce that constraint. I included the migration here, but I am happy to move it to a separate PR is that seems cleaner.
Motivation and Context
https://ebce-lyrasis.atlassian.net/browse/PP-1804
How Has This Been Tested?
New unit tests added. Older unit tests amended.
Checklist