@cap-js-community/event-queue 1.4.5 → 1.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/event-queue",
3
- "version": "1.4.5",
3
+ "version": "1.4.7",
4
4
  "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.",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -44,16 +44,16 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@sap/xssec": "^3.6.1",
47
- "redis": "^4.6.13",
47
+ "redis": "^4.6.14",
48
48
  "verror": "^1.10.1",
49
- "yaml": "^2.4.1"
49
+ "yaml": "^2.4.2"
50
50
  },
51
51
  "devDependencies": {
52
- "@cap-js/hana": "^0.1.0",
53
- "@cap-js/sqlite": "^1.5.0",
54
- "@sap/cds": "^7.8.0",
52
+ "@cap-js/hana": "^0.4.0",
53
+ "@cap-js/sqlite": "^1.7.1",
54
+ "@sap/cds": "^7.9.2",
55
55
  "@sap/cds-dk": "^7.8.0",
56
- "eslint": "^8.56.0",
56
+ "eslint": "^8.57.0",
57
57
  "eslint-config-prettier": "^9.1.0",
58
58
  "eslint-plugin-jest": "^27.9.0",
59
59
  "eslint-plugin-node": "^11.1.0",
package/src/config.js CHANGED
@@ -10,7 +10,8 @@ const { Priorities } = require("./constants");
10
10
  const FOR_UPDATE_TIMEOUT = 10;
11
11
  const GLOBAL_TX_TIMEOUT = 30 * 60 * 1000;
12
12
  const REDIS_CONFIG_CHANNEL = "EVENT_QUEUE_CONFIG_CHANNEL";
13
- const REDIS_CONFIG_BLOCKLIST_CHANNEL = "REDIS_CONFIG_BLOCKLIST_CHANNEL";
13
+ const REDIS_OFFBOARD_TENANT_CHANNEL = "REDIS_OFFBOARD_TENANT_CHANNEL";
14
+ const REDIS_CONFIG_BLOCKLIST_CHANNEL = "EVENT_QUEUE_REDIS_CONFIG_BLOCKLIST_CHANNEL";
14
15
  const COMPONENT_NAME = "/eventQueue/config";
15
16
  const MIN_INTERVAL_SEC = 10;
16
17
  const DEFAULT_LOAD = 1;
@@ -19,8 +20,8 @@ const SUFFIX_PERIODIC = "_PERIODIC";
19
20
  const COMMAND_BLOCK = "EVENT_QUEUE_EVENT_BLOCK";
20
21
  const COMMAND_UNBLOCK = "EVENT_QUEUE_EVENT_UNBLOCK";
21
22
  const CAP_EVENT_TYPE = "CAP_OUTBOX";
22
-
23
23
  const CAP_PARALLEL_DEFAULT = 5;
24
+ const DELETE_TENANT_BLOCK_AFTER_MS = 5 * 60 * 1000;
24
25
 
25
26
  const BASE_PERIODIC_EVENTS = [
26
27
  {
@@ -67,6 +68,8 @@ class Config {
67
68
  #cleanupLocksAndEventsForDev;
68
69
  #redisOptions;
69
70
  #insertEventsBeforeCommit;
71
+ #unsubscribeHandlers = [];
72
+ #unsubscribedTenants = {};
70
73
  static #instance;
71
74
  constructor() {
72
75
  this.#logger = cds.log(COMPONENT_NAME);
@@ -126,6 +129,51 @@ class Config {
126
129
  });
127
130
  }
128
131
 
132
+ attachRedisUnsubscribeHandler() {
133
+ this.#logger.info("attached redis handle for unsubscribe events");
134
+ redis.subscribeRedisChannel(this.#redisOptions, REDIS_OFFBOARD_TENANT_CHANNEL, (messageData) => {
135
+ try {
136
+ const { tenantId } = JSON.parse(messageData);
137
+ this.#logger.info("received unsubscribe broadcast event", { tenantId });
138
+ this.executeUnsubscribeHandlers(tenantId);
139
+ } catch (err) {
140
+ this.#logger.error("could not parse unsubscribe broadcast event", err, {
141
+ messageData,
142
+ });
143
+ }
144
+ });
145
+ }
146
+
147
+ executeUnsubscribeHandlers(tenantId) {
148
+ this.#unsubscribedTenants[tenantId] = true;
149
+ setTimeout(() => delete this.#unsubscribedTenants[tenantId], DELETE_TENANT_BLOCK_AFTER_MS);
150
+ for (const unsubscribeHandler of this.#unsubscribeHandlers) {
151
+ try {
152
+ unsubscribeHandler(tenantId);
153
+ } catch (err) {
154
+ this.#logger.error("could executing unsubscribe handler", err, {
155
+ tenantId,
156
+ });
157
+ }
158
+ }
159
+ }
160
+
161
+ handleUnsubscribe(tenantId) {
162
+ if (this.redisEnabled) {
163
+ redis
164
+ .publishMessage(this.#redisOptions, REDIS_OFFBOARD_TENANT_CHANNEL, JSON.stringify({ tenantId }))
165
+ .catch((error) => {
166
+ this.#logger.error(`publishing tenant unsubscribe failed. tenantId: ${tenantId}`, error);
167
+ });
168
+ } else {
169
+ this.executeUnsubscribeHandlers(tenantId);
170
+ }
171
+ }
172
+
173
+ attachUnsubscribeHandler(cb) {
174
+ this.#unsubscribeHandlers.push(cb);
175
+ }
176
+
129
177
  publishConfigChange(key, value) {
130
178
  if (!this.redisEnabled) {
131
179
  this.#logger.info("redis not connected, config change won't be published", { key, value });
@@ -320,6 +368,10 @@ class Config {
320
368
  delete this.#eventMap[this.generateKey(type, subType)];
321
369
  }
322
370
 
371
+ isTenantUnsubscribed(tenantId) {
372
+ return this.#unsubscribedTenants[tenantId];
373
+ }
374
+
323
375
  get fileContent() {
324
376
  return this.#config;
325
377
  }
package/src/dbHandler.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const cds = require("@sap/cds");
4
4
 
5
- const { broadcastEvent } = require("./redis/redisPub");
5
+ const redisPub = require("./redis/redisPub");
6
6
  const config = require("./config");
7
7
 
8
8
  const COMPONENT_NAME = "/eventQueue/dbHandler";
@@ -25,7 +25,7 @@ const registerEventQueueDbHandler = (dbService) => {
25
25
  req.tx._ = req.tx._ ?? {};
26
26
  req.tx._.eventQueuePublishEvents = req.tx._.eventQueuePublishEvents ?? {};
27
27
  const eventQueuePublishEvents = req.tx._.eventQueuePublishEvents;
28
- const data = Array.isArray(req.data) ? req.data : [req.data];
28
+ const data = Array.isArray(req.query.INSERT.entries) ? req.query.INSERT.entries : [req.query.INSERT.entries];
29
29
  const eventCombinations = Object.keys(
30
30
  data.reduce((result, event) => {
31
31
  const key = [event.type, event.subType].join("##");
@@ -45,7 +45,7 @@ const registerEventQueueDbHandler = (dbService) => {
45
45
  return { type, subType };
46
46
  });
47
47
 
48
- broadcastEvent(req.tenant, events).catch((err) => {
48
+ redisPub.broadcastEvent(req.tenant, events).catch((err) => {
49
49
  cds.log(COMPONENT_NAME).error("db handler failure during broadcasting event", err, {
50
50
  tenant: req.tenant,
51
51
  events,
package/src/index.d.ts CHANGED
@@ -136,3 +136,75 @@ export function processEventQueue(
136
136
  eventSubType: string,
137
137
  startTime: Date
138
138
  ): Promise<any>;
139
+
140
+ declare class Config {
141
+ constructor();
142
+
143
+ getEventConfig(type: string, subType: string): any;
144
+ isCapOutboxEvent(type: string): boolean;
145
+ hasEventAfterCommitFlag(type: string, subType: string): boolean;
146
+ _checkRedisIsBound(): boolean;
147
+ checkRedisEnabled(): boolean;
148
+ attachConfigChangeHandler(): void;
149
+ attachRedisUnsubscribeHandler(): void;
150
+ executeUnsubscribeHandlers(tenantId: string): void;
151
+ handleUnsubscribe(tenantId: string): void;
152
+ attachUnsubscribeHandler(cb: Function): void;
153
+ publishConfigChange(key: string, value: any): void;
154
+ blockEvent(type: string, subType: string, isPeriodic: boolean, tenant?: string): void;
155
+ clearPeriodicEventBlockList(): void;
156
+ unblockEvent(type: string, subType: string, isPeriodic: boolean, tenant?: string): void;
157
+ addCAPOutboxEvent(serviceName: string, config: any): void;
158
+ isEventBlocked(type: string, subType: string, isPeriodicEvent: boolean, tenant: string): boolean;
159
+ get isEventQueueActive(): boolean;
160
+ set isEventQueueActive(value: boolean);
161
+ set fileContent(config: any);
162
+ get fileContent(): any;
163
+ get events(): any[];
164
+ get periodicEvents(): any[];
165
+ isPeriodicEvent(type: string, subType: string): boolean;
166
+ get allEvents(): any[];
167
+ get forUpdateTimeout(): number;
168
+ get globalTxTimeout(): number;
169
+ set forUpdateTimeout(value: number);
170
+ set globalTxTimeout(value: number);
171
+ get runInterval(): number | null;
172
+ set runInterval(value: number);
173
+ get redisEnabled(): boolean | null;
174
+ set redisEnabled(value: boolean | null);
175
+ get initialized(): boolean;
176
+ set initialized(value: boolean);
177
+ get instanceLoadLimit(): number;
178
+ set instanceLoadLimit(value: number);
179
+ get isEventBlockedCb(): any;
180
+ set isEventBlockedCb(value: any);
181
+ get tableNameEventQueue(): string;
182
+ get tableNameEventLock(): string;
183
+ set configFilePath(value: string | null);
184
+ get configFilePath(): string | null;
185
+ set processEventsAfterPublish(value: any);
186
+ get processEventsAfterPublish(): any;
187
+ set skipCsnCheck(value: any);
188
+ get skipCsnCheck(): any;
189
+ set disableRedis(value: any);
190
+ get disableRedis(): any;
191
+ set updatePeriodicEvents(value: any);
192
+ get updatePeriodicEvents(): any;
193
+ set registerAsEventProcessor(value: any);
194
+ get registerAsEventProcessor(): any;
195
+ set thresholdLoggingEventProcessing(value: any);
196
+ get thresholdLoggingEventProcessing(): any;
197
+ set useAsCAPOutbox(value: any);
198
+ get useAsCAPOutbox(): any;
199
+ set userId(value: any);
200
+ get userId(): any;
201
+ set cleanupLocksAndEventsForDev(value: any);
202
+ get cleanupLocksAndEventsForDev(): any;
203
+ set redisOptions(value: any);
204
+ get redisOptions(): any;
205
+ set insertEventsBeforeCommit(value: any);
206
+ get insertEventsBeforeCommit(): any;
207
+ get isMultiTenancy(): boolean;
208
+ }
209
+
210
+ export const config: Config;
package/src/initialize.js CHANGED
@@ -128,6 +128,24 @@ const readConfigFromFile = async (configFilepath) => {
128
128
  };
129
129
 
130
130
  const registerEventProcessors = () => {
131
+ cds.on("listening", () => {
132
+ cds.connect
133
+ .to("cds.xt.DeploymentService")
134
+ .then((ds) => {
135
+ cds.log(COMPONENT).info("event-queue unsubscribe handler registered", {
136
+ redisEnabled: config.redisEnabled,
137
+ });
138
+ ds.after("unsubscribe", async (_, req) => {
139
+ const { tenant } = req.data;
140
+ config.handleUnsubscribe(tenant);
141
+ });
142
+ })
143
+ .catch(
144
+ () => {} // ignore errors as the DeploymentService is most of the time only available in the mtx sidecar
145
+ );
146
+ });
147
+ config.redisEnabled && config.attachRedisUnsubscribeHandler();
148
+
131
149
  if (!config.registerAsEventProcessor) {
132
150
  return;
133
151
  }
@@ -268,10 +268,15 @@ const _checkEventIsBlocked = async (baseInstance) => {
268
268
  );
269
269
  }
270
270
 
271
+ if (!eventBlocked) {
272
+ eventBlocked = config.isTenantUnsubscribed(baseInstance.context.tenant);
273
+ }
274
+
271
275
  if (eventBlocked) {
272
276
  baseInstance.logger.info("skipping run because event is blocked by configuration", {
273
277
  type: baseInstance.eventType,
274
278
  subType: baseInstance.eventSubType,
279
+ tenantUnsubscribed: config.isTenantUnsubscribed(baseInstance.context.tenant),
275
280
  });
276
281
  }
277
282
  return eventBlocked;
@@ -10,7 +10,10 @@ const COMPONENT_NAME = "/eventQueue/shared/eventScheduler";
10
10
  let instance;
11
11
  class EventScheduler {
12
12
  #scheduledEvents = {};
13
- constructor() {}
13
+ #eventsByTenants = {};
14
+ constructor() {
15
+ config.attachUnsubscribeHandler(this.clearForTenant.bind(this));
16
+ }
14
17
 
15
18
  scheduleEvent(tenantId, type, subType, startAfter) {
16
19
  const { date, relative } = this.calculateOffset(type, subType, startAfter);
@@ -37,6 +40,8 @@ class EventScheduler {
37
40
  }, relative).unref();
38
41
  }
39
42
 
43
+ clearForTenant() {}
44
+
40
45
  calculateOffset(type, subType, startAfter) {
41
46
  const eventConfig = config.getEventConfig(type, subType);
42
47
  const scheduleWithoutDelay = config.isPeriodicEvent(type, subType) && eventConfig.interval < 30 * 1000;