Skip to content

Duplicate concurrent get_types (and other dropdown) requests when many rows mount at once #10155

Description

@kundansable

Describe the bug

getNodeAjaxOptions() in web/pgadmin/browser/static/js/node_ajax.js — the shared helper behind every AJAX-backed dropdown in pgAdmin (Data Type, foreign keys, triggers, backup/restore options, cloud wizards, ~35 call sites) — caches responses, but only after they resolve:

let data = cacheNode.cache(nodeObj.type + '#' + url, treeNodeInfo, cacheLevel);
if (_.isUndefined(data) || _.isNull(data)) {
  api.get(fullUrl, {...}).then((res) => {
    ...
    cacheNode.cache(..., data);   // only written once the response comes back
  });
}

There is no in-flight request deduplication. If several components call this for the same URL before the first response lands, every one of them sees an empty cache and fires its own HTTP GET. Each row's Data Type dropdown (column.ui.js, cacheLevel: 'table') does exactly this on mount.

This code path exists on master as-is — node_ajax.js isn't touched by #10146. This issue was found while testing #10146 ("fix: skip row virtualization for small data grids"); we haven't separately confirmed how many duplicate requests fire on plain master for the same table, so we're not claiming that PR changes the severity — only reporting what we observed.

To Reproduce

  1. Create a table with 150+ columns.
  2. Open DevTools → Network tab.
  3. Right-click the table → Properties → Columns tab.
  4. Observe ~150 concurrent GET /browser/table/get_types/<gid>/<sid>/<did>/<scid>/ requests, all firing within the same ~50-100ms window — one per column row, all for the identical options list.

Expected behavior

A single get_types request should be shared by all concurrent callers; the rest should await that same in-flight request rather than issuing their own.

Additional context

x
Found while testing #10146 — noting that for traceability, not to imply it's the cause. The underlying race is in node_ajax.js, which that PR does not modify.

Proposed fix

Add in-flight promise sharing to getNodeAjaxOptions, keyed by the resolved URL + query params, so concurrent callers await the same underlying request:

const _inflightAjaxRequests = new Map();
...
if (_.isUndefined(data) || _.isNull(data)) {
  const inflightKey = fullUrl + '#' + JSON.stringify(otherParams.urlParams || {});
  let request = _inflightAjaxRequests.get(inflightKey);
  if (!request) {
    request = api.get(fullUrl, { params: otherParams.urlParams }).then((res) => {
      let resData = res.data.data ?? res.data;
      otherParams.useCache && cacheNode.cache(nodeObj.type + '#' + url, treeNodeInfo, cacheLevel, resData);
      return resData;
    }).finally(() => { _inflightAjaxRequests.delete(inflightKey); });
    _inflightAjaxRequests.set(inflightKey, request);
  }
  request.then((resData) => resolve(transform(resData))).catch((err) => reject(...));
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions