Skip to content

Commit

Permalink
fix: redis connections leak (#148)
Browse files Browse the repository at this point in the history
- fix: bump saq lib to 0.12 to avoid Redis connections leak
- chore: bump project dependencies versions
- chore: move to the alpine version of the docker image
  • Loading branch information
alexhook authored Apr 8, 2024
1 parent 0bb3a60 commit c074e52
Show file tree
Hide file tree
Showing 11 changed files with 385 additions and 354 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
run: |
pip install copier==7.1.0
pip install copier-templates-extensions
pip install pyyaml-include==1.4.1
# hardcode pydantic version to avoid dependency conflict with copier
pip install pydantic==1.10.2
Expand Down Expand Up @@ -72,6 +73,7 @@ jobs:
run: |
pip install copier==7.1.0
pip install copier-templates-extensions
pip install pyyaml-include==1.4.1
# hardcode pydantic version to avoid dependency conflict with copier
pip install pydantic==1.10.2
python -m copier --defaults . bot-example
Expand Down Expand Up @@ -127,6 +129,7 @@ jobs:
run: |
pip install copier==7.1.0
pip install copier-templates-extensions
pip install pyyaml-include==1.4.1
# hardcode pydantic version to avoid dependency conflict with copier
pip install pydantic==1.10.2
python -m copier --defaults . bot-example
Expand Down
44 changes: 24 additions & 20 deletions Dockerfile.jinja
Original file line number Diff line number Diff line change
@@ -1,33 +1,38 @@
FROM python:3.10-slim
FROM python:3.11.5-alpine

{% if from_ccsteam %}LABEL Maintainer="eXpress Unlimited Production"{% endif %}

ENV PYTHONUNBUFFERED 1
ENV UVICORN_CMD_ARGS ""

EXPOSE 8000

# Install system-wide dependencies
RUN apt-get update && \
apt-get install --no-install-recommends -y git curl gcc && \
python3 -m pip install setuptools && \
apt-get clean autoclean && \
apt-get autoremove --yes && \
rm -rf /var/lib/apt/lists/*
RUN apk update && \
apk add --no-cache --clean-protected git curl gcc python3-dev && \
rm -rf /var/cache/apk/*

# Create user for app
ENV APP_USER=appuser
RUN useradd --create-home $APP_USER
RUN adduser -D $APP_USER
WORKDIR /home/$APP_USER
USER $APP_USER

# Set python env vars
ENV PYTHONUNBUFFERED 1
ENV PYTHONNODEBUGRANGES 1
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONPATH="/home/$APP_USER:$PYTHONPATH"

# Use venv directly via PATH
ENV VENV_PATH=/home/$APP_USER/.venv/bin
ENV USER_PATH=/home/$APP_USER/.local/bin
ENV PATH="$VENV_PATH:$USER_PATH:$PATH"

RUN pip install --user --no-cache-dir poetry==1.2.2 && \
poetry config virtualenvs.in-project true
# Set app env vars
ENV GUNICORN_CMD_ARGS ""

# Set build env vars
ARG CI_COMMIT_SHA=""
ENV GIT_COMMIT_SHA=${CI_COMMIT_SHA}

RUN pip install --user --no-cache-dir poetry==1.4.2 && \
poetry config virtualenvs.in-project true

COPY poetry.lock pyproject.toml ./

Expand All @@ -38,17 +43,16 @@ ARG GIT_PASSWORD=${CI_JOB_TOKEN}
ARG GIT_LOGIN="gitlab-ci-token"
# Poetry can't read password to download private repos
RUN echo -e "machine ${GIT_HOST}\nlogin ${GIT_LOGIN}\npassword ${GIT_PASSWORD}" > ~/.netrc && \
poetry install --only main && \
rm -rf ~/.netrc
poetry install --only main && \
rm -rf ~/.netrc
{% else %}
RUN poetry install --only main
{% endif %}

COPY alembic.ini .
COPY app app

ARG CI_COMMIT_SHA=""
ENV GIT_COMMIT_SHA=${CI_COMMIT_SHA}
EXPOSE 8000

CMD alembic upgrade head && \
gunicorn "app.main:get_application()" --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0
gunicorn "app.main:get_application()" --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0 $GUNICORN_CMD_ARGS
3 changes: 3 additions & 0 deletions app/api/dependencies/healthcheck.py.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ check_redis_connection_dependency = Depends(check_redis_connection)
async def check_worker_status() -> Optional[str]:
job = await queue.enqueue("healthcheck")

if not job:
return None

try:
await job.refresh(settings.WORKER_TIMEOUT_SEC)
except TimeoutError:
Expand Down
7 changes: 4 additions & 3 deletions app/caching/exception_handlers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Redis custom exception handlers."""

from redis.asyncio.client import PubSub
from redis.asyncio.client import PubSub, PubsubWorkerExceptionHandler

from app.logger import logger


async def pubsub_exception_handler(exc: BaseException, pubsub: PubSub) -> None:
logger.exception("Something went wrong in PubSub")
class PubsubExceptionHandler(PubsubWorkerExceptionHandler):
def __call__(self, exc: BaseException, pubsub: PubSub) -> None:
logger.exception("Something went wrong in PubSub")
4 changes: 2 additions & 2 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from app.api.routers import router
from app.bot.bot import get_bot
from app.caching.callback_redis_repo import CallbackRedisRepo
from app.caching.exception_handlers import pubsub_exception_handler
from app.caching.exception_handlers import PubsubExceptionHandler
from app.caching.redis_repo import RedisRepo
from app.db.sqlalchemy import build_db_session_factory, close_db_connections
from app.resources import strings
Expand All @@ -33,7 +33,7 @@ async def startup(application: FastAPI, raise_bot_exceptions: bool) -> None:
# -- Bot --
callback_repo = CallbackRedisRepo(redis_client)
process_callbacks_task = asyncio.create_task(
callback_repo.pubsub.run(exception_handler=pubsub_exception_handler)
callback_repo.pubsub.run(exception_handler=PubsubExceptionHandler())
)
bot = get_bot(callback_repo, raise_exceptions=raise_bot_exceptions)

Expand Down
10 changes: 8 additions & 2 deletions app/services/botx_user_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
from typing import Optional, Tuple
from uuid import UUID

from pybotx import Bot, BotAccount, UserFromSearch, UserKinds, UserNotFoundError
from pybotx import (
Bot,
BotAccountWithSecret,
UserFromSearch,
UserKinds,
UserNotFoundError,
)


class UserIsBotError(Exception):
Expand All @@ -12,7 +18,7 @@ class UserIsBotError(Exception):

async def search_user_on_each_cts(
bot: Bot, huid: UUID
) -> Optional[Tuple[UserFromSearch, BotAccount]]:
) -> Optional[Tuple[UserFromSearch, BotAccountWithSecret]]:
"""Search user by huid on all cts on which bot is registered.
return type: tuple of UserFromSearch instance and host.
Expand Down
8 changes: 6 additions & 2 deletions app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,15 @@ def _build_credentials_from_string(
credentials_str = credentials_str.replace("|", "@")
assert credentials_str.count("@") == 2, "Have you forgot to add `bot_id`?"

host, secret_key, bot_id = [
cts_url, secret_key, bot_id = [
str_value.strip() for str_value in credentials_str.split("@")
]

if "://" not in cts_url:
cts_url = f"https://{cts_url}"

return BotAccountWithSecret(
id=UUID(bot_id), host=host, secret_key=secret_key
id=UUID(bot_id), cts_url=cts_url, secret_key=secret_key
)

BOT_CREDENTIALS: List[BotAccountWithSecret]
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ services:
build: .
container_name: {{bot_project_name}}-worker
# '$$' prevents docker-compose from interpolating a value
command: bash -c 'PYTHONPATH="$$PYTHONPATH:$$PWD" saq app.worker.worker.settings'
command: /bin/sh -c 'PYTHONPATH="$$PYTHONPATH:$$PWD" saq app.worker.worker.settings'
environment: *environment
restart: always
depends_on: *depends_on
Expand Down
Loading

0 comments on commit c074e52

Please sign in to comment.