@ti-engine/core 1.2.4 → 1.3.1
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 +26 -0
- package/bin/localization/labels.json +64 -64
- package/bin/start-instance.js +1 -1
- package/components/connection-observer.js +19 -4
- package/components/exchange/message-exchange.js +16 -1
- package/components/exchange/message-handler.js +37 -7
- package/components/exchange/message-observer.js +51 -4
- package/components/exchange/message-receiver.js +1 -1
- package/components/service-caller.js +247 -146
- package/components/service-executor.js +19 -4
- package/components/service-instance.js +8 -3
- package/integrations/redis-integration.js +86 -23
- package/package.json +1 -1
- package/utils/cache.js +24 -2
- package/utils/exceptions.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
This document will contain the list of changes made to the framework. The format is based on the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
|
4
4
|
|
|
5
|
+
## Version 1.3.1
|
|
6
|
+
* feat(service caller): refactor the entire service call execution flow for clarity and better performance
|
|
7
|
+
* feat(service caller): create a new private class `ServiceCallProcessor` to handle individual service calls in a contained scope
|
|
8
|
+
* feat(message observer): add new property `priority` to the message observer class. It is used to determine the order in which the observers are notified about the messages
|
|
9
|
+
* feat(message observer)!: change method `onMessage` to now return the message it received. This allows for the message to be modified before it is passed to the next observer based on `priority`
|
|
10
|
+
* feat(message handler)!: change method `onMessage` to `notifyMessageObservers` for clarity. It now implements the `MessageObserver` functionality for prioritization and message modification
|
|
11
|
+
* fix(service caller): fix an issue which did not allow for a service call to be marked as completed thus being entered into the trace log as still pending
|
|
12
|
+
* fix(redis integration): remove hardcoded `#retryMaxAttempts` value in `#setupClient` method
|
|
13
|
+
* fix(redis integration): fix an issue which was setting the client status to `DISCONNECTED` during a normal shut down procedure
|
|
14
|
+
|
|
15
|
+
## Version 1.3.0
|
|
16
|
+
* feat(redis integration)!: change `reconnectOnError` behavior to also resubmit the failed command in case the error was of type `READONLY`
|
|
17
|
+
* feat(redis integration): improve the reliability and usage of the event notification mechanism for connection observers
|
|
18
|
+
* feat(redis integration): implement listener to the `end` event on Redis connection to capture when connection can no longer be recovered
|
|
19
|
+
* feat(redis integration): notify connection observers `onConnectionLost` event
|
|
20
|
+
* feat(redis integration): add redis client platform-specific status. It is used internally by the platform and can also be accessed via `redisClient.clientStatus` property
|
|
21
|
+
* feat(connection observer): add new event handler `onConnectionLost` that will be invoked when the observed connection is irrevocably lost
|
|
22
|
+
* feat(cache)!: implement `onConnectionLost` handler that will cause the service instance to immediately stop since it cannot work without the cache
|
|
23
|
+
* fix(service caller): fix multiple promise reject condition when a service call timed out and the service handler still attempted to complete with subsequent error
|
|
24
|
+
* fix(service instance): limit health check reporting to one attempt at a time to avoid unnecessary cache requests and log spam
|
|
25
|
+
* fix(redis integration): fix a multiple promise resolve condition on Redis `reconnect` event
|
|
26
|
+
* fix(redis integration): fix broken event propagation on `disrupted` events to some connection observers
|
|
27
|
+
|
|
28
|
+
## Version 1.2.5
|
|
29
|
+
* feat(cache): extend method `expireValue` to work with has set fields as well
|
|
30
|
+
|
|
5
31
|
## Version 1.2.4
|
|
6
32
|
* feat(cache): expose the cache module as export in `package.json`
|
|
7
33
|
* feat(cache): add method `hashDeleteField` to remove a hash-set field. This implements the `hdel` Redis command
|
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
{
|
|
2
|
-
"labels": {
|
|
3
|
-
"general": {
|
|
4
|
-
"exceptions": {
|
|
5
|
-
"0": {
|
|
6
|
-
"en": "Unidentified error encountered or unrecognized exception code provided."
|
|
7
|
-
},
|
|
8
|
-
"1000": {
|
|
9
|
-
"en": "Error thrown by internal JS source."
|
|
10
|
-
},
|
|
11
|
-
"1001": {
|
|
12
|
-
"en": "Attempt to construct an abstract class detected."
|
|
13
|
-
},
|
|
14
|
-
"1002": {
|
|
15
|
-
"en": "Attempt to call an abstract method detected."
|
|
16
|
-
},
|
|
17
|
-
"1003": {
|
|
18
|
-
"en": "Invalid or no service domain name provided at microservice startup."
|
|
19
|
-
},
|
|
20
|
-
"1004": {
|
|
21
|
-
"en": "The system cache required for proper engine operation is unavailable."
|
|
22
|
-
},
|
|
23
|
-
"1005": {
|
|
24
|
-
"en": "The provided service handler is not a proper function."
|
|
25
|
-
},
|
|
26
|
-
"1006": {
|
|
27
|
-
"en": "The requested feature is not supported by current configuration or version."
|
|
28
|
-
},
|
|
29
|
-
"2000": {
|
|
30
|
-
"en": "Invalid authorization token provided."
|
|
31
|
-
},
|
|
32
|
-
"2001": {
|
|
33
|
-
"en": "Invalid or expired session encountered."
|
|
34
|
-
},
|
|
35
|
-
"2002": {
|
|
36
|
-
"en": "Attempt for unauthorized access detected."
|
|
37
|
-
},
|
|
38
|
-
"2003": {
|
|
39
|
-
"en": "The system detected tampering with the message received via message exchange."
|
|
40
|
-
},
|
|
41
|
-
"3000": {
|
|
42
|
-
"en": "General error during cross-application communication."
|
|
43
|
-
},
|
|
44
|
-
"3001": {
|
|
45
|
-
"en": "The message sender instance is currently unavailable."
|
|
46
|
-
},
|
|
47
|
-
"3002": {
|
|
48
|
-
"en": "The execution of a service could not complete within the allowed timeout."
|
|
49
|
-
},
|
|
50
|
-
"3003": {
|
|
51
|
-
"en": "The specified service is not found in the service registry."
|
|
52
|
-
},
|
|
53
|
-
"3004": {
|
|
54
|
-
"en": "The specified service is not found in the service definition interface."
|
|
55
|
-
},
|
|
56
|
-
"3005": {
|
|
57
|
-
"en": "No handler found in the interface for the specified service or service version."
|
|
58
|
-
},
|
|
59
|
-
"3010": {
|
|
60
|
-
"en": "Connection retry attempts exceeded the configured limit."
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"labels": {
|
|
3
|
+
"general": {
|
|
4
|
+
"exceptions": {
|
|
5
|
+
"0": {
|
|
6
|
+
"en": "Unidentified error encountered or unrecognized exception code provided."
|
|
7
|
+
},
|
|
8
|
+
"1000": {
|
|
9
|
+
"en": "Error thrown by internal JS source."
|
|
10
|
+
},
|
|
11
|
+
"1001": {
|
|
12
|
+
"en": "Attempt to construct an abstract class detected."
|
|
13
|
+
},
|
|
14
|
+
"1002": {
|
|
15
|
+
"en": "Attempt to call an abstract method detected."
|
|
16
|
+
},
|
|
17
|
+
"1003": {
|
|
18
|
+
"en": "Invalid or no service domain name provided at microservice startup."
|
|
19
|
+
},
|
|
20
|
+
"1004": {
|
|
21
|
+
"en": "The system cache required for proper engine operation is unavailable."
|
|
22
|
+
},
|
|
23
|
+
"1005": {
|
|
24
|
+
"en": "The provided service handler is not a proper function."
|
|
25
|
+
},
|
|
26
|
+
"1006": {
|
|
27
|
+
"en": "The requested feature is not supported by current configuration or version."
|
|
28
|
+
},
|
|
29
|
+
"2000": {
|
|
30
|
+
"en": "Invalid authorization token provided."
|
|
31
|
+
},
|
|
32
|
+
"2001": {
|
|
33
|
+
"en": "Invalid or expired session encountered."
|
|
34
|
+
},
|
|
35
|
+
"2002": {
|
|
36
|
+
"en": "Attempt for unauthorized access detected."
|
|
37
|
+
},
|
|
38
|
+
"2003": {
|
|
39
|
+
"en": "The system detected tampering with the message received via message exchange."
|
|
40
|
+
},
|
|
41
|
+
"3000": {
|
|
42
|
+
"en": "General error during cross-application communication."
|
|
43
|
+
},
|
|
44
|
+
"3001": {
|
|
45
|
+
"en": "The message sender instance is currently unavailable."
|
|
46
|
+
},
|
|
47
|
+
"3002": {
|
|
48
|
+
"en": "The execution of a service could not complete within the allowed timeout."
|
|
49
|
+
},
|
|
50
|
+
"3003": {
|
|
51
|
+
"en": "The specified service is not found in the service registry."
|
|
52
|
+
},
|
|
53
|
+
"3004": {
|
|
54
|
+
"en": "The specified service is not found in the service definition interface."
|
|
55
|
+
},
|
|
56
|
+
"3005": {
|
|
57
|
+
"en": "No handler found in the interface for the specified service or service version."
|
|
58
|
+
},
|
|
59
|
+
"3010": {
|
|
60
|
+
"en": "Connection retry attempts exceeded the configured limit."
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
65
|
}
|
package/bin/start-instance.js
CHANGED
|
@@ -151,7 +151,7 @@ try {
|
|
|
151
151
|
setImmediate( () => process.exit( 1 ) );
|
|
152
152
|
} );
|
|
153
153
|
} else {
|
|
154
|
-
logger.log( `Attempting to start a module that does not implement the ServiceInstance abstract class!`, logger.logSeverity.
|
|
154
|
+
logger.log( `Attempting to start a module that does not implement the ServiceInstance abstract class!`, logger.logSeverity.CRITICAL );
|
|
155
155
|
setImmediate( () => process.exit( 1 ) );
|
|
156
156
|
}
|
|
157
157
|
} catch ( error ) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* 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.
|
|
3
|
-
* Copyright © 2021-
|
|
3
|
+
* Copyright © 2021-2025 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
4
4
|
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
5
5
|
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
6
6
|
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
@@ -37,7 +37,8 @@ class ConnectionObserver {
|
|
|
37
37
|
* @virtual
|
|
38
38
|
* @public
|
|
39
39
|
*/
|
|
40
|
-
onConnectionDisrupted( identifier ) {
|
|
40
|
+
onConnectionDisrupted( identifier ) {
|
|
41
|
+
}
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* Needs to be invoked by the connection handler when the connection is recovered.
|
|
@@ -49,8 +50,22 @@ class ConnectionObserver {
|
|
|
49
50
|
* @virtual
|
|
50
51
|
* @public
|
|
51
52
|
*/
|
|
52
|
-
onConnectionRecovered( identifier ) {
|
|
53
|
+
onConnectionRecovered( identifier ) {
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Needs to be invoked by the connection handler when the connection is irrevocably lost.
|
|
58
|
+
* <br/>
|
|
59
|
+
* NOTE: Override this to add custom functionality.
|
|
60
|
+
*
|
|
61
|
+
* @method
|
|
62
|
+
* @param {string} identifier The identifier of the observed connection.
|
|
63
|
+
* @virtual
|
|
64
|
+
* @public
|
|
65
|
+
*/
|
|
66
|
+
onConnectionLost( identifier ) {
|
|
67
|
+
}
|
|
53
68
|
|
|
54
69
|
}
|
|
55
70
|
|
|
56
|
-
module.exports = ConnectionObserver;
|
|
71
|
+
module.exports = ConnectionObserver;
|
|
@@ -74,7 +74,7 @@ class MessageExchange extends MessageObserver {
|
|
|
74
74
|
* @param {string} serviceDomainName The domain name of the microservice using the message exchange.
|
|
75
75
|
*/
|
|
76
76
|
constructor( instanceID, serviceDomainName ) {
|
|
77
|
-
super();
|
|
77
|
+
super( 9 );
|
|
78
78
|
|
|
79
79
|
// make sure this abstract class cannot be instantiated:
|
|
80
80
|
if ( new.target === MessageExchange ) {
|
|
@@ -394,12 +394,25 @@ class MessageExchange extends MessageObserver {
|
|
|
394
394
|
_.pull( this.#disruptedConnections, identifier );
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Used to mark the connection with the provided identifier as disrupted.
|
|
399
|
+
*
|
|
400
|
+
* @method
|
|
401
|
+
* @param {string} identifier The identifier of the observed connection.
|
|
402
|
+
* @override
|
|
403
|
+
* @public
|
|
404
|
+
*/
|
|
405
|
+
onConnectionLost( identifier ) {
|
|
406
|
+
// TODO: implement triggering of a graceful shutdown of the service instance
|
|
407
|
+
}
|
|
408
|
+
|
|
397
409
|
/**
|
|
398
410
|
* Used only for the purposes of the message tracer.
|
|
399
411
|
*
|
|
400
412
|
* @method
|
|
401
413
|
* @param {string} identifier The identifier of the observed connection.
|
|
402
414
|
* @param {Message} message The message for processing.
|
|
415
|
+
* @returns {Message} The message that was received.
|
|
403
416
|
* @override
|
|
404
417
|
* @public
|
|
405
418
|
*/
|
|
@@ -411,6 +424,8 @@ class MessageExchange extends MessageObserver {
|
|
|
411
424
|
} else if ( MessageExchange.connectionNameResponsesIn === identifier ) {
|
|
412
425
|
messageTracer.instance.recordTraceEntry( message, messageTracer.messageType.MESSAGE_RESPONSE, messageTracer.dispatchEvent.RECEIVED, messageTracer.messageState.PROCESSED );
|
|
413
426
|
}
|
|
427
|
+
|
|
428
|
+
return message;
|
|
414
429
|
}
|
|
415
430
|
|
|
416
431
|
/**
|
|
@@ -54,7 +54,9 @@ class MessageHandler extends ConnectionObserver {
|
|
|
54
54
|
* @returns {boolean}
|
|
55
55
|
* @public
|
|
56
56
|
*/
|
|
57
|
-
get isAvailable() {
|
|
57
|
+
get isAvailable() {
|
|
58
|
+
return this.#isAvailable;
|
|
59
|
+
}
|
|
58
60
|
|
|
59
61
|
/**
|
|
60
62
|
* Used to set the isAvailable flag.
|
|
@@ -65,7 +67,9 @@ class MessageHandler extends ConnectionObserver {
|
|
|
65
67
|
* @param {boolean} value
|
|
66
68
|
* @public
|
|
67
69
|
*/
|
|
68
|
-
set isAvailable( value ) {
|
|
70
|
+
set isAvailable( value ) {
|
|
71
|
+
this.#isAvailable = value;
|
|
72
|
+
}
|
|
69
73
|
|
|
70
74
|
/**
|
|
71
75
|
* Returns the connection identifier.
|
|
@@ -74,7 +78,9 @@ class MessageHandler extends ConnectionObserver {
|
|
|
74
78
|
* @returns {string}
|
|
75
79
|
* @public
|
|
76
80
|
*/
|
|
77
|
-
get connectionIdentifier() {
|
|
81
|
+
get connectionIdentifier() {
|
|
82
|
+
return this.#connectionIdentifier;
|
|
83
|
+
}
|
|
78
84
|
|
|
79
85
|
/**
|
|
80
86
|
* Used to initialize and enable the communication capabilities of the handler.
|
|
@@ -128,9 +134,9 @@ class MessageHandler extends ConnectionObserver {
|
|
|
128
134
|
*/
|
|
129
135
|
addMessageObserver( messageObserver ) {
|
|
130
136
|
const MessageObserver = require( "#message-observer" );
|
|
131
|
-
|
|
132
137
|
if ( messageObserver instanceof MessageObserver ) {
|
|
133
138
|
this.#messageObservers.push( messageObserver );
|
|
139
|
+
this.#messageObservers = _.orderBy( this.#messageObservers, [ "priority" ], [ "desc" ] );
|
|
134
140
|
} else {
|
|
135
141
|
logger.log( `Attempting to add '${ messageObserver.constructor.name }' as message observer but it's not a child-class of 'MessageObserver'!`, logger.logSeverity.WARNING );
|
|
136
142
|
}
|
|
@@ -138,19 +144,23 @@ class MessageHandler extends ConnectionObserver {
|
|
|
138
144
|
|
|
139
145
|
/**
|
|
140
146
|
* An event-triggered method that will notify any observers about a new message for handling.
|
|
147
|
+
* <br/>
|
|
148
|
+
* NOTE: Each observer will be notified in the order of their priority via their {@link MessageObserver.onMessage} method. Additionally, the message will be
|
|
149
|
+
* passed through each observer in the order of their priority. If the observer returns a modified message, it will be used instead of the original message!
|
|
141
150
|
*
|
|
142
151
|
* @method
|
|
143
152
|
* @param {Message} message
|
|
144
153
|
* @public
|
|
145
154
|
*/
|
|
146
|
-
|
|
155
|
+
notifyMessageObservers( message ) {
|
|
156
|
+
let modifiedMessage = message;
|
|
147
157
|
_.forEach( this.#messageObservers, ( messageObserver ) => {
|
|
148
|
-
messageObserver.onMessage( this.#connectionIdentifier,
|
|
158
|
+
modifiedMessage = messageObserver.onMessage( this.#connectionIdentifier, modifiedMessage );
|
|
149
159
|
} );
|
|
150
160
|
}
|
|
151
161
|
|
|
152
162
|
/**
|
|
153
|
-
* An event-triggered method that will notify any observers about primary connection recovered state.
|
|
163
|
+
* An event-triggered method that will notify any observers about the primary connection recovered state.
|
|
154
164
|
* <br/>
|
|
155
165
|
* NOTE: You can override this to add custom functionality but make sure to also call the base method
|
|
156
166
|
* using: super.onConnectionRecovered( identifier )
|
|
@@ -189,6 +199,26 @@ class MessageHandler extends ConnectionObserver {
|
|
|
189
199
|
}
|
|
190
200
|
}
|
|
191
201
|
|
|
202
|
+
/**
|
|
203
|
+
* An event-triggered method that will notify any observers about the primary connection having been lost.
|
|
204
|
+
* <br/>
|
|
205
|
+
* NOTE: You can override this to add custom functionality but make sure to also call the base method
|
|
206
|
+
* using: super.onConnectionLost( identifier )
|
|
207
|
+
*
|
|
208
|
+
* @method
|
|
209
|
+
* @param {string} identifier The identifier of the observed connection.
|
|
210
|
+
* @override
|
|
211
|
+
* @private
|
|
212
|
+
*/
|
|
213
|
+
onConnectionLost( identifier ) {
|
|
214
|
+
if ( identifier === this.#connectionIdentifier ) {
|
|
215
|
+
this.#isAvailable = false;
|
|
216
|
+
_.forEach( this.#messageObservers, ( messageObserver ) => {
|
|
217
|
+
messageObserver.onConnectionLost( this.#connectionIdentifier );
|
|
218
|
+
} );
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
192
222
|
}
|
|
193
223
|
|
|
194
224
|
module.exports = MessageHandler;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* 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.
|
|
3
|
-
* Copyright © 2021-
|
|
3
|
+
* Copyright © 2021-2025 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
4
4
|
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
5
5
|
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
6
6
|
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const ConnectionObserver = require( "#connection-observer" );
|
|
10
|
+
const _ = require( "lodash" );
|
|
10
11
|
const exceptions = require( "#exceptions" );
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -21,16 +22,45 @@ const exceptions = require( "#exceptions" );
|
|
|
21
22
|
*/
|
|
22
23
|
class MessageObserver extends ConnectionObserver {
|
|
23
24
|
|
|
25
|
+
#priority = 0;
|
|
26
|
+
|
|
24
27
|
/**
|
|
28
|
+
* @param {number} [priority=0] The priority of this observer. Higher values indicate higher priority.
|
|
25
29
|
* @constructor
|
|
26
30
|
*/
|
|
27
|
-
constructor() {
|
|
31
|
+
constructor( priority = 0 ) {
|
|
28
32
|
super();
|
|
29
33
|
|
|
30
34
|
// make sure this abstract class cannot be instantiated:
|
|
31
35
|
if ( new.target === MessageObserver ) {
|
|
32
36
|
throw exceptions.raise( exceptions.exceptionCode.E_GEN_ABSTRACT_CLASS_INIT, { name: this.constructor.name } );
|
|
33
37
|
}
|
|
38
|
+
|
|
39
|
+
this.#priority = _.isNumber( priority ) ? priority : 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns the priority of this observer.
|
|
44
|
+
* <br/>
|
|
45
|
+
* NOTE: Higher values indicate higher priority.
|
|
46
|
+
*
|
|
47
|
+
* @property
|
|
48
|
+
* @returns {number}
|
|
49
|
+
*/
|
|
50
|
+
get priority() {
|
|
51
|
+
return this.#priority;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Used to set the priority of this observer.
|
|
56
|
+
* <br/>
|
|
57
|
+
* NOTE: Higher values indicate higher priority.
|
|
58
|
+
*
|
|
59
|
+
* @property
|
|
60
|
+
* @param {number} value
|
|
61
|
+
*/
|
|
62
|
+
set priority( value ) {
|
|
63
|
+
this.#priority = _.isNumber( value ) ? value : 0;
|
|
34
64
|
}
|
|
35
65
|
|
|
36
66
|
/**
|
|
@@ -41,10 +71,13 @@ class MessageObserver extends ConnectionObserver {
|
|
|
41
71
|
* @method
|
|
42
72
|
* @param {string} identifier The identifier of the observed connection.
|
|
43
73
|
* @param {Message} message The message for processing.
|
|
74
|
+
* @returns {Message} The message that was received.
|
|
44
75
|
* @virtual
|
|
45
76
|
* @public
|
|
46
77
|
*/
|
|
47
|
-
onMessage( identifier, message ) {
|
|
78
|
+
onMessage( identifier, message ) {
|
|
79
|
+
return message;
|
|
80
|
+
}
|
|
48
81
|
|
|
49
82
|
/**
|
|
50
83
|
* Needs to be invoked by the connection handler when the connection is disrupted.
|
|
@@ -74,6 +107,20 @@ class MessageObserver extends ConnectionObserver {
|
|
|
74
107
|
super.onConnectionRecovered( identifier );
|
|
75
108
|
}
|
|
76
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Needs to be invoked by the connection handler when the connection is irrevocably lost.
|
|
112
|
+
* <br/>
|
|
113
|
+
* NOTE: Override this to add custom functionality.
|
|
114
|
+
*
|
|
115
|
+
* @method
|
|
116
|
+
* @param {string} identifier The identifier of the observed connection.
|
|
117
|
+
* @virtual
|
|
118
|
+
* @public
|
|
119
|
+
*/
|
|
120
|
+
onConnectionLost( identifier ) {
|
|
121
|
+
super.onConnectionLost( identifier );
|
|
122
|
+
}
|
|
123
|
+
|
|
77
124
|
}
|
|
78
125
|
|
|
79
|
-
module.exports = MessageObserver;
|
|
126
|
+
module.exports = MessageObserver;
|
|
@@ -117,7 +117,7 @@ class MessageReceiver extends MessageHandler {
|
|
|
117
117
|
this.onReceive().then( ( message ) => {
|
|
118
118
|
return this.#postReceive( message );
|
|
119
119
|
} ).then( ( message ) => {
|
|
120
|
-
this.
|
|
120
|
+
this.notifyMessageObservers( message );
|
|
121
121
|
} ).catch( ( error ) => {
|
|
122
122
|
if ( error.code !== exceptions.exceptionCode.E_COM_MESSAGE_RECEIVER_UNAVAILABLE ) {
|
|
123
123
|
logger.log( `Error while trying to receive the next pending message from memory cache in receiver '${ this.connectionIdentifier }'! Resuming operation...`, logger.logSeverity.ERROR, error );
|
|
@@ -50,13 +50,215 @@ const messageDispatcher = require( "#message-dispatcher" );
|
|
|
50
50
|
* @typedef {Object} ServiceCallResult
|
|
51
51
|
* @property {Object|undefined} exception If there was exception during the service call processing, it will be set here. Otherwise, it will be 'undefined'.
|
|
52
52
|
* @property {boolean} isSuccessful A flag indicating if this service call can be considered successful or not.
|
|
53
|
-
* @property {Object|string|undefined} payload The payload containing the results from the service call processing. If string it is ID of the payload in the memory cache instead.
|
|
53
|
+
* @property {Object|string|undefined} payload The payload containing the results from the service call processing. If a string, it is ID of the payload in the memory cache instead.
|
|
54
54
|
*/
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
|
-
* @
|
|
58
|
-
*
|
|
57
|
+
* Used to assemble and prepare a new {@link ServiceCall} object.
|
|
58
|
+
*
|
|
59
|
+
* @method
|
|
60
|
+
* @param {string} messageID The message ID of the service call. This has to be unique across the whole service call tree, including the current service call, and will be used to identify the service call in the service call tree.
|
|
61
|
+
* @param {ServiceAddress} serviceAddress The service address has to define a valid service domain name, service alias, and optionally a service version.
|
|
62
|
+
* @param {Object} serviceParams Set of named parameters to provide to the called service.
|
|
63
|
+
* @param {ServiceExecContext} serviceExecContext The context in which the service call is being executed.
|
|
64
|
+
* @returns {Promise<ServiceCall>}
|
|
65
|
+
* @private
|
|
59
66
|
*/
|
|
67
|
+
let prepareServiceCall = ( messageID, serviceAddress, serviceParams, serviceExecContext ) => {
|
|
68
|
+
return new Promise( ( resolve ) => {
|
|
69
|
+
const ServiceInstance = require( "#service-instance" );
|
|
70
|
+
|
|
71
|
+
// assemble the new service call:
|
|
72
|
+
let chainID = ( serviceExecContext.previousServiceCall ) ? serviceExecContext.previousServiceCall.chainID : tools.getUUID();
|
|
73
|
+
let chainLevel = ( serviceExecContext.previousServiceCall ) ? serviceExecContext.previousServiceCall.chainLevel + 1 : 0;
|
|
74
|
+
let source = {
|
|
75
|
+
instanceID: ServiceInstance.instanceID,
|
|
76
|
+
route: ServiceInstance.serviceDomainName
|
|
77
|
+
};
|
|
78
|
+
let destination = {
|
|
79
|
+
instanceID: undefined,
|
|
80
|
+
route: serviceAddress.serviceDomainName
|
|
81
|
+
};
|
|
82
|
+
/** @type ServiceCall */
|
|
83
|
+
let serviceCall = {
|
|
84
|
+
authToken: serviceExecContext.authToken,
|
|
85
|
+
chainID: chainID,
|
|
86
|
+
chainLevel: chainLevel,
|
|
87
|
+
createdOn: Date.now(),
|
|
88
|
+
destination: destination,
|
|
89
|
+
executionTime: 0,
|
|
90
|
+
exception: undefined,
|
|
91
|
+
finishedOn: undefined,
|
|
92
|
+
isCompleted: false,
|
|
93
|
+
isSuccessful: undefined,
|
|
94
|
+
messageID: messageID,
|
|
95
|
+
payload: undefined,
|
|
96
|
+
predecessor: ( serviceExecContext.previousServiceCall ) ? serviceExecContext.previousServiceCall.messageID : undefined,
|
|
97
|
+
serviceAddress: serviceAddress,
|
|
98
|
+
serviceParams: serviceParams,
|
|
99
|
+
source: source,
|
|
100
|
+
successors: undefined
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
resolve( serviceCall );
|
|
104
|
+
} );
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Used to verify if the service is registered in the service registry.
|
|
109
|
+
*
|
|
110
|
+
* @method
|
|
111
|
+
* @param {ServiceAddress} serviceAddress
|
|
112
|
+
* @returns {Promise}
|
|
113
|
+
* @private
|
|
114
|
+
*/
|
|
115
|
+
let findServiceInRegistry = ( serviceAddress ) => {
|
|
116
|
+
return new Promise( ( resolve, reject ) => {
|
|
117
|
+
let serviceCatalog = config.getSetting( config.setting.SERVICE_REGISTRY_ADDRESS ) + serviceAddress.serviceDomainName;
|
|
118
|
+
cache.instance.isSetMember( serviceCatalog, serviceAddress.serviceAlias ).then( ( result ) => {
|
|
119
|
+
if ( result === true ) {
|
|
120
|
+
resolve();
|
|
121
|
+
} else {
|
|
122
|
+
reject( exceptions.raise( exceptions.exceptionCode.E_COM_SERVICE_NOT_REGISTERED ) );
|
|
123
|
+
}
|
|
124
|
+
} ).catch( ( error ) => {
|
|
125
|
+
reject( exceptions.raise( error ) );
|
|
126
|
+
} );
|
|
127
|
+
} );
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A class defining a service call processor.
|
|
132
|
+
*
|
|
133
|
+
* @class ServiceCallProcessor
|
|
134
|
+
* @private
|
|
135
|
+
*/
|
|
136
|
+
class ServiceCallProcessor {
|
|
137
|
+
|
|
138
|
+
#messageID;
|
|
139
|
+
#serviceAddress;
|
|
140
|
+
#serviceParams;
|
|
141
|
+
#serviceExecContext;
|
|
142
|
+
#timeoutHandle;
|
|
143
|
+
#taskCompletionHandler;
|
|
144
|
+
#isProcessed = false;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @constructor
|
|
148
|
+
* @param {ServiceAddress} serviceAddress The service address has to define a valid service domain name, service alias, and optionally a service version.
|
|
149
|
+
* @param {Object} serviceParams Set of named parameters to provide to the called service.
|
|
150
|
+
* @param {ServiceExecContext} serviceExecContext The context in which the service call is being executed.
|
|
151
|
+
* @returns {ServiceCallProcessor}
|
|
152
|
+
*/
|
|
153
|
+
constructor( serviceAddress, serviceParams, serviceExecContext ) {
|
|
154
|
+
this.#messageID = tools.getUUID();
|
|
155
|
+
this.#serviceAddress = serviceAddress;
|
|
156
|
+
this.#serviceParams = serviceParams;
|
|
157
|
+
this.#serviceExecContext = serviceExecContext;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* Public interface */
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The unique identifier of the service call.
|
|
164
|
+
*
|
|
165
|
+
* @property
|
|
166
|
+
* @returns {string}
|
|
167
|
+
* @public
|
|
168
|
+
*/
|
|
169
|
+
get messageID() {
|
|
170
|
+
return this.#messageID;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Used to start the execution of the service call.
|
|
175
|
+
* <br/>
|
|
176
|
+
* NOTE: This method will time out after specific preconfigured time, in which case it will resolve with {@link E_COM_SERVICE_EXEC_TIMEOUT} error.
|
|
177
|
+
*
|
|
178
|
+
* @method
|
|
179
|
+
* @returns {Promise<ServiceCallResult>}
|
|
180
|
+
* @public
|
|
181
|
+
*/
|
|
182
|
+
process() {
|
|
183
|
+
return Promise.race( [ this.#execute(), this.#timeout() ] ).then( ( result ) => {
|
|
184
|
+
clearTimeout( this.#timeoutHandle );
|
|
185
|
+
this.#isProcessed = true;
|
|
186
|
+
return result;
|
|
187
|
+
} ).catch( ( error ) => {
|
|
188
|
+
clearTimeout( this.#timeoutHandle );
|
|
189
|
+
this.#isProcessed = true;
|
|
190
|
+
logger.log( `Error during service call execution!`, logger.logSeverity.ERROR, error );
|
|
191
|
+
return {
|
|
192
|
+
isSuccessful: false,
|
|
193
|
+
exception: exceptions.raise( error ),
|
|
194
|
+
payload: undefined
|
|
195
|
+
};
|
|
196
|
+
} );
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Used to complete the execution of the service call that was started within the {@link process} method.
|
|
201
|
+
*
|
|
202
|
+
* @method
|
|
203
|
+
* @param {ServiceCall} serviceCall
|
|
204
|
+
* @public
|
|
205
|
+
*/
|
|
206
|
+
complete( serviceCall ) {
|
|
207
|
+
if ( this.#isProcessed !== true ) {
|
|
208
|
+
let serviceCallResult = {
|
|
209
|
+
exception: serviceCall.exception,
|
|
210
|
+
isSuccessful: ( serviceCall.isSuccessful !== undefined ) ? tools.toBool( serviceCall.isSuccessful ) : true,
|
|
211
|
+
payload: serviceCall.payload
|
|
212
|
+
};
|
|
213
|
+
this.#taskCompletionHandler( serviceCallResult );
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* Private interface */
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Used to execute the service call.
|
|
221
|
+
*
|
|
222
|
+
* @method
|
|
223
|
+
* @returns {Promise<ServiceCallResult>}
|
|
224
|
+
* @private
|
|
225
|
+
*/
|
|
226
|
+
#execute() {
|
|
227
|
+
return new Promise( ( resolve, reject ) => {
|
|
228
|
+
findServiceInRegistry( this.#serviceAddress ).then( () => {
|
|
229
|
+
return prepareServiceCall( this.#messageID, this.#serviceAddress, this.#serviceParams, this.#serviceExecContext );
|
|
230
|
+
} ).then( ( serviceCall ) => {
|
|
231
|
+
return messageDispatcher.instance.sendRequest( serviceCall );
|
|
232
|
+
} ).then( () => {
|
|
233
|
+
this.#taskCompletionHandler = ( serviceCallResult ) => {
|
|
234
|
+
resolve( serviceCallResult );
|
|
235
|
+
};
|
|
236
|
+
} ).catch( ( error ) => {
|
|
237
|
+
if ( this.#isProcessed !== true ) {
|
|
238
|
+
reject( exceptions.raise( error ) );
|
|
239
|
+
}
|
|
240
|
+
} );
|
|
241
|
+
} );
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Used to time out the service call execution.
|
|
246
|
+
*
|
|
247
|
+
* @method
|
|
248
|
+
* @returns {Promise<ServiceCallResult>}
|
|
249
|
+
* @private
|
|
250
|
+
*/
|
|
251
|
+
#timeout() {
|
|
252
|
+
return new Promise( ( resolve, reject ) => {
|
|
253
|
+
this.#timeoutHandle = setTimeout( () => {
|
|
254
|
+
if ( this.#isProcessed !== true ) {
|
|
255
|
+
reject( exceptions.raise( exceptions.exceptionCode.E_COM_SERVICE_EXEC_TIMEOUT ) );
|
|
256
|
+
}
|
|
257
|
+
}, config.getSetting( config.setting.SERVICE_EXECUTION_TIMEOUT ) );
|
|
258
|
+
} );
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
}
|
|
60
262
|
|
|
61
263
|
/**
|
|
62
264
|
* A class defining a service caller behavior.
|
|
@@ -67,13 +269,13 @@ const messageDispatcher = require( "#message-dispatcher" );
|
|
|
67
269
|
*/
|
|
68
270
|
class ServiceCaller extends MessageObserver {
|
|
69
271
|
|
|
70
|
-
#
|
|
272
|
+
#serviceCallProcessors = {};
|
|
71
273
|
|
|
72
274
|
/**
|
|
73
275
|
* @constructor
|
|
74
276
|
*/
|
|
75
277
|
constructor() {
|
|
76
|
-
super();
|
|
278
|
+
super( 10 );
|
|
77
279
|
}
|
|
78
280
|
|
|
79
281
|
/* Public interface */
|
|
@@ -91,44 +293,14 @@ class ServiceCaller extends MessageObserver {
|
|
|
91
293
|
* @public
|
|
92
294
|
*/
|
|
93
295
|
executeServiceCall( serviceAddress, serviceParams, serviceExecContext ) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
this.#addTaskHandler( messageID, ( serviceCall ) => {
|
|
101
|
-
this.#completeServiceCall( serviceCall ).then( ( serviceCall ) => {
|
|
102
|
-
let serviceCallResult = {
|
|
103
|
-
exception: serviceCall.exception,
|
|
104
|
-
isSuccessful: ( serviceCall.isSuccessful !== undefined ) ? serviceCall.isSuccessful : true,
|
|
105
|
-
payload: serviceCall.payload
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
resolve( serviceCallResult );
|
|
109
|
-
} ).catch( ( error ) => {
|
|
110
|
-
reject( exceptions.raise( error ) );
|
|
111
|
-
} );
|
|
112
|
-
} );
|
|
113
|
-
} ).catch( ( error ) => {
|
|
114
|
-
reject( exceptions.raise( error ) );
|
|
296
|
+
return new Promise( ( resolve, reject ) => {
|
|
297
|
+
let processor = new ServiceCallProcessor( serviceAddress, serviceParams, serviceExecContext );
|
|
298
|
+
this.#addProcessor( processor.messageID, processor );
|
|
299
|
+
processor.process().then( ( serviceCallResult ) => {
|
|
300
|
+
this.#removeProcessor( processor.messageID );
|
|
301
|
+
resolve( serviceCallResult );
|
|
115
302
|
} );
|
|
116
303
|
} );
|
|
117
|
-
let timeoutHandle;
|
|
118
|
-
let timeout = new Promise( ( resolve, reject ) => {
|
|
119
|
-
timeoutHandle = setTimeout( () => reject( exceptions.raise( exceptions.exceptionCode.E_COM_SERVICE_EXEC_TIMEOUT ) ), config.getSetting( config.setting.SERVICE_EXECUTION_TIMEOUT ) );
|
|
120
|
-
} );
|
|
121
|
-
|
|
122
|
-
return Promise.race( [ execution, timeout ] ).then( ( result ) => {
|
|
123
|
-
clearTimeout( timeoutHandle );
|
|
124
|
-
return result;
|
|
125
|
-
} ).catch( ( error ) => {
|
|
126
|
-
logger.log( `Error during service call execution!`, logger.logSeverity.ERROR, error );
|
|
127
|
-
return {
|
|
128
|
-
isSuccessful: false,
|
|
129
|
-
exception: exceptions.raise( error )
|
|
130
|
-
};
|
|
131
|
-
} );
|
|
132
304
|
}
|
|
133
305
|
|
|
134
306
|
/**
|
|
@@ -136,18 +308,26 @@ class ServiceCaller extends MessageObserver {
|
|
|
136
308
|
*
|
|
137
309
|
* @method
|
|
138
310
|
* @param {string} identifier The identifier of the observed connection.
|
|
139
|
-
* @param {
|
|
311
|
+
* @param {ServiceCall} serviceCall The service call message for processing.
|
|
312
|
+
* @returns {ServiceCall} The service call message that was received.
|
|
140
313
|
* @override
|
|
141
314
|
* @public
|
|
142
315
|
*/
|
|
143
|
-
onMessage( identifier,
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
316
|
+
onMessage( identifier, serviceCall ) {
|
|
317
|
+
// Complete the service call:
|
|
318
|
+
serviceCall.finishedOn = Date.now();
|
|
319
|
+
serviceCall.executionTime = serviceCall.finishedOn - serviceCall.createdOn;
|
|
320
|
+
serviceCall.isCompleted = true;
|
|
321
|
+
|
|
322
|
+
let processor = this.#getProcessor( serviceCall.messageID );
|
|
323
|
+
if ( processor ) {
|
|
324
|
+
this.#removeProcessor( serviceCall.messageID );
|
|
325
|
+
processor.complete( serviceCall );
|
|
148
326
|
} else {
|
|
149
|
-
logger.log( `Received message with ID '${
|
|
327
|
+
logger.log( `Received service call message with ID '${ serviceCall.messageID }' without registered processor! This may be caused by a service call timeout.`, logger.logSeverity.DEBUG, serviceCall.exception || undefined );
|
|
150
328
|
}
|
|
329
|
+
|
|
330
|
+
return serviceCall;
|
|
151
331
|
}
|
|
152
332
|
|
|
153
333
|
/**
|
|
@@ -174,133 +354,54 @@ class ServiceCaller extends MessageObserver {
|
|
|
174
354
|
super.onConnectionRecovered( identifier );
|
|
175
355
|
}
|
|
176
356
|
|
|
177
|
-
/* Private interface */
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Used to verify if the service is registered in the service registry.
|
|
181
|
-
*
|
|
182
|
-
* @method
|
|
183
|
-
* @param {ServiceAddress} serviceAddress
|
|
184
|
-
* @returns {Promise}
|
|
185
|
-
* @private
|
|
186
|
-
*/
|
|
187
|
-
#findServiceInRegistry( serviceAddress ) {
|
|
188
|
-
return new Promise( ( resolve, reject ) => {
|
|
189
|
-
let serviceCatalog = config.getSetting( config.setting.SERVICE_REGISTRY_ADDRESS ) + serviceAddress.serviceDomainName;
|
|
190
|
-
cache.instance.isSetMember( serviceCatalog, serviceAddress.serviceAlias ).then( ( result ) => {
|
|
191
|
-
if ( result === true ) {
|
|
192
|
-
resolve();
|
|
193
|
-
} else {
|
|
194
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_COM_SERVICE_NOT_REGISTERED ) );
|
|
195
|
-
}
|
|
196
|
-
} ).catch( ( error ) => {
|
|
197
|
-
reject( exceptions.raise( error ) );
|
|
198
|
-
} );
|
|
199
|
-
} );
|
|
200
|
-
}
|
|
201
|
-
|
|
202
357
|
/**
|
|
203
|
-
*
|
|
358
|
+
* Needs to be invoked by the connection handler when the connection is irrevocably lost.
|
|
204
359
|
*
|
|
205
360
|
* @method
|
|
206
|
-
* @param {
|
|
207
|
-
* @
|
|
208
|
-
* @
|
|
209
|
-
* @returns {Promise<ServiceCall>}
|
|
210
|
-
* @private
|
|
361
|
+
* @param {string} identifier The identifier of the observed connection.
|
|
362
|
+
* @override
|
|
363
|
+
* @public
|
|
211
364
|
*/
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const ServiceInstance = require( "#service-instance" );
|
|
215
|
-
|
|
216
|
-
// assemble the new service call:
|
|
217
|
-
let chainID = ( serviceExecContext.previousServiceCall ) ? serviceExecContext.previousServiceCall.chainID : tools.getUUID();
|
|
218
|
-
let chainLevel = ( serviceExecContext.previousServiceCall ) ? serviceExecContext.previousServiceCall.chainLevel + 1 : 0;
|
|
219
|
-
let source = {
|
|
220
|
-
instanceID: ServiceInstance.instanceID,
|
|
221
|
-
route: ServiceInstance.serviceDomainName
|
|
222
|
-
};
|
|
223
|
-
let destination = {
|
|
224
|
-
instanceID: undefined,
|
|
225
|
-
route: serviceAddress.serviceDomainName
|
|
226
|
-
};
|
|
227
|
-
/** @type ServiceCall */
|
|
228
|
-
let serviceCall = {
|
|
229
|
-
authToken: serviceExecContext.authToken,
|
|
230
|
-
chainID: chainID,
|
|
231
|
-
chainLevel: chainLevel,
|
|
232
|
-
createdOn: Date.now(),
|
|
233
|
-
destination: destination,
|
|
234
|
-
executionTime: 0,
|
|
235
|
-
exception: undefined,
|
|
236
|
-
finishedOn: undefined,
|
|
237
|
-
isCompleted: false,
|
|
238
|
-
isSuccessful: undefined,
|
|
239
|
-
messageID: tools.getUUID(),
|
|
240
|
-
payload: undefined,
|
|
241
|
-
predecessor: ( serviceExecContext.previousServiceCall ) ? serviceExecContext.previousServiceCall.messageID : undefined,
|
|
242
|
-
serviceAddress: serviceAddress,
|
|
243
|
-
serviceParams: serviceParams,
|
|
244
|
-
source: source,
|
|
245
|
-
successors: undefined
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
resolve( serviceCall );
|
|
249
|
-
} );
|
|
365
|
+
onConnectionLost( identifier ) {
|
|
366
|
+
super.onConnectionLost( identifier );
|
|
250
367
|
}
|
|
251
368
|
|
|
252
|
-
|
|
253
|
-
* Used to complete the provided {@link ServiceCall} object by setting all required properties to their correct values.
|
|
254
|
-
*
|
|
255
|
-
* @method
|
|
256
|
-
* @param {ServiceCall} serviceCall
|
|
257
|
-
* @returns {Promise<ServiceCall>}
|
|
258
|
-
* @private
|
|
259
|
-
*/
|
|
260
|
-
#completeServiceCall( serviceCall ) {
|
|
261
|
-
return new Promise( ( resolve ) => {
|
|
262
|
-
serviceCall.finishedOn = Date.now();
|
|
263
|
-
serviceCall.executionTime = serviceCall.finishedOn - serviceCall.createdOn;
|
|
264
|
-
serviceCall.isCompleted = true;
|
|
265
|
-
|
|
266
|
-
resolve( serviceCall );
|
|
267
|
-
} );
|
|
268
|
-
}
|
|
369
|
+
/* Private interface */
|
|
269
370
|
|
|
270
371
|
/**
|
|
271
372
|
* Used to add a new task handler to the list of current tasks.
|
|
272
373
|
*
|
|
273
374
|
* @method
|
|
274
|
-
* @param {string}
|
|
275
|
-
* @param {
|
|
375
|
+
* @param {string} messageID
|
|
376
|
+
* @param {ServiceCallProcessor} processor
|
|
276
377
|
* @private
|
|
277
378
|
*/
|
|
278
|
-
#
|
|
279
|
-
this.#
|
|
379
|
+
#addProcessor( messageID, processor ) {
|
|
380
|
+
this.#serviceCallProcessors[ messageID ] = processor;
|
|
280
381
|
}
|
|
281
382
|
|
|
282
383
|
/**
|
|
283
384
|
* Used to fetch a task handler from the list of current tasks.
|
|
284
385
|
*
|
|
285
386
|
* @method
|
|
286
|
-
* @param {string}
|
|
287
|
-
* @returns {
|
|
387
|
+
* @param {string} messageID
|
|
388
|
+
* @returns {ServiceCallProcessor}
|
|
288
389
|
* @private
|
|
289
390
|
*/
|
|
290
|
-
#
|
|
291
|
-
return this.#
|
|
391
|
+
#getProcessor( messageID ) {
|
|
392
|
+
return this.#serviceCallProcessors[ messageID ];
|
|
292
393
|
}
|
|
293
394
|
|
|
294
395
|
/**
|
|
295
396
|
* Used to remove a task handler from the list of current tasks.
|
|
296
397
|
*
|
|
297
398
|
* @method
|
|
298
|
-
* @param {string}
|
|
399
|
+
* @param {string} messageID
|
|
299
400
|
* @private
|
|
300
401
|
*/
|
|
301
|
-
#
|
|
302
|
-
if ( this.#
|
|
303
|
-
delete this.#
|
|
402
|
+
#removeProcessor( messageID ) {
|
|
403
|
+
if ( this.#serviceCallProcessors[ messageID ] ) {
|
|
404
|
+
delete this.#serviceCallProcessors[ messageID ];
|
|
304
405
|
}
|
|
305
406
|
}
|
|
306
407
|
|
|
@@ -91,16 +91,19 @@ class ServiceExecutor extends MessageObserver {
|
|
|
91
91
|
*
|
|
92
92
|
* @method
|
|
93
93
|
* @param {string} identifier The identifier of the observed connection.
|
|
94
|
-
* @param {
|
|
94
|
+
* @param {ServiceCall} serviceCall The service call message for processing.
|
|
95
|
+
* @returns {ServiceCall} The service call message that was received.
|
|
95
96
|
* @override
|
|
96
97
|
* @public
|
|
97
98
|
*/
|
|
98
|
-
onMessage( identifier,
|
|
99
|
-
this.#processServiceCall(
|
|
99
|
+
onMessage( identifier, serviceCall ) {
|
|
100
|
+
this.#processServiceCall( serviceCall ).then( ( serviceCall ) => {
|
|
100
101
|
return messageDispatcher.instance.sendResponse( serviceCall );
|
|
101
102
|
} ).catch( ( error ) => {
|
|
102
|
-
logger.log( `Failed to send service call response after processing! Service call ID was: '${
|
|
103
|
+
logger.log( `Failed to send service call response after processing! Service call message ID was: '${ serviceCall.messageID }'`, logger.logSeverity.ERROR, error );
|
|
103
104
|
} );
|
|
105
|
+
|
|
106
|
+
return serviceCall;
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
/**
|
|
@@ -127,6 +130,18 @@ class ServiceExecutor extends MessageObserver {
|
|
|
127
130
|
super.onConnectionRecovered( identifier );
|
|
128
131
|
}
|
|
129
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Needs to be invoked by the connection handler when the connection is irrevocably lost.
|
|
135
|
+
*
|
|
136
|
+
* @method
|
|
137
|
+
* @param {string} identifier The identifier of the observed connection.
|
|
138
|
+
* @override
|
|
139
|
+
* @public
|
|
140
|
+
*/
|
|
141
|
+
onConnectionLost( identifier ) {
|
|
142
|
+
super.onConnectionLost( identifier );
|
|
143
|
+
}
|
|
144
|
+
|
|
130
145
|
/**
|
|
131
146
|
* Used to set up the method for service access verification.
|
|
132
147
|
*
|
|
@@ -37,6 +37,7 @@ class ServiceInstance {
|
|
|
37
37
|
#serviceConfig;
|
|
38
38
|
#serviceHealthCheck;
|
|
39
39
|
#reportHealthyJob;
|
|
40
|
+
#healthReportUnderway = false;
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
43
|
* @constructor
|
|
@@ -225,10 +226,14 @@ class ServiceInstance {
|
|
|
225
226
|
* @public
|
|
226
227
|
*/
|
|
227
228
|
reportHealthy() {
|
|
228
|
-
if ( cache.instance.isOperational ) {
|
|
229
|
+
if ( cache.instance.isOperational && !this.#healthReportUnderway ) {
|
|
230
|
+
this.#healthReportUnderway = true;
|
|
229
231
|
let timestamp = new Date();
|
|
230
|
-
cache.instance.setValue( this.#serviceHealthCheck, timestamp.toISOString(), config.getSetting( config.setting.SERVICE_HEALTH_CHECK_TIMEOUT ) ).
|
|
231
|
-
|
|
232
|
+
cache.instance.setValue( this.#serviceHealthCheck, timestamp.toISOString(), config.getSetting( config.setting.SERVICE_HEALTH_CHECK_TIMEOUT ) ).then( () => {
|
|
233
|
+
this.#healthReportUnderway = false;
|
|
234
|
+
} ).catch( ( error ) => {
|
|
235
|
+
this.#healthReportUnderway = false;
|
|
236
|
+
logger.log( `Failed to report for health check from instance '${ ServiceInstance.instanceID }'!`, logger.logSeverity.WARNING, error );
|
|
232
237
|
} );
|
|
233
238
|
}
|
|
234
239
|
}
|
|
@@ -28,6 +28,7 @@ let cacheCommandsEnum = tools.enum( {
|
|
|
28
28
|
HASH_GET: [ "hget", "hash get", "https://redis.io/docs/latest/commands/hget/" ],
|
|
29
29
|
HASH_GET_ALL: [ "hgetall", "hash get all", "https://redis.io/docs/latest/commands/hgetall/" ],
|
|
30
30
|
HASH_REMOVE: [ "hdel", "hash remove", "https://redis.io/docs/latest/commands/hdel/" ],
|
|
31
|
+
HASH_EXPIRE: [ "hexpire", "hash expire", "https://redis.io/docs/latest/commands/hexpire/" ],
|
|
31
32
|
HASH_SET: [ "hset", "", "https://redis.io/docs/latest/commands/hset/" ],
|
|
32
33
|
HASH_SET_MANY: [ "hmset", "(deprecated) use HSET with multiple fields", "https://redis.io/docs/latest/commands/hmset/" ],
|
|
33
34
|
IS_SET_MEMBER: [ "sismember", "", "https://redis.io/docs/latest/commands/sismember/" ],
|
|
@@ -43,6 +44,21 @@ let cacheCommandsEnum = tools.enum( {
|
|
|
43
44
|
UNION_OF_SETS: [ "sunion", "union of sets", "https://redis.io/docs/latest/commands/sunion/" ]
|
|
44
45
|
} );
|
|
45
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Enum for listing all client statuses.
|
|
49
|
+
*
|
|
50
|
+
* @readonly
|
|
51
|
+
* @enum {number}
|
|
52
|
+
*/
|
|
53
|
+
let clientStatusEnum = tools.enum( {
|
|
54
|
+
UNINITIALIZED: [ 0, "uninitialized", "Redis client is offline and not yet initialized." ],
|
|
55
|
+
CONNECTED: [ 1, "connected", "Redis client is connected and online." ],
|
|
56
|
+
CONNECTING: [ 2, "connecting", "Redis client is connecting to server." ],
|
|
57
|
+
DISRUPTED: [ 3, "disrupted", "Redis client is temporarily disconnected from server due to a disruption." ],
|
|
58
|
+
SHUTTING_DOWN: [ 4, "shutting down", "Redis client is shutting down." ],
|
|
59
|
+
DISCONNECTED: [ 5, "disconnected", "Redis client is permanently disconnected from server." ]
|
|
60
|
+
} );
|
|
61
|
+
|
|
46
62
|
/**
|
|
47
63
|
* Enum for listing the Redis key override modes.
|
|
48
64
|
*
|
|
@@ -63,6 +79,10 @@ module.exports.cacheCommands = cacheCommandsEnum;
|
|
|
63
79
|
* @typedef {string} TiRedisOverrideMode
|
|
64
80
|
*/
|
|
65
81
|
module.exports.cacheOverrideMode = cacheOverrideModeEnum;
|
|
82
|
+
/**
|
|
83
|
+
* @typedef {number} TiRedisClientStatus
|
|
84
|
+
*/
|
|
85
|
+
module.exports.clientStatus = clientStatusEnum;
|
|
66
86
|
|
|
67
87
|
/**
|
|
68
88
|
* Used to create a Redis Cache client.
|
|
@@ -76,6 +96,7 @@ module.exports.cacheOverrideMode = cacheOverrideModeEnum;
|
|
|
76
96
|
class RedisClient {
|
|
77
97
|
|
|
78
98
|
#clientIdentifier;
|
|
99
|
+
#clientStatus = clientStatusEnum.UNINITIALIZED;
|
|
79
100
|
#retryMaxInterval = 1000;
|
|
80
101
|
#retryMaxAttempts = undefined;
|
|
81
102
|
#redisConnection = undefined;
|
|
@@ -118,6 +139,17 @@ class RedisClient {
|
|
|
118
139
|
return this.#redisClientID;
|
|
119
140
|
}
|
|
120
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Used to return the Redis client status.
|
|
144
|
+
*
|
|
145
|
+
* @property
|
|
146
|
+
* @returns {number}
|
|
147
|
+
* @public
|
|
148
|
+
*/
|
|
149
|
+
get clientStatus() {
|
|
150
|
+
return this.#clientStatus;
|
|
151
|
+
}
|
|
152
|
+
|
|
121
153
|
/**
|
|
122
154
|
* Used to return the Redis server version.
|
|
123
155
|
*
|
|
@@ -167,12 +199,6 @@ class RedisClient {
|
|
|
167
199
|
} ).then( ( clientID ) => {
|
|
168
200
|
// Store the client ID:
|
|
169
201
|
this.#redisClientID = clientID;
|
|
170
|
-
|
|
171
|
-
// Notify all connection observers about the initialization success:
|
|
172
|
-
_.forEach( this.#connectionObservers, ( connectionObservers ) => {
|
|
173
|
-
connectionObservers.onConnectionRecovered( this.#clientIdentifier );
|
|
174
|
-
} );
|
|
175
|
-
|
|
176
202
|
resolve();
|
|
177
203
|
} ).catch( ( error ) => {
|
|
178
204
|
reject( exceptions.raise( error ) );
|
|
@@ -359,6 +385,7 @@ class RedisClient {
|
|
|
359
385
|
shutDown( timeoutMs = 1000 ) {
|
|
360
386
|
return new Promise( ( resolve ) => {
|
|
361
387
|
let finished = false;
|
|
388
|
+
this.#clientStatus = clientStatusEnum.SHUTTING_DOWN;
|
|
362
389
|
const done = () => {
|
|
363
390
|
if ( !finished ) {
|
|
364
391
|
finished = true;
|
|
@@ -403,7 +430,8 @@ class RedisClient {
|
|
|
403
430
|
* @param {number} defaultDB
|
|
404
431
|
* @param {number} [retryMaxIntervalMs=1000] Optional max backoff interval.
|
|
405
432
|
* @param {number|undefined} [retryMaxAttempts=undefined] Optional max (re)connection attempts before abort.
|
|
406
|
-
* @
|
|
433
|
+
* @returns {Promise}
|
|
434
|
+
* @private
|
|
407
435
|
*/
|
|
408
436
|
#setupClient( host, port, authKey, user, defaultDB, retryMaxIntervalMs, retryMaxAttempts ) {
|
|
409
437
|
return new Promise( ( resolve, reject ) => {
|
|
@@ -412,19 +440,22 @@ class RedisClient {
|
|
|
412
440
|
this.#retryMaxAttempts = retryMaxAttempts;
|
|
413
441
|
|
|
414
442
|
let retryStrategy = ( attempt ) => {
|
|
415
|
-
let
|
|
416
|
-
|
|
443
|
+
let retryInterval = Math.min( attempt * 50, this.#retryMaxInterval );
|
|
417
444
|
if ( this.#retryMaxAttempts != null && attempt > this.#retryMaxAttempts ) {
|
|
418
445
|
logger.log( "In Redis retry strategy: reached max attempts for command retry. Aborting...", logger.logSeverity.WARNING, { attempts: attempt } );
|
|
419
|
-
|
|
446
|
+
retryInterval = exceptions.raise( exceptions.exceptionCode.E_COM_RETRY_ATTEMPTS_EXCEEDED );
|
|
420
447
|
}
|
|
421
|
-
|
|
422
|
-
return result;
|
|
448
|
+
return retryInterval;
|
|
423
449
|
};
|
|
424
450
|
|
|
425
451
|
let reconnectOnError = ( error ) => {
|
|
426
452
|
logger.log( `In Redis reconnect on error strategy: ${ error.message }`, logger.logSeverity.ERROR, error );
|
|
427
|
-
|
|
453
|
+
if ( error.message.includes( "READONLY" ) ) {
|
|
454
|
+
// Returning 2 will also resubmit the failed command:
|
|
455
|
+
return 2;
|
|
456
|
+
} else {
|
|
457
|
+
return 0;
|
|
458
|
+
}
|
|
428
459
|
};
|
|
429
460
|
|
|
430
461
|
let options = {
|
|
@@ -441,22 +472,33 @@ class RedisClient {
|
|
|
441
472
|
|
|
442
473
|
/** @type Redis */
|
|
443
474
|
this.#redisConnection = new Redis( options );
|
|
475
|
+
this.#clientStatus = clientStatusEnum.CONNECTING;
|
|
476
|
+
|
|
477
|
+
this.#redisConnection.once( "ready", () => {
|
|
478
|
+
this.#clientStatus = clientStatusEnum.CONNECTED;
|
|
479
|
+
this.#notifyConnectionObservers();
|
|
480
|
+
|
|
481
|
+
this.#redisConnection.on( "ready", () => {
|
|
482
|
+
this.#clientStatus = clientStatusEnum.CONNECTED;
|
|
483
|
+
this.#notifyConnectionObservers();
|
|
484
|
+
} );
|
|
444
485
|
|
|
445
|
-
this.#redisConnection.on( "ready", () => {
|
|
446
|
-
logger.log( `Connection to Redis server ${ host }:${ port } (re)established by client '${ this.identifier }' and is ready to be used.`, logger.logSeverity.INFO );
|
|
447
486
|
resolve();
|
|
448
487
|
} );
|
|
449
488
|
this.#redisConnection.on( "error", ( error ) => {
|
|
489
|
+
this.#clientStatus = clientStatusEnum.DISRUPTED;
|
|
450
490
|
logger.log( `Error received in Redis client '${ this.identifier }'.`, logger.logSeverity.ERROR, error );
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
} );
|
|
491
|
+
this.#notifyConnectionObservers();
|
|
492
|
+
} );
|
|
493
|
+
this.#redisConnection.on( "reconnecting", ( retryInterval ) => {
|
|
494
|
+
this.#clientStatus = clientStatusEnum.CONNECTING;
|
|
495
|
+
logger.log( `Client '${ this.identifier }' reconnecting to Redis server after ${ retryInterval } ms.`, logger.logSeverity.DEBUG );
|
|
456
496
|
} );
|
|
457
|
-
this.#redisConnection.on( "
|
|
458
|
-
if (
|
|
459
|
-
|
|
497
|
+
this.#redisConnection.on( "end", () => {
|
|
498
|
+
if ( this.#clientStatus !== clientStatusEnum.SHUTTING_DOWN ) {
|
|
499
|
+
this.#clientStatus = clientStatusEnum.DISCONNECTED;
|
|
500
|
+
logger.log( `Client '${ this.identifier }' cannot reconnect to Redis server and has been shut down.`, logger.logSeverity.WARNING );
|
|
501
|
+
this.#notifyConnectionObservers();
|
|
460
502
|
}
|
|
461
503
|
} );
|
|
462
504
|
} catch ( error ) {
|
|
@@ -465,10 +507,31 @@ class RedisClient {
|
|
|
465
507
|
} );
|
|
466
508
|
}
|
|
467
509
|
|
|
510
|
+
/**
|
|
511
|
+
* Used to notify all connection observers about the current connection state.
|
|
512
|
+
*
|
|
513
|
+
* @method
|
|
514
|
+
* @private
|
|
515
|
+
*/
|
|
516
|
+
#notifyConnectionObservers() {
|
|
517
|
+
// Notify all connection observers about the event:
|
|
518
|
+
_.forEach( this.#connectionObservers, ( connectionObservers ) => {
|
|
519
|
+
if ( this.#clientStatus === clientStatusEnum.CONNECTED ) {
|
|
520
|
+
logger.log( `Connection to Redis server ${ this.#redisConnection.options.host }:${ this.#redisConnection.options.port } (re)established by client '${ this.identifier }' and is ready to be used.`, logger.logSeverity.INFO );
|
|
521
|
+
connectionObservers.onConnectionRecovered( this.#clientIdentifier );
|
|
522
|
+
} else if ( this.#clientStatus === clientStatusEnum.DISRUPTED ) {
|
|
523
|
+
connectionObservers.onConnectionDisrupted( this.#clientIdentifier );
|
|
524
|
+
} else if ( this.#clientStatus === clientStatusEnum.DISCONNECTED ) {
|
|
525
|
+
connectionObservers.onConnectionLost( this.#clientIdentifier );
|
|
526
|
+
}
|
|
527
|
+
} );
|
|
528
|
+
}
|
|
529
|
+
|
|
468
530
|
/**
|
|
469
531
|
* Used to fetch and store Redis server information.
|
|
470
532
|
*
|
|
471
533
|
* @method
|
|
534
|
+
* @returns {Promise}
|
|
472
535
|
* @private
|
|
473
536
|
*/
|
|
474
537
|
#fetchServerInfo() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ti-engine/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
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": "GPL-3.0-or-later",
|
package/utils/cache.js
CHANGED
|
@@ -124,6 +124,22 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Needs to be invoked by the connection handler when the connection is irrevocably lost.
|
|
129
|
+
*
|
|
130
|
+
* @method
|
|
131
|
+
* @param {string} identifier The identifier of the observed connection.
|
|
132
|
+
* @override
|
|
133
|
+
* @public
|
|
134
|
+
*/
|
|
135
|
+
onConnectionLost( identifier ) {
|
|
136
|
+
if ( identifier === this.#connectionIdentifier ) {
|
|
137
|
+
this.#isOperational = false;
|
|
138
|
+
// TODO: implement forced shut down of the service instance instead of crashing it outright
|
|
139
|
+
throw exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE );
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
127
143
|
/**
|
|
128
144
|
* Used to register a new {@link ConnectionObserver} for events related to the underlying Redis connection state.
|
|
129
145
|
*
|
|
@@ -317,13 +333,19 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
317
333
|
* @method
|
|
318
334
|
* @param {string} key
|
|
319
335
|
* @param {number} seconds
|
|
336
|
+
* @param {string} [name] If you need to expire a field in a hash set instead, provide the name of the set here.
|
|
320
337
|
* @returns {Promise<number>} This will resolve with the seconds as provided initially by the caller.
|
|
321
338
|
* @public
|
|
322
339
|
*/
|
|
323
|
-
expireValue( key, seconds ) {
|
|
340
|
+
expireValue( key, seconds, name ) {
|
|
324
341
|
return new Promise( ( resolve, reject ) => {
|
|
325
342
|
if ( this.#isOperational === true ) {
|
|
326
|
-
let commandExpire = [
|
|
343
|
+
let commandExpire = [];
|
|
344
|
+
if ( name ) {
|
|
345
|
+
commandExpire = [ redis.cacheCommands.HASH_EXPIRE, name, seconds, "FIELDS", 1, key ];
|
|
346
|
+
} else {
|
|
347
|
+
commandExpire = [ redis.cacheCommands.EXPIRE, key, seconds ];
|
|
348
|
+
}
|
|
327
349
|
this.#redisClient.executeCommands( [ commandExpire ] ).then( () => {
|
|
328
350
|
resolve( seconds );
|
|
329
351
|
} ).catch( ( error ) => {
|
package/utils/exceptions.js
CHANGED
|
@@ -38,6 +38,7 @@ let exceptionCodeEnum = tools.enum( {
|
|
|
38
38
|
E_COM_SERVICE_NOT_FOUND: [ 3004, "service not found", "The specified service is not found in the service definition interface." ],
|
|
39
39
|
E_COM_SERVICE_HANDLER_NOT_FOUND: [ 3005, "service handler not found", "No handler found in the interface for the specified service or service version." ],
|
|
40
40
|
E_COM_MESSAGE_RECEIVER_UNAVAILABLE: [ 3006, "message receiver unavailable", "The message receiver instance is currently unavailable." ],
|
|
41
|
+
E_COM_MESSAGE_EXCHANGE_BROKEN: [ 3007, "message exchange broken", "The message exchange is irrevocably broken and cannot be used any longer." ],
|
|
41
42
|
// E_COM_UNRECOGNIZED_API_URL: [ 301, "unrecognized api url", "Attempt to access unrecognized or invalid API URL." ],
|
|
42
43
|
// E_COM_MISSING_REQUIRED_ARGUMENTS: [ 302, "missing required arguments", "Attempt to execute operation without all required arguments." ],
|
|
43
44
|
// E_COM_UNRECOGNIZED_RESPONSE_STRUCTURE: [ 303, "unrecognized response structure", "The received response has unrecognized structure and cannot be parsed or examined." ],
|