@cap-js-community/event-queue 0.1.49
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/LICENSE +201 -0
- package/README.md +160 -0
- package/cds-plugin.js +11 -0
- package/db/Event.cds +24 -0
- package/db/Lock.cds +10 -0
- package/db/index.cds +2 -0
- package/index.cds +1 -0
- package/package.json +55 -0
- package/src/EventQueueError.js +141 -0
- package/src/EventQueueProcessorBase.js +922 -0
- package/src/config.js +196 -0
- package/src/constants.js +11 -0
- package/src/dbHandler.js +40 -0
- package/src/index.js +20 -0
- package/src/initialize.js +216 -0
- package/src/processEventQueue.js +297 -0
- package/src/publishEvent.js +25 -0
- package/src/redisPubSub.js +143 -0
- package/src/runner.js +237 -0
- package/src/shared/PerformanceTracer.js +74 -0
- package/src/shared/WorkerQueue.js +78 -0
- package/src/shared/cdsHelper.js +136 -0
- package/src/shared/common.js +115 -0
- package/src/shared/distributedLock.js +191 -0
- package/src/shared/env.js +9 -0
- package/src/shared/redis.js +70 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const CUSTOM_FIELD_EXECUTION_TIME = "response_time_ms";
|
|
4
|
+
const CUSTOM_FIELD_QUANTITY = "quantity";
|
|
5
|
+
|
|
6
|
+
class PerformanceTracer {
|
|
7
|
+
/**
|
|
8
|
+
* The constructor of the performance tracer
|
|
9
|
+
* @param {Object} logger logger
|
|
10
|
+
* @param {string} name name of the performance trace
|
|
11
|
+
* @param {Object} options An options object with additional properties (optional)
|
|
12
|
+
* @param {string} options.level The level to be used for logging
|
|
13
|
+
* @param {string} options.summary The summary to be used for logging
|
|
14
|
+
* @param {Object} options.startMessage Writes a log record with the provided details when starting the action
|
|
15
|
+
*/
|
|
16
|
+
constructor(logger, name, options = {}) {
|
|
17
|
+
this.__start = new Date();
|
|
18
|
+
this.__logger = logger;
|
|
19
|
+
this.__name = name;
|
|
20
|
+
options.startMessage &&
|
|
21
|
+
logger.info("Performance measurement started", {
|
|
22
|
+
name: name,
|
|
23
|
+
...options.startMessage,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Ends the performance trace
|
|
29
|
+
* @param {Object} options An options object with additional properties (optional)
|
|
30
|
+
* @param {Number} options.quantity A case-specific quantity such as a node count that influences the execution time (optional)
|
|
31
|
+
* @param {Number} options.threshold Only write the log above verbose level, if threshold in ms. is met (optional)
|
|
32
|
+
* @param {Number} options.additionalQuantityThreshold Value multiplied with quantity and added to the threshold (optional)
|
|
33
|
+
*/
|
|
34
|
+
endPerformanceTrace(...args) {
|
|
35
|
+
let options = {};
|
|
36
|
+
//determine, if an options object was provided as first argument
|
|
37
|
+
if (
|
|
38
|
+
typeof args?.[0] === "object" &&
|
|
39
|
+
(args[0].quantity >= 0 ||
|
|
40
|
+
args[0].threshold > 0 ||
|
|
41
|
+
args[0].additionalQuantityThreshold > 0)
|
|
42
|
+
) {
|
|
43
|
+
options = args.shift();
|
|
44
|
+
}
|
|
45
|
+
const currentTime = new Date();
|
|
46
|
+
const executionTime = currentTime - this.__start;
|
|
47
|
+
this.__start = currentTime;
|
|
48
|
+
const isBelowThreshold =
|
|
49
|
+
options &&
|
|
50
|
+
options.threshold &&
|
|
51
|
+
executionTime <
|
|
52
|
+
options.threshold +
|
|
53
|
+
(options.additionalQuantityThreshold > 0 && options.quantity > 0
|
|
54
|
+
? options.additionalQuantityThreshold * options.quantity
|
|
55
|
+
: 0);
|
|
56
|
+
|
|
57
|
+
if (isBelowThreshold) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const customFields = {
|
|
62
|
+
[CUSTOM_FIELD_EXECUTION_TIME]: executionTime,
|
|
63
|
+
[CUSTOM_FIELD_QUANTITY]: options.quantity,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
this.__logger.info("Performance measurement executed", {
|
|
67
|
+
name: this.__name,
|
|
68
|
+
milliseconds: executionTime,
|
|
69
|
+
customFields,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = PerformanceTracer;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const cds = require("@sap/cds");
|
|
4
|
+
|
|
5
|
+
const { getConfigInstance } = require("../config");
|
|
6
|
+
|
|
7
|
+
const COMPONENT_NAME = "eventQueue/WorkerQueue";
|
|
8
|
+
|
|
9
|
+
let instance = null;
|
|
10
|
+
|
|
11
|
+
class WorkerQueue {
|
|
12
|
+
constructor(concurrency) {
|
|
13
|
+
if (Number.isNaN(concurrency) || concurrency <= 0) {
|
|
14
|
+
this.__concurrencyLimit = 1;
|
|
15
|
+
} else {
|
|
16
|
+
this.__concurrencyLimit = concurrency;
|
|
17
|
+
}
|
|
18
|
+
this.__runningPromises = [];
|
|
19
|
+
this.__queue = [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
addToQueue(cb) {
|
|
23
|
+
const p = new Promise((resolve, reject) => {
|
|
24
|
+
this.__queue.push([cb, resolve, reject]);
|
|
25
|
+
});
|
|
26
|
+
this._checkForNext();
|
|
27
|
+
return p;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_executeFunction(cb, resolve, reject) {
|
|
31
|
+
const promise = Promise.resolve().then(() => cb());
|
|
32
|
+
this.__runningPromises.push(promise);
|
|
33
|
+
promise
|
|
34
|
+
.finally(() => {
|
|
35
|
+
this.__runningPromises.splice(
|
|
36
|
+
this.__runningPromises.indexOf(promise),
|
|
37
|
+
1
|
|
38
|
+
);
|
|
39
|
+
this._checkForNext();
|
|
40
|
+
})
|
|
41
|
+
.then((...results) => {
|
|
42
|
+
resolve(...results);
|
|
43
|
+
})
|
|
44
|
+
.catch((err) => {
|
|
45
|
+
cds
|
|
46
|
+
.log(COMPONENT_NAME)
|
|
47
|
+
.error(
|
|
48
|
+
"Error happened in WorkQueue. Errors should be caught before! Error:",
|
|
49
|
+
err
|
|
50
|
+
);
|
|
51
|
+
reject(err);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_checkForNext() {
|
|
56
|
+
if (
|
|
57
|
+
!this.__queue.length ||
|
|
58
|
+
this.__runningPromises.length >= this.__concurrencyLimit
|
|
59
|
+
) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const [cb, resolve, reject] = this.__queue.shift();
|
|
63
|
+
this._executeFunction(cb, resolve, reject);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
getWorkerPoolInstance: () => {
|
|
69
|
+
if (!instance) {
|
|
70
|
+
const configInstance = getConfigInstance();
|
|
71
|
+
instance = new WorkerQueue(configInstance.parallelTenantProcessing);
|
|
72
|
+
}
|
|
73
|
+
return instance;
|
|
74
|
+
},
|
|
75
|
+
_: {
|
|
76
|
+
WorkerQueue,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const VError = require("verror");
|
|
4
|
+
const cds = require("@sap/cds");
|
|
5
|
+
|
|
6
|
+
const config = require("../config");
|
|
7
|
+
|
|
8
|
+
const subdomainCache = {};
|
|
9
|
+
|
|
10
|
+
const VERROR_CLUSTER_NAME = "ExecuteInNewTransactionError";
|
|
11
|
+
const COMPONENT_NAME = "eventQueue/cdsHelper";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Execute logic in a new managed CDS transaction context, auto-handling commit, rollback and error/exception situations.
|
|
15
|
+
* Includes logging of start, end and error situation with additional info object and unique transaction id (txId)
|
|
16
|
+
* @param context {object} Current CDS request context
|
|
17
|
+
* @param transactionTag A tag identifying the transaction
|
|
18
|
+
* @param fn {function} Callback function (logic) to be executed in context of new managed CDS transaction
|
|
19
|
+
* @param args {array|object|any} Array of function arguments passed to callback function as spread arguments. Object or primitive types are auto-normalized to array.
|
|
20
|
+
* @param info {object} Additional information object attached to logging
|
|
21
|
+
* @returns {Promise<boolean>} Promise resolving to true if everything worked fine / false if an error occurred
|
|
22
|
+
*/
|
|
23
|
+
async function executeInNewTransaction(
|
|
24
|
+
context = {},
|
|
25
|
+
transactionTag,
|
|
26
|
+
fn,
|
|
27
|
+
args,
|
|
28
|
+
{ info = {} } = {}
|
|
29
|
+
) {
|
|
30
|
+
const parameters = Array.isArray(args) ? args : [args];
|
|
31
|
+
const logger = cds.log(COMPONENT_NAME);
|
|
32
|
+
try {
|
|
33
|
+
if (cds.db.kind === "hana") {
|
|
34
|
+
await cds.tx(
|
|
35
|
+
{
|
|
36
|
+
id: context.id,
|
|
37
|
+
tenant: context.tenant,
|
|
38
|
+
locale: context.locale,
|
|
39
|
+
user: context.user,
|
|
40
|
+
headers: context.headers,
|
|
41
|
+
http: context.http,
|
|
42
|
+
},
|
|
43
|
+
async (tx) => {
|
|
44
|
+
tx.context._ = context._ ?? {};
|
|
45
|
+
return await fn(tx, ...parameters);
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
} else {
|
|
49
|
+
const contextTx = cds.tx(context);
|
|
50
|
+
const contextTxState = contextTx.ready;
|
|
51
|
+
if (
|
|
52
|
+
!contextTxState ||
|
|
53
|
+
["committed", "rolled back"].includes(contextTxState)
|
|
54
|
+
) {
|
|
55
|
+
await cds.tx(
|
|
56
|
+
{
|
|
57
|
+
id: context.id,
|
|
58
|
+
tenant: context.tenant,
|
|
59
|
+
locale: context.locale,
|
|
60
|
+
user: context.user,
|
|
61
|
+
headers: context.headers,
|
|
62
|
+
http: context.http,
|
|
63
|
+
},
|
|
64
|
+
async (tx) => fn(tx, ...parameters)
|
|
65
|
+
);
|
|
66
|
+
} else {
|
|
67
|
+
await fn(contextTx, ...parameters);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (!(err instanceof TriggerRollback)) {
|
|
72
|
+
if (err instanceof VError) {
|
|
73
|
+
Object.assign(err.jse_info, {
|
|
74
|
+
newTx: info,
|
|
75
|
+
});
|
|
76
|
+
throw err;
|
|
77
|
+
} else {
|
|
78
|
+
throw new VError(
|
|
79
|
+
{
|
|
80
|
+
name: VERROR_CLUSTER_NAME,
|
|
81
|
+
cause: err,
|
|
82
|
+
info,
|
|
83
|
+
},
|
|
84
|
+
"Execution in new transaction failed"
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
} finally {
|
|
90
|
+
logger.debug("Execution in new transaction finished", info);
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Error class to be used to force rollback in executionInNewTransaction
|
|
97
|
+
* Error will not be logged, as it assumes that error handling has been done before...
|
|
98
|
+
*/
|
|
99
|
+
class TriggerRollback extends VError {
|
|
100
|
+
constructor() {
|
|
101
|
+
super("Rollback triggered");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const getSubdomainForTenantId = async (tenantId) => {
|
|
106
|
+
if (!config.getConfigInstance().isMultiTenancy) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
if (subdomainCache[tenantId]) {
|
|
110
|
+
return subdomainCache[tenantId];
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const ssp = await cds.connect.to("cds.xt.SaasProvisioningService");
|
|
114
|
+
const response = await ssp.get("/tenant", { subscribedTenantId: tenantId });
|
|
115
|
+
subdomainCache[tenantId] = response.subscribedSubdomain;
|
|
116
|
+
return response.subscribedSubdomain;
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const getAllTenantIds = async () => {
|
|
123
|
+
if (!config.getConfigInstance().isMultiTenancy) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const ssp = await cds.connect.to("cds.xt.SaasProvisioningService");
|
|
127
|
+
const response = await ssp.get("/tenant");
|
|
128
|
+
return response.map((tenant) => tenant.subscribedTenantId ?? tenant.tenant);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
executeInNewTransaction,
|
|
133
|
+
TriggerRollback,
|
|
134
|
+
getSubdomainForTenantId,
|
|
135
|
+
getAllTenantIds,
|
|
136
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { floor, abs, min } = Math;
|
|
4
|
+
|
|
5
|
+
const arrayToFlatMap = (array, key = "ID") => {
|
|
6
|
+
return array.reduce((result, element) => {
|
|
7
|
+
result[element[key]] = element;
|
|
8
|
+
return result;
|
|
9
|
+
}, {});
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Establish a "Funnel" instance to limit how much
|
|
14
|
+
* load can be processed in parallel. This is somewhat
|
|
15
|
+
* similar to the limiter function however it has some
|
|
16
|
+
* distinctintly different features. The Funnel will
|
|
17
|
+
* not know in advance which functions and how many
|
|
18
|
+
* loads it will have to process.
|
|
19
|
+
*/
|
|
20
|
+
class Funnel {
|
|
21
|
+
/**
|
|
22
|
+
* Create a funnel with specified capacity
|
|
23
|
+
* @param capacity - the capacity of the funnel (integer, sign will be ignored)
|
|
24
|
+
*/
|
|
25
|
+
constructor(capacity = 100) {
|
|
26
|
+
this.runningPromises = [];
|
|
27
|
+
this.capacity = floor(abs(capacity));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Asynchronously run a function that will put a specified load to the funnel.
|
|
32
|
+
* The total amount of load of all running functions shall not
|
|
33
|
+
* exceed the capacity of the funnel. If the desired load exceeds the capacity
|
|
34
|
+
* the funnel will wait until sufficient capacity is available.
|
|
35
|
+
* If a function requires a load >= capacity, then it will run
|
|
36
|
+
* exclusively.
|
|
37
|
+
* @param load - the load (integer, sign will be ignored)
|
|
38
|
+
* @param f
|
|
39
|
+
* @param args
|
|
40
|
+
* @return {Promise<unknown>}
|
|
41
|
+
*/
|
|
42
|
+
async run(load, f, ...args) {
|
|
43
|
+
load = min(floor(abs(load)), Number.MAX_SAFE_INTEGER);
|
|
44
|
+
|
|
45
|
+
// wait for sufficient capacity
|
|
46
|
+
while (this.capacity < load && this.runningPromises.length > 0) {
|
|
47
|
+
try {
|
|
48
|
+
await Promise.race(this.runningPromises);
|
|
49
|
+
} catch {
|
|
50
|
+
// Yes, we must ignore exceptions here. The
|
|
51
|
+
// caller expects exceptions from f and no
|
|
52
|
+
// exceptions from other workloads.
|
|
53
|
+
// Other exceptions must be handled by the
|
|
54
|
+
// other callers. See (*) below.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// map function call to promise
|
|
59
|
+
const p =
|
|
60
|
+
f.constructor.name === "AsyncFunction"
|
|
61
|
+
? f(...args)
|
|
62
|
+
: Promise.resolve().then(() => f(...args));
|
|
63
|
+
|
|
64
|
+
// create promise for book keeping
|
|
65
|
+
const workload = p.finally(() => {
|
|
66
|
+
// remove workload
|
|
67
|
+
this.runningPromises.splice(this.runningPromises.indexOf(workload), 1);
|
|
68
|
+
// and reclaim its capacity
|
|
69
|
+
this.capacity += load;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// claim the capacity and schedule workload
|
|
73
|
+
this.capacity -= load;
|
|
74
|
+
this.runningPromises.push(workload);
|
|
75
|
+
|
|
76
|
+
// make the caller wait for the workload
|
|
77
|
+
// this also establish the seemingly missing
|
|
78
|
+
// exception handling. See (*) above.
|
|
79
|
+
return workload;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Defines a promise that resolves when all payloads are processed by the iterator, but limits
|
|
85
|
+
* the number concurrent executions.
|
|
86
|
+
*
|
|
87
|
+
* @param limit number of concurrent executions
|
|
88
|
+
* @param payloads array where each element is an array of arguments passed to the iterator
|
|
89
|
+
* @param iterator (async) function to process a payload
|
|
90
|
+
* @returns {Promise<[]>} promise for an array of iterator results
|
|
91
|
+
*/
|
|
92
|
+
const limiter = async (limit, payloads, iterator) => {
|
|
93
|
+
const returnPromises = [];
|
|
94
|
+
const runningPromises = [];
|
|
95
|
+
for (const payload of payloads) {
|
|
96
|
+
const p =
|
|
97
|
+
iterator.constructor.name === "AsyncFunction"
|
|
98
|
+
? iterator(payload)
|
|
99
|
+
: Promise.resolve().then(() => iterator(payload));
|
|
100
|
+
returnPromises.push(p);
|
|
101
|
+
|
|
102
|
+
if (limit <= payloads.length) {
|
|
103
|
+
const e = p
|
|
104
|
+
.catch(() => {})
|
|
105
|
+
.finally(() => runningPromises.splice(runningPromises.indexOf(e), 1));
|
|
106
|
+
runningPromises.push(e);
|
|
107
|
+
if (limit <= runningPromises.length) {
|
|
108
|
+
await Promise.race(runningPromises);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return Promise.allSettled(returnPromises);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
module.exports = { arrayToFlatMap, Funnel, limiter };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const redis = require("./redis");
|
|
4
|
+
const config = require("../config");
|
|
5
|
+
const cdsHelper = require("./cdsHelper");
|
|
6
|
+
const { getConfigInstance } = require("../config");
|
|
7
|
+
|
|
8
|
+
const acquireLock = async (
|
|
9
|
+
context,
|
|
10
|
+
key,
|
|
11
|
+
{
|
|
12
|
+
tenantScoped = true,
|
|
13
|
+
expiryTime = config.getConfigInstance().globalTxTimeout,
|
|
14
|
+
} = {}
|
|
15
|
+
) => {
|
|
16
|
+
const fullKey = _generateKey(context, tenantScoped, key);
|
|
17
|
+
if (config.getConfigInstance().redisEnabled) {
|
|
18
|
+
return await _acquireLockRedis(context, fullKey, expiryTime);
|
|
19
|
+
} else {
|
|
20
|
+
return await _acquireLockDB(context, fullKey, expiryTime);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const setValueWithExpire = async (
|
|
25
|
+
context,
|
|
26
|
+
key,
|
|
27
|
+
value,
|
|
28
|
+
{
|
|
29
|
+
tenantScoped = true,
|
|
30
|
+
expiryTime = config.getConfigInstance().globalTxTimeout,
|
|
31
|
+
overrideValue = false,
|
|
32
|
+
} = {}
|
|
33
|
+
) => {
|
|
34
|
+
const fullKey = _generateKey(context, tenantScoped, key);
|
|
35
|
+
if (config.getConfigInstance().redisEnabled) {
|
|
36
|
+
return await _acquireLockRedis(context, fullKey, expiryTime, {
|
|
37
|
+
value,
|
|
38
|
+
overrideValue,
|
|
39
|
+
});
|
|
40
|
+
} else {
|
|
41
|
+
return await _acquireLockDB(context, fullKey, expiryTime, {
|
|
42
|
+
value,
|
|
43
|
+
overrideValue,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const releaseLock = async (context, key, { tenantScoped = true } = {}) => {
|
|
49
|
+
const fullKey = _generateKey(context, tenantScoped, key);
|
|
50
|
+
if (config.getConfigInstance().redisEnabled) {
|
|
51
|
+
return await _releaseLockRedis(context, fullKey);
|
|
52
|
+
} else {
|
|
53
|
+
return await _releaseLockDb(context, fullKey);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const checkLockExistsAndReturnValue = async (
|
|
58
|
+
context,
|
|
59
|
+
key,
|
|
60
|
+
{ tenantScoped = true } = {}
|
|
61
|
+
) => {
|
|
62
|
+
const fullKey = _generateKey(context, tenantScoped, key);
|
|
63
|
+
if (config.getConfigInstance().redisEnabled) {
|
|
64
|
+
return await _checkLockExistsRedis(context, fullKey);
|
|
65
|
+
} else {
|
|
66
|
+
return await _checkLockExistsDb(context, fullKey);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const _acquireLockRedis = async (
|
|
71
|
+
context,
|
|
72
|
+
fullKey,
|
|
73
|
+
expiryTime,
|
|
74
|
+
{ value = "true", overrideValue = false } = {}
|
|
75
|
+
) => {
|
|
76
|
+
const client = await redis.createMainClientAndConnect();
|
|
77
|
+
const result = await client.set(fullKey, value, {
|
|
78
|
+
PX: expiryTime,
|
|
79
|
+
...(overrideValue ? null : { NX: true }),
|
|
80
|
+
});
|
|
81
|
+
return result === "OK";
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const _checkLockExistsRedis = async (context, fullKey) => {
|
|
85
|
+
const client = await redis.createMainClientAndConnect();
|
|
86
|
+
return await client.get(fullKey);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const _checkLockExistsDb = async (context, fullKey) => {
|
|
90
|
+
let result;
|
|
91
|
+
const configInstance = getConfigInstance();
|
|
92
|
+
await cdsHelper.executeInNewTransaction(
|
|
93
|
+
context,
|
|
94
|
+
"distributedLock-checkExists",
|
|
95
|
+
async (tx) => {
|
|
96
|
+
result = await tx.run(
|
|
97
|
+
SELECT.one
|
|
98
|
+
.from(configInstance.tableNameEventLock)
|
|
99
|
+
.where("code =", fullKey)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
return result?.value;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const _releaseLockRedis = async (context, fullKey) => {
|
|
107
|
+
const client = await redis.createMainClientAndConnect();
|
|
108
|
+
await client.del(fullKey);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const _releaseLockDb = async (context, fullKey) => {
|
|
112
|
+
const configInstance = getConfigInstance();
|
|
113
|
+
await cdsHelper.executeInNewTransaction(
|
|
114
|
+
context,
|
|
115
|
+
"distributedLock-release",
|
|
116
|
+
async (tx) => {
|
|
117
|
+
await tx.run(
|
|
118
|
+
DELETE.from(configInstance.tableNameEventLock).where("code =", fullKey)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const _acquireLockDB = async (
|
|
125
|
+
context,
|
|
126
|
+
fullKey,
|
|
127
|
+
expiryTime,
|
|
128
|
+
{ value = "true", overrideValue = false } = {}
|
|
129
|
+
) => {
|
|
130
|
+
let result;
|
|
131
|
+
const configInstance = getConfigInstance();
|
|
132
|
+
await cdsHelper.executeInNewTransaction(
|
|
133
|
+
context,
|
|
134
|
+
"distributedLock-acquire",
|
|
135
|
+
async (tx) => {
|
|
136
|
+
try {
|
|
137
|
+
await tx.run(
|
|
138
|
+
INSERT.into(configInstance.tableNameEventLock).entries({
|
|
139
|
+
code: fullKey,
|
|
140
|
+
value,
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
result = true;
|
|
144
|
+
} catch (err) {
|
|
145
|
+
let currentEntry;
|
|
146
|
+
|
|
147
|
+
if (!overrideValue) {
|
|
148
|
+
currentEntry = await tx.run(
|
|
149
|
+
SELECT.one
|
|
150
|
+
.from(configInstance.tableNameEventLock)
|
|
151
|
+
.forUpdate({ wait: config.getConfigInstance().forUpdateTimeout })
|
|
152
|
+
.where("code =", fullKey)
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (
|
|
156
|
+
overrideValue ||
|
|
157
|
+
(currentEntry &&
|
|
158
|
+
new Date(currentEntry.createdAt).getTime() + expiryTime <=
|
|
159
|
+
Date.now())
|
|
160
|
+
) {
|
|
161
|
+
await tx.run(
|
|
162
|
+
UPDATE.entity(configInstance.tableNameEventLock)
|
|
163
|
+
.set({
|
|
164
|
+
createdAt: new Date().toISOString(),
|
|
165
|
+
value,
|
|
166
|
+
})
|
|
167
|
+
.where("code =", currentEntry.code)
|
|
168
|
+
);
|
|
169
|
+
result = true;
|
|
170
|
+
} else {
|
|
171
|
+
result = false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
return result;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const _generateKey = (context, tenantScoped, key) => {
|
|
180
|
+
const keyParts = [];
|
|
181
|
+
tenantScoped && keyParts.push(context.tenant);
|
|
182
|
+
keyParts.push(key);
|
|
183
|
+
return keyParts.join("##");
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
acquireLock,
|
|
188
|
+
releaseLock,
|
|
189
|
+
checkLockExistsAndReturnValue,
|
|
190
|
+
setValueWithExpire,
|
|
191
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const redis = require("redis");
|
|
4
|
+
|
|
5
|
+
const config = require("../config");
|
|
6
|
+
const EventQueueError = require("../EventQueueError");
|
|
7
|
+
|
|
8
|
+
const COMPONENT_NAME = "eventQueue/shared/redis";
|
|
9
|
+
|
|
10
|
+
let subscriberClientPromise;
|
|
11
|
+
|
|
12
|
+
const createMainClientAndConnect = () => {
|
|
13
|
+
if (subscriberClientPromise) {
|
|
14
|
+
return subscriberClientPromise;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const errorHandlerCreateClient = (err) => {
|
|
18
|
+
cds
|
|
19
|
+
.log(COMPONENT_NAME)
|
|
20
|
+
.error("error from redis client for pub/sub failed", err);
|
|
21
|
+
subscriberClientPromise = null;
|
|
22
|
+
setTimeout(createMainClientAndConnect, 5 * 1000).unref();
|
|
23
|
+
};
|
|
24
|
+
subscriberClientPromise = createClientAndConnect(errorHandlerCreateClient);
|
|
25
|
+
return subscriberClientPromise;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const _createClientBase = () => {
|
|
29
|
+
const configInstance = config.getConfigInstance();
|
|
30
|
+
if (configInstance.isOnCF) {
|
|
31
|
+
try {
|
|
32
|
+
const credentials = configInstance.getRedisCredentialsFromEnv();
|
|
33
|
+
// NOTE: settings the user explicitly to empty resolves auth problems, see
|
|
34
|
+
// https://github.com/go-redis/redis/issues/1343
|
|
35
|
+
const url = credentials.uri.replace(/(?<=rediss:\/\/)[\w-]+?(?=:)/, "");
|
|
36
|
+
return redis.createClient({ url });
|
|
37
|
+
} catch (err) {
|
|
38
|
+
throw EventQueueError.redisConnectionFailure(err);
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
return redis.createClient({
|
|
42
|
+
socket: { reconnectStrategy: _localReconnectStrategy },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const createClientAndConnect = async (errorHandlerCreateClient) => {
|
|
48
|
+
let client = null;
|
|
49
|
+
try {
|
|
50
|
+
client = _createClientBase();
|
|
51
|
+
} catch (err) {
|
|
52
|
+
throw EventQueueError.redisConnectionFailure(err);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
client.on("error", errorHandlerCreateClient);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await client.connect();
|
|
59
|
+
} catch (err) {
|
|
60
|
+
errorHandlerCreateClient(err);
|
|
61
|
+
}
|
|
62
|
+
return client;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const _localReconnectStrategy = () => EventQueueError.redisNoReconnect();
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
createClientAndConnect,
|
|
69
|
+
createMainClientAndConnect,
|
|
70
|
+
};
|