From 5078463ba3bc69ef5df9302edc38852043e77d91 Mon Sep 17 00:00:00 2001 From: Nikolay Nechaev Date: Mon, 22 Apr 2024 16:03:40 +0300 Subject: [PATCH] Lab 12, task 1 --- app_python/Dockerfile | 2 ++ app_python/README.md | 3 +++ app_python/moscow_time/__init__.py | 24 +++++++++++++++++++++++- app_python/moscow_time/visits.py | 28 ++++++++++++++++++++++++++++ monitoring/docker-compose.yml | 5 ++++- 5 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 app_python/moscow_time/visits.py diff --git a/app_python/Dockerfile b/app_python/Dockerfile index a163a9821a..491680aae6 100644 --- a/app_python/Dockerfile +++ b/app_python/Dockerfile @@ -7,6 +7,8 @@ COPY requirements.txt /app/ COPY moscow_time/ /app/moscow_time # Note: keeping the project files owned by root so # the web server has less privileges over them +RUN ["mkdir", "--mode", "777", "/app/persistent"] +VOLUME /app/persistent USER flask:flask RUN ["pip", "install", "--user", "-r", "requirements.txt"] CMD ["python", "-m", "moscow_time"] diff --git a/app_python/README.md b/app_python/README.md index e6ec6b72e1..95d88fac9c 100644 --- a/app_python/README.md +++ b/app_python/README.md @@ -54,6 +54,9 @@ docker run --rm -d -p 5000 kolay0ne/app_py Replace `kolay0ne/app_py` with your image/tag name if you built it manually. +One may want to mount a volume or a bind-mount at `/app/persistent`, which acts +as a persistent storage for the visits counter of the web app. + ## Unit Tests To run unit tests: diff --git a/app_python/moscow_time/__init__.py b/app_python/moscow_time/__init__.py index 3055268770..1a9d9be12d 100644 --- a/app_python/moscow_time/__init__.py +++ b/app_python/moscow_time/__init__.py @@ -1,11 +1,13 @@ import datetime +from os import makedirs from time import monotonic -from flask import Flask, request, Response +from flask import Flask, request, Response, send_from_directory import requests import prometheus_client from .cache import cache_for +from .visits import increment_on_call app = Flask(__name__) @@ -42,12 +44,32 @@ def get_time(): return dt.time() +VISITS_FILENAME = 'persistent/visits.bin' + +# Create it on start +try: + basename_at = VISITS_FILENAME.rindex('/') +except ValueError: + pass +else: + makedirs(VISITS_FILENAME[:basename_at], exist_ok=True) +open(VISITS_FILENAME, 'a+').close() # The `a+` mode ensures we have write perm + + @app.route('/') +@increment_on_call(VISITS_FILENAME) def index(): time = get_time() return f"In MSK it's {time.hour}:{time.minute}:{time.second}. " \ "Have you brushed your teeth today yet?" @app.route('/metrics') +@increment_on_call(VISITS_FILENAME) def prometheus_metrics(): return Response(prometheus_client.generate_latest(), mimetype='text/plain') + +@app.route('/visits') +@increment_on_call(VISITS_FILENAME) +def visits(): + with open(VISITS_FILENAME, 'rb') as f: + return str(int.from_bytes(f.read(), byteorder='little')) diff --git a/app_python/moscow_time/visits.py b/app_python/moscow_time/visits.py new file mode 100644 index 0000000000..7e79a38e31 --- /dev/null +++ b/app_python/moscow_time/visits.py @@ -0,0 +1,28 @@ +import fcntl +from functools import wraps +from threading import Lock +from typing import Callable + + +def increment(filename: str) -> None: + with open(filename, 'rb+') as f: + try: + fcntl.flock(f.fileno(), fcntl.LOCK_EX) + cur = int.from_bytes(f.read(), byteorder='little') + f.seek(0) + cur += 1 + f.write(cur.to_bytes(byteorder='little', length=(cur.bit_length() // 8 + 1))) + f.truncate() # Not necessary, as larger numbers take more bytes + finally: + # Note: will be unlocked anyway when the file is closed + fcntl.flock(f.fileno(), fcntl.LOCK_UN) + + +def increment_on_call(filename: str) -> Callable[[Callable, ...], Callable]: + def decorator(func: Callable) -> Callable: + @wraps(func) + def with_increment(*args, **kwargs): + increment(filename) + return func(*args, **kwargs) + return with_increment + return decorator diff --git a/monitoring/docker-compose.yml b/monitoring/docker-compose.yml index 30cb930e5c..95d0db6dd4 100644 --- a/monitoring/docker-compose.yml +++ b/monitoring/docker-compose.yml @@ -4,10 +4,11 @@ networks: volumes: grafana-storage: + py-persistent: services: app_py: - image: kolay0ne/app_py:lab8 + image: kolay0ne/app_py:lab12 ports: - "5000:5000" logging: @@ -17,6 +18,8 @@ services: resources: {limits: {memory: 30M}} networks: - prometheus + volumes: + - py-persistent:/app/persistent app_go: image: kolay0ne/app_go:lab8