Improve channels API performance, remove all channels CRUD code - #3388
Conversation
OpenAPI Changes66 changes: 18 error, 0 warning, 48 info Unexpected changes? Ensure your branch is up-to-date with |
There was a problem hiding this comment.
Pull request overview
This PR improves channel API performance by adding shared caching for cache-safe channel responses, centralizing channel detail prefetching, and adding load tests to compare channel endpoint behavior.
Changes:
- Adds request-time cache timeout resolution and channel-specific cache bypassing for moderators/staff.
- Moves detailed channel queryset prefetch/select logic into
ChannelQuerySet.with_detail_relations(). - Adds tests and k6 load-testing scripts for channel list/detail/count endpoints.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
channels/models.py |
Adds reusable queryset helper for channel detail serialization relations. |
channels/views.py |
Applies optimized querysets and cache decorators to channel endpoints. |
channels/views_test.py |
Adds query-count and channel cache behavior coverage. |
main/utils.py |
Allows cache decorators to default to REDIS_VIEW_CACHE_DURATION at request time. |
main/utils_test.py |
Tests deferred default timeout behavior. |
load_testing/backend/client/client.ts |
Allows extra headers in generated k6 API clients. |
load_testing/backend/channels.ts |
Adds k6 channel endpoint discovery and request scenarios. |
load_testing/channels.compare.ts |
Adds a k6 entrypoint for channel load comparison runs. |
load_testing/userinfo.ts |
Adds helpers for building load-test user identity headers. |
openapi/specs/v0.yaml |
Updates counts endpoint description text. |
24909fe to
3f30e15
Compare
| (see ``ChannelQuerySet.with_detail_relations``) so serializing a | ||
| channel does not trigger an extra query per list. | ||
| """ | ||
| return [channel_list.channel_list for channel_list in self.lists.all()] |
There was a problem hiding this comment.
should this be ordered by position?
There was a problem hiding this comment.
ChannelList.Meta1 sets ordering = ["position"], so self.lists.all() is position-ordered even on the non-prefetched path.
with_detail_relations prefetches lists with an explicit .order_by("position") now, so the prefetched path is too.
I'll add a unit test to make sure though
| "topic_detail", | ||
| "department_detail", | ||
| "unit_detail", | ||
| "pathway_detail", |
There was a problem hiding this comment.
Would be nice to pull just the relevant one for the channel type when making an api call for one channel but probably it's not a big deal to pull them since the queries are small when the relevant detail is null
There was a problem hiding this comment.
The list endpoint for all channels would need all of them I think, inclined to think it's not worth creating different queries here for a channel detail view vs a list view. On the other hand not sure if the list view is actually used for anything anyway.
3376029 to
544ffad
Compare
cb2b0bb to
b4c9d88
Compare
abeglova
left a comment
There was a problem hiding this comment.
lgtm once you remove
docs/superpowers/specs/2026-06-03-channel-readonly-design.md
d568969 to
5298dee
Compare
| return queryset | ||
|
|
||
| @method_decorator( | ||
| cache_page_for_all_users( | ||
| settings.REDIS_VIEW_CACHE_DURATION, cache="redis", key_prefix="channels" | ||
| ) | ||
| ) | ||
| def list(self, request, *args, **kwargs): | ||
| @method_decorator(cache_channel_response()) | ||
| def list(self, request: Request, *args, **kwargs) -> Response: | ||
| """List channel counts by resource type.""" | ||
| return super().list(request, *args, **kwargs) |
There was a problem hiding this comment.
Bug: The overridden get_object method in ChannelByTypeNameDetailView bypasses object-level permission checks by not calling self.check_object_permissions(), creating a potential future vulnerability.
Severity: LOW
Suggested Fix
After retrieving the obj in the get_object method, add a call to self.check_object_permissions(self.request, obj) before returning the object. This will ensure that any future object-level permission classes added to the view are correctly enforced, adhering to DRF best practices.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: channels/views.py#L148-L153
Potential issue: The `get_object` method in `ChannelByTypeNameDetailView` is overridden
to fetch a `Channel` object but omits the standard call to
`self.check_object_permissions(self.request, obj)`. While access is currently controlled
by a `published=True` filter in the queryset and no object-level permissions are defined
for `Channel` models, this omission creates a latent bug. If object-level permissions
are added to this view in the future (e.g., to restrict access based on ownership), they
will not be enforced, potentially leading to unauthorized data access. This violates
Django REST Framework's documented best practices and creates a maintenance trap.
5298dee to
8413656
Compare
What are the relevant tickets?
Related to https://github.com/mitodl/hq/issues/9157
Description (What does it do?)
Improves the performance of the channels API in two ways:
ChannelQuerySet.with_detail_relations()applies the fullprefetch_related/select_related/annotatechain theChannelSerializerneeds, so the list and detail endpoints render with a constant number of queries instead of scaling with the number of lists/sub-channels per channel.Also adds a k6 load test for the channel endpoints used to measure the before/after improvement.
How can this be tested?
Automated tests
Covers the prefetch query counts, the moderator/staff cache-bypass behavior, and the key-prefixed cache invalidation.
Load test (k6) — measuring the perf improvement
loadtest-*channels):BACKEND_BASE_URL=http://learn.odl.local:8061hits Django directly (no APISIX gateway required). The Docker helper mountsload_testing/at/app(note:scripts/k6.shalready supplies therunsubcommand, so don't pass it yourself):load_testing/results/channels..json.