@nsshunt/stsappframework 2.19.210 → 2.19.211

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/run3.sh ADDED
@@ -0,0 +1,25 @@
1
+ #!/bin/sh
2
+ # openssl req -nodes -new -x509 -keyout server.key -out server.cert
3
+ clear; \
4
+ export STS_PROJ_ROOT=./..; \
5
+ STSENVFILE=$STS_PROJ_ROOT/stsglobalresources/.env \
6
+ DB_SCRIPT_FOLDER=$STS_PROJ_ROOT/stsglobalresources/db-scripts \
7
+ REST01_PORT=3003 \
8
+ REST01_HOST_PORT=3003 \
9
+ REST01_SERVICE_NAME="STSRest01-3003" \
10
+ REST01_API_IDENTIFIER="https://stsmda.com.au/stsrest01api/v1.0/" \
11
+ REST01_ENDPOINT="https://stsrest.stsmda.org" \
12
+ MAX_CPU=4 \
13
+ AS_ENDPOINT=https://stscore.stsmda.org \
14
+ AS_HOST_PORT=3002 \
15
+ AS_PORT=3002 \
16
+ DB_HOST=localhost \
17
+ DB_PORT=5432 \
18
+ DB_PASSWORD=postgres \
19
+ HTTPS_SERVER_KEY_PATH=/etc/letsencrypt/live/stsmda.org/privkey.pem \
20
+ HTTPS_SERVER_CERT_PATH=/etc/letsencrypt/live/stsmda.org/fullchain.pem \
21
+ DEBUG=zzproc* \
22
+ PUBLISH_DEBUG=false \
23
+ UV_THREADPOOL_SIZE=64 \
24
+ SERVICE_INDEX=2 \
25
+ node dist/tcpserver/app;
@@ -0,0 +1,11 @@
1
+ import { ServiceConfigOptions } from './appConfig.js'
2
+ import { WorkerProcessBase } from '../index'
3
+ import { AppMaster } from './appmaster'
4
+
5
+ import cluster from 'cluster';
6
+
7
+ if (cluster.isPrimary) {
8
+ new AppMaster(ServiceConfigOptions(cluster.isPrimary)).SetupServer();
9
+ } else {
10
+ //new WorkerProcessBase(ServiceConfigOptions(cluster.isPrimary)).SetupServer();
11
+ }
@@ -0,0 +1,66 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import express from 'express';
3
+
4
+ import { $Options } from '@nsshunt/stsconfig'
5
+ const goptions = $Options()
6
+
7
+ import { ProcessOptions, IProcessBase, STSServerType } from '../index'
8
+
9
+ /*
10
+ // nid: `${goptions.rest01servicename} @ ${goptions.rest01serviceversion} | ${this.options.globalServiceData.serviceInstanceId} @ ${os.hostname()} ^ ${process.pid} @ ${(cluster.isMaster ? process.pid : process.ppid)}`,
11
+ // <serviceId> <serviceInstanceId> <serviceInstanceProcessId>
12
+ // <serviceName> <serviceVersion> <sid> <hostName> <pid> <ppid>
13
+ // << ............... Static Nid ............... >> << ............... Dynamic Nid ............... >>
14
+ // Note: The final nid will NOT contain the NID_SEPERATOR character. This will be replaced with the SEPERATOR character.
15
+ const Context = (isMaster, serviceInstanceId) => {
16
+ return {
17
+ nid: `\
18
+ ${goptions.rest01servicename}${ModelDelimeter.COMPONENT_SEPERATOR}${goptions.rest01serviceversion}\
19
+ ${ModelDelimeter.SEPERATOR}\
20
+ ${serviceInstanceId}${ModelDelimeter.COMPONENT_SEPERATOR}${os.hostname()}\
21
+ ${ModelDelimeter.NID_SEPERATOR}\
22
+ ${process.pid}${ModelDelimeter.COMPONENT_SEPERATOR}${(isMaster ? process.pid : process.ppid)}`
23
+ }
24
+ }
25
+ */
26
+
27
+ export function ServiceConfigOptions(isMaster: boolean): ProcessOptions {
28
+ if (isMaster === true) {
29
+ const serviceInstanceId = uuidv4();
30
+ const data: ProcessOptions = {
31
+ serverType: STSServerType.TCPRAW_TLS,
32
+
33
+ wssServer: false,
34
+ useLatency: false,
35
+ httpsServerKeyPath: goptions.httpsserverkeypath,
36
+ httpsServerCertificatePath: goptions.httpsservercertpath,
37
+ processExitOnTerminate: true,
38
+ serviceInstanceId: serviceInstanceId,
39
+ useDatabase: true,
40
+ useInstrumentationWorkers: true,
41
+
42
+ isMaster: isMaster,
43
+ endpoint: goptions.rest01endpoint,
44
+ apiRoot: goptions.rest01apiroot,
45
+ listenPort: goptions.rest01hostport,
46
+ port: goptions.rest01port,
47
+
48
+ prometheusSupport: false,
49
+ prometheusClusterPort: goptions.rest01prometheusclusterport,
50
+
51
+ serviceName: goptions.rest01servicename,
52
+ serviceVersion: goptions.rest01serviceversion,
53
+ consoleLogging: true,
54
+ instrumentLogging: true,
55
+
56
+ instrumentationObservationInterval: goptions.instrumentationObservationInterval,
57
+ instrumentationTimeWindow: goptions.instrumentationTimeWindow,
58
+
59
+ useRedisAdaptor: false
60
+ }
61
+ return data;
62
+ } else {
63
+ const data: ProcessOptions = JSON.parse(process.env['STS_GSD_SII'] as string) as ProcessOptions;
64
+ return data;
65
+ }
66
+ }
@@ -0,0 +1,542 @@
1
+ import fs from "fs"
2
+ import crypto from 'crypto';
3
+ import jayson from 'jayson'
4
+ import net from 'node:net'
5
+ import tls from 'node:tls'
6
+
7
+ import { JSONObject, Sleep } from '@nsshunt/stsutils';
8
+ import { MasterProcessBase } from './../masterprocessbase'
9
+ import { ProcessOptions } from './../processoptions'
10
+
11
+ // Colour codes
12
+ // ---------------------------------
13
+ // Red - Error
14
+ // Green - Vote YES
15
+ // Yellow - Vote No
16
+ // White - standard logging message
17
+ // Magenta - node state change
18
+ // Cyan - node term change
19
+
20
+ export enum AppMasterRAFTState {
21
+ IDLE = "IDLE",
22
+ FOLLOWER = "FOLLOWER",
23
+ CANDIDATE = "CANDIDATE",
24
+ LEADER = "LEADER"
25
+ }
26
+
27
+ export interface IServiceDetail {
28
+ host: string
29
+ port: number
30
+ }
31
+
32
+ export interface ILogItem {
33
+ term: number
34
+ index: number
35
+ data: JSONObject | null
36
+ }
37
+
38
+ export class AppMaster extends MasterProcessBase
39
+ {
40
+ #lastRequiredVotes = -1; // used for logging only
41
+ #lastVotes = -1; // used for logging only
42
+ #RAFTState: AppMasterRAFTState;
43
+ #minElectionTimeout: number;
44
+ #maxElectionTimeout: number;
45
+ #electionTimeout: number;
46
+ #heartBeatTimeout: number;
47
+ #startRAFTProtocolTimeout: number;
48
+ #votes: JSONObject; // keyed by service key (host:port)
49
+ #services: IServiceDetail[];
50
+ #thisService: IServiceDetail;
51
+ #canidateTimeout: NodeJS.Timeout | null = null;
52
+ #appendEntriesTimeout: NodeJS.Timeout | null = null;
53
+ #voteRegister = 0;
54
+ #server?: jayson.TlsServer;
55
+ #serverPort: number;
56
+ #votedLog: JSONObject = { }; // keyed by term number
57
+ #appendedEntries: JSONObject = { };
58
+ #leaderNode: IServiceDetail | null = null;
59
+
60
+ #currentTerm: number;
61
+ #log: ILogItem[];
62
+ #currentIndex = 0;
63
+
64
+ constructor(serviceConfig: ProcessOptions)
65
+ {
66
+ super(serviceConfig);
67
+ this.#RAFTState = AppMasterRAFTState.IDLE;
68
+ this.#currentTerm = 0;
69
+ this.#votes = { };
70
+ this.#services = [{
71
+ host: "stscore.stsmda.org",
72
+ port: 3006
73
+ }, {
74
+ host: "stscore.stsmda.org",
75
+ port: 3007
76
+ }, {
77
+ host: "stscore.stsmda.org",
78
+ port: 3008
79
+ }]
80
+
81
+ this.#thisService = this.#services[parseInt(process.env.SERVICE_INDEX as string)]; // set self service
82
+ this.#serverPort = this.#thisService.port;
83
+
84
+ //this.#thisService = this.#services[0]; // set self service
85
+ this.#log = [ ]; // Need to get from storage - volume, redis, etc.
86
+
87
+ // Set default config items
88
+ this.#minElectionTimeout = 1000; //@@ config
89
+ this.#maxElectionTimeout = 1200; //@@ config
90
+ this.#startRAFTProtocolTimeout = 2000; //@@ config
91
+ this.#electionTimeout = crypto.randomInt(this.#minElectionTimeout, this.#maxElectionTimeout);
92
+ this.#heartBeatTimeout = 750; //@@ config - also must be smaller than electionTimeout
93
+ }
94
+
95
+ #GetTLSOptions = (): JSONObject => {
96
+ return {
97
+ key: fs.readFileSync(this.options.httpsServerKeyPath),
98
+ cert: fs.readFileSync(this.options.httpsServerCertificatePath)
99
+ };
100
+ }
101
+
102
+ #SetupRPCServer = async (socket: net.Socket): Promise<void> => {
103
+ /*
104
+ console.log('CONNECTED: ' + socket.remoteAddress + ':' + socket.remotePort + ' ' + process.pid);
105
+
106
+ socket.on('close', function(data: any) {
107
+ console.log('CLOSED: ' + socket.remoteAddress + ' ' + socket.remotePort + ' ' + process.pid);
108
+ });
109
+
110
+ socket.on('data', function(data: any) {
111
+ console.log('DATA ' + socket.remoteAddress + ': ' + data);
112
+ });
113
+ */
114
+ }
115
+
116
+ #GetLogPrefix = () => {
117
+ return `${this.#thisService.host}:${this.#thisService.port}: currentTerm: [${this.#currentTerm}] index: [${this.#currentIndex}]: `;
118
+ }
119
+
120
+ #VoteYes = (callback: any, term: number, message: string) => {
121
+ // Log the fact that I have now voted for this currentTerm
122
+ this.#votedLog[term] = true;
123
+ // Update my currentTerm
124
+ this.#currentTerm = term;
125
+ // Return my yes vote
126
+ console.log(`${message} - Vote Yes`.green)
127
+ callback(null, true); // YES vote
128
+ }
129
+
130
+ #VoteNo = (callback: any, term: number, message: string) => {
131
+ // Log the fact that I have now voted for this currentTerm
132
+ this.#votedLog[term] = true;
133
+ // Return my yes vote
134
+ console.log(`${message} - Vote No`.grey)
135
+ callback(null, false); // NO vote
136
+ }
137
+
138
+ #FindService = (host: string, port: number): IServiceDetail | null => {
139
+ let retService = null;
140
+ this.#services.forEach((service, index) => {
141
+ if (service.host.localeCompare(host) === 0 && service.port === port) {
142
+ retService = service;
143
+ return;
144
+ }
145
+ });
146
+ return retService;
147
+ }
148
+
149
+ #CheckMoreRecentTerm = (term: number, lastLogItemTerm: number, lastLogItemIndex: number) => {
150
+ // Check if the request to vote is newer than my current term
151
+ let msg = '';
152
+ let retVal: boolean;
153
+ if (term > this.#currentTerm) {
154
+
155
+ msg = `${this.#GetLogPrefix()}: canidate term: [${term}] greater than my currentTerm: [${this.#currentTerm}]`;
156
+ retVal = true;
157
+ } else {
158
+ // Check if the request to vote is older than my current term
159
+ if (term === this.#currentTerm) {
160
+ if (lastLogItemIndex > this.#currentIndex) {
161
+ msg = `${this.#GetLogPrefix()}: canidate term: [${term}] the same as my currentTerm: [${this.#currentTerm}]. Canidate index: [${lastLogItemIndex}] greater than my index: [${this.#currentIndex}]`;
162
+ retVal = true;
163
+ } else if (lastLogItemIndex === this.#currentIndex) {
164
+ msg = `${this.#GetLogPrefix()}: canidate term: [${term}] the same as my currentTerm: [${this.#currentTerm}]. Canidate index: [${lastLogItemIndex}] the same as my index: [${this.#currentIndex}]`;
165
+ retVal = true;
166
+ } else {
167
+ msg = `${this.#GetLogPrefix()}: canidate term: [${term}] the same as my currentTerm: [${this.#currentTerm}]. Canidate index: [${lastLogItemIndex}] NOT greater than my index: [${this.#currentIndex}]`;
168
+ retVal = false;
169
+ }
170
+ } else {
171
+ msg = `${this.#GetLogPrefix()}: canidate term: [${term}] less than my currentTerm: [${this.#currentTerm}]`;
172
+ retVal = false;
173
+ }
174
+ }
175
+ return {
176
+ msg,
177
+ retVal
178
+ }
179
+ }
180
+
181
+ #SetupJSONRPCServer = async (): Promise<jayson.TlsServer> => {
182
+ const jaysonServer = new jayson.server();
183
+
184
+ jaysonServer.method("appendEntries", async (args: any, callback: any) => {
185
+ // Just received a message - reset my electionTimeout
186
+ this.#ResetElectionTimeout();
187
+
188
+ // Do something with this new log entry
189
+
190
+ // Needs to check if current, otherwise get old entries and duplicate
191
+
192
+ });
193
+
194
+ jaysonServer.method("vote", async (args: any, callback: any) => {
195
+ // Just received a message - reset my electionTimeout
196
+ this.#ResetElectionTimeout();
197
+
198
+ const term = args[0];
199
+ const lastLogItemTerm = args[1];
200
+ const lastLogItemIndex = args[2];
201
+
202
+ // Now determine whether this requestror gets my vote
203
+
204
+ // Check already voted
205
+ if (this.#votedLog[term]) {
206
+ this.#VoteNo(callback, term, `${this.#GetLogPrefix()}: Already voted - vote NO`)
207
+ return;
208
+ }
209
+
210
+ // Check if the request to vote is newer than my current term
211
+ const retVal = this.#CheckMoreRecentTerm(term, lastLogItemTerm, lastLogItemIndex);
212
+ if (retVal.retVal === true) {
213
+ this.#VoteYes(callback, term, retVal.msg);
214
+ } else {
215
+ this.#VoteNo(callback, term, retVal.msg);
216
+ }
217
+ });
218
+
219
+ jaysonServer.method("appendEntry", async (args: any, callback: any) => {
220
+ // Just received a message - reset my electionTimeout
221
+ this.#ResetElectionTimeout();
222
+
223
+ const host = args[0];
224
+ const port = args[1];
225
+ const term = args[2];
226
+ const lastLogItemTerm = args[3];
227
+ const lastLogItemIndex = args[4];
228
+
229
+ if (this.#RAFTState.localeCompare(AppMasterRAFTState.CANDIDATE) === 0) {
230
+ const retVal = this.#CheckMoreRecentTerm(term, lastLogItemTerm, lastLogItemIndex);
231
+ if (retVal.retVal === true) {
232
+ console.log(`${retVal.msg}`);
233
+ console.log(`${this.#GetLogPrefix()}: New leader detected, Host: [${host}], Port: [${port}], while in CANDIDATE mode, reverting to FOLLOWER.`.magenta);
234
+ // This sending node is the leader so revert to follower
235
+ this.#currentTerm = term;
236
+ this.#ChangeState(AppMasterRAFTState.FOLLOWER);
237
+ } else {
238
+ console.log(`${retVal.msg}`);
239
+ console.log(`${this.#GetLogPrefix()}: appendEntry called but log is older than my current state, Host: [${host}], Port: [${port}].`.magenta);
240
+ // Reject this RPC and continue in the CANDIDATE state
241
+ callback(null, false);
242
+ return;
243
+ }
244
+ } else if (this.#RAFTState.localeCompare(AppMasterRAFTState.LEADER) === 0) {
245
+ console.log(`${this.#GetLogPrefix()}: appendEntry called while I am the current leader, rejecting RPC call.`.red);
246
+ // Reject this RPC and continue in the CANDIDATE state
247
+ callback(null, false);
248
+ return;
249
+ } else {
250
+ // FOLLOWER
251
+ const retVal = this.#CheckMoreRecentTerm(term, lastLogItemTerm, lastLogItemIndex);
252
+ if (retVal.retVal === true) {
253
+ if (term > this.#currentTerm) {
254
+ console.log(`${retVal.msg}`);
255
+ this.#currentTerm = term;
256
+ }
257
+ } else {
258
+ console.log(`${retVal.msg}`);
259
+ console.log(`${this.#GetLogPrefix()}: appendEntry called but log is older than my current state, Host: [${host}], Port: [${port}].`.magenta);
260
+ // Reject this RPC and continue in the CANDIDATE state
261
+ callback(null, false);
262
+ return;
263
+ }
264
+ }
265
+
266
+ if (this.#leaderNode) {
267
+ const sentLeaderService = this.#FindService(host, port);
268
+ if (sentLeaderService) {
269
+ if (sentLeaderService.host.localeCompare(this.#leaderNode.host) !== 0 || sentLeaderService.port !== this.#leaderNode.port) {
270
+ this.#leaderNode = sentLeaderService;
271
+ console.log(`${this.#GetLogPrefix()}: Leader Service: Host:[${this.#leaderNode.host}] Port: [${this.#leaderNode.port}]`.yellow);
272
+ }
273
+ }
274
+ } else {
275
+ this.#leaderNode = this.#FindService(host, port);
276
+ if (this.#leaderNode) {
277
+ console.log(`${this.#GetLogPrefix()}: Leader Service: Host:[${this.#leaderNode.host}] Port: [${this.#leaderNode.port}]`.yellow);
278
+ }
279
+ }
280
+
281
+ // Now update the log if only more recent
282
+ callback(null, true);
283
+ });
284
+
285
+ jaysonServer.method("ping", async function(args: any, callback: any) {
286
+ // do something, do nothing
287
+ console.log('server got ping');
288
+ callback();
289
+ });
290
+
291
+ const server = jaysonServer.tls(this.#GetTLSOptions());
292
+ (server as tls.Server).on('secureConnection', this.#SetupRPCServer);
293
+ //server.listen(this.options.listenPort, () => { // 'stscore.stsmda.org'
294
+ server.listen(this.#serverPort, () => { // 'stscore.stsmda.org'
295
+ console.log('JSON RPC 2.0 Server is running on port ' + this.options.listenPort + '.');
296
+ }).on('listening', () =>
297
+ {
298
+ this.LogEx(`JSON RPC 2.0 live on ${this.options.endpoint}:${this.options.listenPort}${this.options.apiRoot}`);
299
+ });
300
+ return server;
301
+ }
302
+
303
+ #RequestVote = (service: IServiceDetail) => {
304
+ const options: jayson.TcpClientOptions = {
305
+ port: service.port,
306
+ host: service.host
307
+ };
308
+
309
+ const client = jayson.client.tls(options);
310
+ /*
311
+ client.on('tcp socket', (socket: tls.TLSSocket) => {
312
+ socket.setTimeout(200); //@@ config
313
+ socket.on('timeout', () => {
314
+ console.log('socket timeout');
315
+ socket.end();
316
+ });
317
+ });
318
+ client.on('tcp error', (error: any) => {
319
+ console.log(`ERROR: [${error}]`.red);
320
+ });
321
+ */
322
+
323
+ let lastLogItem: ILogItem;
324
+ if (this.#log.length > 0) {
325
+ lastLogItem = this.#log[this.#log.length-1];
326
+ } else {
327
+ lastLogItem = {
328
+ term: 0,
329
+ index: 0,
330
+ data: null
331
+ }
332
+ }
333
+
334
+ client.request('vote', [ this.#currentTerm, lastLogItem.term, lastLogItem.index ], (error: any, response: any) => {
335
+ if (error) {
336
+ //console.log(`vote attempt fail: [${JSON.stringify(options)}: ${error}]`.red);
337
+ } else {
338
+ // Log the result
339
+ if (response.result === true) {
340
+ this.#votes[this.#GetServiceKey(service)] = 1;
341
+ }
342
+ this.#CheckMajority();
343
+ }
344
+ });
345
+ }
346
+
347
+ #AppendEntry = (service: IServiceDetail) => {
348
+ const options: jayson.TcpClientOptions = {
349
+ port: service.port,
350
+ host: service.host
351
+ };
352
+
353
+ const client = jayson.client.tls(options);
354
+ /*
355
+ client.on('tcp socket', (socket: tls.TLSSocket) => {
356
+ socket.setTimeout(200); //@@ config
357
+ socket.on('timeout', () => {
358
+ console.log('socket timeout');
359
+ socket.end();
360
+ });
361
+ });
362
+ client.on('tcp error', (error: any) => {
363
+ console.log(`ERROR: [${error}]`.red);
364
+ });
365
+ */
366
+
367
+ let lastLogItem: ILogItem;
368
+ if (this.#log.length > 0) {
369
+ lastLogItem = this.#log[this.#log.length-1];
370
+ } else {
371
+ lastLogItem = {
372
+ term: 0,
373
+ index: 0,
374
+ data: null
375
+ }
376
+ }
377
+
378
+ client.request('appendEntry', [ this.#thisService.host, this.#thisService.port, this.#currentTerm, lastLogItem.term, lastLogItem.index ], (error: any, response: any) => {
379
+ if (error) {
380
+ //console.log(`appendEntry attempt fail: [${JSON.stringify(options)}: ${error}]`.red);
381
+ } else {
382
+ this.#appendedEntries[this.#GetServiceKey(service)] = 1;
383
+ // Log the result
384
+ /*
385
+ if (response.result === true) {
386
+ this.#votes[this.#GetServiceKey(service)] = 1;
387
+ }
388
+ */
389
+ }
390
+ });
391
+ }
392
+
393
+ #AppendEntries = () => {
394
+ // Reset all appendEntries for this iteration
395
+ this.#appendedEntries = { };
396
+
397
+ // Reset all votes
398
+ this.#services.forEach((service, index) => {
399
+ this.#appendedEntries[this.#GetServiceKey(service)] = 0;
400
+ });
401
+
402
+ // Request vote for all other services - include my logs current item
403
+ this.#services.forEach((service, index) => {
404
+ if (service.host.localeCompare(this.#thisService.host) === 0 && service.port === this.#thisService.port) {
405
+ // Myself
406
+ this.#ResetElectionTimeout();
407
+ this.#appendedEntries[this.#GetSelfServiceKey()] = 1;
408
+ } else {
409
+ // Send vote request
410
+ this.#AppendEntry(service);
411
+ }
412
+ });
413
+ }
414
+
415
+ #GetServiceKey = (service: IServiceDetail) => {
416
+ return `${service.host}:${service.port}`;
417
+ }
418
+
419
+ #GetSelfServiceKey = () => {
420
+ return this.#GetServiceKey(this.#thisService);
421
+ }
422
+
423
+ // set interval ???
424
+ #StartAppendEntries = () => {
425
+ this.#AppendEntries();
426
+ this.#appendEntriesTimeout = setTimeout(() => {
427
+ // Now check if we still have majority
428
+ const majority: boolean = this.#CheckMajorityWithCollection(this.#appendedEntries);
429
+ if (majority) {
430
+ this.#StartAppendEntries();
431
+ } else {
432
+ console.log(`${this.#GetLogPrefix()}: Not enough response for valid cluster state.`.red);
433
+ }
434
+ }, this.#heartBeatTimeout);
435
+ }
436
+
437
+ #Promote = () => {
438
+ this.#ChangeState(AppMasterRAFTState.LEADER);
439
+ console.log(`${this.#GetLogPrefix()}: New LEADER is elected: [${this.#GetSelfServiceKey()}]`.green);
440
+ // As leader, now start the regular pings
441
+ this.#StartAppendEntries();
442
+ }
443
+
444
+ #CheckMajority = () => {
445
+ if (this.#CheckMajorityWithCollection(this.#votes)) {
446
+ this.#Promote();
447
+ }
448
+ }
449
+
450
+ #CheckMajorityWithCollection = (col: JSONObject): boolean => {
451
+ let votes = 0;
452
+ for (const [key, val] of Object.entries(col)) {
453
+ votes += val;
454
+ }
455
+ const requiredVotes = Math.floor(this.#services.length / 2) + 1;
456
+
457
+ if (requiredVotes !== this.#lastRequiredVotes || votes !== this.#lastVotes) {
458
+ console.log(`${this.#GetLogPrefix()}votes: [${votes} need: [${requiredVotes}]`.cyan);
459
+ this.#lastRequiredVotes = requiredVotes;
460
+ this.#lastVotes = votes;
461
+ }
462
+
463
+ return votes >= requiredVotes;
464
+ }
465
+
466
+ #StartCanidateVote = () => {
467
+
468
+ // Get the election timeout going again
469
+ this.#StartElectionTimeout();
470
+
471
+ console.log(`${this.#GetLogPrefix()}: Start CANDIDATE vote`.yellow);
472
+ // Start in the CANDIDATE state
473
+ this.#ChangeState(AppMasterRAFTState.CANDIDATE);
474
+ // Reset votes
475
+ this.#votes = { };
476
+
477
+ // Reset all votes from all service instances
478
+ this.#services.forEach((service, index) => {
479
+ this.#votes[this.#GetServiceKey(service)] = 0;
480
+ });
481
+
482
+ // Start a new term
483
+ this.#currentTerm++;
484
+
485
+ // Request vote for all other services - include my logs current item
486
+ this.#services.forEach((service, index) => {
487
+ if (service.host.localeCompare(this.#thisService.host) === 0 && service.port === this.#thisService.port) {
488
+ // Log the fact that I have now voted for this term
489
+ this.#votedLog[this.#currentTerm] = true;
490
+ // Log the result - Vote for myself
491
+ this.#votes[this.#GetSelfServiceKey()] = 1;
492
+ // Return my yes vote
493
+ console.log(`${this.#GetLogPrefix()}: SELF VOTE: canidate currentTerm: [${this.#currentTerm}], vote YES`.green)
494
+ this.#CheckMajority();
495
+ } else {
496
+ // Send vote request
497
+ this.#RequestVote(service);
498
+ }
499
+ });
500
+ }
501
+
502
+ #ReceiveVoteRequest = () => {
503
+ // Check if the request term and meta-data is more recent than me
504
+
505
+ // If request more recent ...
506
+ this.#voteRegister = 1;
507
+ // Send vote back to requestor
508
+ }
509
+
510
+ #ResetElectionTimeout = () => {
511
+ if (this.#canidateTimeout) {
512
+ clearTimeout(this.#canidateTimeout);
513
+ }
514
+ this.#StartElectionTimeout();
515
+ }
516
+
517
+ #StartElectionTimeout = () => {
518
+ if (this.#canidateTimeout) {
519
+ clearTimeout(this.#canidateTimeout);
520
+ }
521
+ this.#canidateTimeout = setTimeout(() => {
522
+ this.#StartCanidateVote();
523
+ }, this.#electionTimeout).unref();
524
+ }
525
+
526
+ #StartRAFTService = async () => {
527
+ this.LogEx(`---[ RAFT Protocol Starting ]---`.green);
528
+ this.#server = await this.#SetupJSONRPCServer();
529
+ this.#StartElectionTimeout();
530
+ }
531
+
532
+ #ChangeState(newState: AppMasterRAFTState) {
533
+ this.LogEx(`${this.#GetLogPrefix()}: State Change, From: [${this.#RAFTState.toString()}] --> To: [${newState.toString()}]`.green);
534
+ this.#RAFTState = newState;
535
+ }
536
+
537
+ MasterStarted(): void {
538
+ setTimeout(() => {
539
+ this.#StartRAFTService();
540
+ }, this.#startRAFTProtocolTimeout).unref();
541
+ }
542
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/tcpserver/app.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ import { ProcessOptions } from '../index';
2
+ export declare function ServiceConfigOptions(isMaster: boolean): ProcessOptions;
3
+ //# sourceMappingURL=appConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appConfig.d.ts","sourceRoot":"","sources":["../../src/tcpserver/appConfig.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,cAAc,EAA+B,MAAM,UAAU,CAAA;AAoBtE,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,OAAO,GAAG,cAAc,CAuCtE"}
@@ -0,0 +1,24 @@
1
+ import { JSONObject } from '@nsshunt/stsutils';
2
+ import { MasterProcessBase } from './../masterprocessbase';
3
+ import { ProcessOptions } from './../processoptions';
4
+ export declare enum AppMasterRAFTState {
5
+ IDLE = "IDLE",
6
+ FOLLOWER = "FOLLOWER",
7
+ CANDIDATE = "CANDIDATE",
8
+ LEADER = "LEADER"
9
+ }
10
+ export interface IServiceDetail {
11
+ host: string;
12
+ port: number;
13
+ }
14
+ export interface ILogItem {
15
+ term: number;
16
+ index: number;
17
+ data: JSONObject | null;
18
+ }
19
+ export declare class AppMaster extends MasterProcessBase {
20
+ #private;
21
+ constructor(serviceConfig: ProcessOptions);
22
+ MasterStarted(): void;
23
+ }
24
+ //# sourceMappingURL=appmaster.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appmaster.d.ts","sourceRoot":"","sources":["../../src/tcpserver/appmaster.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,UAAU,EAAS,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAWpD,oBAAY,kBAAkB;IAC1B,IAAI,SAAS;IACb,QAAQ,aAAa;IACrB,SAAS,cAAc;IACvB,MAAM,WAAW;CACpB;AAED,MAAM,WAAW,cAAc;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,QAAQ;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,UAAU,GAAG,IAAI,CAAA;CAC1B;AAED,qBAAa,SAAU,SAAQ,iBAAiB;;gBA0BhC,aAAa,EAAE,cAAc;IAydzC,aAAa,IAAI,IAAI;CAKxB"}