NotHimmel opened a new issue, #2567:
URL: https://github.com/apache/age/issues/2567
**Describe the bug**
`manage_GRAPH_global_contexts()` (`age_global_graph.c:1024`) builds a full
in-memory copy
of every vertex and edge of a graph in `TopMemoryContext` (`:1033`), once
per backend.
There is no memory ceiling and no GUC, and the load ignores the label scope
of the calling
query. A 3-hop `shortest_path()` on a 500k-vertex graph takes ~3.3 s the
first time, of
which ~1 ms is the search and the rest is the load. On a 3M-vertex / 9M-edge
graph the
cache is ~2.3 GB per backend, so a few concurrent graph queries OOM a modest
server.
Three separate observations:
1. The load is not shared between backends. A second session pays the full
cost again.
2. The load ignores the query's label scope. `load_vertex_hashtable()`
(`:715`) and
`load_edge_hashtable()` (`:817`) call `table_beginscan(..., snapshot, 0,
NULL)` at
`:748` and `:850` — zero scan keys. Passing `edge_types` does not reduce
the load;
the filter is applied inside the traversal, after the load.
3. There is no upper bound. The context grows to whatever the graph size
demands and is
released only when the graph version changes (`is_ggctx_invalid()`,
`:235`) or on an
explicit `age_delete_global_graphs()`. A read-only workload holds it for
the life of
the session, and nothing stops the OOM killer.
This is not a regression, and not a defect in the new shortest-path code:
`sp_run_bfs()`
(`age_vle.c:2983`) and the `SP_MAX_RESULT_PATHS` cap (`:3215`) are fine. But
`age_shortest_path` calls `manage_GRAPH_global_contexts()` at
`age_vle.c:3632`, so it
inherits this from VLE and makes it much easier to hit — one
`shortest_path()` call now
pays the whole load.
**How are you accessing AGE (Command line, driver, etc.)?**
- psql (command line)
**What data setup do we need to do?**
Two graphs with identical vertices and an identical `E1` ring. `sp3`
additionally has
`E2`/`E3`, which every query below explicitly excludes. Takes about a minute
to build.
```pgsql
LOAD 'age'; SET search_path = ag_catalog, public;
SET maintenance_work_mem = '1GB';
-- sp3: 500k vertices, THREE edge labels x 500k edges
SELECT create_graph('sp3');
SELECT create_vlabel('sp3','V');
SELECT create_elabel('sp3','E1');
SELECT create_elabel('sp3','E2');
SELECT create_elabel('sp3','E3');
INSERT INTO sp3."V"(id, properties)
SELECT _graphid(3,i::bigint), ('{"k":'||i||'}')::agtype FROM
generate_series(1,500000) i;
-- E1 is a ring i -> i+1, so vertex 1 reaches vertex 4 in exactly 3 hops
INSERT INTO sp3."E1"(id, start_id, end_id, properties)
SELECT _graphid(4,i::bigint), _graphid(3,i::bigint),
_graphid(3,(i%500000)+1), '{}'::agtype
FROM generate_series(1,500000) i;
-- E2/E3 are chords, irrelevant to any E1-only query
INSERT INTO sp3."E2"(id, start_id, end_id, properties)
SELECT _graphid(5,i::bigint), _graphid(3,i::bigint),
_graphid(3,((i::bigint*104729)%500000)+1), '{}'::agtype
FROM generate_series(1,500000) i;
INSERT INTO sp3."E3"(id, start_id, end_id, properties)
SELECT _graphid(6,i::bigint), _graphid(3,i::bigint),
_graphid(3,((i::bigint*15485863)%500000)+1), '{}'::agtype
FROM generate_series(1,500000) i;
-- sp1: same vertices, ONLY the E1 ring
SELECT create_graph('sp1');
SELECT create_vlabel('sp1','V');
SELECT create_elabel('sp1','E1');
INSERT INTO sp1."V"(id, properties)
SELECT _graphid(3,i::bigint), ('{"k":'||i||'}')::agtype FROM
generate_series(1,500000) i;
INSERT INTO sp1."E1"(id, start_id, end_id, properties)
SELECT _graphid(4,i::bigint), _graphid(3,i::bigint),
_graphid(3,(i%500000)+1), '{}'::agtype
FROM generate_series(1,500000) i;
ANALYZE sp3."V"; ANALYZE sp3."E1"; ANALYZE sp3."E2"; ANALYZE sp3."E3";
ANALYZE sp1."V"; ANALYZE sp1."E1";
```
**What is the necessary configuration info needed?**
- Nothing beyond a default install. `shared_buffers = 1GB` here; no
concurrent writers,
so the cache stays valid across the measurements.
- On PG 18, `detect_version_mode()` selects `VERSION_MODE_DSM`, so
snapshot-based
invalidation is not involved.
**What is the command that caused the error?**
There is no error message — the symptom is time and memory. Run this in a
**fresh
session** each time, changing the graph name for step 5:
```pgsql
LOAD 'age'; SET search_path = ag_catalog, public;
SELECT pg_backend_pid() AS pid \gset
\setenv PID :pid
\! grep VmRSS /proc/$PID/status
\timing on
SELECT count(*) FROM age_shortest_path('"sp3"'::agtype,
_graphid(3,1)::agtype, _graphid(3,4)::agtype,
'"E1"'::agtype, NULL, NULL, 3::agtype);
\timing off
\! grep VmRSS /proc/$PID/status
```
Every step returns the one correct 3-hop path. `VmRSS` is the serving
backend's.
| step | session | graph | time | backend RSS |
|---|---|---|---|---|
| 1 | A | - | baseline after `LOAD 'age'` | 15.7 MB |
| 2 | A | sp3 | **3296 ms** | 15.7 MB -> **439 MB** |
| 3 | A | sp3 | **1.15 ms** (identical call again) | 439 MB |
| 4 | B (new) | sp3 | **3751 ms** | 19.9 MB -> **439 MB** |
| 5 | C (new) | sp1 | **1252 ms** | 15.6 MB -> **307 MB** |
Step 4 repeated in three further fresh sessions: 3162 / 3383 / 3437 ms,
438.6 MB each
time.
- **2 vs 3** — the search costs 1.15 ms; the other ~3295 ms is the cache
build.
- **2 vs 4** — a second backend pays it again. The cache is backend-private.
- **4 vs 5** — same query, same answer, `edge_types = '"E1"'` in both. `sp3`
merely *also
contains* `E2`/`E3`; those 1M excluded edges still cost **+132 MB and
+2.50 s**.
Same build at 3M vertices + 3 x 3M edges, `max_hops = 4`: first call 22.4 s
/ **2.27 GB**,
identical call in the same session 3.0 ms, a new session 24.8 s / 2.27 GB
again, and
`edge_types = '"E1"'` in a fresh session still 23.4 s / 2.27 GB. A user
reported a backend
killed with `anon-rss 3.3 GB` + `signal 9` on a graph of this shape (their
report, not my
measurement).
**Expected behavior**
Two things, in order of how badly they are needed:
1. A graph query should not be able to drive the backend into the OOM killer
with no
configurable limit. Exceeding a bound should fail the query with a clear
message.
2. A 3-hop query on a graph with a start vertex should not require the
entire graph to be
resident, should not re-pay the load in every backend, and should not
load edge labels
the query has explicitly excluded.
**Environment (please complete the following information):**
- Version: AGE 1.8.0 (`PG18/v1.8.0-rc0`, commit `e43dc1a1`)
- PostgreSQL 18.4
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]