-
Notifications
You must be signed in to change notification settings - Fork 11
fix: enrollment thread and temp storage issue #1187
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
base: dev
Are you sure you want to change the base?
Conversation
WalkthroughReplaces a global requests Session with per-thread sessions via Changes
Sequence Diagram(s)sequenceDiagram
participant Main as Main Thread
participant Worker as Worker Thread
participant get_session as get_session()
participant TLS as _session_local (thread-local)
participant Adapter as HTTPAdapter/Retry
participant Remote as Remote Service
rect rgb(220,235,255)
Note over Worker,Main: First call in a thread
Worker->>get_session: request session
get_session->>TLS: check for session
alt none present
get_session->>Adapter: create HTTPAdapter + Retry
get_session->>TLS: store session
end
TLS-->>get_session: session
get_session-->>Worker: session
end
rect rgb(235,245,220)
Note over Worker,Remote: Use session for requests
Worker->>Remote: session.get(url)
Remote-->>Worker: response
Worker->>Worker: handle response / retry logic
end
rect rgb(245,230,230)
Note over Main,Worker: Shutdown on KeyboardInterrupt
Main->>Worker: KeyboardInterrupt
Main->>Main: executor.shutdown()
Main->>Main: django.db.connections.close_all()
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Poem
Pre-merge checks and finishing touches✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Actionable comments posted: 3
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
tcf_website/management/commands/fetch_enrollment.py(6 hunks)tcf_website/tests/test_fetch_enrollment.py(5 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tcf_website/management/commands/fetch_enrollment.py (1)
tcf_website/models/models.py (1)
Section(942-992)
tcf_website/tests/test_fetch_enrollment.py (2)
tcf_website/management/commands/fetch_enrollment.py (2)
fetch_section_data(76-137)get_session(49-65)tcf_website/models/models.py (1)
SectionEnrollment(1055-1099)
🪛 GitHub Actions: Continuous Integration
tcf_website/management/commands/fetch_enrollment.py
[error] 168-168: Pylint: Trailing whitespace. (C0303)
tcf_website/tests/test_fetch_enrollment.py
[error] 116-116: Pylint: Line too long (101/100). (C0301)
[error] 125-125: Pylint: Line too long (105/100). (C0301)
[error] 132-132: Pylint: Line too long (104/100). (C0301)
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.
Actionable comments posted: 1
♻️ Duplicate comments (1)
tcf_website/management/commands/fetch_enrollment.py (1)
168-168: Remove trailing whitespace on the blank line.Pylint flags trailing whitespace on this blank line (C0303). Remove the trailing spaces to pass CI.
print(f"Found {total_sections} sections") - + sections = list(sections_queryset)
🧹 Nitpick comments (1)
tcf_website/tests/test_fetch_enrollment.py (1)
107-118: Enhance the test to verify concurrent access.The test correctly uses the real
get_session()implementation (not patched), but it runs threads sequentially (start-join-start-join) rather than concurrently. This verifies that different threads get different sessions, but doesn't test the primary concern: that concurrent threads can safely use thread-local sessions without contention.Consider this enhanced version that spawns all threads before joining:
def test_thread_local_sessions(self): """Test that each thread gets its own session instance.""" sessions = [] + threads = [] + + def capture_session(): + sessions.append(get_session()) + for _ in range(5): - thread = threading.Thread(target=lambda: sessions.append(get_session())) - thread.start() - thread.join() + thread = threading.Thread(target=capture_session) + threads.append(thread) + + for thread in threads: + thread.start() + for thread in threads: + thread.join() + self.assertEqual( len(set(id(s) for s in sessions)), 5, "Each thread should have its own session" )This change also eliminates the lambda closure and tests true concurrent access.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
tcf_website/management/commands/fetch_enrollment.py(6 hunks)tcf_website/tests/test_fetch_enrollment.py(5 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tcf_website/management/commands/fetch_enrollment.py (3)
tcf_website/models/models.py (1)
Section(942-992)tcf_website/api/enrollment.py (2)
process_section(93-102)update_enrollment_data(68-108)tcf_website/api/views.py (1)
SectionEnrollmentViewSet(187-221)
tcf_website/tests/test_fetch_enrollment.py (1)
tcf_website/management/commands/fetch_enrollment.py (2)
fetch_section_data(76-137)get_session(49-65)
🔇 Additional comments (13)
tcf_website/management/commands/fetch_enrollment.py (7)
10-10: LGTM!The new imports are used appropriately:
threadingfor thread-local storage andconnectionsfor cleanup on interrupt.Also applies to: 17-17
27-27: LGTM!Reducing
MAX_WORKERSfrom 20 to 5 aligns with the PR objectives to prevent thread and memory leaks.
35-36: LGTM!Using
threading.local()is the correct pattern for thread-local storage to avoid session contention across threads.
49-66: LGTM!The thread-local session implementation is correct. Each thread gets its own
requests.Sessionwith appropriate retry logic and connection pooling. Thehasattrcheck ensures lazy initialization per thread.
93-93: LGTM!Correctly obtains the thread-local session via
get_session(), eliminating the shared global session that caused thread contention.
165-169: LGTM! Materializing the queryset prevents lazy evaluation issues.Converting the queryset to a list before parallel processing is good practice and prevents each thread from independently evaluating the queryset, which could cause database connection issues.
183-187: LGTM! Proper cleanup on interrupt.Calling
connections.close_all()after shutting down the executor ensures Django database connections are cleaned up when the script is interrupted, preventing connection leaks.tcf_website/tests/test_fetch_enrollment.py (6)
3-3: LGTM!The
threadingimport is used appropriately in the new thread-local session test.
8-8: LGTM!The
get_sessionimport is necessary for testing the new thread-local session functionality.
23-28: LGTM!The test correctly patches
get_sessionand configures the mock session. This properly isolates the HTTP layer during testing.
48-62: LGTM!The test correctly patches
get_sessionand configures the mock session, consistent with the other tests.
82-88: LGTM!The test correctly patches
get_sessionand configures the mock session.
95-100: LGTM!The test correctly patches
get_sessionand configures the mock session with error behavior.
f6903db to
50d6f71
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.
Actionable comments posted: 0
🧹 Nitpick comments (1)
tcf_website/management/commands/fetch_enrollment.py (1)
49-66: Consider reducing per-thread pool sizes.Each thread now creates its own session with
pool_connections=MAX_POOL_SIZE(100) andpool_maxsize=MAX_POOL_SIZE(100). WithMAX_WORKERS=5, this allocates potentially large connection pools per thread. Since each thread typically connects to a single host (sisuva.admin.virginia.edu), consider reducing these to more modest values like:- adapter = HTTPAdapter( - pool_connections=MAX_POOL_SIZE, - pool_maxsize=MAX_POOL_SIZE, - max_retries=retry_strategy, - ) + adapter = HTTPAdapter( + pool_connections=10, + pool_maxsize=20, + max_retries=retry_strategy, + )This would reduce memory overhead while maintaining adequate connection reuse.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
tcf_website/api/views.py(2 hunks)tcf_website/management/commands/fetch_enrollment.py(6 hunks)tcf_website/tests/test_fetch_enrollment.py(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- tcf_website/tests/test_fetch_enrollment.py
🧰 Additional context used
🧬 Code graph analysis (1)
tcf_website/management/commands/fetch_enrollment.py (1)
tcf_website/models/models.py (1)
Section(942-992)
🔇 Additional comments (4)
tcf_website/api/views.py (1)
5-5: LGTM! Thread-local connection cleanup is appropriate.The change from
connection.close()toconnections.close_all()ensures all thread-local database connections are properly cleaned up in the background thread, preventing connection leaks. This aligns well with the thread-local session pattern introduced infetch_enrollment.py.Also applies to: 200-200
tcf_website/management/commands/fetch_enrollment.py (3)
93-93: LGTM! Thread-local session usage fixes the concurrency issue.Switching from a global session to
get_session()ensures each worker thread uses its own requests session, eliminating race conditions and thread contention that could occur with a shared global session.
165-169: LGTM! Materializing the queryset is appropriate for thread safety.Converting the queryset to a list before parallel processing ensures all section objects are fetched upfront, avoiding potential issues with lazy evaluation and database access patterns in the threaded context.
186-186: LGTM! Connection cleanup on interrupt prevents leaks.Calling
connections.close_all()after shutting down the executor ensures all thread-local database connections are properly closed when the user interrupts the process, preventing connection leaks.
d891339 to
50d6f71
Compare
GitHub Issues addressed
What I did
Stopped the leaking fetch_enrollment.py script: replaced the global session shared by the threads with thread-local sessions via threading.local(), closed all threads after updating, and added connection cleanup on interrupt. Tests and a run on 9,887 sections showed no thread or memory leaks.
Screenshots
Testing
Run tests
docker exec tcf_django python manage.py test tcf_website.tests.test_fetch_enrollmentTest on dataset
Before running, click on a few different courses and run the commands to see the stats below and ensure that the threads are being terminated.
The command below is for all sections.
docker exec tcf_django python manage.py fetch_enrollmentVerify no orphans
docker exec tcf_django bash -c "pgrep -a python"Monitor memory
docker stats tcf_djangoQuestions/Discussions/Notes
Summary by CodeRabbit
New Features
Bug Fixes
Performance
Tests