Cache Webex group list in-process + fix log typo
The /user/groups/find endpoint used to hit Webex on every request,
paginating through ~10k groups 500 at a time (roughly 20 sequential
round trips per compose-page load). Now the whole list lives in a
process-local cache that refreshes on startup and once a day at 03:00
CRON_TIMEZONE.
- New groupsCache = { data, lastRefreshed, refreshing }. refreshing is
a shared in-flight Promise so a startup refresh and the daily cron
can't stampede if their timing overlaps.
- refreshGroupsCache() wraps findWebexGroup() with load timing +
error logging.
- getGroupsFromCache() returns the cached list immediately; only the
very first request after startup waits (and only if the startup
refresh hasn't completed yet).
- Warm the cache in the background right after loadBotProfiles() in
the app.listen callback.
- Third cron ('0 0 3 * * *') refreshes the cache daily, offset from
the 01:10 jobs cleanup and per-minute token/scheduled-jobs tick so
they don't fight for the event loop.
Bug fixes rolled in while I was in findWebexGroup:
- Return reject(...) on a non-ok Webex response instead of also
continuing the while loop, which used to race resolve/reject.
- Return reject(error) from the try/catch so a Webex hiccup no longer
hangs the paginator forever; previously it caught+logged and let
the loop spin.
- Drop the stray `memberSize = 0` assignment (memberSize was never
declared in scope).
- Drop the per-page console.log noise; failures now go through the
timestamped logger under the findWebexGroup tag.
Route cleanup:
- /user/groups/find now serves getGroupsFromCache() and returns 502
with a logged reason if the cache is empty AND the refresh failed.
- Dropped the stray saveConfig(response, "./testData.json") that
used to persist the entire fetched group list to a scratch file on
every request.
Also: fix "identify" -> "identity" in the whoAmI startup log line.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
04fb4f6442
commit
452673b9be
1 changed files with 67 additions and 21 deletions
88
index.js
88
index.js
|
|
@ -100,6 +100,50 @@ function getBotProfile(appName) {
|
|||
return botProfiles[appName] || null;
|
||||
}
|
||||
|
||||
// Cached list of every Webex group in the org, used by the "Additional groups"
|
||||
// dropdown on the compose page. findWebexGroup() paginates through ~10k groups
|
||||
// 500 at a time, which used to run on every /user/groups/find request; we now
|
||||
// refresh once at startup and once a day and serve everything else from
|
||||
// memory. `refreshing` is a shared in-flight Promise so a concurrent refresh
|
||||
// (e.g. the cron firing during a slow startup fetch) can't stampede.
|
||||
var groupsCache = {
|
||||
data: null,
|
||||
lastRefreshed: null,
|
||||
refreshing: null,
|
||||
};
|
||||
|
||||
function refreshGroupsCache() {
|
||||
if (groupsCache.refreshing) return groupsCache.refreshing;
|
||||
var startedAt = Date.now();
|
||||
logger('groupsCache', 'Refreshing group list from Webex...');
|
||||
groupsCache.refreshing = findWebexGroup()
|
||||
.then(function (groups) {
|
||||
groupsCache.data = groups;
|
||||
groupsCache.lastRefreshed = new Date();
|
||||
logger('groupsCache',
|
||||
'Cached ' + groups.length + ' groups in ' +
|
||||
(Date.now() - startedAt) + 'ms.');
|
||||
return groups;
|
||||
})
|
||||
.catch(function (err) {
|
||||
logger('groupsCache', 'Refresh failed: ' + (err && err.message || err));
|
||||
throw err;
|
||||
})
|
||||
.finally(function () {
|
||||
groupsCache.refreshing = null;
|
||||
});
|
||||
return groupsCache.refreshing;
|
||||
}
|
||||
|
||||
// Returns the cached groups if we have any; otherwise waits for whatever
|
||||
// refresh is currently in flight (or kicks one off). Requests only ever block
|
||||
// on the very first call after startup — every subsequent call resolves
|
||||
// synchronously from memory.
|
||||
function getGroupsFromCache() {
|
||||
if (groupsCache.data) return Promise.resolve(groupsCache.data);
|
||||
return refreshGroupsCache();
|
||||
}
|
||||
|
||||
// Express middleware: 404s any /CollabCentral/:app/* request whose `:app` is
|
||||
// not a known + enabled bot. Attaches the bot config to req.botConfig for
|
||||
// downstream handlers.
|
||||
|
|
@ -180,6 +224,10 @@ var server = app.listen(serverPort, function () {
|
|||
loadBotProfiles()
|
||||
.then(() => logger("startup", "Loaded " + Object.keys(botProfiles).length + " bot profile(s)."))
|
||||
.catch(err => logger("startup", "loadBotProfiles error: " + (err && err.message || err)));
|
||||
// Warm the groups cache in the background so the very first user hitting
|
||||
// /user/groups/find gets an instant response. Failures are non-fatal:
|
||||
// getGroupsFromCache() will simply try again on the first request.
|
||||
refreshGroupsCache().catch(function () { /* logged inside */ });
|
||||
});
|
||||
server.setTimeout(3000000);
|
||||
const upload = multer({ limits: { fileSize: 4000000 } }).fields(
|
||||
|
|
@ -222,6 +270,12 @@ cron.schedule('0 * * * * *', () => {
|
|||
.catch(error => logger("checkScheduledJobs", error));
|
||||
}, { timezone: cronTimezone })
|
||||
|
||||
// Daily at 03:00 local time: rebuild the org-wide groups cache. Offset from
|
||||
// the 01:10 jobs cleanup so the two crons don't fight over the event loop.
|
||||
cron.schedule('0 0 3 * * *', function () {
|
||||
refreshGroupsCache().catch(function () { /* logged inside */ });
|
||||
}, { timezone: cronTimezone })
|
||||
|
||||
//Routes to be used
|
||||
// Validate the :app segment for every /CollabCentral/:app/* request. This
|
||||
// runs before the static mount and all the per-route handlers below, so any
|
||||
|
|
@ -401,15 +455,13 @@ app.get('/CollabCentral/:app/user/:scope/:action', (req, res) => {
|
|||
res.status(200).send(config.webex.bot[req.params.app].authorized[req.cookies.id].groups)
|
||||
} else if (req.params.action == "find") {
|
||||
logger("apiEndpoint(" + req.params.app + ")", "GET /user/groups/find");
|
||||
|
||||
findWebexGroup()
|
||||
.then(response => {
|
||||
//console.log(response)
|
||||
saveConfig(response, "./testData.json")
|
||||
|
||||
res.status(200).send(response);
|
||||
})
|
||||
|
||||
getGroupsFromCache()
|
||||
.then(function (groups) { res.status(200).send(groups); })
|
||||
.catch(function (err) {
|
||||
logger('apiEndpoint(' + req.params.app + ')',
|
||||
'groups/find failed: ' + (err && err.message || err));
|
||||
res.status(502).send('Failed to load groups from Webex.');
|
||||
});
|
||||
}
|
||||
} else { res.status(404) }
|
||||
} else { res.status(401) }
|
||||
|
|
@ -673,38 +725,32 @@ function findWebexGroup() {
|
|||
'Content-Type': `application/json`
|
||||
}
|
||||
};
|
||||
console.log(url)
|
||||
while (groupSize > groups.length) {
|
||||
console.log("groupSize: " + groupSize)
|
||||
console.log("groups.length: " + groups.length)
|
||||
try {
|
||||
console.log(url);
|
||||
var fetchedGroups = await fetchWithRateLimit(url, requestOptions)
|
||||
var fetchedGroups = await fetchWithRateLimit(url, requestOptions);
|
||||
if (fetchedGroups.ok) {
|
||||
var groupData = await fetchedGroups.json();
|
||||
console.log("Total Results: " + groupData.totalResults)
|
||||
groupSize = groupData.totalResults;
|
||||
for (var group of groupData.groups) {
|
||||
groups.push(group);
|
||||
}
|
||||
startIndex = startIndex + count;
|
||||
url.searchParams.delete("startIndex");
|
||||
url.searchParams.append("startIndex", startIndex)
|
||||
url.searchParams.append("startIndex", startIndex);
|
||||
} else {
|
||||
memberSize = 0;
|
||||
reject(fetchedGroups.status + ": " + fetchedGroups.statusText);
|
||||
return reject(fetchedGroups.status + ": " + fetchedGroups.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error findGroupById: " + error);
|
||||
logger('findWebexGroup', 'page fetch failed: ' + (error && error.message || error));
|
||||
return reject(error);
|
||||
}
|
||||
}
|
||||
console.log("Groups found: " + groups.length);
|
||||
resolve(groups);
|
||||
})
|
||||
}
|
||||
function whoAmI(bearerToken) {
|
||||
return new Promise(async function (resolve, reject) {
|
||||
logger("whoAmI", "Person is having an existential crisis of identify.")
|
||||
logger("whoAmI", "Person is having an existential crisis of identity.")
|
||||
var myHeaders = {
|
||||
"Authorization": "Bearer " + bearerToken,
|
||||
"Content-Type": "application/json"
|
||||
|
|
|
|||
Loading…
Reference in a new issue