Skip to content
Draft
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
15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ QUERY_LIST_CACHE_MAX_ENTRIES=4
GLOBAL_TREND_CACHE_MAX_ENTRIES=64
QUERY_LIST_CACHE_MAX_ROWS=100000
QUERY_LIST_CACHE_MAX_BYTES=67108864
# Kalici query-metrics materialized snapshotlarini ayri worker yeniler. API
# request yolunda advisor.query_metrics() calismaz. Concurrent refresh boyunca
# onceki tamamlanmis snapshot servis edilmeye devam eder.
QUERY_METRICS_SNAPSHOT_POLL_SECONDS=15
QUERY_METRICS_SNAPSHOT_1H_REFRESH_SECONDS=900
QUERY_METRICS_SNAPSHOT_24H_REFRESH_SECONDS=3600
QUERY_METRICS_SNAPSHOT_7D_REFRESH_SECONDS=21600
QUERY_METRICS_SNAPSHOT_30D_REFRESH_SECONDS=43200
QUERY_METRICS_SNAPSHOT_STATEMENT_TIMEOUT_SECONDS=1800
QUERY_METRICS_SNAPSHOT_RETRY_SECONDS=60
QUERY_METRICS_SNAPSHOT_WORKER_MEMORY_LIMIT=256m
# Tam snapshotlarin Python nesne ek yükü ayrıca container seviyesinde sınırlıdır.
API_MEMORY_LIMIT=1g
# Eszamanli repository migration runner'larinin advisory lock bekleme siniri.
Expand Down Expand Up @@ -91,6 +102,10 @@ WORKLOAD_PROFILE=normal
WORKLOAD_DURATION_SECONDS=0
WORKLOAD_WORKERS=6
WORKLOAD_INTERVAL_SECONDS=0.25
WORKLOAD_INTERVAL_JITTER_RATIO=0
WORKLOAD_TRAFFIC_PHASE_SECONDS=0
WORKLOAD_TRAFFIC_MIN_INTERVAL_MULTIPLIER=1
WORKLOAD_TRAFFIC_MAX_INTERVAL_MULTIPLIER=1
WORKLOAD_REPORT_INTERVAL_SECONDS=10
WORKLOAD_RANDOM_SEED=20260725
WORKLOAD_STATEMENT_TIMEOUT_MS=15000
Expand Down
46 changes: 35 additions & 11 deletions .github/workflows/postgres-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,18 @@ jobs:

- name: Verify health and migration idempotency
run: |
curl --fail --silent --show-error \
http://127.0.0.1:8000/api/v1/health \
| python -c '
import json, sys
health = json.load(sys.stdin)
assert health["status"] == "healthy", health
assert health["repository"] == "healthy", health
assert health["collector"] == "healthy", health
'
health_ready=false
for attempt in $(seq 1 18); do
if curl --fail --silent --show-error \
http://127.0.0.1:8000/api/v1/health \
| python -c 'import json, sys; health = json.load(sys.stdin); assert health["status"] == "healthy", health; assert health["repository"] == "healthy", health; assert health["collector"] == "healthy", health'; then
health_ready=true
break
fi
printf 'collector health attempt=%s is not ready yet\n' "$attempt"
sleep 10
done
test "$health_ready" = true
docker compose run --rm repository-migrate
bash scripts/verify-temporal-reliability.sh
docker compose exec -T repository-db \
Expand All @@ -118,7 +121,7 @@ jobs:
psql -X --set=ON_ERROR_STOP=1 --username postgres --dbname powa --file=- \
< sql/tests/join_outbox_guardrail_integration.sql

- name: Verify a persisted 0013 database upgrades to 0014
- name: Verify a persisted 0013 database upgrades through 0016
shell: bash
run: |
set -Eeuo pipefail
Expand Down Expand Up @@ -170,7 +173,7 @@ jobs:
release record;
BEGIN
SELECT * INTO STRICT release FROM advisor.release_info();
IF release.current_migration <> '0014' OR release.applied_count <> 14 THEN
IF release.current_migration <> '0016' OR release.applied_count <> 16 THEN
RAISE EXCEPTION 'unexpected upgraded release state: %', row_to_json(release);
END IF;
IF NOT has_function_privilege(
Expand Down Expand Up @@ -207,6 +210,27 @@ jobs:
- name: Prepare the deterministic runtime candidate
run: bash scripts/verify.sh

- name: Refresh the disposable 30d runtime replay snapshot
run: |
docker compose exec -T query-metrics-snapshot-worker python - <<'PY'
import time

from app.config import get_settings
from app.snapshot_worker import open_connection, refresh_snapshot


settings = get_settings()
with open_connection(settings) as connection:
for _ in range(30):
if refresh_snapshot(connection, "30d"):
break
time.sleep(2)
else:
raise SystemExit(
"30d query metrics snapshot advisory lock could not be acquired"
)
PY

- name: Boot disposable clone services
run: |
docker compose --profile real-validation up -d --wait \
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ Bu proje [Semantic Versioning](https://semver.org/) kullanır. Repository
migration'ları ileri yönlüdür; uygulama sürümü ile şema uyumluluğu yükseltme
runbook'unda belirtilir.

## 1.1.1 — 2026-07-30

- 1h/24h/7d/30d query-metrics sonuçları kalıcı materialized snapshot olarak
request yolundan önce hesaplanır.
- Ayrı worker snapshot'ları sırayla ve atomik olarak yeniler; hesaplama sürerken
önceki tamamlanmış sonuç servis edilir.
- Repository migration hedefi `0016`; query-metrics snapshot'larına ek olarak
global/server/database overview trendleri de önceden hesaplanır.

## 1.1.0 — 2026-07-26

- Ana kaynakta yalnız persisted, salt-okunur sorgular için gerçek `EXPLAIN ANALYZE`.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# PostgreSQL Sorgu Performansı ve Öneri Motoru

Güncel sürüm: `1.1.0`. Değişiklikler [CHANGELOG.md](CHANGELOG.md), güvenli
Güncel sürüm: `1.1.1`. Değişiklikler [CHANGELOG.md](CHANGELOG.md), güvenli
yükseltme ve geri dönüş adımları [upgrade/rollback runbook'unda](docs/UPGRADE_ROLLBACK.md).

PDF v1.1'de tarif edilen ilk iterasyonun çalışan referans uygulamasıdır. Tek bir Docker/OrbStack hostu üzerinde **iki ayrı PostgreSQL sunucu süreci** çalışır: demo kaynak instance `5432`, PoWA repository instance `5433`. PoWA Collector istatistikleri kaynaktan repository'ye taşır; FastAPI yalnız repository'yi okur ve React arayüzü sonuçları gösterir. Aynı repository/collector, `scripts/register-source.sh` ile birden fazla gerçek PostgreSQL kaynağı izleyebilir.
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.1.0
1.1.1
34 changes: 34 additions & 0 deletions backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@
"30d": "1 day",
}

QUERY_METRICS_SNAPSHOT_VIEWS: dict[str, str] = {
"1h": "query_metrics_snapshot_1h",
"24h": "query_metrics_snapshot_24h",
"7d": "query_metrics_snapshot_7d",
"30d": "query_metrics_snapshot_30d",
}

GLOBAL_TREND_SNAPSHOT_VIEWS: dict[str, str] = {
"1h": "global_trend_snapshot_1h",
"24h": "global_trend_snapshot_24h",
"7d": "global_trend_snapshot_7d",
"30d": "global_trend_snapshot_30d",
}


PrincipalRole = Literal["analyst", "annotator", "admin"]

Expand Down Expand Up @@ -91,6 +105,26 @@ class Settings(BaseSettings):
ge=1024 * 1024,
le=1024 * 1024 * 1024,
)
# A dedicated worker refreshes persistent materialized snapshots. API
# requests only read those snapshots and therefore never execute the
# expensive query_metrics function on a cold process-local cache.
query_metrics_snapshot_poll_seconds: float = Field(default=15.0, ge=1, le=300)
query_metrics_snapshot_1h_refresh_seconds: int = Field(
default=15 * 60, ge=60, le=86_400
)
query_metrics_snapshot_24h_refresh_seconds: int = Field(
default=60 * 60, ge=60, le=7 * 86_400
)
query_metrics_snapshot_7d_refresh_seconds: int = Field(
default=6 * 60 * 60, ge=60, le=30 * 86_400
)
query_metrics_snapshot_30d_refresh_seconds: int = Field(
default=12 * 60 * 60, ge=60, le=30 * 86_400
)
query_metrics_snapshot_statement_timeout_seconds: int = Field(
default=30 * 60, ge=60, le=6 * 60 * 60
)
query_metrics_snapshot_retry_seconds: int = Field(default=60, ge=5, le=3_600)
sql_text_visibility: str = "authorized"
retention_days: int = 90
log_level: str = "INFO"
Expand Down
17 changes: 16 additions & 1 deletion backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from prometheus_client import CONTENT_TYPE_LATEST, generate_latest
from starlette import status
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.responses import Response

from app.api.router import router
from app.config import get_settings
from app.db import close_pool, open_pool
from app.repositories.powa import repository
from app.repositories.powa import QueryMetricsSnapshotWarming, repository
from app.version import APPLICATION_VERSION


Expand Down Expand Up @@ -58,6 +61,18 @@ async def lifespan(_: FastAPI):
app.include_router(router)


@app.exception_handler(QueryMetricsSnapshotWarming)
async def snapshot_warming(
_: Request,
__: QueryMetricsSnapshotWarming,
) -> JSONResponse:
return JSONResponse(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
content={"detail": "Dashboard verileri ilk kez hazirlaniyor; kisa sure sonra deneyin."},
headers={"Retry-After": "30"},
)


@app.get("/", include_in_schema=False)
async def root() -> dict[str, str]:
return {"name": app.title, "docs": "/docs", "health": "/api/v1/health"}
Expand Down
Loading
Loading