Ytelse: profiler PG-spørringer, optimaliser node_access-oppdatering (oppgave 12.4)
Profilert alle kritiske PG-spørringer med EXPLAIN ANALYZE. Identifiserte at recompute_access brukte single-column index (idx_edges_type) med lav selektivitet, og RLS-policyer manglet composite indexes for effektive oppslag. Endringer: Migrasjon 017_query_performance.sql: - 6 nye composite indexes: - idx_edges_target_type (target_id, edge_type) — recompute_access + belongs_to - idx_edges_source_type (source_id, edge_type) — alias-oppslag - idx_edges_target_memberof (partial, member_of) — team-propagering - idx_nodes_created_at_desc — ORDER BY created_at DESC - idx_nodes_kind_created — filtrer på kind + sorter - idx_na_subject_covering INCLUDE (object_id) — RLS without heap lookup - Optimalisert recompute_access(): steg 3 og 4 kjøres nå bare når det er relevant (EXISTS-sjekk først). For vanlige brukere (ikke team) unngår dette to fulle INSERT-SELECT-operasjoner. - via_edge oppdateres nå korrekt ved access-nivå-endring. Slow query logging (maskinrommet): - Forespørsler >200ms logges som WARN med tag slow_request - PG-spørringer >100ms logges som WARN med tag slow_query - recompute_access-kall logges med varighet for overvåking - Nytt pg_stats-felt i /metrics med tabell- og index-statistikk, cache hit ratio, og node_access-telling Dokumentasjon oppdatert i docs/infra/observerbarhet.md.
This commit is contained in:
parent
7f8b749443
commit
b31ee59868
6 changed files with 405 additions and 13 deletions
|
|
@ -82,6 +82,38 @@ Aggregerte AI-kostnader fra `ai_usage_log` for tre tidsperioder
|
||||||
Kostnadsestimatet er basert på konservative gjennomsnittspriser
|
Kostnadsestimatet er basert på konservative gjennomsnittspriser
|
||||||
($3/MTok input, $15/MTok output).
|
($3/MTok input, $15/MTok output).
|
||||||
|
|
||||||
|
### pg_stats
|
||||||
|
|
||||||
|
PG-ytelsesstatistikk for kjernetabellene (nodes, edges, node_access):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tables": [
|
||||||
|
{
|
||||||
|
"table_name": "nodes",
|
||||||
|
"row_count": 19,
|
||||||
|
"index_scan_count": 50,
|
||||||
|
"seq_scan_count": 10,
|
||||||
|
"index_scan_ratio": 0.83
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"index_name": "idx_edges_target_type",
|
||||||
|
"table_name": "edges",
|
||||||
|
"scan_count": 25,
|
||||||
|
"size_bytes": 16384
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"node_access_count": 4,
|
||||||
|
"cache_hit_ratio": 0.9995
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`index_scan_ratio` bør være >0.95 for tabeller med >1000 rader.
|
||||||
|
`cache_hit_ratio` bør være >0.99 — lavere indikerer at PG trenger mer
|
||||||
|
`shared_buffers`.
|
||||||
|
|
||||||
## Request-logging middleware
|
## Request-logging middleware
|
||||||
|
|
||||||
Alle HTTP-forespørsler logges med:
|
Alle HTTP-forespørsler logges med:
|
||||||
|
|
@ -91,8 +123,33 @@ Alle HTTP-forespørsler logges med:
|
||||||
- `status` — HTTP-statuskode
|
- `status` — HTTP-statuskode
|
||||||
- `duration_ms` — Total responstid
|
- `duration_ms` — Total responstid
|
||||||
|
|
||||||
Dette erstatter `tower_http::TraceLayer` med en mer strukturert
|
Forespørsler over 200ms logges med `level=WARN` og tag `slow_request`.
|
||||||
variant som også mater metrikk-samleren.
|
|
||||||
|
## Slow query logging
|
||||||
|
|
||||||
|
PG-spørringer som tar over 100ms logges med `level=WARN` og tag
|
||||||
|
`slow_query`. Terskelen er definert i `queries.rs::SLOW_QUERY_THRESHOLD_MS`.
|
||||||
|
|
||||||
|
Instrumenterte spørringer:
|
||||||
|
- `query_nodes` — søk og filtrering med RLS
|
||||||
|
- `query_graph` — rekursiv CTE graf-traversering med RLS
|
||||||
|
- `recompute_access` — node_access-oppdatering (logges i `pg_writes.rs`)
|
||||||
|
|
||||||
|
## pg_stat_statements (valgfritt)
|
||||||
|
|
||||||
|
For detaljert per-spørring statistikk, aktiver pg_stat_statements:
|
||||||
|
|
||||||
|
1. Legg til i docker-compose.yml for postgres:
|
||||||
|
`command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all`
|
||||||
|
2. Restart postgres-containeren
|
||||||
|
3. Kjør: `CREATE EXTENSION pg_stat_statements;`
|
||||||
|
4. Se topp-spørringer:
|
||||||
|
```sql
|
||||||
|
SELECT query, calls, total_exec_time, mean_exec_time
|
||||||
|
FROM pg_stat_statements
|
||||||
|
ORDER BY total_exec_time DESC
|
||||||
|
LIMIT 20;
|
||||||
|
```
|
||||||
|
|
||||||
## Arkitektur
|
## Arkitektur
|
||||||
|
|
||||||
|
|
@ -100,5 +157,5 @@ Metrikker samles i minnet (ingen ekstern avhengighet som Prometheus).
|
||||||
Designvalg:
|
Designvalg:
|
||||||
|
|
||||||
- **Enkel:** Ingen nye avhengigheter eller ekstern infrastruktur
|
- **Enkel:** Ingen nye avhengigheter eller ekstern infrastruktur
|
||||||
- **Tilstrekkelig:** Queue depth og AI-kostnad fra PG, latency in-memory
|
- **Tilstrekkelig:** Queue depth, AI-kostnad og PG-stats fra PG, latency in-memory
|
||||||
- **Utvidbar:** Kan legge til Prometheus-eksport senere ved behov
|
- **Utvidbar:** Kan legge til Prometheus-eksport senere ved behov
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@
|
||||||
//
|
//
|
||||||
// Samler request-latency per rute, eksponerer via /metrics-endepunkt.
|
// Samler request-latency per rute, eksponerer via /metrics-endepunkt.
|
||||||
// Queue depth og AI-kostnad hentes direkte fra PG ved forespørsel.
|
// Queue depth og AI-kostnad hentes direkte fra PG ved forespørsel.
|
||||||
|
// Slow query logging: spørringer over terskel logges med kontekst.
|
||||||
//
|
//
|
||||||
// Ref: oppgave 12.1
|
// Ref: oppgave 12.1, 12.4
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
|
|
@ -271,7 +272,12 @@ async fn query_ai_cost(db: &PgPool) -> Result<AiCostSummary, sqlx::Error> {
|
||||||
|
|
||||||
// --- Middleware ---
|
// --- Middleware ---
|
||||||
|
|
||||||
|
/// Terskelverdi for treg forespørsel (millisekunder).
|
||||||
|
/// Forespørsler over denne verdien logges som warning.
|
||||||
|
const SLOW_REQUEST_THRESHOLD_MS: f64 = 200.0;
|
||||||
|
|
||||||
/// Axum-middleware som måler request-latency og logger strukturert.
|
/// Axum-middleware som måler request-latency og logger strukturert.
|
||||||
|
/// Forespørsler over SLOW_REQUEST_THRESHOLD_MS logges som warning med ekstra kontekst.
|
||||||
pub async fn latency_middleware(
|
pub async fn latency_middleware(
|
||||||
State(state): State<crate::AppState>,
|
State(state): State<crate::AppState>,
|
||||||
request: Request<Body>,
|
request: Request<Body>,
|
||||||
|
|
@ -285,17 +291,28 @@ pub async fn latency_middleware(
|
||||||
|
|
||||||
let duration = start.elapsed();
|
let duration = start.elapsed();
|
||||||
let duration_us = duration.as_micros() as u64;
|
let duration_us = duration.as_micros() as u64;
|
||||||
|
let duration_ms = duration.as_secs_f64() * 1000.0;
|
||||||
let status = response.status().as_u16();
|
let status = response.status().as_u16();
|
||||||
|
|
||||||
state.metrics.record(&path, duration_us);
|
state.metrics.record(&path, duration_us);
|
||||||
|
|
||||||
|
if duration_ms >= SLOW_REQUEST_THRESHOLD_MS {
|
||||||
|
tracing::warn!(
|
||||||
|
method = %method,
|
||||||
|
path = %path,
|
||||||
|
status = status,
|
||||||
|
duration_ms = duration_ms,
|
||||||
|
"slow_request"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
method = %method,
|
method = %method,
|
||||||
path = %path,
|
path = %path,
|
||||||
status = status,
|
status = status,
|
||||||
duration_ms = duration.as_secs_f64() * 1000.0,
|
duration_ms = duration_ms,
|
||||||
"request"
|
"request"
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
@ -307,6 +324,7 @@ struct MetricsResponse {
|
||||||
request_latency: MetricsSnapshot,
|
request_latency: MetricsSnapshot,
|
||||||
queue_depth: Option<QueueDepth>,
|
queue_depth: Option<QueueDepth>,
|
||||||
ai_cost: Option<AiCostSummary>,
|
ai_cost: Option<AiCostSummary>,
|
||||||
|
pg_stats: Option<PgQueryStats>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /metrics — samlet observerbarhets-endepunkt.
|
/// GET /metrics — samlet observerbarhets-endepunkt.
|
||||||
|
|
@ -317,10 +335,128 @@ pub async fn metrics_endpoint(
|
||||||
|
|
||||||
let queue_depth = query_queue_depth(&state.db).await.ok();
|
let queue_depth = query_queue_depth(&state.db).await.ok();
|
||||||
let ai_cost = query_ai_cost(&state.db).await.ok();
|
let ai_cost = query_ai_cost(&state.db).await.ok();
|
||||||
|
let pg_stats = query_pg_stats(&state.db).await.ok();
|
||||||
|
|
||||||
Ok(Json(MetricsResponse {
|
Ok(Json(MetricsResponse {
|
||||||
request_latency,
|
request_latency,
|
||||||
queue_depth,
|
queue_depth,
|
||||||
ai_cost,
|
ai_cost,
|
||||||
|
pg_stats,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- PG Query Stats ---
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct PgTableStats {
|
||||||
|
pub table_name: String,
|
||||||
|
pub row_count: i64,
|
||||||
|
pub index_scan_count: i64,
|
||||||
|
pub seq_scan_count: i64,
|
||||||
|
/// Ratio av index scans til totale scans (0.0-1.0). Høyere er bedre.
|
||||||
|
pub index_scan_ratio: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct PgIndexStats {
|
||||||
|
pub index_name: String,
|
||||||
|
pub table_name: String,
|
||||||
|
pub scan_count: i64,
|
||||||
|
pub size_bytes: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct PgQueryStats {
|
||||||
|
pub tables: Vec<PgTableStats>,
|
||||||
|
pub indexes: Vec<PgIndexStats>,
|
||||||
|
pub node_access_count: i64,
|
||||||
|
pub cache_hit_ratio: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query_pg_stats(db: &PgPool) -> Result<PgQueryStats, sqlx::Error> {
|
||||||
|
// Tabell-statistikk: seq_scan vs index_scan for kjernetabellene
|
||||||
|
let tables = sqlx::query_as::<_, (String, i64, i64, i64)>(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
relname::text,
|
||||||
|
COALESCE(n_live_tup, 0),
|
||||||
|
COALESCE(idx_scan, 0),
|
||||||
|
COALESCE(seq_scan, 0)
|
||||||
|
FROM pg_stat_user_tables
|
||||||
|
WHERE relname IN ('nodes', 'edges', 'node_access', 'job_queue', 'auth_identities')
|
||||||
|
ORDER BY relname
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_all(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let table_stats: Vec<PgTableStats> = tables
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, rows, idx_scans, seq_scans)| {
|
||||||
|
let total = idx_scans + seq_scans;
|
||||||
|
PgTableStats {
|
||||||
|
table_name: name,
|
||||||
|
row_count: rows,
|
||||||
|
index_scan_count: idx_scans,
|
||||||
|
seq_scan_count: seq_scans,
|
||||||
|
index_scan_ratio: if total > 0 {
|
||||||
|
idx_scans as f64 / total as f64
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Index-bruksstatistikk for de viktigste indeksene
|
||||||
|
let indexes = sqlx::query_as::<_, (String, String, i64, i64)>(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
indexrelname::text,
|
||||||
|
relname::text,
|
||||||
|
COALESCE(idx_scan, 0),
|
||||||
|
pg_relation_size(indexrelid)
|
||||||
|
FROM pg_stat_user_indexes
|
||||||
|
WHERE relname IN ('nodes', 'edges', 'node_access')
|
||||||
|
ORDER BY idx_scan DESC
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_all(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let index_stats: Vec<PgIndexStats> = indexes
|
||||||
|
.into_iter()
|
||||||
|
.map(|(idx_name, tbl_name, scans, size)| PgIndexStats {
|
||||||
|
index_name: idx_name,
|
||||||
|
table_name: tbl_name,
|
||||||
|
scan_count: scans,
|
||||||
|
size_bytes: size,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// node_access row count
|
||||||
|
let (na_count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM node_access")
|
||||||
|
.fetch_one(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Cache hit ratio
|
||||||
|
let (hit_ratio,): (f64,) = sqlx::query_as(
|
||||||
|
r#"
|
||||||
|
SELECT COALESCE(
|
||||||
|
SUM(heap_blks_hit)::float / NULLIF(SUM(heap_blks_hit) + SUM(heap_blks_read), 0),
|
||||||
|
1.0
|
||||||
|
)
|
||||||
|
FROM pg_statio_user_tables
|
||||||
|
WHERE relname IN ('nodes', 'edges', 'node_access')
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_one(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(PgQueryStats {
|
||||||
|
tables: table_stats,
|
||||||
|
indexes: index_stats,
|
||||||
|
node_access_count: na_count,
|
||||||
|
cache_hit_ratio: (hit_ratio * 10000.0).round() / 10000.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,7 @@ pub async fn handle_insert_edge(
|
||||||
|
|
||||||
if let Some(level) = access_level {
|
if let Some(level) = access_level {
|
||||||
// Tilgangsgivende edge: transaksjon med recompute_access
|
// Tilgangsgivende edge: transaksjon med recompute_access
|
||||||
|
let recompute_start = std::time::Instant::now();
|
||||||
let mut tx = db.begin().await.map_err(|e| format!("PG begin: {e}"))?;
|
let mut tx = db.begin().await.map_err(|e| format!("PG begin: {e}"))?;
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
|
|
@ -254,12 +255,21 @@ pub async fn handle_insert_edge(
|
||||||
|
|
||||||
tx.commit().await.map_err(|e| format!("PG commit: {e}"))?;
|
tx.commit().await.map_err(|e| format!("PG commit: {e}"))?;
|
||||||
|
|
||||||
|
let recompute_ms = recompute_start.elapsed().as_secs_f64() * 1000.0;
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
edge_id = %edge_id,
|
edge_id = %edge_id,
|
||||||
edge_type = %edge_type,
|
edge_type = %edge_type,
|
||||||
access_level = %level,
|
access_level = %level,
|
||||||
|
recompute_ms = recompute_ms,
|
||||||
"Edge + node_access persistert til PostgreSQL (via jobbkø)"
|
"Edge + node_access persistert til PostgreSQL (via jobbkø)"
|
||||||
);
|
);
|
||||||
|
if recompute_ms >= 100.0 {
|
||||||
|
tracing::warn!(
|
||||||
|
edge_id = %edge_id,
|
||||||
|
duration_ms = recompute_ms,
|
||||||
|
"slow_recompute_access"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Synk node_access til STDB (best-effort, feil logger men feiler ikke jobben)
|
// Synk node_access til STDB (best-effort, feil logger men feiler ikke jobben)
|
||||||
sync_node_access_to_stdb(db, stdb, source_id).await;
|
sync_node_access_to_stdb(db, stdb, source_id).await;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,21 @@ use crate::auth::AuthUser;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use crate::intentions::ErrorResponse;
|
use crate::intentions::ErrorResponse;
|
||||||
|
|
||||||
|
/// Terskelverdi for treg PG-spørring (millisekunder).
|
||||||
|
const SLOW_QUERY_THRESHOLD_MS: f64 = 100.0;
|
||||||
|
|
||||||
|
/// Logger en spørring som treg hvis varigheten overstiger terskelen.
|
||||||
|
fn log_slow_query(query_name: &str, duration_ms: f64) {
|
||||||
|
if duration_ms >= SLOW_QUERY_THRESHOLD_MS {
|
||||||
|
tracing::warn!(
|
||||||
|
query = query_name,
|
||||||
|
duration_ms = duration_ms,
|
||||||
|
threshold_ms = SLOW_QUERY_THRESHOLD_MS,
|
||||||
|
"slow_query"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// GET /query/segments — transkripsjons-segmenter for en node
|
// GET /query/segments — transkripsjons-segmenter for en node
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -474,6 +489,7 @@ async fn run_query_nodes(
|
||||||
limit: i64,
|
limit: i64,
|
||||||
offset: i64,
|
offset: i64,
|
||||||
) -> Result<QueryNodesResponse, sqlx::Error> {
|
) -> Result<QueryNodesResponse, sqlx::Error> {
|
||||||
|
let query_start = std::time::Instant::now();
|
||||||
let mut tx = db.begin().await?;
|
let mut tx = db.begin().await?;
|
||||||
set_rls_context(&mut tx, user_node_id).await?;
|
set_rls_context(&mut tx, user_node_id).await?;
|
||||||
|
|
||||||
|
|
@ -555,6 +571,8 @@ async fn run_query_nodes(
|
||||||
// Transaksjon avsluttes — SET LOCAL tilbakestilles automatisk
|
// Transaksjon avsluttes — SET LOCAL tilbakestilles automatisk
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
|
log_slow_query("query_nodes", query_start.elapsed().as_secs_f64() * 1000.0);
|
||||||
|
|
||||||
Ok(QueryNodesResponse { nodes, total })
|
Ok(QueryNodesResponse { nodes, total })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1072,6 +1090,7 @@ async fn run_query_graph(
|
||||||
edge_type_filter: &Option<Vec<String>>,
|
edge_type_filter: &Option<Vec<String>>,
|
||||||
node_kind_filter: &Option<Vec<String>>,
|
node_kind_filter: &Option<Vec<String>>,
|
||||||
) -> Result<QueryGraphResponse, sqlx::Error> {
|
) -> Result<QueryGraphResponse, sqlx::Error> {
|
||||||
|
let query_start = std::time::Instant::now();
|
||||||
let mut tx = db.begin().await?;
|
let mut tx = db.begin().await?;
|
||||||
set_rls_context(&mut tx, user_node_id).await?;
|
set_rls_context(&mut tx, user_node_id).await?;
|
||||||
|
|
||||||
|
|
@ -1146,6 +1165,7 @@ async fn run_query_graph(
|
||||||
};
|
};
|
||||||
|
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
log_slow_query("query_graph", query_start.elapsed().as_secs_f64() * 1000.0);
|
||||||
|
|
||||||
// Appliser klientside-filtre
|
// Appliser klientside-filtre
|
||||||
let mut graph_nodes: Vec<GraphNode> = nodes
|
let mut graph_nodes: Vec<GraphNode> = nodes
|
||||||
|
|
|
||||||
170
migrations/017_query_performance.sql
Normal file
170
migrations/017_query_performance.sql
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
-- 017_query_performance.sql
|
||||||
|
-- Ytelsesoptimalisering: composite indexes og forbedret recompute_access.
|
||||||
|
--
|
||||||
|
-- Profileringsresultater viste at:
|
||||||
|
-- 1. recompute_access step 2 bruker idx_edges_type (lav selektivitet) og
|
||||||
|
-- filtrerer på target_id etterpå — trenger composite index.
|
||||||
|
-- 2. RLS edge_select policy gjør IN-sjekk mot node_access for source_id
|
||||||
|
-- og target_id — composite index på edges (target_id, edge_type) hjelper
|
||||||
|
-- joinmønsteret i recompute_access og belongs_to-oppslag.
|
||||||
|
-- 3. Alias-oppslag (source_id + edge_type + system) mangler composite index.
|
||||||
|
-- 4. nodes-oppslag sortert på created_at DESC mangler index.
|
||||||
|
-- 5. recompute_access kjører steg 3 og 4 uansett, selv om de sjelden
|
||||||
|
-- treffer — optimaliser med betingede sjekker.
|
||||||
|
--
|
||||||
|
-- Ref: oppgave 12.4, docs/retninger/datalaget.md
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- Composite indexes på edges
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- Dekker recompute_access steg 2 (belongs_to-oppslag mot target)
|
||||||
|
-- og generelle "hent barn av node"-spørringer (query_board, etc.)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_edges_target_type
|
||||||
|
ON edges (target_id, edge_type);
|
||||||
|
|
||||||
|
-- Dekker alias-oppslag og "finn utgående edges av type"-spørringer
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_edges_source_type
|
||||||
|
ON edges (source_id, edge_type);
|
||||||
|
|
||||||
|
-- Dekker member_of-oppslag i recompute_access steg 3 (covering index)
|
||||||
|
-- Inkluderer source_id for å unngå heap-oppslag
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_edges_target_memberof
|
||||||
|
ON edges (target_id)
|
||||||
|
WHERE edge_type = 'member_of';
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- Composite index på nodes for sortering
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- query_nodes bruker ORDER BY created_at DESC med LIMIT/OFFSET.
|
||||||
|
-- Med RLS kreves sekvensielt scan, men for ikke-RLS-spørringer
|
||||||
|
-- (superuser context) gir dette stor gevinst.
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_nodes_created_at_desc
|
||||||
|
ON nodes (created_at DESC);
|
||||||
|
|
||||||
|
-- Dekker vanlig mønster: filtrer på node_kind, sorter på created_at
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_nodes_kind_created
|
||||||
|
ON nodes (node_kind, created_at DESC);
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- Covering index på node_access for RLS
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- RLS-policyen gjør: WHERE subject_id = current_node_id()
|
||||||
|
-- og returnerer object_id. Covering index unngår heap-oppslag.
|
||||||
|
-- Erstatter ikke idx_na_subject som brukes av PK, men PostgreSQL
|
||||||
|
-- velger denne fordi den dekker hele spørringen.
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_na_subject_covering
|
||||||
|
ON node_access (subject_id) INCLUDE (object_id);
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- Optimalisert recompute_access
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- Hovedforbedring: steg 3 (team→medlemmer) og steg 4 (arv fra team)
|
||||||
|
-- kjøres nå bare når det er relevant. Steg 3 sjekker om subject faktisk
|
||||||
|
-- er et team (har member_of-edges mot seg). Steg 4 sjekker om root_node
|
||||||
|
-- faktisk har egne node_access-rader (= er et team/entitet med tilgang).
|
||||||
|
-- For den vanlige casen (bruker→samling owner/admin) gjør dette at
|
||||||
|
-- steg 3 og 4 er billige EXISTS-sjekker i stedet for fulle INSERT-selects.
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION recompute_access(
|
||||||
|
p_subject_id UUID,
|
||||||
|
p_root_node_id UUID,
|
||||||
|
p_access access_level,
|
||||||
|
p_via_edge UUID
|
||||||
|
) RETURNS void AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Steg 1: Direkte tilgang til roten
|
||||||
|
INSERT INTO node_access (subject_id, object_id, access, via_edge)
|
||||||
|
VALUES (p_subject_id, p_root_node_id, p_access, p_via_edge)
|
||||||
|
ON CONFLICT (subject_id, object_id)
|
||||||
|
DO UPDATE SET access = GREATEST(node_access.access, p_access),
|
||||||
|
via_edge = CASE
|
||||||
|
WHEN p_access > node_access.access THEN p_via_edge
|
||||||
|
ELSE node_access.via_edge
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Steg 2: Transitiv: noder som tilhører roten (belongs_to)
|
||||||
|
-- Bruker idx_edges_target_type (target_id, edge_type) for rask oppslag
|
||||||
|
INSERT INTO node_access (subject_id, object_id, access, via_edge)
|
||||||
|
SELECT p_subject_id, e.source_id, p_access, p_via_edge
|
||||||
|
FROM edges e
|
||||||
|
WHERE e.target_id = p_root_node_id
|
||||||
|
AND e.edge_type = 'belongs_to'
|
||||||
|
ON CONFLICT (subject_id, object_id)
|
||||||
|
DO UPDATE SET access = GREATEST(node_access.access, p_access),
|
||||||
|
via_edge = CASE
|
||||||
|
WHEN p_access > node_access.access THEN p_via_edge
|
||||||
|
ELSE node_access.via_edge
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Steg 3: Hvis subject er et team: propager til alle teammedlemmer.
|
||||||
|
-- Sjekk først om det finnes member_of-edges (= subject er et team).
|
||||||
|
-- For vanlige brukere er dette en billig EXISTS som returnerer false.
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM edges
|
||||||
|
WHERE target_id = p_subject_id AND edge_type = 'member_of'
|
||||||
|
LIMIT 1
|
||||||
|
) THEN
|
||||||
|
INSERT INTO node_access (subject_id, object_id, access, via_edge)
|
||||||
|
SELECT e.source_id, na.object_id, na.access, na.via_edge
|
||||||
|
FROM node_access na
|
||||||
|
JOIN edges e ON e.target_id = p_subject_id
|
||||||
|
AND e.edge_type = 'member_of'
|
||||||
|
WHERE na.subject_id = p_subject_id
|
||||||
|
ON CONFLICT (subject_id, object_id)
|
||||||
|
DO UPDATE SET access = GREATEST(node_access.access, EXCLUDED.access),
|
||||||
|
via_edge = CASE
|
||||||
|
WHEN EXCLUDED.access > node_access.access THEN EXCLUDED.via_edge
|
||||||
|
ELSE node_access.via_edge
|
||||||
|
END;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Steg 4: Team-transitivitet — arv tilgang fra teamet.
|
||||||
|
-- Bare relevant når subject meldes inn i et team (root_node er team).
|
||||||
|
-- Sjekk om root_node har egne node_access-rader som subject.
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM node_access
|
||||||
|
WHERE subject_id = p_root_node_id
|
||||||
|
LIMIT 1
|
||||||
|
) THEN
|
||||||
|
INSERT INTO node_access (subject_id, object_id, access, via_edge)
|
||||||
|
SELECT p_subject_id, na.object_id, na.access, na.via_edge
|
||||||
|
FROM node_access na
|
||||||
|
WHERE na.subject_id = p_root_node_id
|
||||||
|
ON CONFLICT (subject_id, object_id)
|
||||||
|
DO UPDATE SET access = GREATEST(node_access.access, EXCLUDED.access),
|
||||||
|
via_edge = CASE
|
||||||
|
WHEN EXCLUDED.access > node_access.access THEN EXCLUDED.via_edge
|
||||||
|
ELSE node_access.via_edge
|
||||||
|
END;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- pg_stat_statements (krever manuell konfigurasjon)
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- pg_stat_statements krever shared_preload_libraries i postgresql.conf.
|
||||||
|
-- For å aktivere:
|
||||||
|
-- 1. Legg til i docker-compose.yml for postgres:
|
||||||
|
-- command: postgres -c shared_preload_libraries=pg_stat_statements
|
||||||
|
-- -c pg_stat_statements.track=all
|
||||||
|
-- 2. Restart postgres-containeren
|
||||||
|
-- 3. Kjør: CREATE EXTENSION pg_stat_statements;
|
||||||
|
-- 4. Se topp-spørringer: SELECT * FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20;
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- Oppdater statistikk for query planner
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
ANALYZE nodes;
|
||||||
|
ANALYZE edges;
|
||||||
|
ANALYZE node_access;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
3
tasks.md
3
tasks.md
|
|
@ -270,8 +270,7 @@ kaller dem direkte. Samme verktøy, to brukere.
|
||||||
- [x] 12.1 Observerbarhet: strukturert logging, metrikker (request latency, queue depth, AI cost).
|
- [x] 12.1 Observerbarhet: strukturert logging, metrikker (request latency, queue depth, AI cost).
|
||||||
- [x] 12.2 Backup: PG-dump rutine, STDB → PG gjenoppbygging ved krasj.
|
- [x] 12.2 Backup: PG-dump rutine, STDB → PG gjenoppbygging ved krasj.
|
||||||
- [x] 12.3 Feilhåndtering: retry med backoff i skrivestien, dead letter queue for feilede PG-skrivinger.
|
- [x] 12.3 Feilhåndtering: retry med backoff i skrivestien, dead letter queue for feilede PG-skrivinger.
|
||||||
- [~] 12.4 Ytelse: profiler PG-spørringer, optimaliser node_access-oppdatering.
|
- [x] 12.4 Ytelse: profiler PG-spørringer, optimaliser node_access-oppdatering.
|
||||||
> Påbegynt: 2026-03-18T11:30
|
|
||||||
|
|
||||||
## Fase 22: SpacetimeDB-migrering — PG LISTEN/NOTIFY
|
## Fase 22: SpacetimeDB-migrering — PG LISTEN/NOTIFY
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue