Skip to content

Commit 7d379dc

Browse files
Cache parameterized Mapnik maps per render thread to fix PostGIS memory leak
When parameterize_style is configured, render() previously created a full copy of the Mapnik Map object and called set_datasource() on each affected layer for every single tile render. Each set_datasource() call creates a new PostGIS datasource, which registers a new connection pool entry in Mapnik's global ConnectionManager singleton. These pools were never released, causing renderd to exhaust memory rapidly under load. SQLite-backed styles were unaffected because the SQLite datasource has no global connection pool. Fix: add a std::map<string, mapnik::Map> parameterized_map_cache to xmlmapconfig. On the first render for a given options string the parameterized Map is built as before and then moved into the cache. Subsequent renders with the same options reuse the cached Map directly, calling set_datasource() only once per (thread, style, options) combination. Document the remaining render_thread startup allocation leaks in CLAUDE.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f3e3ed6 commit 7d379dc

2 files changed

Lines changed: 40 additions & 18 deletions

File tree

CLAUDE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ Test infrastructure uses `tests/httpd.conf.in` and `tests/renderd.conf.in` templ
134134
- **`src/request_queue.c:request_queue_close`** — queued render items are not freed on shutdown (items in all five priority lists leak). The TODO comment is present in the source. Safe in practice because renderd only shuts down at process exit, but should be fixed for clean valgrind runs.
135135
- **`src/store_ro_http_proxy.c:strcpy` at line 165**`xmlconfig` is copied into `ctx->cache.xmlname[XMLCONFIG_MAX]` (41 bytes) without a prior length check. The caller is the storage backend interface which in practice receives validated xmlconfig names, but the copy is not bounds-safe.
136136
- **`src/renderd.c` / `src/mod_tile.c``bzero` usage** — several files use the deprecated `bzero()` instead of `memset(..., 0, ...)`. Functionally equivalent on Linux but not strictly portable.
137+
- **`src/gen_tile.cpp:render_thread` — startup `strndup`/`malloc` leaks**`output_format`, `xmlfile`, `xmlname` (`strndup`), `prj` (`malloc`), and `store` (`init_storage_backend`) are allocated once per thread at startup and never freed. The render thread runs in an infinite loop and never exits, so these do not accumulate in practice.
138+
139+
## Parameterized rendering cache
140+
141+
When `parameterize_style` is configured (e.g. for multilingual maps), `render()` in `src/gen_tile.cpp` maintains a per-`xmlmapconfig` cache (`parameterized_map_cache`) mapping options strings to pre-built `mapnik::Map` copies. This prevents Mapnik's PostGIS datasource plugin from creating a new PostgreSQL connection pool entry on every render, which previously caused rapid memory exhaustion. The cache is per render-thread and unbounded in size; in practice the number of distinct options values is small (e.g. one per requested language).
137142

138143
## Repository Layout
139144

src/gen_tile.cpp

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ struct projectionconfig {
8989

9090
struct xmlmapconfig {
9191
Map map;
92+
/* Cache of parameterized Map copies keyed by the options string.
93+
* Avoids recreating PostGIS datasource objects (and their connection
94+
* pools) on every render, which would accumulate in Mapnik's global
95+
* ConnectionManager and exhaust memory under sustained load. */
96+
std::map<std::string, mapnik::Map> parameterized_map_cache;
9297
const char *host;
9398
const char *htcphost;
9499
const char *output_format;
@@ -247,31 +252,43 @@ static enum protoCmd render(struct xmlmapconfig *map, int x, int y, int z, char
247252
unsigned int render_size_tx = MIN(METATILE, map->prj->aspect_x * (1 << z));
248253
unsigned int render_size_ty = MIN(METATILE, map->prj->aspect_y * (1 << z));
249254

250-
map->map.resize(render_size_tx * map->tilesize, render_size_ty * map->tilesize);
251-
map->map.zoom_to_box(tile2prjbounds(map->prj, x, y, z));
255+
/* Select the map to render with: a cached parameterized copy, or the base map. */
256+
mapnik::Map *render_map;
257+
258+
if (map->parameterize_function) {
259+
/* Look up or create a parameterized map for this options string.
260+
* Re-using a cached copy avoids calling set_datasource() on every
261+
* render, which would create a new PostGIS connection pool entry in
262+
* Mapnik's global ConnectionManager each time and leak memory. */
263+
auto it = map->parameterized_map_cache.find(std::string(options));
264+
265+
if (it == map->parameterized_map_cache.end()) {
266+
mapnik::Map parameterized = map->map;
267+
map->parameterize_function(parameterized, options);
268+
parameterized.load_fonts();
269+
auto inserted = map->parameterized_map_cache.emplace(std::string(options), std::move(parameterized));
270+
it = inserted.first;
271+
g_logger(G_LOG_LEVEL_DEBUG, "Cached new parameterized map for options '%s' (cache size: %zu)",
272+
options, map->parameterized_map_cache.size());
273+
}
252274

253-
if (map->map.buffer_size() == 0) { // Only set buffer size if the buffer size isn't explicitly set in the mapnik stylesheet.
254-
map->map.set_buffer_size((map->tilesize >> 1) * map->scale);
275+
render_map = &it->second;
276+
} else {
277+
render_map = &map->map;
255278
}
256279

257-
// m.zoom(size+1);
280+
render_map->resize(render_size_tx * map->tilesize, render_size_ty * map->tilesize);
281+
render_map->zoom_to_box(tile2prjbounds(map->prj, x, y, z));
282+
283+
if (render_map->buffer_size() == 0) { // Only set buffer size if the buffer size isn't explicitly set in the mapnik stylesheet.
284+
render_map->set_buffer_size((map->tilesize >> 1) * map->scale);
285+
}
258286

259287
mapnik::image_32 buf(render_size_tx * map->tilesize, render_size_ty * map->tilesize);
260288

261289
try {
262-
if (map->parameterize_function) {
263-
Map map_parameterized = map->map;
264-
265-
map->parameterize_function(map_parameterized, options);
266-
267-
map_parameterized.load_fonts();
268-
269-
mapnik::agg_renderer<mapnik::image_32> ren(map_parameterized, buf, map->scale);
270-
ren.apply();
271-
} else {
272-
mapnik::agg_renderer<mapnik::image_32> ren(map->map, buf, map->scale);
273-
ren.apply();
274-
}
290+
mapnik::agg_renderer<mapnik::image_32> ren(*render_map, buf, map->scale);
291+
ren.apply();
275292
} catch (std::exception const &ex) {
276293
g_logger(G_LOG_LEVEL_ERROR, "failed to render TILE %s %d %d-%d %d-%d", map->xmlname, z, x, x + render_size_tx - 1, y, y + render_size_ty - 1);
277294
g_logger(G_LOG_LEVEL_ERROR, " reason: %s", ex.what());

0 commit comments

Comments
 (0)