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:

  1. 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.
  2. Prevent Stale Fetches from Discarding Newer Writes: Implement a write generation mechanism. When refresh() completes, it can check if any set() 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.
  3. 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.

Start with the tools

Explore migration tools

See options, compare methods, and pick the path that fits your store.

Explore migration tools