Skip to content

Commit aa0441f

Browse files
authored
Disable suspended tenants (#379)
* add authInfo to align with CDS * upgrade cds * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip
1 parent b09953b commit aa0441f

File tree

10 files changed

+61
-25
lines changed

10 files changed

+61
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1010
### Added
1111

1212
- Added `authInfo` to cds.User as CDS 9.3 deprecated `tokenInfo`.
13+
- Disable the automatic processing of suspended tenants. Can be turned off using `disableProcessingOfSuspendedTenants`.
1314

1415
## v1.10.10 - 2025-07-09
1516

docs/setup/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ The table includes the parameter name, a description of its purpose, and the def
8484
| redisNamespace | Prefix applied to all Redis interactions. Use this when multiple microservices share a Redis instance to prevent naming conflicts. | null | no |
8585
| redisOptions | The option is provided to customize settings when creating Redis clients. The object is spread at the root level for creating a client and within the `default` options for cluster clients. | {} | no |
8686
| crashOnRedisUnavailable | If enabled, the application will crash if Redis is unavailable during the connection check. | false | false |
87+
| disableProcessingOfSuspendedTenants | Disables the processing of suspended tenants. A tenant is considered suspended if XSUAA returns `404` for the given tenant. | true | true |
8788

8889
# Configure Redis
8990

package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cap-js-community/event-queue",
3-
"version": "1.10.11",
3+
"version": "1.11.0-beta.4",
44
"description": "An event queue that enables secure transactional processing of asynchronous and periodic events, featuring instant event processing with Redis Pub/Sub and load distribution across all application instances.",
55
"main": "src/index.js",
66
"types": "src/index.d.ts",

src/config.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ class Config {
119119
#configEvents;
120120
#configPeriodicEvents;
121121
#enableAdminService;
122+
#disableProcessingOfSuspendedTenants;
122123
static #instance;
123124
constructor() {
124125
this.#logger = cds.log(COMPONENT_NAME);
@@ -984,6 +985,14 @@ class Config {
984985
this.#enableAdminService = value;
985986
}
986987

988+
get disableProcessingOfSuspendedTenants() {
989+
return this.#disableProcessingOfSuspendedTenants;
990+
}
991+
992+
set disableProcessingOfSuspendedTenants(value) {
993+
this.#disableProcessingOfSuspendedTenants = value;
994+
}
995+
987996
/**
988997
@return { Config }
989998
**/

src/initialize.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const CONFIG_VARS = [
4747
["publishEventBlockList", true],
4848
["crashOnRedisUnavailable", false],
4949
["enableAdminService", false],
50+
["disableProcessingOfSuspendedTenants", true],
5051
];
5152

5253
/**

src/shared/cdsHelper.js

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ const cds = require("@sap/cds");
66
const config = require("../config");
77
const common = require("./common");
88
const { TenantIdCheckTypes } = require("../constants");
9+
const { limiter } = require("./common");
910

1011
const VERROR_CLUSTER_NAME = "ExecuteInNewTransactionError";
1112
const COMPONENT_NAME = "/eventQueue/cdsHelper";
1213

14+
const CONCURRENCY_AUTH_INFO = 3;
15+
1316
/**
1417
* Execute logic in a new managed CDS transaction context, auto-handling commit, rollback and error/exception situations.
1518
* Includes logging of start, end and error situation with additional info object and unique transaction id (txId)
@@ -140,15 +143,26 @@ const getAllTenantIds = async () => {
140143
return null;
141144
}
142145

143-
return response
144-
.map((tenant) => tenant.subscribedTenantId ?? tenant.tenant)
145-
.reduce(async (result, tenantId) => {
146-
result = await result;
147-
if (await common.isTenantIdValidCb(TenantIdCheckTypes.eventProcessing, tenantId)) {
148-
result.push(tenantId);
146+
const tenantIds = response.map((tenant) => tenant.subscribedTenantId ?? tenant.tenant);
147+
const suspendedTenants = {};
148+
if (config.disableProcessingOfSuspendedTenants) {
149+
await limiter(CONCURRENCY_AUTH_INFO, tenantIds, async (tenantId) => {
150+
const result = await common.getAuthContext(tenantId, { returnError: true });
151+
// NOTE: only 404 errors are propagated all others are ignored
152+
if (result?.[0]) {
153+
suspendedTenants[tenantId] = true;
154+
cds.log(COMPONENT_NAME).info("skip event-queue processing, tenant suspended", { tenantId });
149155
}
150-
return result;
151-
}, []);
156+
});
157+
}
158+
159+
return tenantIds.reduce(async (result, tenantId) => {
160+
result = await result;
161+
if (!suspendedTenants[tenantId] && (await common.isTenantIdValidCb(TenantIdCheckTypes.eventProcessing, tenantId))) {
162+
result.push(tenantId);
163+
}
164+
return result;
165+
}, []);
152166
};
153167

154168
const TENANT_COLUMNS = ["subscribedSubdomain", "createdAt", "modifiedAt"];

src/shared/common.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ const config = require("../config");
1010
const { ExpiringLazyCache } = require("./lazyCache");
1111
const { TenantIdCheckTypes } = require("../constants");
1212

13-
const MARGIN_AUTH_INFO_EXPIRY = 60 * 1000;
13+
const EXPIRE_TIME_TENANT_404 = 10 * 60 * 1000; // 10 minutes
14+
1415
const COMPONENT_NAME = "/eventQueue/common";
1516

1617
const arrayToFlatMap = (array, key = "ID") => {
@@ -97,18 +98,22 @@ const _getNewAuthContext = async (tenantId) => {
9798
const token = await authService.fetchClientCredentialsToken({ zid: tenantId });
9899
const tokenInfo = new xssec.XsuaaToken(token.access_token);
99100
const authInfo = new xssec.XsuaaSecurityContext(authService, tokenInfo);
100-
return [tokenInfo.getExpirationDate().getTime() - Date.now(), authInfo];
101+
return [tokenInfo.getExpirationDate().getTime() - Date.now(), [null, authInfo]];
101102
} catch (err) {
102103
cds.log(COMPONENT_NAME).warn("failed to request authContext", {
103104
err: err.message,
104105
responseCode: err.responseCode,
105106
responseText: err.responseText,
106107
});
108+
109+
if (err.responseCode === 404) {
110+
return [EXPIRE_TIME_TENANT_404, [err, null]];
111+
}
107112
return [0, null];
108113
}
109114
};
110115

111-
const getAuthContext = async (tenantId) => {
116+
const getAuthContext = async (tenantId, { returnError = false } = {}) => {
112117
if (!(await isTenantIdValidCb(TenantIdCheckTypes.getAuthContext, tenantId))) {
113118
return null;
114119
}
@@ -125,8 +130,13 @@ const getAuthContext = async (tenantId) => {
125130
return null;
126131
}
127132

128-
getAuthContext._cache = getAuthContext._cache ?? new ExpiringLazyCache({ expirationGap: MARGIN_AUTH_INFO_EXPIRY });
129-
return await getAuthContext._cache.getSetCb(tenantId, async () => _getNewAuthContext(tenantId));
133+
getAuthContext._cache = getAuthContext._cache ?? new ExpiringLazyCache();
134+
const result = await getAuthContext._cache.getSetCb(tenantId, async () => _getNewAuthContext(tenantId));
135+
if (returnError) {
136+
return result;
137+
} else {
138+
return result?.[1];
139+
}
130140
};
131141

132142
const isTenantIdValidCb = async (checkType, tenantId) => {

src/shared/lazyCache.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use strict";
22

33
const DEFAULT_SEPARATOR = "##";
4-
const DEFAULT_EXPIRATION_GAP = 5000; // 5 seconds
4+
const DEFAULT_EXPIRATION_GAP = 60 * 1000; // 60 seconds
55

66
class LazyCache {
77
constructor({ separator = DEFAULT_SEPARATOR } = {}) {

test/common.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,14 +140,14 @@ describe("getAuthContext", () => {
140140
it("should handle error", async () => {
141141
jest.spyOn(xssec.XsuaaService.prototype, "fetchClientCredentialsToken").mockRejectedValueOnce(new Error());
142142
const result = await getAuthContext(tenantId1);
143-
expect(result).toBeNull();
143+
expect(result).toBeUndefined();
144144
expect(cds.log().warn.mock.calls).toMatchSnapshot();
145145
});
146146

147147
it("should clear cache for error case", async () => {
148148
jest.spyOn(xssec.XsuaaService.prototype, "fetchClientCredentialsToken").mockRejectedValueOnce(new Error());
149149
const result = await getAuthContext(tenantId1);
150-
expect(result).toBeNull();
150+
expect(result).toBeUndefined();
151151
expect(cds.log().warn.mock.calls).toMatchSnapshot();
152152

153153
jest

0 commit comments

Comments
 (0)