Skip to content
Open

Cache #533

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
8 changes: 8 additions & 0 deletions files/scripts/run_httpd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ fi
# open HTTP/2 connection gets reused
API_SERVER_NAME="${DEPLOYMENT}.packit.dev"

# Pre-warm the usage cache at startup and schedule daily refresh at 00:01
PYTHONPATH=/src python3 -m packit_dashboard.usage.warm_cache || true
(while true; do
seconds_until_midnight=$(( $(date -d "tomorrow 00:01" +%s) - $(date +%s) ))
sleep "${seconds_until_midnight}"
PYTHONPATH=/src python3 -m packit_dashboard.usage.warm_cache || true
done) &

# See "mod_wsgi-express-3 start-server --help" for details on
# these options, and the configuration documentation of mod_wsgi:
# https://modwsgi.readthedocs.io/en/master/configuration.html
Expand Down
1 change: 0 additions & 1 deletion frontend/src/components/usage/Usage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ const Usage = () => {
<CardBody>
<Tabs
isFilled
mountOnEnter
activeKey={activeTabKey}
onSelect={handleTabClick}
isBox={true}
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/components/usage/UsageInterval.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ import { UsageListData } from "./UsageListData";

const fetchDataByGranularity = (granularity: UsageIntervalProps) =>
fetch(
`${import.meta.env.VITE_API_URL}/usage/intervals?days=${
granularity.days
}&hours=${granularity.hours}&count=${granularity.count}`,
`/api/usage/intervals?days=${granularity.days}&hours=${granularity.hours}&count=${granularity.count}`,
).then((response) => {
if (!response.ok) {
throw Promise.reject(response);
Expand Down
14 changes: 6 additions & 8 deletions frontend/src/components/usage/UsageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,12 @@ import { Preloader } from "../shared/Preloader";
import { UsageListData } from "./UsageListData";

const fetchDataByGranularity = (granularity: UsageListProps["what"]) =>
fetch(`${import.meta.env.VITE_API_URL}/usage/${granularity}`).then(
(response) => {
if (!response.ok) {
throw Promise.reject(response);
}
return response.json();
},
);
fetch(`/api/usage/${granularity}`).then((response) => {
if (!response.ok) {
throw Promise.reject(response);
}
return response.json();
});

interface UsageListProps {
what: "past-day" | "past-week" | "past-month" | "past-year" | "total";
Expand Down
3 changes: 3 additions & 0 deletions frontend/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export default defineConfig(() => ({
],
server: {
open: true,
proxy: {
"/api/usage": "http://localhost:5000",
},
},
test: {
environment: "happy-dom",
Expand Down
2 changes: 2 additions & 0 deletions packit_dashboard/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from packit_dashboard.api.routes import api
from packit_dashboard.home.routes import home
from packit_dashboard.usage.routes import usage

app = Flask(
"Packit Service Dashboard",
Expand All @@ -16,6 +17,7 @@

# Note: Declare any other flask blueprints or routes above this.
# Routes declared below this will be rendered by React
app.register_blueprint(usage)
app.register_blueprint(home)
app.register_blueprint(api)

Expand Down
2 changes: 2 additions & 0 deletions packit_dashboard/usage/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright Contributors to the Packit project.
# SPDX-License-Identifier: MIT
41 changes: 41 additions & 0 deletions packit_dashboard/usage/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright Contributors to the Packit project.
# SPDX-License-Identifier: MIT

import json
from datetime import date
from pathlib import Path

import requests

CACHE_DIR = Path("/tmp/packit-dashboard-usage-cache")


def _cache_path(cache_key: str) -> Path:
today = date.today().isoformat()
return CACHE_DIR / f"{cache_key}_{today}.json"


def evict_stale():
if not CACHE_DIR.exists():
return
today = date.today().isoformat()
for f in CACHE_DIR.iterdir():
if not f.name.endswith(f"_{today}.json"):
f.unlink(missing_ok=True)


def get_cached_or_fetch(cache_key: str, url: str) -> dict:
path = _cache_path(cache_key)

if path.exists():
return json.loads(path.read_text())

evict_stale()

response = requests.get(url, timeout=60)
response.raise_for_status()
data = response.json()

CACHE_DIR.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data))
return data
42 changes: 42 additions & 0 deletions packit_dashboard/usage/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright Contributors to the Packit project.
# SPDX-License-Identifier: MIT

from logging import getLogger

from flask import Blueprint, jsonify, request

from packit_dashboard.config import API_URL
from packit_dashboard.usage.cache import get_cached_or_fetch

logger = getLogger("packit_dashboard")

usage = Blueprint("usage", __name__)


@usage.route("/api/usage/<granularity>")
def usage_by_granularity(granularity):
try:
data = get_cached_or_fetch(
f"usage-{granularity}",
f"{API_URL}/usage/{granularity}",
)
return jsonify(data)
except Exception:
logger.exception("Failed to fetch usage data for %s", granularity)
return jsonify({"error": "Failed to fetch usage data"}), 502


@usage.route("/api/usage/intervals")
def usage_intervals():
days = request.args.get("days", "")
hours = request.args.get("hours", "")
count = request.args.get("count", "")
try:
data = get_cached_or_fetch(
f"usage-intervals-{days}-{hours}-{count}",
f"{API_URL}/usage/intervals?days={days}&hours={hours}&count={count}",
)
return jsonify(data)
except Exception:
logger.exception("Failed to fetch usage intervals")
return jsonify({"error": "Failed to fetch usage data"}), 502
64 changes: 64 additions & 0 deletions packit_dashboard/usage/warm_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env python3
# Copyright Contributors to the Packit project.
# SPDX-License-Identifier: MIT

"""Pre-generate usage cache files. Run via cron at 00:01 daily."""

import logging
import sys

from packit_dashboard.config import API_URL
from packit_dashboard.usage.cache import evict_stale, get_cached_or_fetch

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("packit_dashboard")

GRANULARITIES = ["past-day", "past-week", "past-month", "past-year", "total"]
INTERVALS = [
{"days": 0, "hours": 1, "count": 24},
{"days": 1, "hours": 0, "count": 7},
{"days": 1, "hours": 0, "count": 30},
{"days": 7, "hours": 0, "count": 52},
]


def main():
evict_stale()
errors = 0

for granularity in GRANULARITIES:
try:
get_cached_or_fetch(
f"usage-{granularity}",
f"{API_URL}/usage/{granularity}",
)
logger.info("Cached usage/%s", granularity)
except Exception:
logger.exception("Failed to fetch usage/%s", granularity)
errors += 1

for interval in INTERVALS:
days, hours, count = interval["days"], interval["hours"], interval["count"]
try:
get_cached_or_fetch(
f"usage-intervals-{days}-{hours}-{count}",
f"{API_URL}/usage/intervals?days={days}&hours={hours}&count={count}",
)
logger.info(
"Cached usage/intervals days=%s hours=%s count=%s", days, hours, count
)
except Exception:
logger.exception(
"Failed to fetch usage/intervals days=%s hours=%s count=%s",
days,
hours,
count,
)
errors += 1

if errors:
sys.exit(1)


if __name__ == "__main__":
main()
Loading