Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,6 @@ dmypy.json

# macOS
.DS_Store

shop.db
test.db
22 changes: 22 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# syntax=docker/dockerfile:1
FROM python:3.11-slim AS base

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1


WORKDIR /app


COPY hw2/hw/requirements.txt ./requirements.txt
RUN pip install --no-cache-dir -r requirements.txt


COPY hw2/hw ./hw

ENV PYTHONPATH=/app/hw


EXPOSE 8000

CMD ["uvicorn", "shop_api.main:app", "--host", "0.0.0.0", "--port", "8000"]
76 changes: 76 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
version: "3.9"

services:
app:
build: .
container_name: shop_api
ports:
- "8000:8000"
environment:
- PYTHONPATH=/app/hw
- DATABASE_URL=postgresql+psycopg://shop:shop@db:5432/shopdb
depends_on:
- db
networks:
- monitor-net

db:
image: postgres:16
container_name: postgres
environment:
- POSTGRES_DB=shopdb
- POSTGRES_USER=shop
- POSTGRES_PASSWORD=shop
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- monitor-net

adminer:
image: adminer
container_name: adminer
ports:
- "8080:8080"
depends_on:
- db
networks:
- monitor-net

prometheus:
image: prom/prometheus:latest
container_name: prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prom-data:/prometheus
ports:
- "9090:9090"
networks:
- monitor-net
depends_on:
- app

grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin
networks:
- monitor-net
depends_on:
- prometheus

volumes:
prom-data:
grafana-data:
pgdata:

networks:
monitor-net:
driver: bridge
114 changes: 113 additions & 1 deletion hw1/app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from typing import Any, Awaitable, Callable
import json
from math import prod
from urllib.parse import parse_qs


async def application(
Expand All @@ -12,7 +15,116 @@ async def application(
receive: Корутина для получения сообщений от клиента
send: Корутина для отправки сообщений клиенту
"""
# TODO: Ваша реализация здесь

if scope.get("type") != "http":
await _send_json(send, {"detail": "unsupported scope type"}, status=422)
return

method = scope.get("method", "GET").upper()
path: str = scope.get("path", "/")
query = parse_qs((scope.get("query_string") or b"").decode(
"utf-8"), keep_blank_values=True)

if path.startswith("/fibonacci/"):
n_str = path[len("/fibonacci/"):].strip("/")
if n_str == "":
await _send_json(send, {"detail": "n is required"}, status=422)
return
try:
n = int(n_str)
except ValueError:
await _send_json(send, {"detail": "n must be an integer"}, status=422)
return
if n < 0:
await _send_json(send, {"detail": "n must be non-negative"}, status=400)
return

a, b = 0, 1
for _ in range(n):
a, b = b, a + b
await _send_json(send, {"result": a}, status=200)
return

if path == "/factorial":
if method != "GET":
await _send_json(send, {"detail": "not found"}, status=404)
return

raw = query.get("n", [None])[0]
if raw is None or raw == "":
await _send_json(send, {"detail": "n is required"}, status=422)
return
try:
n = int(raw)
except ValueError:
await _send_json(send, {"detail": "n must be an integer"}, status=422)
return
if n < 0:
await _send_json(send, {"detail": "n must be non-negative"}, status=400)
return

result = 1 if n == 0 else prod(range(1, n + 1))
await _send_json(send, {"result": result}, status=200)
return

if path == "/mean":
if method != "GET":
await _send_json(send, {"detail": "not found"}, status=404)
return

body_bytes = await _read_body(receive)
if not body_bytes:
await _send_json(send, {"detail": "json body required (array of numbers)"}, status=422)
return

try:
data = json.loads(body_bytes.decode("utf-8"))
except json.JSONDecodeError:
await _send_json(send, {"detail": "invalid JSON"}, status=422)
return

if not isinstance(data, list):
await _send_json(send, {"detail": "body must be a JSON array"}, status=422)
return

if len(data) == 0:
await _send_json(send, {"detail": "array must not be empty"}, status=400)
return

try:
nums = [float(x) for x in data]
except (TypeError, ValueError):
await _send_json(send, {"detail": "array must contain only numbers"}, status=422)
return

mean_value = sum(nums) / len(nums)
await _send_json(send, {"result": mean_value}, status=200)
return

await _send_json(send, {"detail": "not found"}, status=404)


async def _read_body(receive: Callable[[], Awaitable[dict[str, Any]]]) -> bytes:
body = b""
more = True
while more:
message = await receive()
if message["type"] != "http.request":
break
body += message.get("body", b"")
more = message.get("more_body", False)
return body


async def _send_json(send: Callable[[dict[str, Any]], Awaitable[None]], payload: dict, status: int = 200):
body = json.dumps(payload).encode("utf-8")
headers = [
(b"content-type", b"application/json; charset=utf-8"),
(b"content-length", str(len(body)).encode("ascii")),
]
await send({"type": "http.response.start", "status": status, "headers": headers})
await send({"type": "http.response.body", "body": body})


if __name__ == "__main__":
import uvicorn
Expand Down
6 changes: 6 additions & 0 deletions hw2/hw/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
# Основные зависимости для ASGI приложения
fastapi>=0.117.1
uvicorn>=0.24.0
prometheus-fastapi-instrumentator>=6.1.0

# Зависимости для тестирования
pytest>=7.4.0
pytest-asyncio>=0.21.0
httpx>=0.27.2
Faker>=37.8.0

SQLAlchemy>=2.0
psycopg[binary]>=3.2

alembic>=1.13
Binary file added hw2/hw/shop_api/HW_results/Dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added hw2/hw/shop_api/HW_results/Dashboard_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added hw2/hw/shop_api/HW_results/Prometheus.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 52 additions & 0 deletions hw2/hw/shop_api/HW_results/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# сценарии:

docker compose exec app python -m shop_api.HW_results.tx.txn_demos nrr_rc # non-repeatable read на READ COMMITTED

docker compose exec app python -m shop_api.HW_results.tx.txn_demos nrr_rr # отсутствие non-repeatable read на REPEATABLE READ

docker compose exec app python -m shop_api.HW_results.tx.txn_demos phantom_rc # phantom на READ COMMITTED

docker compose exec app python -m shop_api.HW_results.tx.txn_demos phantom_rr # отсутствие phantom на REPEATABLE READ

docker compose exec app python -m shop_api.HW_results.tx.txn_demos serializable # строгая изоляция


nrr_rc

Running scenario: nrr_rc
[T1] first read val=100
[T2] committed UPDATE
[T1] second read val=101

второе чтение видит новое значение —non‑repeatable read

Running scenario: nrr_rr
[T1] first read val=100
[T2] committed UPDATE
[T1] second read val=100

в REPEATABLE READ snapshot фиксирован, значение не меняется

phantom_rc

Running scenario: phantom_rc
[T1] first count=2
[T2] committed INSERT
[T1] second count=3

количество строк изменилось — phantom.

Running scenario: phantom_rr
[T1] first count=2
[T2] committed INSERT
[T1] second count=2

snapshot предотвращает появление «призрачных» строк в той же транзакции.

serializable — строгая изоляция

Running scenario: serializable
[T1] bump +10 committed
[T2] bump +20 committed

за счёт блокировок операции выполняются последовательно, конфликтов нет
4 changes: 4 additions & 0 deletions hw2/hw/shop_api/HW_results/tx/dirty_read.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
print("""
PostgreSQL не поддерживает уровень READ UNCOMMITTED. Любая попытка
установить его сводится к READ COMMITTED, поэтому dirty read невозможен.
""")
Loading