[WEB-4533] feat: read replica functionality (#7453)

* feat: read replica functionality

* fix: set use_read_replica to false

* chore: add use_read_replica to external APIs

* chore: remove use_read_replica on read endpoints

* chore: remove md files

* Updated all the necessary endpoints to use read replica

---------

Co-authored-by: Dheeraj Kumar Ketireddy <dheeru0198@gmail.com>
This commit is contained in:
Sangeetha 2025-07-28 17:41:02 +05:30 committed by GitHub
parent b1162395ed
commit 84879ee3bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 884 additions and 6 deletions

View file

@ -24,6 +24,7 @@ from rest_framework.viewsets import ModelViewSet
from plane.authentication.session import BaseSessionAuthentication
from plane.utils.exception_logger import log_exception
from plane.utils.paginator import BasePaginator
from plane.utils.core.mixins import ReadReplicaControlMixin
class TimezoneMixin:
@ -40,7 +41,7 @@ class TimezoneMixin:
timezone.deactivate()
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
class BaseViewSet(TimezoneMixin, ReadReplicaControlMixin, ModelViewSet, BasePaginator):
model = None
permission_classes = [IsAuthenticated]
@ -53,6 +54,8 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
search_fields = []
use_read_replica = False
def get_queryset(self):
try:
return self.model.objects.all()
@ -149,7 +152,7 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
return expand if expand else None
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
class BaseAPIView(TimezoneMixin, ReadReplicaControlMixin, APIView, BasePaginator):
permission_classes = [IsAuthenticated]
filter_backends = (DjangoFilterBackend, SearchFilter)
@ -160,6 +163,8 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
search_fields = []
use_read_replica = False
def filter_queryset(self, queryset):
for backend in list(self.filter_backends):
queryset = backend().filter_queryset(self.request, queryset, self)

View file

@ -19,6 +19,7 @@ from plane.db.models import IssueActivity, IssueComment, CommentReaction, Intake
class IssueActivityEndpoint(BaseAPIView):
permission_classes = [ProjectEntityPermission]
use_read_replica = True
@method_decorator(gzip_page)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])

View file

@ -232,6 +232,8 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
class UnreadNotificationEndpoint(BaseAPIView):
use_read_replica = True
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)

View file

@ -46,6 +46,7 @@ class ProjectViewSet(BaseViewSet):
serializer_class = ProjectListSerializer
model = Project
webhook_event = "project"
use_read_replica = True
def get_queryset(self):
sort_order = ProjectMember.objects.filter(

View file

@ -312,6 +312,7 @@ class ProjectMemberUserEndpoint(BaseAPIView):
class UserProjectRolesEndpoint(BaseAPIView):
permission_classes = [WorkspaceUserPermission]
use_read_replica = True
def get(self, request, slug):
project_members = ProjectMember.objects.filter(

View file

@ -44,6 +44,7 @@ from django.views.decorators.vary import vary_on_cookie
class UserEndpoint(BaseViewSet):
serializer_class = UserSerializer
model = User
use_read_replica = True
def get_object(self):
return self.request.user

View file

@ -177,6 +177,7 @@ class WorkSpaceViewSet(BaseViewSet):
class UserWorkSpacesEndpoint(BaseAPIView):
search_fields = ["name"]
filterset_fields = ["owner"]
use_read_replica = True
def get(self, request):
fields = [field for field in request.GET.get("fields", "").split(",") if field]

View file

@ -12,6 +12,7 @@ from plane.utils.cache import cache_response
class WorkspaceEstimatesEndpoint(BaseAPIView):
permission_classes = [WorkspaceEntityPermission]
use_read_replica = True
@cache_response(60 * 60 * 2)
def get(self, request, slug):

View file

@ -14,6 +14,8 @@ from plane.app.permissions import allow_permission, ROLE
class WorkspaceFavoriteEndpoint(BaseAPIView):
use_read_replica = True
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def get(self, request, slug):
# the second filter is to check if the user is a member of the project

View file

@ -12,6 +12,7 @@ from plane.utils.cache import cache_response
class WorkspaceLabelsEndpoint(BaseAPIView):
permission_classes = [WorkspaceViewerPermission]
use_read_replica = True
@cache_response(60 * 60 * 2)
def get(self, request, slug):

View file

@ -28,6 +28,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
model = WorkspaceMember
search_fields = ["member__display_name", "member__first_name"]
use_read_replica = True
def get_queryset(self):
return self.filter_queryset(
@ -214,6 +215,8 @@ class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
class WorkspaceMemberUserEndpoint(BaseAPIView):
use_read_replica = True
def get(self, request, slug):
draft_issue_count = (
DraftIssue.objects.filter(

View file

@ -11,6 +11,7 @@ from plane.app.permissions import allow_permission, ROLE
class QuickLinkViewSet(BaseViewSet):
model = WorkspaceUserLink
use_read_replica = True
def get_serializer_class(self):
return WorkspaceUserLinkSerializer

View file

@ -12,6 +12,7 @@ from plane.app.permissions import allow_permission, ROLE
class UserRecentVisitViewSet(BaseViewSet):
model = UserRecentVisit
use_read_replica = True
def get_serializer_class(self):
return WorkspaceRecentVisitSerializer

View file

@ -13,6 +13,7 @@ from collections import defaultdict
class WorkspaceStatesEndpoint(BaseAPIView):
permission_classes = [WorkspaceEntityPermission]
use_read_replica = True
@cache_response(60 * 60 * 2)
def get(self, request, slug):

View file

@ -12,6 +12,7 @@ from plane.app.serializers import StickySerializer
class WorkspaceStickyViewSet(BaseViewSet):
serializer_class = StickySerializer
model = Sticky
use_read_replica = True
def get_queryset(self):
return self.filter_queryset(

View file

@ -13,6 +13,7 @@ from rest_framework import status
class WorkspaceUserPreferenceViewSet(BaseAPIView):
model = WorkspaceUserPreference
use_read_replica = True
def get_serializer_class(self):
return WorkspaceUserPreferenceSerializer