From 321b53d6d848fd2f1091a434476307c9af86e7ea Mon Sep 17 00:00:00 2001 From: Marzuk Rashid Date: Mon, 16 Dec 2024 05:35:02 -0600 Subject: [PATCH 1/3] Move user state to redis --- .env.example | 5 +- .githooks/{pre-commit => pre-push} | 0 README.md | 7 +- api/Dockerfile | 9 +- .../api.dev.sh} | 7 +- .../api.prod.sh} | 7 +- api/entrypoints/worker.dev.sh | 7 ++ api/entrypoints/worker.prod.sh | 7 ++ api/requirements.txt | 13 +++ api/src/app.py | 84 +++++-------------- api/src/pigeon.py | 69 ++++----------- api/src/props.py | 4 - api/src/sessions.py | 26 ++++++ api/src/settings.py | 8 +- api/src/tasks.py | 65 ++++++++++++++ app/.dockerignore | 2 + app/Dockerfile | 4 +- docker-compose.dev.yml | 47 +++++++++-- docker-compose.prod.yml | 79 ++++++++++++----- 19 files changed, 264 insertions(+), 186 deletions(-) rename .githooks/{pre-commit => pre-push} (100%) rename api/{entrypoint.dev.sh => entrypoints/api.dev.sh} (56%) rename api/{entrypoint.prod.sh => entrypoints/api.prod.sh} (61%) create mode 100644 api/entrypoints/worker.dev.sh create mode 100644 api/entrypoints/worker.prod.sh create mode 100644 api/src/sessions.py create mode 100644 api/src/tasks.py create mode 100644 app/.dockerignore diff --git a/.env.example b/.env.example index c240343..997ae4c 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,6 @@ DD_API_KEY=PLACEHOLDER DD_ENV=prod MAX_THREADS=1 STRATEGY_LOCATION=PLACEHOLDER -MAX_SESSIONS=20 -SESSION_ID_BYTES=8 -SESSION_TIMEOUT=1800 # in seconds +SESSION_TIMEOUT=86400 # in seconds PIGEON_EXECUTION_TIMEOUT=5 # in seconds +REDIS_MAXMEMORY=256mb diff --git a/.githooks/pre-commit b/.githooks/pre-push similarity index 100% rename from .githooks/pre-commit rename to .githooks/pre-push diff --git a/README.md b/README.md index 369463a..8ef24aa 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,12 @@ poker. Inspired by the Pluribus poker bot developed by CMU and Facebook. ```sh python -m venv api/venv api/venv/bin/pip install -r api/requirements.txt -cp .githooks/pre-commit .git/hooks/pre-commit +cp .githooks/pre-push .git/hooks/pre-push git submodule update --init --recursive cp ai/src/mccfr/hyperparameters.h.dev ai/src/mccfr/hyperparameters.h docker compose -f docker-compose.dev.yml build ai docker run --rm -v ./ai/out:/build/out fishbait-ai /ai/dev_blueprint.sh -docker compose -f docker-compose.dev.yml build -docker compose -f docker-compose.dev.yml up +docker compose -f docker-compose.dev.yml up --build ``` ## Deployment @@ -22,7 +21,7 @@ docker compose -f docker-compose.dev.yml up 3. `cp nginx.conf.example nginx.conf` * Set the `server_name` property to be the deployment url of the interface 4. `cp .env.example .env` and configure -5. `docker compose -f docker-compose.prod.yml up` +5. `docker compose -f docker-compose.prod.yml up --build -d` 6. Configure HTTPS with AWS Application Load Balancer ## Testing diff --git a/api/Dockerfile b/api/Dockerfile index e8b24ef..8c754ee 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -12,10 +12,5 @@ RUN python3 -m venv venv COPY requirements.txt . RUN venv/bin/pip install --no-cache-dir -r requirements.txt -COPY entrypoint.dev.sh . -RUN chmod +x entrypoint.dev.sh - -COPY entrypoint.prod.sh . -RUN chmod +x entrypoint.prod.sh - -RUN mkdir /api/out +COPY ./entrypoints ./entrypoints +RUN chmod +x ./entrypoints/* diff --git a/api/entrypoint.dev.sh b/api/entrypoints/api.dev.sh similarity index 56% rename from api/entrypoint.dev.sh rename to api/entrypoints/api.dev.sh index 876dfc9..1d4a210 100644 --- a/api/entrypoint.dev.sh +++ b/api/entrypoints/api.dev.sh @@ -1,11 +1,6 @@ #!/bin/sh +set -e -while [ ! -f /libvol/done ]; do - echo "Waiting for AI shared library..." - sleep 2 -done - -rm /libvol/done export LD_LIBRARY_PATH=/libvol/lib:$LD_LIBRARY_PATH cd /api/src diff --git a/api/entrypoint.prod.sh b/api/entrypoints/api.prod.sh similarity index 61% rename from api/entrypoint.prod.sh rename to api/entrypoints/api.prod.sh index 7474e46..1096f4a 100644 --- a/api/entrypoint.prod.sh +++ b/api/entrypoints/api.prod.sh @@ -1,11 +1,6 @@ #!/bin/sh +set -e -while [ ! -f /libvol/done ]; do - echo "Waiting for AI shared library..." - sleep 2 -done - -rm /libvol/done export LD_LIBRARY_PATH=/libvol/lib:$LD_LIBRARY_PATH cd /api/src diff --git a/api/entrypoints/worker.dev.sh b/api/entrypoints/worker.dev.sh new file mode 100644 index 0000000..6d7bef4 --- /dev/null +++ b/api/entrypoints/worker.dev.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -e + +export LD_LIBRARY_PATH=/libvol/lib:$LD_LIBRARY_PATH + +cd /api/src +/api/venv/bin/celery -A tasks worker --loglevel=INFO diff --git a/api/entrypoints/worker.prod.sh b/api/entrypoints/worker.prod.sh new file mode 100644 index 0000000..355a411 --- /dev/null +++ b/api/entrypoints/worker.prod.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -e + +export LD_LIBRARY_PATH=/libvol/lib:$LD_LIBRARY_PATH + +cd /api/src +/api/venv/bin/ddtrace-run /api/venv/bin/celery -A tasks worker --loglevel=INFO diff --git a/api/requirements.txt b/api/requirements.txt index 8be83fc..1402711 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -1,10 +1,16 @@ +amqp==5.3.1 astroid==3.3.6 attrs==23.2.0 +billiard==4.2.1 bytecode==0.15.1 cattrs==23.2.3 +celery==5.4.0 certifi==2024.2.2 charset-normalizer==3.3.2 click==8.1.3 +click-didyoumean==0.3.1 +click-plugins==1.1.1 +click-repl==0.3.0 cpplint==1.6.1 datadog==0.44.0 ddsketch==2.0.4 @@ -20,6 +26,7 @@ importlib-metadata==6.11.0 isort==5.10.1 itsdangerous==2.1.2 Jinja2==3.1.2 +kombu==5.4.2 lazy-object-proxy==1.7.1 MarkupSafe==2.1.1 mccabe==0.7.0 @@ -27,15 +34,21 @@ mypy==0.981 mypy-extensions==0.4.3 opentelemetry-api==1.22.0 platformdirs==2.5.2 +prompt_toolkit==3.0.48 protobuf==4.25.2 pylint==3.3.2 +python-dateutil==2.9.0.post0 +redis==5.2.1 requests==2.31.0 setuptools==75.6.0 six==1.16.0 tomli==2.0.1 tomlkit==0.11.4 typing_extensions==4.3.0 +tzdata==2024.2 urllib3==2.2.0 +vine==5.1.0 +wcwidth==0.2.13 Werkzeug==2.2.2 wrapt==1.14.1 xmltodict==0.13.0 diff --git a/api/src/app.py b/api/src/app.py index 0e7c726..89aef17 100644 --- a/api/src/app.py +++ b/api/src/app.py @@ -1,28 +1,27 @@ """Webserver for the FISHBAIT web interface.""" -from datetime import datetime, timezone -from multiprocessing.managers import RemoteError -import re from typing import Any -import secrets import os from flask import Flask, request, make_response +import redis.exceptions from werkzeug.exceptions import BadRequest import datadog from datadog import statsd +import redis -from pigeon import PigeonProxy, PigeonInterface +from pigeon import PigeonInterface import settings -import error from error import ( ApiError, ServerOverloadedError, MissingSessionIdError, UnknownSessionIdError, ValidationError ) from props import ( - SetHandProps, ApplyProps, SetBoardProps, ResetProps, JoinEmailListProps + SetHandProps, ApplyProps, SetBoardProps, ResetProps ) from utils import get_logger +import tasks +import sessions app = Flask(__name__) log = get_logger(__name__) @@ -30,19 +29,8 @@ datadog.initialize() def handle_api_error(e: ApiError): - log.exception(e) return e.flask_tuple() -def handle_remote_error(e: RemoteError): - match = re.search(r'error\.([A-z]*?): ((.|\n)*)\n-{75}', f'{e}') - if match is None: - log.error('Could not parse PigeonProxy error') - raise ApiError() from e - error_name, error_msg = match.group(1, 2) - error_type = getattr(error, error_name) - parsed_error = error_type(error_msg) - raise parsed_error from e - def handle_bad_request(e: BadRequest): raise ValidationError() from e @@ -51,8 +39,6 @@ def handle_exceptions(e: Exception): error_handler: Any = None if isinstance(e, ApiError): error_handler = handle_api_error - elif isinstance(e, RemoteError): - error_handler = handle_remote_error elif isinstance(e, BadRequest): error_handler = handle_bad_request else: @@ -64,11 +50,9 @@ def handle_exceptions(e: Exception): log.exception(e) return result except Exception as new_exc: # pylint: disable=broad-except - if type(e) is type(new_exc): - log.exception(new_exc) - rec_er = RecursionError('Could not reduce the encountered error') - raise rec_er from new_exc - return handle_exceptions(new_exc) + log.exception(new_exc) + log.exception(f"Exception while handling exception: {e}") + return ApiError().flask_tuple() @app.before_request def record_api_metric(): @@ -78,26 +62,18 @@ def record_api_metric(): # Session Management ----------------------------------------------------------- # ------------------------------------------------------------------------------ -class Session: - def __init__(self): - self.updated = datetime.now(timezone.utc).timestamp() - self.revere = PigeonProxy() - -sessions: dict[str, Session] = {} - def session_guard(route): def guarded_route(): session_id = request.cookies.get(settings.SESSION_ID_KEY) if session_id is None: raise MissingSessionIdError() - session = sessions.get(session_id) - if session is None: + if not sessions.does_session_exist(session_id): raise UnknownSessionIdError() - session.updated = datetime.now(timezone.utc).timestamp() + revere = tasks.PigeonProxy(session_id) try: - return route(session.revere) + return route(revere) except ApiError: # These errors are anticipated and should be passed through raise @@ -105,35 +81,12 @@ def guarded_route(): # We should not get any other type of error. If we do, something may have # gone horribly wrong and we need to delete this session to preserve the # integrity of the server: - sessions.pop(session_id) + sessions.delete_session(session_id) raise guarded_route.__name__ = route.__name__ return guarded_route -def create_new_session(): - token_candidate = secrets.token_hex(settings.SESSION_ID_BYTES) - while token_candidate in sessions: - token_candidate = secrets.token_hex(settings.SESSION_ID_BYTES) - sessions[token_candidate] = Session() - return token_candidate - -def try_create_new_session(): - if len(sessions) >= settings.MAX_SESSIONS: - current_time = datetime.now(timezone.utc).timestamp() - to_remove = None - for session_id, session in sessions.items(): - if current_time - session.updated >= settings.SESSION_TIMEOUT: - to_remove = session_id - break - if to_remove is not None: - sessions.pop(to_remove) - return create_new_session() - else: - return None - else: - return create_new_session() - # ------------------------------------------------------------------------------ # Routes ----------------------------------------------------------------------- # ------------------------------------------------------------------------------ @@ -144,9 +97,13 @@ def api_status(): @app.route('/api/new-session', methods=['GET']) def new_session(): - new_session_id = try_create_new_session() - if new_session_id is None: + try: + new_session_id = tasks.create_new_session.delay().get( + timeout=settings.PIGEON_EXECUTION_TIMEOUT + ) + except redis.exceptions.OutOfMemoryError: raise ServerOverloadedError() + resp = make_response() resp.set_cookie(settings.SESSION_ID_KEY, new_session_id) return resp @@ -196,7 +153,4 @@ def reset(revere: PigeonInterface): @app.route('/api/join-email-list', methods=['POST']) def join_email_list(): - props = JoinEmailListProps(request.get_json()) - with open(settings.EMAIL_LIST_LOCATION, 'a', encoding='utf-8') as email_list: - email_list.write(f'{props.email}\n') return make_response() diff --git a/api/src/pigeon.py b/api/src/pigeon.py index 9cf3fdd..5c56f34 100644 --- a/api/src/pigeon.py +++ b/api/src/pigeon.py @@ -21,8 +21,7 @@ Structure, CFUNCTYPE, ) -from multiprocessing.managers import BaseManager -import concurrent.futures +from functools import cached_property import settings import props @@ -573,9 +572,7 @@ class Pigeon(PigeonInterface): '''Sends messages between python clients and the fishbait C++ AI.''' def __init__(self): - def commander_callback(data, size): - self._commander = bytes(data[:size]) - self._commander_callback = CallbackFunc(commander_callback) + self._commander = b'' strategy_loc = settings.STRATEGY_LOCATION commander_create(bytes(strategy_loc, 'utf-8'), self._commander_callback) @@ -583,6 +580,18 @@ def commander_callback(data, size): self._state: PigeonState = PigeonState() self._update_state() + @cached_property + def _commander_callback(self): + def callback(data, size): + self._commander = bytes(data[:size]) + return CallbackFunc(callback) + + def __getstate__(self) -> object: + return (self._commander, self._state) + + def __setstate__(self, state: tuple[bytes, PigeonState]): + self._commander, self._state = state + @staticmethod def _auto_advance[**P, T](fn: Callable[Concatenate['Pigeon', P], T]): # pylint: disable=protected-access @@ -722,53 +731,3 @@ def _award_pot(self): self._commander, self._commander_callback ) self._update_state() - -class PigeonManager(BaseManager): - pass -PigeonManager.register('Pigeon', Pigeon) - -class PigeonProxy(PigeonInterface): - ''' - An object that behaves like a local Pigeon to an outside observer but spawns - a Pigeon on a new process and sends messages to it. - ''' - - def __init__(self) -> None: - super().__init__() - self.manager = PigeonManager() - self.manager.start() # pylint: disable=consider-using-with - self.revere = self.manager.Pigeon() - - def __del__(self): - log.info('Shutting down manager for %s', self) - self.manager.shutdown() - log.info('Completed manager shutdown for %s', self) - - class PigeonMessage(): - ''' - A descriptor that applies the given function on the managed Pigeon - ''' - - def __set_name__(self, owner, name): - self.name = name - - def __get__(self, obj, objtype): - def wrapped_fn(*args, **kwargs): - log.info( - 'Calling %s for %s with args %s and kwargs %s', - self.name, obj, args, kwargs - ) - with concurrent.futures.ThreadPoolExecutor() as executor: - fn = getattr(obj.revere, self.name) - future = executor.submit(fn, *args, **kwargs) - result = future.result(timeout=settings.PIGEON_EXECUTION_TIMEOUT) - log.info('Completed %s for %s', self.name, obj) - return result - return wrapped_fn - - reset = PigeonMessage() - set_hand = PigeonMessage() - apply = PigeonMessage() - set_board = PigeonMessage() - state_dict = PigeonMessage() - new_hand = PigeonMessage() diff --git a/api/src/props.py b/api/src/props.py index 4e7411d..f8b7bec 100644 --- a/api/src/props.py +++ b/api/src/props.py @@ -326,7 +326,3 @@ class ResetProps(BaseProps): small_blind: Prop[ChipCount] = Prop(ChipCount) fishbait_seat: Prop[PlayerNumber] = Prop(PlayerNumber) player_names: Prop[StrPlayerList] = Prop(StrPlayerList) - -@propclass -class JoinEmailListProps(BaseProps): - email: Prop[Email] = Prop(Email) diff --git a/api/src/sessions.py b/api/src/sessions.py new file mode 100644 index 0000000..36f1232 --- /dev/null +++ b/api/src/sessions.py @@ -0,0 +1,26 @@ +import pickle + +import redis + +import settings +from pigeon import Pigeon + +session_manager = redis.Redis( + host='redis', port=6379, db=settings.REDIS_SESSION_DB +) + +def get_pigeon(session_id: str) -> Pigeon: + serialized_revere = session_manager.get(session_id) + return pickle.loads(serialized_revere) + +def set_pigeon(session_id: str, revere: Pigeon): + serialized_revere = pickle.dumps(revere) + session_manager.set( + session_id, serialized_revere, ex=settings.SESSION_TIMEOUT + ) + +def does_session_exist(session_id: str) -> bool: + return bool(session_manager.exists(session_id)) + +def delete_session(session_id: str): + session_manager.delete(session_id) diff --git a/api/src/settings.py b/api/src/settings.py index 4b3b610..2eccaec 100644 --- a/api/src/settings.py +++ b/api/src/settings.py @@ -5,9 +5,8 @@ STRATEGY_LOCATION: str = os.getenv("STRATEGY_LOCATION", "") RELAY_LIB_LOCATION: str = os.getenv("RELAY_LIB_LOCATION", "") -MAX_SESSIONS: int = int(os.getenv("MAX_SESSIONS", "1000")) -SESSION_ID_BYTES: int = int(os.getenv("SESSION_ID_BYTES", "8")) -SESSION_TIMEOUT: int = int(os.getenv("SESSION_TIMEOUT", "1800")) # in seconds +SESSION_ID_BYTES: int = 16 +SESSION_TIMEOUT: int = int(os.getenv("SESSION_TIMEOUT", "86400")) # in seconds SESSION_ID_KEY = os.getenv("SESSION_ID_KEY", "session_id") PIGEON_EXECUTION_TIMEOUT: int = ( int(os.getenv("PIGEON_EXECUTION_TIMEOUT", "5")) # in seconds @@ -18,4 +17,5 @@ BOARD_CARDS: int = 5 CARDS_IN_DECK: int = 52 -EMAIL_LIST_LOCATION: str = "/tmp/emails.txt" +REDIS_SESSION_DB: int = 0 +REDIS_CELERY_DB: int = 1 diff --git a/api/src/tasks.py b/api/src/tasks.py new file mode 100644 index 0000000..dd64846 --- /dev/null +++ b/api/src/tasks.py @@ -0,0 +1,65 @@ +import secrets + +from celery import Celery + +import settings +from pigeon import Pigeon, PigeonInterface +from sessions import get_pigeon, set_pigeon + +app = Celery( + 'tasks', + broker=f'redis://redis:6379/{settings.REDIS_CELERY_DB}', + backend=f'redis://redis:6379/{settings.REDIS_CELERY_DB}' +) +app.conf.update( + task_serializer='pickle', + result_serializer='pickle', + accept_content=['pickle'], +) + +@app.task +def create_new_session(): + session_id = secrets.token_hex(settings.SESSION_ID_BYTES) + revere = Pigeon() + set_pigeon(session_id, revere) + return session_id + +@app.task +def run_pigeon(session_id: str, message: str, *args, **kwargs): + revere = get_pigeon(session_id) + fn = getattr(revere, message) + result = fn(*args, **kwargs) + set_pigeon(session_id, revere) + return result + +class PigeonProxy(PigeonInterface): + ''' + An object that behaves like a local Pigeon to an outside observer but runs the + Pigeon's methods in celery tasks and saves the Pigeon's state in redis. + ''' + + def __init__(self, session_id: str) -> None: + super().__init__() + self.session_id = session_id + + class PigeonMessage(): + ''' + A descriptor that applies the given function on the managed Pigeon + ''' + + def __set_name__(self, owner, name): + self.name = name + + def __get__(self, obj, obj_type): + def wrapped_fn(*args, **kwargs): + return run_pigeon.delay( + obj.session_id, self.name, *args, **kwargs + ).get(timeout=settings.PIGEON_EXECUTION_TIMEOUT) + return wrapped_fn + + reset = PigeonMessage() + set_hand = PigeonMessage() + apply = PigeonMessage() + set_board = PigeonMessage() + state_dict = PigeonMessage() + new_hand = PigeonMessage() diff --git a/app/.dockerignore b/app/.dockerignore new file mode 100644 index 0000000..e3fbd98 --- /dev/null +++ b/app/.dockerignore @@ -0,0 +1,2 @@ +build +node_modules diff --git a/app/Dockerfile b/app/Dockerfile index 6464c96..48d6e1b 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -2,6 +2,8 @@ FROM node:22 WORKDIR /app -COPY package.json yarn.lock ./ +COPY . . RUN yarn install + +RUN yarn build diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 4c2759e..2736786 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -7,44 +7,73 @@ services: args: MAX_THREADS: ${MAX_THREADS:-1} image: fishbait-ai - container_name: fishbait-ai volumes: - lib:/libvol - command: ["sh", "-c", "cp -r /build/lib /libvol && touch /libvol/done"] + command: ["sh", "-c", "cp -r /build/lib /libvol"] api: build: context: ./api image: fishbait-api - container_name: fishbait-api environment: - STRATEGY_LOCATION=/aivol/blueprint_dev.hdf - RELAY_LIB_LOCATION=/libvol/lib/librelay.so - - MAX_SESSIONS=${MAX_SESSIONS:-1000} - - SESSION_ID_BYTES=${SESSION_ID_BYTES:-8} - - SESSION_TIMEOUT=${SESSION_TIMEOUT:-1800} + - SESSION_TIMEOUT=${SESSION_TIMEOUT:-86400} - PIGEON_EXECUTION_TIMEOUT=${PIGEON_EXECUTION_TIMEOUT:-5} depends_on: - - ai + ai: + condition: service_completed_successfully ports: - "5001:5000" volumes: - lib:/libvol - ./ai/out/ai/mccfr/dev:/aivol - ./api/src:/api/src - entrypoint: ["/api/entrypoint.dev.sh"] + entrypoint: ["/api/entrypoints/api.dev.sh"] + + worker: + build: + context: ./api + image: fishbait-api + environment: + - STRATEGY_LOCATION=/aivol/blueprint_dev.hdf + - RELAY_LIB_LOCATION=/libvol/lib/librelay.so + - SESSION_TIMEOUT=${SESSION_TIMEOUT:-86400} + - PIGEON_EXECUTION_TIMEOUT=${PIGEON_EXECUTION_TIMEOUT:-5} + depends_on: + ai: + condition: service_completed_successfully + redis: + condition: service_started + user: "1000:1000" + volumes: + - lib:/libvol + - ./ai/out/ai/mccfr/dev:/aivol + - ./api/src:/api/src + entrypoint: ["/api/entrypoints/worker.dev.sh"] + restart: always app: build: context: ./app image: fishbait-app - container_name: fishbait-app ports: - "3000:3000" volumes: - ./app:/app - /app/node_modules + - /app/build command: ["yarn", "start"] + redis: + image: redis:7.4.1 + entrypoint: + - sh + - -c + - | + redis-server \ + --maxmemory ${REDIS_MAXMEMORY:-256mb} \ + --maxmemory-policy volatile-lru + volumes: lib: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 2b5a393..2af2f92 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -3,68 +3,102 @@ name: fishbait services: nginx: image: nginx:1.27 - container_name: nginx ports: - "80:80" volumes: - ./nginx.conf:/etc/nginx/nginx.conf - app-build:/usr/share/nginx/html:ro depends_on: - - api - - app + app: + condition: service_completed_successfully + restart: always ai: build: context: ./ai args: - MAX_THREADS: ${MAX_THREADS} + MAX_THREADS: ${MAX_THREADS:-1} image: fishbait-ai - container_name: fishbait-ai volumes: - lib:/libvol - command: ["sh", "-c", "cp -r /build/lib /libvol && touch /libvol/done"] + command: ["sh", "-c", "cp -r /build/lib /libvol"] api: build: context: ./api image: fishbait-api - container_name: fishbait-api environment: - - STRATEGY_LOCATION=/strategy + - STRATEGY_LOCATION=/strategy.hdf - RELAY_LIB_LOCATION=/libvol/lib/librelay.so - - MAX_SESSIONS=${MAX_SESSIONS} - - SESSION_ID_BYTES=${SESSION_ID_BYTES} - - SESSION_TIMEOUT=${SESSION_TIMEOUT} - - PIGEON_EXECUTION_TIMEOUT=${PIGEON_EXECUTION_TIMEOUT} + - SESSION_TIMEOUT=${SESSION_TIMEOUT:-86400} + - PIGEON_EXECUTION_TIMEOUT=${PIGEON_EXECUTION_TIMEOUT:-5} - DD_AGENT_HOST=datadog-agent - DD_SERVICE=fishbait-api - DD_ENV=${DD_ENV} - - DD_LOGS_INJECTION=true - - DD_PROFILING_ENABLED=true + - DD_LOGS_INJECTION=true + - DD_PROFILING_ENABLED=true - DD_APPSEC_ENABLED=true depends_on: - - ai - - datadog-agent + ai: + condition: service_completed_successfully volumes: - lib:/libvol - ./api/src:/api/src - - ${STRATEGY_LOCATION}:/strategy - entrypoint: ["/api/entrypoint.prod.sh"] + - ${STRATEGY_LOCATION}:/strategy.hdf:ro + entrypoint: ["/api/entrypoints/api.prod.sh"] + restart: always + + worker: + build: + context: ./api + image: fishbait-api + environment: + - STRATEGY_LOCATION=/strategy.hdf + - RELAY_LIB_LOCATION=/libvol/lib/librelay.so + - SESSION_TIMEOUT=${SESSION_TIMEOUT:-86400} + - PIGEON_EXECUTION_TIMEOUT=${PIGEON_EXECUTION_TIMEOUT:-5} + - DD_AGENT_HOST=datadog-agent + - DD_SERVICE=fishbait-worker + - DD_ENV=${DD_ENV} + - DD_LOGS_INJECTION=true + - DD_PROFILING_ENABLED=true + - DD_APPSEC_ENABLED=true + depends_on: + ai: + condition: service_completed_successfully + redis: + condition: service_started + datadog-agent: + condition: service_started + user: "1000:1000" + volumes: + - lib:/libvol + - ./api/src:/api/src + - ${STRATEGY_LOCATION}:/strategy.hdf:ro + entrypoint: ["/api/entrypoints/worker.prod.sh"] + restart: always app: build: context: ./app image: fishbait-app - container_name: fishbait-app volumes: - - ./app:/app - - /app/node_modules - app-build:/app/build command: ["yarn", "build"] + redis: + image: redis:7.4.1 + entrypoint: + - sh + - -c + - | + redis-server \ + --maxmemory ${REDIS_MAXMEMORY:-256mb} \ + --maxmemory-policy volatile-lru + restart: always + datadog-agent: image: "datadog/agent:7" - container_name: datadog-agent environment: - DD_API_KEY=${DD_API_KEY} - DD_APM_ENABLED=true @@ -72,6 +106,7 @@ services: - DD_LOGS_CONFIG_CONTAINER_COLLECT_ALL=true volumes: - /var/run/docker.sock:/var/run/docker.sock:ro + restart: always volumes: lib: From 97e86cfe22d8a14d94ab921774f0cf9471faf815 Mon Sep 17 00:00:00 2001 From: Marzuk Rashid Date: Mon, 16 Dec 2024 05:40:50 -0600 Subject: [PATCH 2/3] pylint fixes --- api/src/app.py | 8 +++----- api/src/sessions.py | 2 ++ api/src/tasks.py | 2 ++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/api/src/app.py b/api/src/app.py index 89aef17..54259bd 100644 --- a/api/src/app.py +++ b/api/src/app.py @@ -4,7 +4,6 @@ import os from flask import Flask, request, make_response -import redis.exceptions from werkzeug.exceptions import BadRequest import datadog from datadog import statsd @@ -50,8 +49,7 @@ def handle_exceptions(e: Exception): log.exception(e) return result except Exception as new_exc: # pylint: disable=broad-except - log.exception(new_exc) - log.exception(f"Exception while handling exception: {e}") + log.exception('Exception while handling exception: %s', e) return ApiError().flask_tuple() @app.before_request @@ -101,8 +99,8 @@ def new_session(): new_session_id = tasks.create_new_session.delay().get( timeout=settings.PIGEON_EXECUTION_TIMEOUT ) - except redis.exceptions.OutOfMemoryError: - raise ServerOverloadedError() + except redis.exceptions.OutOfMemoryError as e: + raise ServerOverloadedError() from e resp = make_response() resp.set_cookie(settings.SESSION_ID_KEY, new_session_id) diff --git a/api/src/sessions.py b/api/src/sessions.py index 36f1232..31eb3e4 100644 --- a/api/src/sessions.py +++ b/api/src/sessions.py @@ -1,3 +1,5 @@ +'''Manages sessions in redis.''' + import pickle import redis diff --git a/api/src/tasks.py b/api/src/tasks.py index dd64846..329b699 100644 --- a/api/src/tasks.py +++ b/api/src/tasks.py @@ -1,3 +1,5 @@ +'''Defines celery tasks for the api.''' + import secrets from celery import Celery From 7478ef40837005fd58b3432bd4faa860cc66122e Mon Sep 17 00:00:00 2001 From: Marzuk Rashid Date: Mon, 16 Dec 2024 05:41:15 -0600 Subject: [PATCH 3/3] pylint fixes --- api/src/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/app.py b/api/src/app.py index 54259bd..cfa9bae 100644 --- a/api/src/app.py +++ b/api/src/app.py @@ -49,7 +49,7 @@ def handle_exceptions(e: Exception): log.exception(e) return result except Exception as new_exc: # pylint: disable=broad-except - log.exception('Exception while handling exception: %s', e) + log.exception('Exception while handling exception: %s', new_exc) return ApiError().flask_tuple() @app.before_request