Skip to content

TTL-cache CMS (WordPress) reads — removes the ~3s per-request floor - #257

Merged
anuveyatsu merged 1 commit into
oddk/custom-facetfrom
oddk/cache-wp-cms
Sep 9, 2026
Merged

anuveyatsu merged 1 commit into
oddk/custom-facetfrom
oddk/cache-wp-cms

Conversation

@anuveyatsu

Copy link
Copy Markdown
Member

Follow-up to #256. That PR cached CKAN's organization_list and took the ODDK home page from 11.7s to ~2.4s in production — but ~2.4s is still slow, and the remaining time is not CKAN.

The measurement

The ODDK theme awaits a CMS call in global app.use middleware on every non-static request, so every page pays a WordPress round trip before its own work starts:

path time notes
/static/js/home_page.js 0.44s express.static, mounted before the middleware
/robots.txt 2.9s trivial route, still pays the middleware
any 404 13.3s middleware + 404 render

A ~3s floor on anything routed through the app. /robots.txt returns a few hundred bytes and touches no data source, so that time is essentially all WordPress.

The specific call is CmsModel.getListOfPosts({ type: 'page' }), used to build the navbar.

What this does

Every method on CmsModel is a read of slow-changing blog content, so this caches them using the same TtlCache already shipped for CKAN actions in #256:

  • single-flight — concurrent misses collapse into one upstream call rather than each hitting WordPress
  • stale-on-error — a WordPress blip serves the last good value. This is a real improvement on today's behaviour: the theme catches CMS errors and falls back to [], so a WP outage currently silently empties the navbar
  • opt-in via CMS_CACHE_TTL (default 0), so portals that have not asked for it are unaffected

Cached: getListOfPostsWithMeta (which getListOfPosts and getListOfPages both route through), getPost, getCategories, getSiteInfo.

Two correctness fixes that fall out

Keying a cache on the query exposed argument mutation:

  • getListOfPostsWithMeta did Object.assign(query, this.baseQuery) — mutating the caller's object and writing status into it. A caller reusing an object would therefore produce a different cache key on the second call. Now Object.assign({}, query, ...).
  • getListOfPages likewise set query.type = 'page' on the caller's object.

Both are covered by tests asserting the passed object is untouched.

Design note

The cache instance is injectable so tests get isolation; at runtime every model shares one, which is the point — plugins/wp and plugins/ckan_pages each construct their own CmsModel and should not each hold a private copy of the same blog content. (The first draft of the tests failed precisely because they assumed isolation from a shared cache — worth knowing if you add more.)

Testing

tests/plugins/wp-cache.test.js8 tests, all passing: the middleware call is cached, distinct queries stay separate, getSiteInfo/getCategories cached, caching is a no-op at TTL 0, concurrent reads collapse to one call, a failed refresh serves stale, and neither list method mutates its argument.

tests/plugins/wp.test.js has 4 pre-existing failures on this branch — verified identical with and without this commit by stashing it, so not introduced here.

Rollout

Nothing changes until a portal sets CMS_CACHE_TTL. For ODDK I would suggest 300s: the navbar and featured posts are editorial content where a few minutes of staleness is unremarkable, and even 300s removes essentially every per-request call under real traffic.

Expected effect on ODDK, based on the numbers above: the ~3s floor collapses, so the cached home page should drop from ~2.4s to well under a second, and /robots.txt and other light routes become near-instant.

Worth noting separately: the 13.3s 404 path is not addressed here and looks like its own liability.

…nd trip

The ODDK theme awaits a CMS call in global `app.use` middleware on every
non-static request, so every page paid a WordPress round trip before its own
work started. Measured on staging:

  /static/js/home_page.js   0.44s   (express.static, before the middleware)
  /robots.txt               2.9s
  a 404                    13.3s

That ~3s floor is why the home page still took ~2.4s after the CKAN
organization_list cache landed - the remaining time was not CKAN.

The specific call is CmsModel.getListOfPosts({type: 'page'}), used to build the
navbar. Every method on CmsModel is a read of slow-changing blog content, so
this caches them with the same TtlCache already used for CKAN actions:
single-flight so concurrent misses collapse into one upstream call, and
stale-on-error so a WordPress blip serves the last good value instead of
silently emptying the navbar (the theme currently catches and falls back to []).

Opt-in via CMS_CACHE_TTL, default 0, so portals that have not asked for it are
unaffected.

Two small correctness fixes fall out of keying a cache on the query:

- getListOfPostsWithMeta used Object.assign(query, this.baseQuery), mutating
  the caller's object and writing `status` into it. A caller reusing an object
  therefore produced a different cache key on the second call. Now
  Object.assign({}, query, ...).
- getListOfPages likewise set query.type = 'page' on the caller's object.

The cache instance is injectable so tests get isolation; at runtime every model
shares one, which is the point - plugins/wp and plugins/ckan_pages each build
their own model and should not each hold a private copy of the same content.

tests/plugins/wp-cache.test.js: 8 tests, all passing. The 4 pre-existing
failures in tests/plugins/wp.test.js are unchanged - verified identical with
and without this commit by stashing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 87e8d64e-dceb-4531-abdd-446174773a58

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@anuveyatsu
anuveyatsu merged commit 72edcd6 into oddk/custom-facet Sep 9, 2026
1 of 9 checks passed
@anuveyatsu
anuveyatsu deleted the oddk/cache-wp-cms branch September 9, 2026 10:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant