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

Update service to use the new ext_lti_assignment_id #3161

Merged
merged 4 commits into from
Oct 11, 2021

Conversation

marcospri
Copy link
Member

  • Added .exists() .get_for_canvas_launch() .merge_canvas_assignments()
  • Get doesn't return None but raises NoResultFound

Updated PR from the last refactor.

@marcospri marcospri force-pushed the ext_lti_assignment_id-column-service branch 2 times, most recently from 4c28491 to 410c2b2 Compare September 29, 2021 07:40

def set_document_url( # pylint:disable=too-many-arguments
self,
document_url,
Copy link
Member Author

Choose a reason for hiding this comment

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

Args order changed. I think document_url makes sense as first or last argument but as I can't be mixed with other ones with default values and I don't think document_url=None makes sense I put it on front.

There's some changes on the launch views and test assertions because of this.

Copy link
Collaborator

@seanh seanh left a comment

Choose a reason for hiding this comment

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

Two small suggestions: one tweak to an if and one to add a docstring. I'm gonna re-test it as well...

)
return assignments[0]

if not resource_link_id:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
if not resource_link_id:
if ext_lti_assignment_id:

At this point since we've gotten past the two previous if's above I think we know that exactly one of resource_link_id or ext_lti_assignment_id is None. So I think if ext_lti_assignment_id is the same thing as if not resource_link_id but more direct

Copy link
Member Author

@marcospri marcospri Sep 30, 2021

Choose a reason for hiding this comment

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

Yes, it lines better with the method naming as well 👍

if not assignments:
raise NoResultFound()

return assignments
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this method either returns a single assignment (which has either the matching resource_link_id or ext_lti_assignment_id or both) or two assignments (one with the matching resource_link_id and no ext_lti_assignment_id and the other with the matching ext_lti_assignment_id but no resource_link_id). Also the order_by() guarantees that when two assignments are returned the one with the resource_link_id comes first in the sequence. It'd be nice to document that here in get_for_canvas_launch(). Suggestion:

    def get_for_canvas_launch(
        self, tool_consumer_instance_guid, resource_link_id, ext_lti_assignment_id
    ):
        """
        Return the assignment(s) with resource_link_id or ext_lti_assignment_id.

        Return all the assignments in the DB that have the given tool_consumer_instance_guid and
        either the given resource_link_id or ext_lti_assignment_id or both. This could be:

        1. A single assignment in the DB that has either the given resource_link_id or
           ext_lti_assignment_id or both

        2. Or two assignments:

           i.  One with the matching resource_link_id and no ext_lti_assignment_id
           ii. And one with the matching ext_lti_assignment_id and no resource_link_id

           The assignment with the resource_link_id will always be first in the sequence.

        :rtype: sequence of either 1 or 2 models.Assignment objects

        :raise NoResultFound: if there are no matching assignments in the DB
        """

This is a bit duplicative because the canvas_db_configured_basic_lti_launch() view in #3127 also has some code comments and assertions about this. But I think it's probably based for get_for_canvas_launch() to have a docstring that fully explains its somewhat-complicated contract. The docstring is limited to just documenting what get_for_canvas_launch() returns whereas the comment in canvas_db_configured_basic_lti_launch() is more about why/how this happens and about merging the two assignments.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Mentioned in another comment above but I wonder if instead of:

assignments = (
    ...
).all()

if not assignments:
    return None

return assignments

This could just be:

return (
    ...
).all()

Then instead of:

:rtype: sequence of either 1 or 2 models.Assignment objects or None for not results.

the docstring would say:

:rtype: sequence of either 0, 1 or 2 models.Assignment objects

and I think it would make the calling code slightly simpler as list things like len() or iteration will always work even on an empty list.

Copy link
Collaborator

@seanh seanh left a comment

Choose a reason for hiding this comment

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

I don't think the caching is working. Here's what I'm seeing when testing this PR (on its own, without the views PRs):

When launching a newly-created Canvas assignment for the first time (so there's no record of the assignment in the DB at all):

  1. Pyramid calls the db_configured predicate which calls exists() which calls get(). get() raises NoResultFound and exists() returns False

  2. Pyramid calls the configured predicate which calls the blackboard_copied predicate which calls the db_configured predicate again. Same thing happens: the db_configured predicate calls exists() which calls get() which raises NoResultFound and exists() returns False

  3. This repeats several times with Pyramid calling predicates and predicates calling other predicates, with exists() and get() being called each time

  4. The canvas_file_basic_lti_launch() view calls set_document_url() which calls get() which again raises NoResultFound

Basically I think the same DB query might be getting done several times over.

I suspect the reason might be that the @lru_cache on get() isn't working because get() is raising exceptions? I'm guessing that @lru_cache doesn't cache if you raise an exception.

Possible solutions:

  • Put an @lru_cache on exists(). This'll reduce the number of calls to get() but it doesn't fully solve the problem as get() isn't always called by exists(), so I think get() will still be called more than once
  • Put @lru_cache's on the three get_*() and _get_*() methods that contain the actual SQLAlchemy queries. But I think this would require changing these methods to return things instead of raising
  • Change get() to return None instead of raising

@marcospri
Copy link
Member Author

  • Put an @lru_cache on exists(). This'll reduce the number of calls to get() but it doesn't fully solve the problem as get() isn't always called by exists(), so I think get() will still be called more than once
  • Put @lru_cache's on the three get_*() and _get_*() methods that contain the actual SQLAlchemy queries. But I think this would require changing these methods to return things instead of raising
  • Change get() to return None instead of raising

Thanks, well spotted.Yes lru_cache doesn't save invocations that raise which seems like a good decision.

I went for a bit of a mix of those solutions:

  • Exists is now also cached. This fixes the immediate problem on the predictes usage of exists.
  • Methods called from .get return None (changed .one() for .one_or_none(). This makes .get() cacheable on its own.
  • Still kept the raise for the case when .get() finds duplicates. I don't think you wan't to just get None in that scenario and we are making that query repeatedly something else is wrong IMO.

Copy link
Collaborator

@seanh seanh left a comment

Choose a reason for hiding this comment

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

I'm still seeing get() called twice when launching a Blackboard assignment.

I think the reason is that the predicates call exists() which calls get() like this:

self.get(
tool_consumer_instance_guid, resource_link_id, ext_lti_assignment_id
)

ext_lti_assignment_id is always None in the case of a Blackboard launch.

Then later the db_configured_basic_lti_launch() view calls get() like this:

document_url = self.assignment_service.get(
tool_consumer_instance_guid, resource_link_id
).document_url

That's effectively the same call but the ext_lti_assignment_id (None) argument is missing so I'm guessing that @lru_cache considers the two calls to be different.

Fixable?

@marcospri
Copy link
Member Author

marcospri commented Sep 30, 2021

Fixable?

I removed the optional values from get so now both calls are the same, we could still call .get with args vs kwargs and break the caching but that's true of any @lru_cache'd method.

edit: Cached the privated method instead, get might not get cached but the cache on those will prevent the hit to the DB which is we care about mostly.

I think the right answer here is to long term move to not doing queries on the predicates and do that logic on the view. Caching a value is strictly a side effect after all.

@marcospri marcospri force-pushed the ext_lti_assignment_id-column-service branch from 9e4e695 to 5f8a46b Compare September 30, 2021 08:22
Copy link
Collaborator

@seanh seanh left a comment

Choose a reason for hiding this comment

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

Tiny suggestion for a change to get_for_canvas_launch()'s contract.

I think get_for_canvas_launch() should have unit tests as well, but they could be added in a follow-up PR.

assignments = self.get_for_canvas_launch(
tool_consumer_instance_guid, resource_link_id, ext_lti_assignment_id
)
if assignments and len(assignments) > 1:
Copy link
Collaborator

Choose a reason for hiding this comment

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

It might be possible for this to be slightly simpler if get_for_canvas_launch() would return an empty sequence instead of None:

Suggested change
if assignments and len(assignments) > 1:
if len(assignments) > 1:

http://thecodelesscode.com/case/6 :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep 👍 , I have had a bit of tunnel vision on these few last changes, changing the problematic line and not zooming out a bit.

if not assignments:
raise NoResultFound()

return assignments
Copy link
Collaborator

Choose a reason for hiding this comment

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

Mentioned in another comment above but I wonder if instead of:

assignments = (
    ...
).all()

if not assignments:
    return None

return assignments

This could just be:

return (
    ...
).all()

Then instead of:

:rtype: sequence of either 1 or 2 models.Assignment objects or None for not results.

the docstring would say:

:rtype: sequence of either 0, 1 or 2 models.Assignment objects

and I think it would make the calling code slightly simpler as list things like len() or iteration will always work even on an empty list.

resource_link_id=assignment_canvas.resource_link_id,
ext_lti_assignment_id=assignment_canvas.ext_lti_assignment_id,
)
assert retrieved_assignment == assignment_canvas
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since it's a public method with its own contract (and a pretty non-trivial contract) I think get_for_canvas_launch() should probably have its own direct unit tests as well. But we can add them in a follow-up PR

@marcospri marcospri force-pushed the ext_lti_assignment_id-column-service branch from 5f8a46b to c315d8e Compare September 30, 2021 09:44
@marcospri marcospri force-pushed the ext_lti_assignment_id-column-model branch from f1bb7fa to cb5460d Compare October 5, 2021 12:55
Base automatically changed from ext_lti_assignment_id-column-model to master October 5, 2021 13:03
@marcospri marcospri force-pushed the ext_lti_assignment_id-column-service branch 2 times, most recently from 718d60a to 48b1427 Compare October 7, 2021 11:22
@marcospri marcospri force-pushed the ext_lti_assignment_id-column-service branch 3 times, most recently from 48b1427 to 471356a Compare October 11, 2021 12:59
marcospri and others added 4 commits October 11, 2021 15:00
- Added .exists() .get_for_canvas_launch() .merge_canvas_assignments()
- Get doesn't return None but raises NoResultFound
Currently it uses exceptions when not results are found and invocations that raise are not cached.

.exists() is also now cached.
The `extra` argument doesn't appear to be used.

I'm not sure whether we should add an `extra` argument to
`set_document_url()`: it's `set_document_url()` not
`set_document_url_and_merge_extras()`. Let's at least hold off until we
have some code that uses the new argument.
@marcospri marcospri force-pushed the ext_lti_assignment_id-column-service branch from 471356a to 197274d Compare October 11, 2021 13:01
@marcospri marcospri merged commit 023a1b6 into master Oct 11, 2021
@marcospri marcospri deleted the ext_lti_assignment_id-column-service branch October 11, 2021 13:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants