Skip to content

Commit 2d57580

Browse files
committed
Limit per exchange polling via LRU cache.
1 parent 050ac63 commit 2d57580

File tree

3 files changed

+63
-33
lines changed

3 files changed

+63
-33
lines changed

lib/config.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,16 @@ config.ensureConfigOverride.fields.push(meterServiceName);
5151
// optional interaction config
5252
config.interactions = {
5353
enabled: false,
54-
// optional named workflows for interactions
54+
caches: {
55+
exchangePolling: {
56+
// each cache value is only a boolean (the key is ~64 bytes); one entry
57+
// per exchange being actively polled, 1M = ~60 MiB
58+
max: 1000000,
59+
// polling allowed no more than once per second by default
60+
ttl: 1000
61+
}
62+
},
63+
// named workflows for interactions
5564
/* Spec:
5665
{
5766
<workflow name>: {

lib/interactions.js

Lines changed: 51 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,48 +5,57 @@ import * as bedrock from '@bedrock/core';
55
import * as schemas from '../schemas/bedrock-profile-http.js';
66
import {asyncHandler} from '@bedrock/express';
77
import {ensureAuthenticated} from '@bedrock/passport';
8+
import {LRUCache} from 'lru-cache';
89
import {createValidateMiddleware as validate} from '@bedrock/validation';
910
import {ZCAP_CLIENT} from './zcapClient.js';
1011

1112
const {config, util: {BedrockError}} = bedrock;
1213

1314
let WORKFLOWS_BY_NAME_MAP;
1415
let WORKFLOWS_BY_ID_MAP;
16+
let EXCHANGE_POLLING_CACHE;
1517

1618
bedrock.events.on('bedrock.init', () => {
17-
const cfg = bedrock.config['profile-http'];
19+
const cfg = config['profile-http'];
1820

19-
// parse workflow configs when interactions are enabled
21+
// interactions feature is optional, return early if not enabled
2022
const {interactions} = cfg;
21-
if(interactions?.enabled) {
22-
const {workflows = {}} = interactions;
23-
WORKFLOWS_BY_NAME_MAP = new Map();
24-
WORKFLOWS_BY_ID_MAP = new Map();
25-
for(const workflowName in workflows) {
26-
const {localInteractionId, zcaps} = workflows[workflowName];
27-
if(!localInteractionId) {
28-
throw new TypeError(
29-
'"bedrock.config.profile-http.interactions.workflows" must each ' +
30-
'have "localInteractionId".');
31-
}
32-
const workflow = {
33-
localInteractionId,
34-
name: workflowName,
35-
zcaps: new Map()
36-
};
37-
for(const zcapName in zcaps) {
38-
const zcap = zcaps[zcapName];
39-
workflow.zcaps.set(zcapName, JSON.parse(zcap));
40-
}
41-
if(!workflow.zcaps.has('readWriteExchanges')) {
42-
throw new TypeError(
43-
'"bedrock.config.profile-http.interactions.workflows" must each ' +
44-
'have "zcaps.readWriteExchanges".');
45-
}
46-
WORKFLOWS_BY_NAME_MAP.set(workflowName, workflow);
47-
WORKFLOWS_BY_ID_MAP.set(localInteractionId, workflow);
23+
if(!interactions?.enabled) {
24+
return;
25+
}
26+
27+
// parse workflow configs when interactions are enabled
28+
const {workflows = {}} = interactions;
29+
WORKFLOWS_BY_NAME_MAP = new Map();
30+
WORKFLOWS_BY_ID_MAP = new Map();
31+
for(const workflowName in workflows) {
32+
const {localInteractionId, zcaps} = workflows[workflowName];
33+
if(!localInteractionId) {
34+
throw new TypeError(
35+
'"bedrock.config.profile-http.interactions.workflows" must each ' +
36+
'have "localInteractionId".');
37+
}
38+
const workflow = {
39+
localInteractionId,
40+
name: workflowName,
41+
zcaps: new Map()
42+
};
43+
for(const zcapName in zcaps) {
44+
const zcap = zcaps[zcapName];
45+
workflow.zcaps.set(zcapName, JSON.parse(zcap));
46+
}
47+
if(!workflow.zcaps.has('readWriteExchanges')) {
48+
throw new TypeError(
49+
'"bedrock.config.profile-http.interactions.workflows" must each ' +
50+
'have "zcaps.readWriteExchanges".');
4851
}
52+
WORKFLOWS_BY_NAME_MAP.set(workflowName, workflow);
53+
WORKFLOWS_BY_ID_MAP.set(localInteractionId, workflow);
4954
}
55+
56+
// setup caches
57+
const {caches} = interactions;
58+
EXCHANGE_POLLING_CACHE = new LRUCache(caches.exchangePolling);
5059
});
5160

5261
bedrock.events.on('bedrock-express.configure.routes', app => {
@@ -64,6 +73,8 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
6473
interaction: `${interactionsPath}/:localInteractionId/:localExchangeId`
6574
};
6675

76+
const retryAfter = Math.ceil(EXCHANGE_POLLING_CACHE.ttl / 1000);
77+
6778
// create an interaction to exchange VCs
6879
app.post(
6980
routes.interactions,
@@ -125,8 +136,14 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
125136
});
126137
}
127138

128-
// FIXME: use in-memory cache to return exchange state if it was
129-
// polled recently
139+
// check if entry is in exchange polling cache, if so, return
140+
// "too many requests" error response
141+
const key = `${localExchangeId}/${localExchangeId}`;
142+
if(EXCHANGE_POLLING_CACHE.get(key)) {
143+
res.set('retry-after', retryAfter);
144+
res.status(429).json({retryAfter});
145+
return;
146+
}
130147

131148
// fetch exchange
132149
const capability = workflow.zcaps.get('readWriteExchanges');
@@ -135,6 +152,9 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
135152
capability
136153
});
137154

155+
// prevent more polling on the same exchange for another second
156+
EXCHANGE_POLLING_CACHE.set(key, true);
157+
138158
// ensure `accountId` matches exchange variables
139159
const {exchange: {state, variables}} = response;
140160
if(variables.accountId !== accountId) {

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
"homepage": "https://github.com/digitalbazaar/bedrock-profile-http",
3131
"dependencies": {
3232
"@digitalbazaar/ed25519-signature-2020": "^5.2.0",
33-
"@digitalbazaar/ezcap": "^4.0.0"
33+
"@digitalbazaar/ezcap": "^4.0.0",
34+
"lru-cache": "^10.2.2"
3435
},
3536
"peerDependencies": {
3637
"@bedrock/app-identity": "^4.0.0",

0 commit comments

Comments
 (0)