@ti-engine/core 1.0.11 → 1.1.0
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/CHANGELOG.md +47 -3
- package/README.md +35 -21
- package/bin/settings.json +6 -3
- package/bin/start-instance.js +1 -1
- package/components/auditing.js +1 -1
- package/components/connection-observer.js +1 -1
- package/components/exchange/default/default-message-exchange.js +1 -1
- package/components/exchange/default/default-message-receiver.js +1 -1
- package/components/exchange/default/default-message-sender.js +1 -1
- package/components/exchange/message-dispatcher.js +1 -1
- package/components/exchange/message-exchange.js +1 -1
- package/components/exchange/message-handler.js +3 -3
- package/components/exchange/message-memory-cache.js +3 -2
- package/components/exchange/message-observer.js +1 -1
- package/components/exchange/message-receiver.js +1 -1
- package/components/exchange/message-sender.js +1 -1
- package/components/exchange/message-tracer.js +45 -10
- package/components/service-caller.js +1 -1
- package/components/service-consumer.js +1 -1
- package/components/service-executor.js +13 -7
- package/components/service-instance.js +7 -5
- package/components/service-provider.js +1 -1
- package/integrations/gcloud-integration.js +1 -1
- package/integrations/redis-integration.js +107 -17
- package/package.json +8 -7
- package/utils/cache.js +144 -5
- package/utils/config.js +9 -1
- package/utils/exceptions.js +2 -1
- package/utils/logger.js +1 -1
- package/utils/tools.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,51 @@
|
|
|
1
|
-
|
|
1
|
+
# ti-engine changelog
|
|
2
|
+
|
|
3
|
+
## Version 1.1.0
|
|
4
|
+
|
|
5
|
+
* feat(redis integration): implement better process for fetching and storing the remote Redis server settings and enabled features
|
|
6
|
+
* feat(redis integration): add getter method `serverVersion` that returns the Redis server version
|
|
7
|
+
* feat(redis integration): add support for RedisJSON functionality
|
|
8
|
+
* feat(redis integration): add getter method `isJSONSupported` to verify if this module is enabled/supported in Redis server
|
|
9
|
+
* feat(redis integration): add separate enum for override modes `TiRedisOverrideMode`
|
|
10
|
+
* feat(redis integration): add support for executing any single command in Redis that is otherwise unsupported by the underlying framework. Use method `callCommand` for that in case you need to access something more exotic or currently unimplemented
|
|
11
|
+
* feat(cache): add support for working with JSON values. Currently supporting operations `set`, `get`, and `append array` with more coming in future versions
|
|
12
|
+
* feat(cache): add support for setting manual expiration of values
|
|
13
|
+
* feat(message tracer): change log level of trace entries from `DEBUG` to `NOTICE` for all events
|
|
14
|
+
* feat(message tracer): add functionality for storing the trace entries in the `cache` in JSON format. This is the default behavior and will always work. Traces can later be retrieved from the `cache` for further processing by specialized application
|
|
15
|
+
* feat(message tracer): add new property `traceTimestamp` to the trace entries as it may differ from the actual message timestamp
|
|
16
|
+
* feat(config): add new setting `messageExchange.traceExpirationTime` that controls the preservation time for all trace entries. Default is one hour and will be refreshed every time a new trace is generated. Setting this to `0` will disable
|
|
17
|
+
expiration altogether
|
|
18
|
+
* feat(config): add new setting `messageExchange.traceRepository` that specifies the name of the key in `cache` that will store the trace entries. The default one should be sufficient for most cases
|
|
19
|
+
* feat(config): change default of setting `messageExchange.traceLogEnabled` to `false`. With this change trace entries will not be written to the standard log output
|
|
20
|
+
* feat(exceptions): add new exception code `1006` as `E_GEN_FEATURE_UNSUPPORTED` to indicate functionality that is unsupported by the system or integrated application
|
|
21
|
+
* refactor(config)!: correct the name of the ENV variable controlling `memoryCache.user` to `TI_MEMORY_CACHE_USER` from previous `MEMORY_CACHE_USER`
|
|
22
|
+
* build(npm): update npm development dependencies to their latest versions
|
|
23
|
+
* docs: update some JSDoc types and other entries to reflect the current state of the code
|
|
24
|
+
|
|
25
|
+
## Version 1.0.14
|
|
26
|
+
|
|
27
|
+
* feat(message exchange): improve the behavior of `onConnectionRecovered` and `onConnectionDisrupted` events in modules `cache`, `message-handler` and `service-executor` to prevent cross-activation or deactivation
|
|
28
|
+
* feat(redis integration): improve Redis integration for greater stability, support of Redis Cloud, and support for Redis 7
|
|
29
|
+
* feat(config): enable setting `auditing.logDetails` by default for all logs
|
|
30
|
+
* docs: update general documentation
|
|
31
|
+
|
|
32
|
+
## Version 1.0.13
|
|
33
|
+
|
|
34
|
+
* build(npm): update npm dependencies to their latest versions
|
|
35
|
+
|
|
36
|
+
## Version 1.0.12
|
|
37
|
+
|
|
38
|
+
* build(npm): update npm dependencies to their latest versions
|
|
39
|
+
|
|
40
|
+
## Version 1.0.11
|
|
41
|
+
|
|
2
42
|
* feat(message exchange): implement message tampering and insertion protection
|
|
3
43
|
* feat(config): provide option to turn on/off the message tampering and insertion protection
|
|
4
44
|
* feat(config): provide option to turn on/off message tracing
|
|
5
|
-
*
|
|
45
|
+
* build(npm): update npm dependencies to their latest versions
|
|
6
46
|
* refactor(config)!: rename all environment variables that set configuration settings to match their related setting
|
|
7
|
-
* docs: add a change log
|
|
47
|
+
* docs: add a change log
|
|
48
|
+
|
|
49
|
+
## Version 1.0.0
|
|
50
|
+
|
|
51
|
+
* feat: create initial version of the framework
|
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# ti-engine core
|
|
2
2
|
Flexible framework for the creation of microservices with [node.js](https://nodejs.org/).
|
|
3
3
|
|
|
4
|
-
##
|
|
4
|
+
## Introduction
|
|
5
5
|
The **ti-engine** is an open source, free to use - both for personal and commercial projects - framework for the creation of microservice-based solutions using **node.js**. The architectural concept of the framework is based on a standard _messaging system_ that allows for certain customization but also provides predictability and traceability of its behavior.
|
|
6
6
|
|
|
7
|
-
Being a messaging system, the **ti-engine** relies on a message broker for the actual exchange of messages between microservice instances. The default implementation of the framework uses [Redis](https://redis.io/) cache, however, you could create your own implementation using something like [Rabbit MQ](https://www.rabbitmq.com/). See the [
|
|
7
|
+
Being a messaging system, the **ti-engine** relies on a message broker for the actual exchange of messages between microservice instances. The default implementation of the framework uses [Redis](https://redis.io/) cache, however, you could create your own implementation using something like [Rabbit MQ](https://www.rabbitmq.com/). See the [Advanced topics](#advanced-topics) section of this documentation for guides on how to do this.
|
|
8
8
|
|
|
9
9
|
Please be aware, that this framework is under active development and will expand in the near future. Also, this documentation is still in the process of being created and refined. Make sure to keep an eye on the changes in case you want to use it in the meantime.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Why ti-engine?
|
|
12
12
|
The framework is created based on a decade of professional experience with the utilized technologies and architectural approach. It's primary goal is to provide you with a lightweight and flexible solution that can help you build quickly a microservice ecosystem with any degree of size and complexity.
|
|
13
13
|
|
|
14
14
|
This is what you gain by using **ti-engine** in your project:
|
|
@@ -21,16 +21,16 @@ This is what you gain by using **ti-engine** in your project:
|
|
|
21
21
|
|
|
22
22
|
These are just some benefits **ti-engine** offers. Get to know it better to find out more ways in which it can help you improve productivity.
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## Prerequisites & installation
|
|
25
25
|
In order to run the basic ti-engine framework you will need a couple of things:
|
|
26
26
|
* A local [node.js installation](https://nodejs.org/en/download/) with a minimum version of **14.17.0**
|
|
27
27
|
* A local or remote [Redis cache installation](https://redis.io/download) with a minimum version of **5.0.14**
|
|
28
28
|
|
|
29
|
-
If you are working under Windows OS and you need to install Redis, take a look at this [guide](https://redis.com/blog/redis-on-windows-10/).
|
|
29
|
+
If you are working under Windows 10+ OS and you need to install Redis, take a look at this [guide](https://redis.com/blog/redis-on-windows-10/). You could also use [Redis Cloud](https://app.redislabs.com/) for development purposes as it offers free basic account.
|
|
30
30
|
|
|
31
31
|
To get the framework itself, use the command `npm install @ti-engine/core`. And to include it directly in your package.json dependencies execute `npm install @ti-engine/core --save-prod`.
|
|
32
32
|
|
|
33
|
-
##
|
|
33
|
+
## Getting started
|
|
34
34
|
To start using the **ti-engine**, you will have to make sure that all prerequisites are available and operational. However, before we get to the fun part you need to also consider a couple of very important things while working with this framework:
|
|
35
35
|
1. The runtime configuration of the framework can be customized using ENV variables. These can be provided to node.js in all the standard ways, but there is also an option to include an `.env` file.
|
|
36
36
|
2. It loads your framework-related custom scripts and files dynamically, but it always assumes their provided paths are relative to the _current working directory_ (i.e. it uses `process.cwd()`). Be mindful of that whenever you declare file paths in the various settings.
|
|
@@ -61,14 +61,14 @@ At the start of the output log you can see a NOTICE that tells you a couple of i
|
|
|
61
61
|
* The instance name - in this case `tester-service`. In the terminology of the framework, this is also known as a _service domain name_.
|
|
62
62
|
* The _instance identificator_. It is an uuid string with a `ti-` prefix, that is generated by the framework at process start. It can and will be used to trace the messages during their movement through the microservice ecosystem. But more on that later.
|
|
63
63
|
|
|
64
|
-
Following that come a couple of INFO lines that inform you about the microservice interface state. The framework starts with the process of registration of _business services_ within the service domain of the microservice `tester-service` and successfully adds 2 such services. The necessary information for this is read from a JSON config file included in the package. We'll get into more details on what this all means in the section [
|
|
64
|
+
Following that come a couple of INFO lines that inform you about the microservice interface state. The framework starts with the process of registration of _business services_ within the service domain of the microservice `tester-service` and successfully adds 2 such services. The necessary information for this is read from a JSON config file included in the package. We'll get into more details on what this all means in the section [Creating a microservice](#creating-a-microservice).
|
|
65
65
|
|
|
66
66
|
Once the initialization sequence has completed the framework informs you that the microservice instance has started successfully. If the framework encountered an error during initialization instead, you would see something like this:
|
|
67
67
|
```text
|
|
68
68
|
[timestamp]: [instance-id] - notice - Starting new instance of type 'tester-service' with instance ID '[instance-id]'.
|
|
69
69
|
[timestamp]: [instance-id] - alert - Error detected in the instance startup script!
|
|
70
70
|
```
|
|
71
|
-
The following 5 lines inform you about the successful connection to Redis. The default configuration assumes that your Redis is running on localhost and uses the default port. If you have a different setup, you can provide the host and port via ENV variables. We'll cover that in the section [
|
|
71
|
+
The following 5 lines inform you about the successful connection to Redis. The default configuration assumes that your Redis is running on localhost and uses the default port. If you have a different setup, you can provide the host and port via ENV variables. We'll cover that in the section [Using the framework](#using-the-framework).
|
|
72
72
|
|
|
73
73
|
Finally, you should see a couple of execution statements with their results in JSON format.
|
|
74
74
|
|
|
@@ -84,24 +84,24 @@ The tester module gets its starting configuration from an `.env` file included i
|
|
|
84
84
|
TI_INSTANCE_CLASS=tester-service.js
|
|
85
85
|
TI_INSTANCE_CONFIG=tester-service.json
|
|
86
86
|
TI_INSTANCE_NAME=tester-service
|
|
87
|
-
|
|
87
|
+
TI_AUDITING_LOG_MIN_LEVEL=200
|
|
88
88
|
```
|
|
89
89
|
The first variable `TI_INSTANCE_CLASS` is mandatory for every microservice you create with the **ti-engine**. It needs to specify the path to the module that is your microservice. Remember, that this path has to be relative to the working directory in which you plan to execute the `node` command. This is especially important when you're configuring your microservices to work in containers. You can find the full list of available ENV variables and what they do below.
|
|
90
90
|
|
|
91
91
|
Before moving on, also take a good look at the file `bin/start-instance.js`. It should give you an idea on how to the process of starting and stopping a microservice operates. In most cases this file should be sufficient as a starting script for your **ti-engine** based microservice applications. You can, of course, create your own starting script, but then you'll have to consider all necessary steps to properly handle the microservice instance.
|
|
92
92
|
|
|
93
|
-
##
|
|
93
|
+
## Architecture
|
|
94
94
|
The architectural approach for the **ti-engine** is done in _tiers_ with lower tiers being unaware of the tiers above them. The framework prefers a high level of abstraction in all its tiers and provides many options for customization and extension. While the language is JavaScript, the structuring of the framework follows the OOP principles, and you will find a lot of abstract classes and methods that require you to implement them. These are always marked with the `@abstract` annotation but if you happen to miss one, the framework will raise an exception when you try to use it in your solution.
|
|
95
95
|
|
|
96
96
|
There are three general tiers in the **ti-engine**:
|
|
97
|
-
1.
|
|
98
|
-
2.
|
|
99
|
-
3.
|
|
97
|
+
1. Message exchange
|
|
98
|
+
2. Service domains
|
|
99
|
+
3. Solution implementation
|
|
100
100
|
|
|
101
101
|
See the following sections for more information on each of them.
|
|
102
102
|
|
|
103
|
-
###
|
|
104
|
-
This is the lowest framework tier, unless we count the actual data objects processed by the framework. As you already know, the foundational **ti-engine** concept is that of a messaging system. Therefore, the first tier provides an abstraction over a chosen message broker (Redis by default). That abstraction makes it easy to switch between message brokers whenever you want to without having to change anything above tier 1. It also provides several added bonuses that can accelerate your work - message encryption, message tracing, message observers, and others. More details about each of these features will be covered in section [
|
|
103
|
+
### Tier 1 - Message exchange
|
|
104
|
+
This is the lowest framework tier, unless we count the actual data objects processed by the framework. As you already know, the foundational **ti-engine** concept is that of a messaging system. Therefore, the first tier provides an abstraction over a chosen message broker (Redis by default). That abstraction makes it easy to switch between message brokers whenever you want to without having to change anything above tier 1. It also provides several added bonuses that can accelerate your work - message encryption, message tracing, message observers, and others. More details about each of these features will be covered in section [Using the framework](#using-the-framework).
|
|
105
105
|
|
|
106
106
|
Another important aspect for you to remember is that the message exchange is entirely _asynchronous_. This helps reduce system load and optimizes the usage of the available resources. Even so each node.js process can handle a limited amount of load. Therefore, you should plan for running multiple identical senders and receives in order to scale your solution. But more on that later.
|
|
107
107
|
|
|
@@ -115,21 +115,35 @@ After the processing is done the message payload is modified and the receiver se
|
|
|
115
115
|
|
|
116
116
|
In this scenario the framework utilizes _Redis lists_ as queues for the message envelopes and _Redis hash_ as message payload storage. Other message brokers might utilize a slightly different approach, but they should still adhere to the same logical flow.
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
The modules associated with this tier are all located in the `components/exchange/` folder. This is a short list of some terminology used here and in the JDoc inside the sourcecode itself:
|
|
119
|
+
* Message - this is the actual data object processed by the framework. It consists of two parts: an envelope containing service information and a payload containing the actual data to be processed.
|
|
120
|
+
* Message sender - a specialized connector that is responsible for sending a message on its way to its destination. It does not handle the actual dispatch and delivery.
|
|
121
|
+
* Message receiver - a specialized connector that is responsible for receiving messages at predefined destination.
|
|
122
|
+
* Message exchange - this is the actual message processing engine. It handles sending and receiving messages via preconfigured message senders and message receivers.
|
|
123
|
+
* Message observer - a custom event listener that can be used to react on message `send` and `received` events.
|
|
124
|
+
|
|
125
|
+
### Tier 2 - Service domains
|
|
119
126
|
This tier focuses on hosting and executing the _business logic_ of your application. It's comprised of _business services_ that process input data and return the result of the processing as output data. The business services are grouped in _service domains_, which are in turn hosted inside stateless _microservices_ also named _service instances_. There are two types of service instances in **ti-engine**:
|
|
120
127
|
* Service consumers - these are service instances, that can call business services in any available service domain.
|
|
121
128
|
* Service providers - these are service instances, that host and run a set of business services in a particular service domain. Every service provider is also a service consumer.
|
|
122
129
|
|
|
123
130
|
The various service instances in a solution represent a network of interconnected service domains that contain the business logic of your application. All business services exchange data via _service calls_ using abstract _service addresses_. These service calls are transported from one address in the network to another via the underlying message exchange tier. This, however, is completely transparent to the service instances. In essence, tier 2 does not care about the actual data transportation method or protocol. You could in fact change completely the tier 1 approach without having to modify anything in your business logic and business flow.
|
|
124
131
|
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
This tier is the place to utilize any databases, file storages, integrations with other applications, scheduling jobs, and so on. In general, it should focus on executing any granular tasks that are essential to backbone operation of your application. The business logic here should remain stateless and any user context should be provided at runtime to each invoked business service. We'll see more concrete examples for that later in section [Using the framework](#using-the-framework).
|
|
133
|
+
|
|
134
|
+
### Tier 3 - Solution implementation
|
|
135
|
+
This tier comprises the actual implementation of your application. Its structure and behavior depends entirely on your vision and business goals. There are still a couple of points that remain constant:
|
|
136
|
+
* It needs to utilize the business logic defined in tier 2 by calling the business services
|
|
137
|
+
* It needs to take care of any type of stateful behavior like user sessions or transactions
|
|
138
|
+
* It needs to act as the primary interface between users and your application thus handling access management and user interactions
|
|
139
|
+
|
|
140
|
+
Depending on the type of software you are building, tier 3 can be an API Gateway, a Web application, backend for a Mobile application, or anything like that.
|
|
127
141
|
|
|
128
|
-
##
|
|
142
|
+
## Creating a microservice
|
|
129
143
|
Under development...
|
|
130
144
|
|
|
131
|
-
##
|
|
145
|
+
## Using the framework
|
|
132
146
|
Under development...
|
|
133
147
|
|
|
134
|
-
##
|
|
148
|
+
## Advanced topics
|
|
135
149
|
Under development...
|
package/bin/settings.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"auditing": {
|
|
3
3
|
"logConsoleEnabled": true,
|
|
4
|
-
"logDetails":
|
|
4
|
+
"logDetails": true,
|
|
5
5
|
"logMinLevel": 0,
|
|
6
6
|
"logUsesJSON": false
|
|
7
7
|
},
|
|
@@ -15,14 +15,17 @@
|
|
|
15
15
|
"authKey": null,
|
|
16
16
|
"redisDB": 0,
|
|
17
17
|
"redisHost": "127.0.0.1",
|
|
18
|
-
"redisPort": 6379
|
|
18
|
+
"redisPort": 6379,
|
|
19
|
+
"user": "default"
|
|
19
20
|
},
|
|
20
21
|
"messageExchange": {
|
|
21
22
|
"messageQueuePrefix": "ti:messages:",
|
|
22
23
|
"messageStore": "ti:messages:store",
|
|
23
24
|
"securityHashEnabled": true,
|
|
24
25
|
"securityHashKey": "23e7bdc7-a793-41f9-856e-6760332f0c73",
|
|
25
|
-
"
|
|
26
|
+
"traceExpirationTime": 3600,
|
|
27
|
+
"traceLogEnabled": false,
|
|
28
|
+
"traceRepository": "ti:messages:trace"
|
|
26
29
|
},
|
|
27
30
|
"serviceConfig": {
|
|
28
31
|
"executionTimeout": 180000,
|
package/bin/start-instance.js
CHANGED
package/components/auditing.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
2
|
+
* SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
3
3
|
* SPDX-License-Identifier: ICU
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -158,7 +158,7 @@ class MessageHandler extends ConnectionObserver {
|
|
|
158
158
|
* @private
|
|
159
159
|
*/
|
|
160
160
|
onConnectionRecovered( identifier ) {
|
|
161
|
-
if ( this.#isAvailable === false ) {
|
|
161
|
+
if ( this.#isAvailable === false && identifier === this.#connectionIdentifier ) {
|
|
162
162
|
this.#isAvailable = true;
|
|
163
163
|
_.forEach( this.#messageObservers, ( messageObserver ) => {
|
|
164
164
|
messageObserver.onConnectionRecovered( this.#connectionIdentifier );
|
|
@@ -178,7 +178,7 @@ class MessageHandler extends ConnectionObserver {
|
|
|
178
178
|
* @private
|
|
179
179
|
*/
|
|
180
180
|
onConnectionDisrupted( identifier ) {
|
|
181
|
-
if ( this.#isAvailable === true ) {
|
|
181
|
+
if ( this.#isAvailable === true && identifier === this.#connectionIdentifier ) {
|
|
182
182
|
this.#isAvailable = false;
|
|
183
183
|
_.forEach( this.#messageObservers, ( messageObserver ) => {
|
|
184
184
|
messageObserver.onConnectionDisrupted( this.#connectionIdentifier );
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
2
|
+
* SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
3
3
|
* SPDX-License-Identifier: ICU
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -27,7 +27,8 @@ class MessageMemoryCache {
|
|
|
27
27
|
let port = config.getSetting( config.setting.MEMORY_CACHE_REDIS_PORT );
|
|
28
28
|
let db = config.getSetting( config.setting.MEMORY_CACHE_REDIS_DB );
|
|
29
29
|
let authKey = config.getSetting( config.setting.MEMORY_CACHE_AUTH_KEY );
|
|
30
|
-
|
|
30
|
+
let user = config.getSetting( config.setting.MEMORY_CACHE_USER );
|
|
31
|
+
this.#redisClient = redis.createRedisClient( identifier, host, port, authKey, user, db );
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/* Public interface */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
2
|
+
* SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
3
3
|
* SPDX-License-Identifier: ICU
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -7,6 +7,8 @@ const _ = require( "lodash" );
|
|
|
7
7
|
const tools = require( "#tools" );
|
|
8
8
|
const config = require( "#config" );
|
|
9
9
|
const logger = require( "#logger" );
|
|
10
|
+
const exceptions = require( "#exceptions" );
|
|
11
|
+
const cache = require( "#cache" );
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* @typedef {Object} TiTraceEntry
|
|
@@ -14,13 +16,18 @@ const logger = require( "#logger" );
|
|
|
14
16
|
* @property {string} dispatchEvent
|
|
15
17
|
* @property {string} fromAddress
|
|
16
18
|
* @property {string} messageID
|
|
17
|
-
* @property {
|
|
19
|
+
* @property {Object} messageSnapshot
|
|
18
20
|
* @property {string} messageState
|
|
19
21
|
* @property {string} messageType
|
|
20
22
|
* @property {string} toAddress
|
|
21
23
|
* @property {string} traceID
|
|
24
|
+
* @property {number} traceTimestamp
|
|
22
25
|
*/
|
|
23
26
|
|
|
27
|
+
const traceRoot = {
|
|
28
|
+
trace: []
|
|
29
|
+
};
|
|
30
|
+
|
|
24
31
|
/**
|
|
25
32
|
* Enum for listing message types.
|
|
26
33
|
*
|
|
@@ -96,22 +103,28 @@ let formatLogEntry = ( traceEntry ) => {
|
|
|
96
103
|
};
|
|
97
104
|
|
|
98
105
|
/**
|
|
99
|
-
* Used to obscure sensitive data in the message
|
|
106
|
+
* Used to obscure sensitive data in the message, remove the payload, and return a snapshot.
|
|
100
107
|
*
|
|
101
108
|
* @method
|
|
102
109
|
* @param {Message} message
|
|
103
|
-
* @returns {
|
|
110
|
+
* @returns {Message}
|
|
104
111
|
* @private
|
|
105
112
|
*/
|
|
106
113
|
let obscureSensitiveData = ( message ) => {
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
/** @type Message */
|
|
115
|
+
let messageSnapshot = tools.parseJSON( _.replace( tools.stringifyJSON( message ), /("\w*?pin\w*?"|"\w*?pass\w*?"|"\w*?otp\w*?"):"(.*?)"/gmi, "\"SENSITIVE_PROPERTY\":\"OBSCURED_BY_SYSTEM\"" ) );
|
|
116
|
+
delete messageSnapshot.payload;
|
|
117
|
+
return messageSnapshot;
|
|
109
118
|
};
|
|
110
119
|
|
|
111
120
|
/**
|
|
112
121
|
* Used to create a trace entry for the provided {@link Message} and parameters.
|
|
113
122
|
* <br/>
|
|
114
|
-
* NOTE:
|
|
123
|
+
* NOTE: By default all trace events are stored in the memory cache for further processing and analysis. The
|
|
124
|
+
* location is configured in the MESSAGE_EXCHANGE_TRACE_REPOSITORY setting.
|
|
125
|
+
* <br/>
|
|
126
|
+
* NOTE: Trace events are logged with severity level NOTICE or ERROR for failed dispatches. They still might be
|
|
127
|
+
* filtered out if the minimum log level setting is set too high.
|
|
115
128
|
*
|
|
116
129
|
* @method
|
|
117
130
|
* @param {Message} message The message to trace.
|
|
@@ -124,6 +137,10 @@ module.exports.recordTraceEntry = ( message, messageType, dispatchEvent, message
|
|
|
124
137
|
// depending on whether the message comes as request or response, the from and to addresses will be opposite:
|
|
125
138
|
let source = message.source.route + "." + message.source.instanceID;
|
|
126
139
|
let destination = message.destination.route + ( ( message.destination.instanceID != null ) ? "." + message.destination.instanceID : "" );
|
|
140
|
+
let messageSnapshot = obscureSensitiveData( message );
|
|
141
|
+
delete messageSnapshot.chainID;
|
|
142
|
+
delete messageSnapshot.messageID;
|
|
143
|
+
let currentDate = new Date();
|
|
127
144
|
|
|
128
145
|
/** @type TiTraceEntry */
|
|
129
146
|
let traceEntry = {
|
|
@@ -131,16 +148,34 @@ module.exports.recordTraceEntry = ( message, messageType, dispatchEvent, message
|
|
|
131
148
|
dispatchEvent: tools.getEnumName( dispatchEventEnum, dispatchEvent ),
|
|
132
149
|
fromAddress: ( messageType === messageTypeEnum.MESSAGE_REQUEST ) ? source : destination,
|
|
133
150
|
messageID: message.messageID,
|
|
134
|
-
messageSnapshot:
|
|
151
|
+
messageSnapshot: messageSnapshot,
|
|
135
152
|
messageState: tools.getEnumName( messageStateEnum, messageState ),
|
|
136
153
|
messageType: tools.getEnumName( messageTypeEnum, messageType ),
|
|
137
154
|
toAddress: ( messageType === messageTypeEnum.MESSAGE_REQUEST ) ? destination : source,
|
|
155
|
+
traceTimestamp: currentDate.getTime(),
|
|
138
156
|
traceID: tools.getUUID()
|
|
139
157
|
};
|
|
140
158
|
|
|
159
|
+
// only write the trace in the general log if this is enabled:
|
|
141
160
|
if ( config.getSetting( config.setting.MESSAGE_EXCHANGE_TRACE_LOG_ENABLED ) === true ) {
|
|
142
|
-
createLogEntry( traceEntry, ( dispatchEvent === dispatchEventEnum.FAILED ) ? logger.logSeverity.ERROR : logger.logSeverity.
|
|
161
|
+
createLogEntry( traceEntry, ( dispatchEvent === dispatchEventEnum.FAILED ) ? logger.logSeverity.ERROR : logger.logSeverity.NOTICE );
|
|
143
162
|
}
|
|
144
163
|
|
|
145
|
-
//
|
|
164
|
+
// add the trace entry to the repository in the memory cache:
|
|
165
|
+
cache.setJSON( config.getSetting( config.setting.MESSAGE_EXCHANGE_TRACE_REPOSITORY ), traceRoot, "$", 1 ).then( () => {
|
|
166
|
+
return cache.arrayAppendJSON( config.getSetting( config.setting.MESSAGE_EXCHANGE_TRACE_REPOSITORY ), traceEntry, "$.trace" );
|
|
167
|
+
} ).then( () => {
|
|
168
|
+
// this will refresh the expiration time for the trace repository on each new record:
|
|
169
|
+
let expiration = config.getSetting( config.setting.MESSAGE_EXCHANGE_TRACE_EXPIRATION_TIME );
|
|
170
|
+
return ( expiration > 0 ) ? cache.expireValue( config.getSetting( config.setting.MESSAGE_EXCHANGE_TRACE_REPOSITORY ), expiration ) : expiration;
|
|
171
|
+
} ).catch( ( error ) => {
|
|
172
|
+
if ( error.code === exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED ) {
|
|
173
|
+
cache.addToSet( config.getSetting( config.setting.MESSAGE_EXCHANGE_TRACE_REPOSITORY ), traceEntry ).catch( ( error ) => {
|
|
174
|
+
logger.log( `Failed to add message trace entry to the trace repository. While this will not prevent the application from running, it might still be a sign of a more serious problem!`, logger.logSeverity.WARNING, error );
|
|
175
|
+
} );
|
|
176
|
+
} else {
|
|
177
|
+
logger.log( `Failed to add message trace entry to the trace repository. While this will not prevent the application from running, it might still be a sign of a more serious problem!`, logger.logSeverity.WARNING, error );
|
|
178
|
+
}
|
|
179
|
+
} );
|
|
180
|
+
|
|
146
181
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
2
|
+
* SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
3
3
|
* SPDX-License-Identifier: ICU
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -102,12 +102,18 @@ class ServiceExecutor extends MessageObserver {
|
|
|
102
102
|
onConnectionRecovered( identifier ) {
|
|
103
103
|
super.onConnectionRecovered( identifier );
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
105
|
+
if ( identifier !== cache.connectionIdentifier ) {
|
|
106
|
+
if ( cache.isOperational ) {
|
|
107
|
+
let serviceCatalog = config.getSetting( config.setting.SERVICE_REGISTRY_ADDRESS ) + ServiceInstance.serviceDomainName;
|
|
108
|
+
_.forOwn( this.#serviceInterface, ( versions, serviceAlias ) => {
|
|
109
|
+
cache.addToSet( serviceCatalog, serviceAlias ).catch( ( error ) => {
|
|
110
|
+
logger.log( `Record for service '${ serviceAlias }' could not be added to the service registry.`, logger.logSeverity.ERROR, error );
|
|
111
|
+
} );
|
|
112
|
+
} );
|
|
113
|
+
} else {
|
|
114
|
+
//TODO: Retry service registration after 0.5 seconds.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
2
|
+
* SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
3
3
|
* SPDX-License-Identifier: ICU
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -269,10 +269,12 @@ class ServiceInstance {
|
|
|
269
269
|
* @private
|
|
270
270
|
*/
|
|
271
271
|
#reportHealthy() {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
272
|
+
if ( cache.isOperational ) {
|
|
273
|
+
let timestamp = new Date();
|
|
274
|
+
cache.setValue( this.#serviceHealthCheck, timestamp.toISOString(), config.getSetting( config.setting.SERVICE_HEALTH_CHECK_TIMEOUT ) ).catch( ( error ) => {
|
|
275
|
+
logger.log( `Error while trying to report for health check from '${ ServiceInstance.instanceID }'!`, logger.logSeverity.WARNING, error );
|
|
276
|
+
} );
|
|
277
|
+
}
|
|
276
278
|
}
|
|
277
279
|
|
|
278
280
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
2
|
+
* SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
3
3
|
* SPDX-License-Identifier: ICU
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const ConnectionObserver = require( "#connection-observer" );
|
|
7
|
-
const
|
|
7
|
+
const Redis = require( "ioredis" );
|
|
8
8
|
const tools = require( "#tools" );
|
|
9
9
|
const logger = require( "#logger" );
|
|
10
10
|
const exceptions = require( "#exceptions" );
|
|
@@ -19,6 +19,7 @@ const _ = require( "lodash" );
|
|
|
19
19
|
let cacheCommandsEnum = tools.enum( {
|
|
20
20
|
ADD_TO_SET: [ "sadd", "add to set", "https://redis.io/commands/sadd" ],
|
|
21
21
|
DELETE_VALUE: [ "del", "delete value", "https://redis.io/commands/del" ],
|
|
22
|
+
EXPIRE: [ "expire", "expire", "https://redis.io/commands/expire" ],
|
|
22
23
|
GET_ALL_FROM_SET: [ "smembers", "get all set members", "https://redis.io/commands/smembers" ],
|
|
23
24
|
GET_VALUE: [ "get", "get value", "https://redis.io/commands/get" ],
|
|
24
25
|
HASH_GET: [ "hget", "hash get", "https://redis.io/commands/hget" ],
|
|
@@ -27,6 +28,9 @@ let cacheCommandsEnum = tools.enum( {
|
|
|
27
28
|
HASH_SET: [ "hset", "", "https://redis.io/commands/hset" ],
|
|
28
29
|
HASH_SET_MANY: [ "hmset", "", "https://redis.io/commands/hmset" ],
|
|
29
30
|
IS_SET_MEMBER: [ "sismember", "", "https://redis.io/commands/sismember" ],
|
|
31
|
+
JSON_ARRAY_APPEND: [ "json.arrappend", "", "https://redis.io/commands/json.arrappend" ],
|
|
32
|
+
JSON_GET: [ "json.get", "", "https://redis.io/commands/json.get" ],
|
|
33
|
+
JSON_SET: [ "json.set", "", "https://redis.io/commands/json.set" ],
|
|
30
34
|
KEYS: [ "keys", "", "https://redis.io/commands/keys" ],
|
|
31
35
|
LIST_PUSH: [ "lpush", "list push", "https://redis.io/commands/lpush" ],
|
|
32
36
|
LIST_POP_TAIL_BLOCKING: [ "brpop", "list pop tail blocking", "https://redis.io/commands/brpop" ],
|
|
@@ -36,10 +40,26 @@ let cacheCommandsEnum = tools.enum( {
|
|
|
36
40
|
UNION_OF_SETS: [ "sunion", "union of sets", "https://redis.io/commands/sunion" ]
|
|
37
41
|
} );
|
|
38
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Enum for listing the Redis key override modes.
|
|
45
|
+
*
|
|
46
|
+
* @readonly
|
|
47
|
+
* @enum {string}
|
|
48
|
+
*/
|
|
49
|
+
let cacheOverrideModeEnum = tools.enum( {
|
|
50
|
+
DEFAULT: [ "", "default", "Standard Redis behaviour when setting new key." ],
|
|
51
|
+
NX: [ "nx", "nx", "Sets the key only if it does not already exist." ],
|
|
52
|
+
XX: [ "xx", "xx", "Sets the key only if it already exists." ]
|
|
53
|
+
} );
|
|
54
|
+
|
|
39
55
|
/**
|
|
40
56
|
* @typedef {string} TiRedisCommand
|
|
41
57
|
*/
|
|
42
58
|
module.exports.cacheCommands = cacheCommandsEnum;
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {string} TiRedisOverrideMode
|
|
61
|
+
*/
|
|
62
|
+
module.exports.cacheOverrideMode = cacheOverrideModeEnum;
|
|
43
63
|
|
|
44
64
|
/**
|
|
45
65
|
* Used to create a Redis Cache client.
|
|
@@ -49,10 +69,12 @@ module.exports.cacheCommands = cacheCommandsEnum;
|
|
|
49
69
|
*/
|
|
50
70
|
class RedisClient {
|
|
51
71
|
|
|
52
|
-
#clientIdentifier
|
|
72
|
+
#clientIdentifier;
|
|
53
73
|
#retryMaxInterval = 1000;
|
|
54
74
|
#retryMaxAttempts = undefined;
|
|
55
75
|
#redisClient = undefined;
|
|
76
|
+
#serverInfo = {};
|
|
77
|
+
#serverFeatures = {};
|
|
56
78
|
#connectionObservers = [];
|
|
57
79
|
|
|
58
80
|
/**
|
|
@@ -61,12 +83,13 @@ class RedisClient {
|
|
|
61
83
|
* @param {string} host
|
|
62
84
|
* @param {number} port
|
|
63
85
|
* @param {string} authKey
|
|
86
|
+
* @param {string} user
|
|
64
87
|
* @param {number} defaultDB
|
|
65
88
|
* @param {boolean} autoRetryUnfulfilled
|
|
66
89
|
* @param {number} maxRetries
|
|
67
90
|
*/
|
|
68
|
-
constructor( identifier, host, port, authKey, defaultDB, autoRetryUnfulfilled, maxRetries ) {
|
|
69
|
-
this.#clientIdentifier = identifier ||
|
|
91
|
+
constructor( identifier, host, port, authKey, user, defaultDB, autoRetryUnfulfilled, maxRetries ) {
|
|
92
|
+
this.#clientIdentifier = identifier || "redis-client-" + tools.getUUID();
|
|
70
93
|
|
|
71
94
|
let retryStrategy = ( attempt ) => {
|
|
72
95
|
let result = Math.min( attempt * 50, this.#retryMaxInterval );
|
|
@@ -87,6 +110,7 @@ class RedisClient {
|
|
|
87
110
|
let options = {
|
|
88
111
|
port: port,
|
|
89
112
|
host: host,
|
|
113
|
+
username: user,
|
|
90
114
|
password: authKey,
|
|
91
115
|
db: defaultDB,
|
|
92
116
|
autoResendUnfulfilledCommands: autoRetryUnfulfilled,
|
|
@@ -96,23 +120,43 @@ class RedisClient {
|
|
|
96
120
|
};
|
|
97
121
|
|
|
98
122
|
/** @type Redis */
|
|
99
|
-
this.#redisClient = new
|
|
123
|
+
this.#redisClient = new Redis( options );
|
|
100
124
|
|
|
101
125
|
this.#redisClient.on( "ready", () => {
|
|
102
|
-
logger.log( `Connection to Redis server ${ host }:${ port } (re)established by client '${ this.identifier }' and is ready to be used.`, logger.logSeverity.INFO
|
|
103
|
-
redis_version: this.#redisClient.serverInfo.redis_version,
|
|
104
|
-
redis_mode: this.#redisClient.serverInfo.redis_mode,
|
|
105
|
-
os: this.#redisClient.serverInfo.os,
|
|
106
|
-
uptime_in_days: this.#redisClient.serverInfo.uptime_in_days,
|
|
107
|
-
connected_clients: this.#redisClient.serverInfo.connected_clients,
|
|
108
|
-
role: this.#redisClient.serverInfo.role,
|
|
109
|
-
connected_slaves: this.#redisClient.serverInfo.connected_slaves
|
|
110
|
-
} );
|
|
126
|
+
logger.log( `Connection to Redis server ${ host }:${ port } (re)established by client '${ this.identifier }' and is ready to be used.`, logger.logSeverity.INFO );
|
|
111
127
|
|
|
112
128
|
// notify all connection observers about this event:
|
|
113
129
|
_.forEach( this.#connectionObservers, ( connectionObservers ) => {
|
|
114
130
|
connectionObservers.onConnectionRecovered( this.#clientIdentifier );
|
|
115
131
|
} );
|
|
132
|
+
|
|
133
|
+
// fetch the server information and store it:
|
|
134
|
+
this.#redisClient.info().then( ( result ) => {
|
|
135
|
+
this.#serverInfo = {};
|
|
136
|
+
if ( _.isString( result ) ) {
|
|
137
|
+
let rawData = _.split( result, "\r\n" );
|
|
138
|
+
_.forEach( rawData, ( entry ) => {
|
|
139
|
+
let details = _.split( entry, ":" );
|
|
140
|
+
if ( !_.startsWith( details[ 0 ], "#" ) && details[ 0 ] !== "" && details[ 0 ] ) {
|
|
141
|
+
if ( _.isNaN( _.toNumber( details[ 1 ] ) ) ) {
|
|
142
|
+
this.#serverInfo[ details[ 0 ] ] = details[ 1 ];
|
|
143
|
+
} else {
|
|
144
|
+
this.#serverInfo[ details[ 0 ] ] = _.toNumber( details[ 1 ] );
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} );
|
|
148
|
+
}
|
|
149
|
+
return this.#redisClient.module( "LIST" );
|
|
150
|
+
} ).then( ( result ) => {
|
|
151
|
+
this.#serverFeatures = {};
|
|
152
|
+
if ( _.isArray( result ) ) {
|
|
153
|
+
_.forEach( result, ( entry ) => {
|
|
154
|
+
this.#serverFeatures[ entry[ 1 ] ] = entry[ 3 ];
|
|
155
|
+
} );
|
|
156
|
+
}
|
|
157
|
+
} ).catch( ( error ) => {
|
|
158
|
+
logger.log( `Failed to fetch server information by client '${ this.identifier }'!`, logger.logSeverity.WARNING, error );
|
|
159
|
+
} );
|
|
116
160
|
} );
|
|
117
161
|
this.#redisClient.on( "error", ( error ) => {
|
|
118
162
|
logger.log( `Error received in Redis client '${ this.identifier }'.`, logger.logSeverity.ERROR, error );
|
|
@@ -142,6 +186,28 @@ class RedisClient {
|
|
|
142
186
|
return this.#clientIdentifier;
|
|
143
187
|
}
|
|
144
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Used to return the Redis server version.
|
|
191
|
+
*
|
|
192
|
+
* @property
|
|
193
|
+
* @return {number}
|
|
194
|
+
* @public
|
|
195
|
+
*/
|
|
196
|
+
get serverVersion() {
|
|
197
|
+
return this.#serverInfo[ "redis_version" ];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Verify if Redis server supports JSON data types.
|
|
202
|
+
*
|
|
203
|
+
* @property
|
|
204
|
+
* @returns {boolean}
|
|
205
|
+
* @public
|
|
206
|
+
*/
|
|
207
|
+
get isJSONSupported() {
|
|
208
|
+
return !_.isNil( this.#serverFeatures[ "ReJSON" ] );
|
|
209
|
+
}
|
|
210
|
+
|
|
145
211
|
/**
|
|
146
212
|
* Used to register a new {@link ConnectionObserver} for events related to the Redis connection state.
|
|
147
213
|
*
|
|
@@ -177,6 +243,7 @@ class RedisClient {
|
|
|
177
243
|
|
|
178
244
|
/**
|
|
179
245
|
* Used to send a new blocking command to Redis.
|
|
246
|
+
* <br/>
|
|
180
247
|
* WARNING: This will reserve the client connection until a result is received.
|
|
181
248
|
*
|
|
182
249
|
* @method
|
|
@@ -240,6 +307,28 @@ class RedisClient {
|
|
|
240
307
|
} );
|
|
241
308
|
}
|
|
242
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Used to execute any Redis command in an unmanaged way.
|
|
312
|
+
* <br/>
|
|
313
|
+
* WARNING: Use this only if there is no other implemented function in this module and the command
|
|
314
|
+
* you want to execute is not supported by the 'multi' Redis command (implemented in {@link RedisClient.executeCommands}).
|
|
315
|
+
* Make sure to handle the result as it will be returned raw.
|
|
316
|
+
*
|
|
317
|
+
* @method
|
|
318
|
+
* @param {string[]} commandArguments
|
|
319
|
+
* @returns {Promise<Object>}
|
|
320
|
+
* @public
|
|
321
|
+
*/
|
|
322
|
+
callCommand( commandArguments ) {
|
|
323
|
+
return new Promise( ( resolve, reject ) => {
|
|
324
|
+
this.#redisClient[ "call" ].apply( this.#redisClient, commandArguments ).then( ( result ) => {
|
|
325
|
+
resolve( result );
|
|
326
|
+
} ).catch( ( error ) => {
|
|
327
|
+
reject( exceptions.raise( error ) );
|
|
328
|
+
} );
|
|
329
|
+
} );
|
|
330
|
+
}
|
|
331
|
+
|
|
243
332
|
}
|
|
244
333
|
|
|
245
334
|
/**
|
|
@@ -250,12 +339,13 @@ class RedisClient {
|
|
|
250
339
|
* @param {string} host
|
|
251
340
|
* @param {number} [port=6379]
|
|
252
341
|
* @param {string} [authKey=undefined]
|
|
342
|
+
* @param {string} [user="default"]
|
|
253
343
|
* @param {number} [defaultDB=0]
|
|
254
344
|
* @param {boolean} [autoRetryUnfulfilled=true]
|
|
255
345
|
* @param {number} [maxRetries=20]
|
|
256
346
|
* @return {RedisClient}
|
|
257
347
|
* @public
|
|
258
348
|
*/
|
|
259
|
-
module.exports.createRedisClient = ( identifier, host, port = 6379, authKey = undefined, defaultDB = 0, autoRetryUnfulfilled = true, maxRetries = 20 ) => {
|
|
260
|
-
return Object.freeze( new RedisClient( identifier, host, port, authKey, defaultDB, autoRetryUnfulfilled, maxRetries ) );
|
|
349
|
+
module.exports.createRedisClient = ( identifier, host, port = 6379, authKey = undefined, user = "default", defaultDB = 0, autoRetryUnfulfilled = true, maxRetries = 20 ) => {
|
|
350
|
+
return Object.freeze( new RedisClient( identifier, host, port, authKey, user, defaultDB, autoRetryUnfulfilled, maxRetries ) );
|
|
261
351
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ti-engine/core",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "The ti-engine is an open source, free to use - both for personal and commercial projects - framework for the creation of microservice-based solutions using node.js.",
|
|
5
5
|
"author": "Boris Kostadinov <kostadinov.boris@gmail.com>",
|
|
6
6
|
"license": "ISC",
|
|
@@ -41,15 +41,16 @@
|
|
|
41
41
|
"#tools": "./utils/tools.js"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"blake2": "^
|
|
45
|
-
"dotenv": "^
|
|
46
|
-
"fs-extra": "^
|
|
44
|
+
"blake2": "^5.0.0",
|
|
45
|
+
"dotenv": "^16.3.1",
|
|
46
|
+
"fs-extra": "^11.1.1",
|
|
47
47
|
"lodash": "^4.17.21",
|
|
48
|
-
"node-schedule": "^2.1.
|
|
49
|
-
"ioredis": "^
|
|
48
|
+
"node-schedule": "^2.1.1",
|
|
49
|
+
"ioredis": "^5.3.2"
|
|
50
50
|
},
|
|
51
51
|
"optionalDependencies": {
|
|
52
|
-
"@google-cloud/error-reporting": "^
|
|
52
|
+
"@google-cloud/error-reporting": "^3.0.5",
|
|
53
|
+
"zeromq": "^6.0.0-beta.19"
|
|
53
54
|
},
|
|
54
55
|
"repository": {
|
|
55
56
|
"type": "git",
|
package/utils/cache.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
2
|
+
* SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
3
3
|
* SPDX-License-Identifier: ICU
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -23,6 +23,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
23
23
|
static #instance = null;
|
|
24
24
|
#redisClient = null;
|
|
25
25
|
#isOperational = false;
|
|
26
|
+
#connectionIdentifier = "system-cache";
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* @constructor
|
|
@@ -36,7 +37,8 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
36
37
|
let port = config.getSetting( config.setting.MEMORY_CACHE_REDIS_PORT );
|
|
37
38
|
let db = config.getSetting( config.setting.MEMORY_CACHE_REDIS_DB );
|
|
38
39
|
let authKey = config.getSetting( config.setting.MEMORY_CACHE_AUTH_KEY );
|
|
39
|
-
|
|
40
|
+
let user = config.getSetting( config.setting.MEMORY_CACHE_USER );
|
|
41
|
+
this.#redisClient = redis.createRedisClient( this.#connectionIdentifier, host, port, authKey, user, db );
|
|
40
42
|
this.#redisClient.addConnectionObserver( this );
|
|
41
43
|
CommonMemoryCache.#instance = this;
|
|
42
44
|
}
|
|
@@ -54,6 +56,15 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
54
56
|
*/
|
|
55
57
|
get isOperational() { return this.#isOperational; }
|
|
56
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Property returning the connection identifier of the cache service.
|
|
61
|
+
*
|
|
62
|
+
* @property
|
|
63
|
+
* @returns {string}
|
|
64
|
+
* @public
|
|
65
|
+
*/
|
|
66
|
+
get connectionIdentifier() { return this.#connectionIdentifier; }
|
|
67
|
+
|
|
57
68
|
/**
|
|
58
69
|
* Needs to be invoked by the connection handler when the connection is disrupted.
|
|
59
70
|
*
|
|
@@ -63,7 +74,9 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
63
74
|
* @public
|
|
64
75
|
*/
|
|
65
76
|
onConnectionDisrupted( identifier ) {
|
|
66
|
-
this.#
|
|
77
|
+
if ( identifier === this.#connectionIdentifier ) {
|
|
78
|
+
this.#isOperational = false;
|
|
79
|
+
}
|
|
67
80
|
}
|
|
68
81
|
|
|
69
82
|
/**
|
|
@@ -75,7 +88,9 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
75
88
|
* @public
|
|
76
89
|
*/
|
|
77
90
|
onConnectionRecovered( identifier ) {
|
|
78
|
-
this.#
|
|
91
|
+
if ( identifier === this.#connectionIdentifier ) {
|
|
92
|
+
this.#isOperational = true;
|
|
93
|
+
}
|
|
79
94
|
}
|
|
80
95
|
|
|
81
96
|
/**
|
|
@@ -263,6 +278,32 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
263
278
|
} );
|
|
264
279
|
}
|
|
265
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Used to set expiration in seconds to an existing key.
|
|
283
|
+
* <br/>
|
|
284
|
+
* NOTE: For performance optimization reasons, only use this only if the Redis command does not itself support the 'EX' argument.
|
|
285
|
+
*
|
|
286
|
+
* @method
|
|
287
|
+
* @param {string} key
|
|
288
|
+
* @param {number} seconds
|
|
289
|
+
* @returns {Promise<number>} This will resolve with the seconds as provided initially by the caller.
|
|
290
|
+
* @public
|
|
291
|
+
*/
|
|
292
|
+
expireValue( key, seconds ) {
|
|
293
|
+
return new Promise( ( resolve, reject ) => {
|
|
294
|
+
if ( this.#isOperational === true ) {
|
|
295
|
+
let commandExpire = [ redis.cacheCommands.EXPIRE, key, seconds ];
|
|
296
|
+
this.#redisClient.executeCommands( [ commandExpire ] ).then( () => {
|
|
297
|
+
resolve( seconds );
|
|
298
|
+
} ).catch( ( error ) => {
|
|
299
|
+
reject( error );
|
|
300
|
+
} );
|
|
301
|
+
} else {
|
|
302
|
+
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
303
|
+
}
|
|
304
|
+
} );
|
|
305
|
+
}
|
|
306
|
+
|
|
266
307
|
/**
|
|
267
308
|
* Used to add the specified values to a list.
|
|
268
309
|
*
|
|
@@ -298,7 +339,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
298
339
|
*
|
|
299
340
|
* @method
|
|
300
341
|
* @param {string} key
|
|
301
|
-
* @param {string} value
|
|
342
|
+
* @param {string|Object} value
|
|
302
343
|
* @returns {Promise}
|
|
303
344
|
* @public
|
|
304
345
|
*/
|
|
@@ -501,6 +542,104 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
501
542
|
}
|
|
502
543
|
} );
|
|
503
544
|
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Used to store a JSON variable.
|
|
548
|
+
* <br/>
|
|
549
|
+
* NOTE: Requires ReJSON module installed on server to work.
|
|
550
|
+
*
|
|
551
|
+
* @method
|
|
552
|
+
* @param {string} key
|
|
553
|
+
* @param {Object} value
|
|
554
|
+
* @param {string} [path='&']
|
|
555
|
+
* @param {number} [overrideMode=0] By default this allows full override for existing keys.
|
|
556
|
+
* Option 1 will set the key only if it doesn't already exist. Option 2 will set it only if it already exists.
|
|
557
|
+
* @returns {Promise}
|
|
558
|
+
* @public
|
|
559
|
+
*/
|
|
560
|
+
setJSON( key, value, path = "$", overrideMode = 0 ) {
|
|
561
|
+
return new Promise( ( resolve, reject ) => {
|
|
562
|
+
if ( this.#isOperational === true ) {
|
|
563
|
+
if ( this.#redisClient.isJSONSupported ) {
|
|
564
|
+
let commandArguments = [ redis.cacheCommands.JSON_SET, key, path, tools.stringifyJSON( value ) ];
|
|
565
|
+
if ( overrideMode !== 0 ) {
|
|
566
|
+
commandArguments.push( overrideMode === 1 ? redis.cacheOverrideMode.NX : redis.cacheOverrideMode.XX );
|
|
567
|
+
}
|
|
568
|
+
this.#redisClient.callCommand( commandArguments ).then( () => {
|
|
569
|
+
resolve();
|
|
570
|
+
} ).catch( ( error ) => {
|
|
571
|
+
reject( error );
|
|
572
|
+
} );
|
|
573
|
+
} else {
|
|
574
|
+
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED ), { details: "No RedisJSON module installed on server." } );
|
|
575
|
+
}
|
|
576
|
+
} else {
|
|
577
|
+
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
578
|
+
}
|
|
579
|
+
} );
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Used to fetch a JSON variable.
|
|
584
|
+
* <br/>
|
|
585
|
+
* NOTE: Requires ReJSON module installed on server to work.
|
|
586
|
+
*
|
|
587
|
+
* @method
|
|
588
|
+
* @param {string} key
|
|
589
|
+
* @param {string} path
|
|
590
|
+
* @returns {Promise<Object>}
|
|
591
|
+
* @public
|
|
592
|
+
*/
|
|
593
|
+
getJSON( key, path = "$" ) {
|
|
594
|
+
return new Promise( ( resolve, reject ) => {
|
|
595
|
+
if ( this.#isOperational === true ) {
|
|
596
|
+
if ( this.#redisClient.isJSONSupported ) {
|
|
597
|
+
let commandArguments = [ redis.cacheCommands.JSON_GET, key, path ];
|
|
598
|
+
this.#redisClient.callCommand( commandArguments ).then( ( result ) => {
|
|
599
|
+
resolve( tools.parseJSON( result ) );
|
|
600
|
+
} ).catch( ( error ) => {
|
|
601
|
+
reject( error );
|
|
602
|
+
} );
|
|
603
|
+
} else {
|
|
604
|
+
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED ), { details: "No RedisJSON module installed on server." } );
|
|
605
|
+
}
|
|
606
|
+
} else {
|
|
607
|
+
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
608
|
+
}
|
|
609
|
+
} );
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Used to add an item to a JSON array. That array needs to exist already.
|
|
614
|
+
* <br/>
|
|
615
|
+
* NOTE: Requires ReJSON module installed on server to work.
|
|
616
|
+
*
|
|
617
|
+
* @method
|
|
618
|
+
* @param {string} key
|
|
619
|
+
* @param {Object} value
|
|
620
|
+
* @param {string} path
|
|
621
|
+
* @returns {Promise}
|
|
622
|
+
* @public
|
|
623
|
+
*/
|
|
624
|
+
arrayAppendJSON( key, value, path = "$" ) {
|
|
625
|
+
return new Promise( ( resolve, reject ) => {
|
|
626
|
+
if ( this.#isOperational === true ) {
|
|
627
|
+
if ( this.#redisClient.isJSONSupported ) {
|
|
628
|
+
let commandArguments = [ redis.cacheCommands.JSON_ARRAY_APPEND, key, path, tools.stringifyJSON( value ) ];
|
|
629
|
+
this.#redisClient.callCommand( commandArguments ).then( () => {
|
|
630
|
+
resolve();
|
|
631
|
+
} ).catch( ( error ) => {
|
|
632
|
+
reject( error );
|
|
633
|
+
} );
|
|
634
|
+
} else {
|
|
635
|
+
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED ), { details: "No RedisJSON module installed on server." } );
|
|
636
|
+
}
|
|
637
|
+
} else {
|
|
638
|
+
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
639
|
+
}
|
|
640
|
+
} );
|
|
641
|
+
}
|
|
642
|
+
|
|
504
643
|
}
|
|
505
644
|
|
|
506
645
|
const instance = new CommonMemoryCache();
|
package/utils/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
2
|
+
* SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
3
3
|
* SPDX-License-Identifier: ICU
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -27,6 +27,7 @@ const tools = require( "#tools" );
|
|
|
27
27
|
* @property {EnvironmentVariable} env.TI_MEMORY_CACHE_REDIS_DB
|
|
28
28
|
* @property {EnvironmentVariable} env.TI_MEMORY_CACHE_REDIS_HOST
|
|
29
29
|
* @property {EnvironmentVariable} env.TI_MEMORY_CACHE_REDIS_PORT
|
|
30
|
+
* @property {EnvironmentVariable} env.TI_MEMORY_CACHE_USER
|
|
30
31
|
* @property {EnvironmentVariable} env.TI_MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED
|
|
31
32
|
* @property {EnvironmentVariable} env.TI_MESSAGE_EXCHANGE_SECURITY_HASH_KEY
|
|
32
33
|
* @property {EnvironmentVariable} env.TI_MESSAGE_EXCHANGE_TRACE_LOG_ENABLED
|
|
@@ -67,6 +68,7 @@ const tools = require( "#tools" );
|
|
|
67
68
|
* @property {number} redisDB
|
|
68
69
|
* @property {string} redisHost
|
|
69
70
|
* @property {number} redisPort
|
|
71
|
+
* @property {string} user
|
|
70
72
|
*/
|
|
71
73
|
|
|
72
74
|
/**
|
|
@@ -75,7 +77,9 @@ const tools = require( "#tools" );
|
|
|
75
77
|
* @property {string} messageStore
|
|
76
78
|
* @property {boolean} securityHashEnabled
|
|
77
79
|
* @property {string} securityHashKey
|
|
80
|
+
* @property {number} traceExpirationTime
|
|
78
81
|
* @property {boolean} traceLogEnabled
|
|
82
|
+
* @property {string} traceRepository
|
|
79
83
|
*/
|
|
80
84
|
|
|
81
85
|
/**
|
|
@@ -104,11 +108,14 @@ let settingsEnum = tools.enum( {
|
|
|
104
108
|
MEMORY_CACHE_REDIS_DB: [ "memoryCache.redisDB", "redisDB", "" ],
|
|
105
109
|
MEMORY_CACHE_REDIS_HOST: [ "memoryCache.redisHost", "redisHost", "" ],
|
|
106
110
|
MEMORY_CACHE_REDIS_PORT: [ "memoryCache.redisPort", "redisPort", "" ],
|
|
111
|
+
MEMORY_CACHE_USER: [ "memoryCache.user", "user", "" ],
|
|
107
112
|
MESSAGE_EXCHANGE_QUEUE_PREFIX: [ "messageExchange.messageQueuePrefix", "messageQueuePrefix", "" ],
|
|
108
113
|
MESSAGE_EXCHANGE_MESSAGE_STORE: [ "messageExchange.messageStore", "messageStore", "" ],
|
|
109
114
|
MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED: [ "messageExchange.securityHashEnabled", "securityHashEnabled", "" ],
|
|
110
115
|
MESSAGE_EXCHANGE_SECURITY_HASH_KEY: [ "messageExchange.securityHashKey", "securityHashKey", "" ],
|
|
116
|
+
MESSAGE_EXCHANGE_TRACE_EXPIRATION_TIME: [ "messageExchange.traceExpirationTime", "traceExpirationTime", "" ],
|
|
111
117
|
MESSAGE_EXCHANGE_TRACE_LOG_ENABLED: [ "messageExchange.traceLogEnabled", "traceLogEnabled", "" ],
|
|
118
|
+
MESSAGE_EXCHANGE_TRACE_REPOSITORY: [ "messageExchange.traceRepository", "traceRepository", "" ],
|
|
112
119
|
SERVICE_EXECUTION_TIMEOUT: [ "serviceConfig.executionTimeout", "executionTimeout", "" ],
|
|
113
120
|
SERVICE_HEALTH_CHECK_ADDRESS: [ "serviceConfig.healthCheckAddress", "healthCheckAddress", "" ],
|
|
114
121
|
SERVICE_HEALTH_CHECK_INTERVAL: [ "serviceConfig.healthCheckInterval", "healthCheckInterval", "" ],
|
|
@@ -136,6 +143,7 @@ if ( settings.memoryCache ) {
|
|
|
136
143
|
settings.memoryCache.redisDB = ( process.env.TI_MEMORY_CACHE_REDIS_DB !== undefined ) ? process.env.TI_MEMORY_CACHE_REDIS_DB : settings.memoryCache.redisDB;
|
|
137
144
|
settings.memoryCache.redisHost = ( process.env.TI_MEMORY_CACHE_REDIS_HOST !== undefined ) ? process.env.TI_MEMORY_CACHE_REDIS_HOST : settings.memoryCache.redisHost;
|
|
138
145
|
settings.memoryCache.redisPort = ( process.env.TI_MEMORY_CACHE_REDIS_PORT !== undefined ) ? process.env.TI_MEMORY_CACHE_REDIS_PORT : settings.memoryCache.redisPort;
|
|
146
|
+
settings.memoryCache.user = ( process.env.TI_MEMORY_CACHE_USER !== undefined ) ? process.env.TI_MEMORY_CACHE_USER : settings.memoryCache.user;
|
|
139
147
|
}
|
|
140
148
|
if ( settings.messageExchange ) {
|
|
141
149
|
settings.messageExchange.securityHashEnabled = ( process.env.TI_MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED !== undefined ) ? tools.toBool( process.env.TI_MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED ) : settings.messageExchange.securityHashEnabled;
|
package/utils/exceptions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
2
|
+
* SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
3
3
|
* SPDX-License-Identifier: ICU
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -21,6 +21,7 @@ let exceptionCodeEnum = tools.enum( {
|
|
|
21
21
|
E_GEN_INVALID_SERVICE_DOMAIN_NAME: [ 1003, "invalid service domain name", "Invalid or no service domain name provided at microservice startup." ],
|
|
22
22
|
E_GEN_SYSTEM_CACHE_UNAVAILABLE: [ 1004, "system cache unavailable", "The system cache required for proper engine operation is unavailable." ],
|
|
23
23
|
E_GEN_BAD_SERVICE_HANDLER: [ 1005, "bad service handler", "The provided service handler is not a proper function." ],
|
|
24
|
+
E_GEN_FEATURE_UNSUPPORTED: [ 1006, "feature unsupported", "The requested feature is not supported by current configuration or version." ],
|
|
24
25
|
/** Security & Administration related exceptions - codes under 2xxx */
|
|
25
26
|
E_SEC_INVALID_AUTH_TOKEN: [ 2000, "invalid auth token", "Invalid authorization token provided." ],
|
|
26
27
|
E_SEC_INVALID_EXPIRED_SESSION: [ 2001, "invalid or expired session", "Invalid or expired session encountered." ],
|
package/utils/logger.js
CHANGED
package/utils/tools.js
CHANGED