diff --git a/dicquo/web/views.py b/dicquo/web/views.py index 6a6f683..4f28efe 100644 --- a/dicquo/web/views.py +++ b/dicquo/web/views.py @@ -12,6 +12,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.views.generic import DetailView, TemplateView import time import pytils +import random from web.models import TbDictumAndQuotes, TbImages, TbAuthor @@ -98,9 +99,11 @@ class CommonContextMixin: # --- 4. ВЫБОР КАРТИНКИ --- if dq.kImages_id is None: if len(tags) != 0: - tagged_image = TbImages.objects.filter(tags__name__in=tags).order_by('?').first() - if tagged_image: - context.update({'IMAGE': tagged_image.imFile}) + # Используем Python random вместо order_by('?') для производительности + tagged_images = list(TbImages.objects.filter(tags__name__in=tags).values_list('id', 'imFile')) + if tagged_images: + tagged_image = tagged_images[random.randint(0, len(tagged_images) - 1)] + context.update({'IMAGE': tagged_image[1]}) else: context.update({'IMAGE': dq.kImages.imFile}) @@ -110,7 +113,13 @@ class CommonContextMixin: # --- 6. ВЫБОР СЛЕДУЮЩЕЙ ЦИТАТЫ --- # Сначала пробуем найти следующую цитату, которую мы еще не видели - dq_next = queryset.exclude(id__in=seen_ids).order_by('?').first() + # Используем Python random вместо order_by('?') для производительности + unseen_ids = queryset.exclude(id__in=seen_ids).values_list('id', flat=True) + if unseen_ids: + dq_next_id = list(unseen_ids)[random.randint(0, len(unseen_ids) - 1)] + dq_next = queryset.filter(id=dq_next_id).first() + else: + dq_next = None # Если таких нет (мы посмотрели все цитаты в этом контексте/теге) if dq_next is None: @@ -123,7 +132,12 @@ class CommonContextMixin: # Вариант: Очистить seen_ids, чтобы в следующий раз (на некст странице) список был пуст? # Или просто выбрать любую КРОМЕ текущей? - dq_next = queryset.exclude(id=dq.id).order_by('?').first() + other_ids = list(queryset.exclude(id=dq.id).values_list('id', flat=True)) + if other_ids: + dq_next_id = other_ids[random.randint(0, len(other_ids) - 1)] + dq_next = queryset.filter(id=dq_next_id).first() + else: + dq_next = None # Если мы действительно прошли весь цикл по тегу, логично сбросить seen_ids, # чтобы пользователь мог заново проходить этот список случайно, а не "застревать" на последних. @@ -200,13 +214,19 @@ class IndexView(CommonContextMixin, TemplateView): if active_qs is not None: # Если мы в режиме фильтрации, тоже стараемся не показывать то, что уже видели - dq = active_qs.exclude(id__in=seen_ids).order_by('?').first() + unseen_ids = list(active_qs.exclude(id__in=seen_ids).values_list('id', flat=True)) + if unseen_ids: + dq = active_qs.filter(id=unseen_ids[random.randint(0, len(unseen_ids) - 1)]).first() + else: + dq = None # Если после фильтрации ничего не осталось (мы просмотрели все цитаты тега) if dq is None: # Сбрасываем историю и берем любую self.request.session['seen_ids'] = [] - dq = active_qs.order_by('?').first() + all_ids = list(active_qs.values_list('id', flat=True)) + if all_ids: + dq = active_qs.filter(id=all_ids[random.randint(0, len(all_ids) - 1)]).first() if dq is None: # Если тег не задан, или по тегу ничего не нашлось совсем @@ -214,11 +234,14 @@ class IndexView(CommonContextMixin, TemplateView): active_qs = TbDictumAndQuotes.objects.all() # Случайная цитата (с учетом истории, чтобы главная страница тоже не зацикливалась) - dq = active_qs.exclude(id__in=seen_ids).order_by('?').first() - - if dq is None: + unseen_ids = list(active_qs.exclude(id__in=seen_ids).values_list('id', flat=True)) + if unseen_ids: + dq = active_qs.filter(id=unseen_ids[random.randint(0, len(unseen_ids) - 1)]).first() + else: self.request.session['seen_ids'] = [] - dq = active_qs.order_by('?').first() + all_ids = list(active_qs.values_list('id', flat=True)) + if all_ids: + dq = active_qs.filter(id=all_ids[random.randint(0, len(all_ids) - 1)]).first() if dq: # Используем миксин, ОБЯЗАТЕЛЬНО передаем active_qs diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 6e0a96b..8fd24e2 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -16,8 +16,7 @@ services: # 1. ОБРАЗ # В продакшене мы используем готовый, собранный образ из реестра (Gitea) - image: git.cube2.ru/erjemin/2020-dq:v1.0.7 - # image: git.cube2.ru/erjemin/2020-dq:latest + image: git.cube2.ru/erjemin/2020-dq:latest # Если образа в gitae нет, то перенести весь код в прод и можно собирать локально: # build: . @@ -29,13 +28,14 @@ services: # 3. КОМАНДА ЗАПУСКА (Замена entrypoint.sh) # Выполняем цепочку команд внутри контейнера при запуске: - # 1. Миграции - # 2. Collectstatic - # 3. Создаем папку nginx в примонтированном томе конфигов (если нет) - # 4. Копирование конфига Nginx с авто-заменой путей через sed (замену реального пути на хосте получаем через переменную окружения HOST_PROJECT_PATH) - # 5. Инициализация боевого конфига (если нет) - # 6. Создаем папку для ошибок и копируем туда статические страницы 404/500 - # 7. Запуск Gunicorn + # a. Миграции + # b. Collectstatic + # с. Создаем папку nginx в примонтированном томе конфигов (если нет) + # d. Копирование конфига Nginx с авто-заменой путей через sed (замену реального пути на хосте получаем + # через переменную окружения HOST_PROJECT_PATH) + # e. Инициализация боевого конфига (если нет) + # f. Создаем папку для ошибок и копируем туда статические страницы 404/500 (там их увидит Nginx хоста) + # g. Запуск Gunicorn command: > sh -c "python manage.py migrate --noinput && python manage.py collectstatic --noinput && @@ -48,12 +48,12 @@ services: mkdir -p /app/public/media/errors && cp /home/app/web/dicquo/templates/static_404.html /app/public/media/errors/404.html && cp /home/app/web/dicquo/templates/static_500.html /app/public/media/errors/500.html && - gunicorn --workers 2 --timeout 120 --bind 0.0.0.0:8010 dicquo.wsgi:application" + gunicorn --workers 3 --bind 0.0.0.0:8000 dicquo.wsgi:application" # 4. Проброс портов (Внешний Nginx -> localhost:8010) ports: # Слушаем только на localhost хоста, чтобы закрыть прямой доступ из интернета к Gunicorn - - "127.0.0.1:8010:8010" + - "127.0.0.1:8010:8000" # 5. Тома (Volumes) volumes: @@ -69,16 +69,14 @@ services: # Это нужно, чтобы скрипт запуска мог положить туда .example конфиг и прочитать боевой конфиг. - ./config:/nginx_configs_host - # 6. + # 6. Пользователь и права user: "1000:1000" - # Для отладки - cap_add: - - SYS_PTRACE + # Когда нужна отладка процессов внутри контейнера, можно временно раскомментировать эту строку и запустить контейнер с правами root. + # cap_add: + # - SYS_PTRACE - - - # 6. Переменные окружения + # 7. Переменные окружения env_file: - .env environment: @@ -87,31 +85,30 @@ services: # Передаем переменную с путем на хосте внутрь контейнера, чтобы sed мог её использовать - HOST_PROJECT_PATH=${HOST_PROJECT_PATH:-/home/default_user/projects/dq-site} - # 7. Проверка здоровья контейнера (Healthcheck) + # 8. Проверка здоровья контейнера (Healthcheck) # Docker будет периодически проверять статус контейнера. Это критично для Watchtower! # Если контейнер объявлен "unhealthy", Watchtower сначала остановит старый образ, потом запустит новый. - # healthcheck: - # # test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8010/').read()"] - # test: ["CMD", "test", "-f", "/home/app/web/dicquo/gunicorn.ctl"] - # interval: 300s # Проверка каждые 300 секунд - # timeout: 30s # Таймаут ответа - 30 секунды - # start_period: 80s # Даем 30 секунд на стартап перед первой проверкой - # retries: 3 # Unhealthy после 3 неудачных попыток + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/').read()"] + interval: 30s # Проверка каждые 30 секунд + timeout: 3s # Таймаут ответа - 3 секунды + start_period: 10s # Даем 10 секунд на стартап перед первой проверкой + retries: 3 # Unhealthy после 3 неудачных попыток - # 8. Логирование (Ротация) + # 9. Логирование (Ротация) logging: driver: "json-file" options: max-size: "10m" - max-file: "2" + max-file: "3" - # 9. Ресурсы + # 10. Ресурсы deploy: resources: limits: - cpus: '0.50' - memory: 380M - mem_limit: 380M + cpus: '0.40' + memory: 256M + mem_limit: 256m # --- WATCHTOWER: АВТО-ОБНОВЛЕНИЕ ОБРАЗОВ --- # Следит за реестром Gitea и обновляет контейнер web, если появился новый image @@ -136,5 +133,5 @@ services: logging: driver: "json-file" options: - max-size: "1m" - max-file: "1" \ No newline at end of file + max-size: "10m" + max-file: "3" \ No newline at end of file