Resolving ESHOPMAN Search Cache Inconsistencies for Stable Storefronts
Understanding and Mitigating ESHOPMAN Search Module Cache Issues
A recent deep dive into ESHOPMAN's search module has uncovered a critical cache inconsistency that can lead to frustrating 'Search index "products" has no active version yet' errors. This issue, specifically within the ActiveIndexVersionCache utility, can disrupt storefront search functionality and impact the reliability of your ESHOPMAN applications deployed via HubSpot CMS.
The Core Problem: Stale Cache Overwriting Active Versions
The root cause lies in how ESHOPMAN's ActiveIndexVersionCache handles concurrent operations and cold refreshes. When a new search index version is activated in the database, the cache's set() method updates the local map. However, a 'cold' cache refresh, which might have started its database read *before* the activation, can land *after* the set() operation. This stale refresh then overwrites the entire cache map, effectively removing the newly activated index entry. Consequently, subsequent search queries or document operations will fail, reporting that the index has no active version.
This state can persist for up to 30 seconds until a background refresh eventually heals the entry. During this window, ESHOPMAN storefronts could experience failed search results, impacting customer experience and data integrity.
Where This Issue Appears
- Integration Tests: ESHOPMAN developers often observe intermittent CI failures where search operations (like
upsertDocuments,search,deleteDocuments) throw NOT_FOUND errors immediately after an index is logged as active. This indicates the cache issue manifesting in automated testing environments. - Production Environments: While not yet widely measured, it's inferred that a process's initial activation of an ESHOPMAN search index could leave its own cache without the entry for a significant period (up to ~30 seconds), causing writes and reads to fail during this window.
Reproducing the Cache Inconsistency
The following standalone Node.js reproduction demonstrates the exact scenario where a cold refresh overwrites a recently set active index version:
// node repro.cjs
const { ActiveIndexVersionCache } = require(process.argv[2])
let now = 1_000_000; Date.now = () => now
const defer = () => { let r; const p = new Promise((res) => (r = res)); return { p, r } }
const v1 = { name: "products", version: 1 }
const q = []
const c = new ActiveIndexVersionCache(() => { const d = defer(); q.push(d); return d.p })
const read = async (label) => {
try { await c.get("products"); console.log(label, "ok") }
catch (e) { console.log(label, e.type, e.message) }
}
;(async () => {
const a = read("cold read A (its DB read predates activation)")
const b = read("cold read B")
q[1].r(new Map([["products", v1]])); await b // B lands: the entry is present
c.set("products", v1) // the activating process sets it
await read("read after set()") // ok
q[0].r(new Map()); await a // A lands late with the pre-activation (empty) result
await read("next read") // NOT_FOUND: the entry is gone
now += 29_999; await read("read at +29.999 s") // still NOT_FOUND
now += 1; await read("read at +30 s") // still NOT_FOUND, but a background refresh starts
q[q.length - 1].r(new Map([["products", v1]])); await new Promise((r) => setImmediate(r))
await read("read after the background refresh") // ok
})()
Suggested Solutions for ESHOPMAN Developers
Several approaches can resolve this cache inconsistency, ensuring more robust ESHOPMAN search functionality:
- Deduplicate Cold Refreshes: Modify the cache to route all cold path refreshes through a single in-flight promise. This ensures concurrent callers share one database fetch, preventing multiple stale reads from interfering.
- Prevent Stale Fetches from Discarding Newer Writes: Implement a write generation mechanism. When
refresh()completes, it can check if anyset()operations occurred *after* its database fetch started and re-apply those newer entries. This makes the cache monotonic with respect to its own process's writes. - Re-check Database on Cache Miss: As a fallback, if a specific index is missing from the cache and the last successful fetch is older than a small threshold, trigger a synchronous refresh from the database. This helps ensure a negative cache doesn't outlive an activation, even from another process.
Implementing one or a combination of these fixes will significantly improve the stability of ESHOPMAN's search module, leading to a more reliable and seamless experience for both developers and end-users on ESHOPMAN-powered storefronts.