FC-73 Add XqueueViewset with xwatcher services (Feature: XQueueViewSet - Compatible API Endpoints for External Grader Integration)#287
Conversation
|
Thanks for the pull request, @leoaulasneo98! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review. 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. DetailsWhere can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #287 +/- ##
==========================================
+ Coverage 94.79% 95.89% +1.09%
==========================================
Files 18 23 +5
Lines 2423 3022 +599
Branches 99 121 +22
==========================================
+ Hits 2297 2898 +601
+ Misses 115 113 -2
Partials 11 11
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
This comment was marked as resolved.
This comment was marked as resolved.
leoaulasneo98
left a comment
There was a problem hiding this comment.
Hello @ormsbee , good afternoon, greetings. We have a question about this here, we already have the put result service in edx submission (which also exists in Xqueue server). We are missing implementing the call back but this has a drawback, in edx submission we cannot import xqueue_callback or any open edx package because there start to be import errors when running the tests, in addition to the fact that a circular import is created since edx submission is a dependency of edx platform.
It is for this reason that we decided to use an open edx event signal so that the callback is executed once the score of the submission has been stored. However, we have some questions:
1 Is this the best solution at the architectural level
2 Are there possible errors or incompatibilities that we are overlooking
3 Should we make a new callback or recycle the xqueue callback
4 Is there a more efficient solution?
1f3b38b to
77398d7
Compare
07f33a8 to
4decb2f
Compare
I think it's totally reasonable, and one of the best uses of signals.
Nothing comes to mind. We have to be careful about any change to the data being sent in the signal, but I can't think of any other issues.
I think the first thing I would try is to make a new function that listens to the signal and translates the data into whatever the existing xqueue callback function expects. But please feel free to refactor differently if you find it's easier to do otherwise.
I wouldn't worry too much about efficiency with this. As long as the callback is being invoked in-process, I don't think it'll make any difference to use a signal vs. other mechanisms. |
4decb2f to
8cb02ae
Compare
3ceeda0 to
ad18da2
Compare
- Add XqueueViewSet with complete xqueue-watcher service compatibility - Implement get_submission service for retrieving pending submissions - Add put_result service with row-level locking (select_for_update(nowait=True)) to prevent race conditions - Ensure concurrent xqueue-watcher instances process each submission exactly once, even under high load - Implement standardized response format for backward compatibility - Add session management and authentication handling for XWatcher clients - Add comprehensive test coverage for core interactions
…nd add timeout mechanism (Get Submission) - Implemented locking to ensure submissions are processed by a single xqueue watcher. - Added timeout mechanism for submissions stuck in 'pulled' state. - Updated tests to cover new error scenarios and timeout handling.
- Renamed variables from 'submission_record' to 'external_grader' throughout the codebase for better consistency with model naming - Added new 'retry' status to integrate with submission processing retry services - Removed unused 'is_processable' method that wasn't providing any value - Enhanced test coverage - Add new status external grader detail migration
* Remove RETRY status and complex transition validation from ExternalGraderDetail * Streamline update_status() method to accept score_msg parameter * Simplify put_result() error handling to use specific exceptions * Remove retry logic in favor of direct failed status transitions * Consolidate grader_reply updates within status transitions Breaking changes: - Removed ExternalGraderDetail.Status.RETRY enum value - Removed VALID_TRANSITIONS and can_transition_to() validation - Changed update_status() method signature to include score_msg parameter - Simplified error handling removes automatic retry mechanism
ad18da2 to
ef493c7
Compare
- Add comprehensive documentation for admin interface features - Document status management, search capabilities, and security model - Remove retry status from queue filtering logic - Fix pylint unused import warning in api module
7ea84ce to
a9f06a7
Compare
|
@leoaulasneo98: I've started doing a more detailed view, but a couple of very high-level things:
|
ormsbee
left a comment
There was a problem hiding this comment.
This is not a complete review, just one that primarily covers the ViewSet. Please feel free to read through this before our meeting today, but let's make the code changes together today in our meeting. Thank you.
submissions/views/xqueue.py
Outdated
| log = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class XqueueViewSet(viewsets.ViewSet): |
There was a problem hiding this comment.
| class XqueueViewSet(viewsets.ViewSet): | |
| class XQueueViewSet(viewsets.ViewSet): |
Just for naming consistency.
submissions/views/xqueue.py
Outdated
| """ | ||
| Endpoint for authenticating users and creating sessions. | ||
| """ | ||
| log.info(f"Login attempt with data: {request.data}") |
There was a problem hiding this comment.
Please don't write passwords into the log. You can remove this logging entry entirely and replace it with logging for the different errors and success paths.
| return Response( | ||
| {'return_code': 1, 'content': 'Insufficient login info'}, | ||
| status=status.HTTP_400_BAD_REQUEST |
There was a problem hiding this comment.
Please add a comment here to note that this is a change from the existing behavior that basically always returns a 200, regardless of whether there's an error or not (and relies exclusively on the JSON response to convey that). You don't have to change the behavior of this code–I think you're doing the right thing here. But any change in behavior should be clearly called out in case it results in regressions for some reason.
There was a problem hiding this comment.
Actually, given how many places the return codes are changing, it's probably better to add a comment in the docstring for the viewset addressing this.
| ) | ||
|
|
||
| if user is not None: | ||
| login(request, user) | ||
| response = Response( | ||
| {'return_code': 0, 'content': 'Logged in'}, | ||
| status=status.HTTP_200_OK | ||
| ) | ||
|
|
||
| return response | ||
|
|
||
| return Response( | ||
| {'return_code': 1, 'content': 'Incorrect login credentials'}, | ||
| status=status.HTTP_401_UNAUTHORIZED | ||
| ) |
There was a problem hiding this comment.
You're doing early returns on error conditions when you do this:
if 'username' not in request.data or 'password' not in request.data:
return Response(
{'return_code': 1, 'content': 'Insufficient login info'},
status=status.HTTP_400_BAD_REQUEST
)This is good. It prevents the code from getting deeply nested, and keeps people from having to skip around to see where conditionals end. But if you're doing this, please be consistent and make it so that all the early returns are error states, and the last thing is the successful path.
submissions/views/xqueue.py
Outdated
|
|
||
| return Response( | ||
| {'return_code': 1, 'content': 'Incorrect login credentials'}, | ||
| status=status.HTTP_401_UNAUTHORIZED |
There was a problem hiding this comment.
Again, please add a comment here that we're changing the behavior that XQueue had by sending the appropriate HTTP code.
submissions/views/xqueue.py
Outdated
| except ExternalGraderDetail.DoesNotExist: | ||
| log.error( | ||
| "Grader submission_id refers to nonexistent entry in Submission DB: " | ||
| "grader: %s, submission_key: %s, score_msg: %s", |
There was a problem hiding this comment.
This log entry says "grader" but the field being written is "submission_id". Also, please keep the old terminology of "grader_reply" rather than "score_msg", since it contains more than just the score, and it's not just a text message.
submissions/views/xqueue.py
Outdated
|
|
||
| if not external_grader.pullkey or submission_key != external_grader.pullkey: | ||
| log.error(f"Invalid pullkey: submission key from xwatcher {submission_key} " | ||
| f"and submission key stored {external_grader.pullkey} are different") |
There was a problem hiding this comment.
Please log the key they tried to send for this entry, since it might aid debugging, e.g. if they're only different by case, or if they're different by whitespace, etc.
| return fail | ||
|
|
||
| try: | ||
| header_dict = json.loads(header) |
There was a problem hiding this comment.
It looks like the code in this PR is unnecessarily encoding and decoding JSON internally, and I'm not clear on why it's doing so. We should keep things in native data structures as much as possible internally, and only keep things as JSON encoded if we're doing nothing with it but passing it through (i.e. we're not inspecting it), or if backwards compatibility requires that we do it that way.
There was a problem hiding this comment.
Where we're dealing with JSON encoded messages coming in, we should parse it to native data structures as soon as it's practical to do so.
There was a problem hiding this comment.
You're right that this is counter-intuitive, but we're constrained by legacy compatibility.
The legacy XQueue server returns exactly this nested JSON string format:
{
"xqueue_body": "{"grader_payload": "{...}", "student_info": "{...}", "student_response": "..."}"
}
xqueue-watcher clients expect and parse this format:
json.loads(response['content']) → gets payload
json.loads(payload['xqueue_body']) → gets submission data
json.loads(submission_data['grader_payload']) → gets grader config
We have to replicate this exact behavior - the double/triple encoding is required for API compatibility with existing xqueue-watcher deployments.
The encoding/decoding dance exists because that's how the legacy system works and what clients expect.
| if tag not in header_dict: | ||
| return fail | ||
|
|
||
| submission_id = int(header_dict['submission_id']) |
There was a problem hiding this comment.
Why would this ever not already be an int?
There was a problem hiding this comment.
You're technically correct that xqueue-watcher sends it as an int.
However, this int() conversion exists in the legacy code for defensive programming - JSON parsing can sometimes deserialize numbers as strings depending on the client implementation or how the data was originally encoded.
Legacy XQueue does the same conversion:
We're maintaining this pattern for:
Legacy compatibility - same behavior as original
Robustness - handles edge cases where JSON might come as string "22" instead of int 22
The conversion is essentially defensive programming that the legacy system used, so we're keeping it consistent.
| score = json.loads(score_msg) | ||
| points_earned = score.get("score") | ||
| except (TypeError, ValueError): | ||
| return fail |
There was a problem hiding this comment.
Adding error logging to this validation would be nice, since the only thing people running this in production will currently see is that the replies are bad, but not why they're bad.
|
@ormsbee https://www.youtube.com/watch?v=Ac6wa_aiLXw
|
|
@ormsbee The login testing features are implemented as you told me in the last meeting |
- Change XqueueViewSet to XQueueViewSet for naming consistency - Update score_msg parameter to grader_reply throughout codebase test: add comprehensive authorization and validation test coverage - Add unauthenticated access tests - Add tests for users without xqueue group membership - Add validation edge cases for grader_reply parsing - Achieve 100% test coverage on XQueueViewSet validation paths feat: add IsXQueueUser permission class for group-based authorization - Implement xqueue group membership validation for endpoint access control fix: remove sensitive data logging from authentication - Remove password logging and improve error message format per security review
3305a7c to
0e70be6
Compare
|
This change has been merged in the follow up PR. |





FC-73 Feature: XQueueViewSet - Compatible API Endpoints for External Grader Integration
ExternalGraderDetailandSubmissionFileinfrastructure deployed in previous PRs (FC-73). Please ensure those changes are fully deployed before merging.Description
This pull request implements the XQueueViewSet, providing compatible API endpoints that allow external graders (xqueue-watcher) to interact with the new submission and grading architecture. This viewset consolidates authentication, submission retrieval, and result processing services while leveraging the new
ExternalGraderDetailandSubmissionFilemodels.Motivation
The current XQueue implementation requires a strategic update to:
Key Improvements
Authentication Services
XQueueSessionAuthenticationclass with:Submission Distribution
get_submissionendpointSubmissionFileManagerResult Processing
put_resultendpointTechnical Details
Score Processing
set_scoreError Handling
Testing Strategy
Comprehensive test coverage includes:
Open edX Compliance
This implementation adheres to Open edX standards through:
Performance Considerations
BREAKING CHANGES: None. Designed for full backward compatibility with existing xqueue-watcher services.
Documentation
Updated documentation will include:
Implementation References