@@ -197,7 +197,8 @@ DATABASES = { 'PASSWORD': env.str('POSTGRES_PASSWORD'), 'HOST': env.str('POSTGRES_HOST'), 'PORT': env.str('POSTGRES_PORT'), - 'DISABLE_SERVER_SIDE_CURSOR': False, + 'CONN_MAX_AGE': env.str('POSTGRES_CONN_MAX_AGE', 0), + 'CONN_HEALTH_CHECKS': env.str('POSTGRES_CONN_HEALTH_CHECKS', False), } } @@ -280,12 +281,8 @@ CELERY_BEAT_SCHEDULE = { CACHES = { 'default': { - 'BACKEND': 'django_redis.cache.RedisCache', + 'BACKEND': 'django.core.cache.backends.redis.RedisCache', 'LOCATION': env.str('CACHE_BROKER_URL', 'redis://cache-mdb:6379'), - 'OPTIONS': { - 'CLIENT_CLASS': 'django_redis.client.DefaultClient', - }, - 'TIMEOUT': None, } } @@ -1,14 +1,14 @@ import logging +from typing import Literal from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.core.exceptions import ObjectDoesNotExist -from django.http import JsonResponse from django.urls import include, path from django.utils.translation import gettext_lazy as _ from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView -from ninja import NinjaAPI +from ninja import NinjaAPI, Schema from authentication.exceptions import ( InvalidPassword, @@ -16,6 +16,10 @@ from authentication.exceptions import ( ) from backend.public import urlpatterns as public_urlpatterns + +logger = logging.getLogger(__name__) + + api = NinjaAPI(title='AIR API', version='1.0.0', docs_url=None) compatibility_api = NinjaAPI(title='AIR API DEBUG', version='0.0.1', docs_url=None) compatibility_api_v2 = NinjaAPI(title='AIR API DEBUG v2', version='2.0.0', docs_url=None) @@ -31,7 +35,38 @@ compatibility_api.add_router('ml_model/', 'ml_model.routes.v1.router') compatibility_api_v2.add_router('auth/', 'authentication.routes.v2.router') -logger = logging.getLogger(__name__) + +class Status(Schema): + status: Literal['ok', 'dead'] + + +@compatibility_api.get('/healthz', tags=['maintenance'], response={200: Status, 503: Status}) +def healthz_status(request): + try: + from django.db import connections + + for name in connections: + cursor = connections[name].cursor() + cursor.execute('SELECT 1;') + row = cursor.fetchone() + if row is None: + return 503, {'status': 'dead'} + except Exception as e: + logger.exception(e) + return 503, {'status': 'dead'} + + try: + from django.core.cache import caches + from django.core.cache.backends.redis import RedisCache + + for cache in caches.all(): + if isinstance(cache, RedisCache) and not cache._cache.get_client().ping(): + return 503, {'status': 'dead'} + except Exception as e: + logger.exception(e) + return 503, {'status': 'dead'} + + return 200, {'status': 'ok'} @api.exception_handler(ObjectDoesNotExist) @@ -49,10 +84,6 @@ def invalid_password_error_handler(request, exc: InvalidPassword): return api.create_response(request, {'message': _('Wrong password')}, status=401) -def healthz_status(request): - return JsonResponse({'status': 'ok'}, status=200) - - urlpatterns = [] if settings.DEBUG: @@ -79,7 +110,6 @@ if settings.DEBUG: urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += [ - path('api/v1/healthz/', healthz_status), path('admin/', admin.site.urls), path('api/v1/ml_models/', include('ml_model.urls', namespace='ml_model')), path('api/v1/auth/', include('authentication.urls')), @@ -19,6 +19,7 @@ from ninja.errors import HttpError from authentication.models import CustomUserModel from authentication.security import AsyncAuthBearer, SyncAuthBearer from payments.exceptions.payer_not_found import PayerNotFound +from authentication.exceptions.business_host_exceptions.access_denied import AccessDenied from payments.models import ( Invoice, Payment, @@ -161,7 +162,7 @@ def list_expenses(request, data: ExpensesParamsSchema = Query(...)): async def list_payment_plans(request): try: if request.auth.account_type not in {'regular', 'business_host'}: - raise HttpError(401, 'Unauthorized') + raise AccessDenied is_corporate = request.auth.account_type == 'business_host' plans = PaymentPlan.objects.filter( price__gt=0, is_corporate=is_corporate, is_visible=True @@ -199,6 +200,8 @@ async def list_payment_plans(request): ) ) return result + except AccessDenied as exc: + raise HttpError(403, str(exc)) except Exception as exc: logger.exception(exc, exc_info=True) raise HttpError(400, f'{exc}') @@ -208,7 +211,7 @@ async def list_payment_plans(request): async def create_payment_link(request, body: NewSubscriptionSchema): try: if request.auth.account_type not in {'regular', 'business_host'}: - raise HttpError(401, 'Unauthorized') + raise AccessDenied is_corporate = request.auth.account_type == 'business_host' payment_plan = await PaymentPlan.objects.aget( uid=body.uid, price__gt=0, is_corporate=is_corporate, is_visible=True, individual=False @@ -220,6 +223,8 @@ async def create_payment_link(request, body: NewSubscriptionSchema): payment_plan.uid, ) return PaymentLinkSchema(payment_url=payment_url) + except AccessDenied as exc: + raise HttpError(403, str(exc)) except Exception as exc: raise HttpError(400, f'{exc}')