@grest-ts/discovery-local 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/dist/tsconfig.publish.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/index-node.ts +4 -4
- package/src/local/GGDiscoveryIPC.ts +17 -17
- package/src/local/GGLocalDiscoveryClient.ts +94 -94
- package/src/local/GGLocalDiscoveryResilientClient.ts +104 -104
- package/src/local/GGLocalDiscoveryServer.ts +143 -143
- package/src/local/routing/RoutingStrategy.ts +7 -7
- package/src/local/routing/strategies/FirstStrategy.ts +10 -10
- package/src/local/routing/strategies/LastStrategy.ts +10 -10
- package/src/local/routing/strategies/RandomStrategy.ts +10 -10
- package/src/local/routing/strategies/RoundRobinStrategy.ts +16 -16
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
import {GG_DISCOVERY, GGDiscoveryClient, GGServiceRegistration} from "@grest-ts/discovery";
|
|
2
|
-
import {GGLog} from "@grest-ts/logger";
|
|
3
|
-
import {IPCClient} from "@grest-ts/ipc";
|
|
4
|
-
import {GGLocator, GGLocatorServiceType} from "@grest-ts/locator";
|
|
5
|
-
import {GGDiscoveryIPC} from "./GGDiscoveryIPC";
|
|
6
|
-
import {SERVER_ERROR} from "@grest-ts/schema";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* ENV variable key when running in tests, for example - this would be PORT of the service discovery instance.
|
|
10
|
-
* If running in tests, the test itself also acts as a service discovery of sorts, allowing multiple instances to run in parallel.
|
|
11
|
-
*/
|
|
12
|
-
export const GG_LOCAL_ROUTER_PORT = "GG_LOCAL_ROUTER_PORT"
|
|
13
|
-
|
|
14
|
-
export class GGLocalDiscoveryClient extends GGDiscoveryClient {
|
|
15
|
-
|
|
16
|
-
public override readonly isLocal = true;
|
|
17
|
-
public readonly port;
|
|
18
|
-
protected readonly entries: GGServiceDiscoveryEntry[] = [];
|
|
19
|
-
protected readonly client: IPCClient;
|
|
20
|
-
|
|
21
|
-
constructor(port?: number) {
|
|
22
|
-
if (port === undefined) {
|
|
23
|
-
throw new Error("No port specified for local router");
|
|
24
|
-
}
|
|
25
|
-
super();
|
|
26
|
-
this.port = port;
|
|
27
|
-
this.client = new IPCClient(port);
|
|
28
|
-
GGLog.info(this, "Started on port: " + this.port);
|
|
29
|
-
|
|
30
|
-
GGLocator.getScope().setWithLifecycle(GG_DISCOVERY, this, {
|
|
31
|
-
type: GGLocatorServiceType.SERVICE_DISCOVERY,
|
|
32
|
-
start: () => this.register(),
|
|
33
|
-
teardown: () => this.unregister()
|
|
34
|
-
})
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
public registerRoutes(registrations: GGServiceRegistration[]): void {
|
|
38
|
-
for (const reg of registrations) {
|
|
39
|
-
this.entries.push({
|
|
40
|
-
api: reg.api,
|
|
41
|
-
baseUrl: `${reg.protocol}://localhost:${reg.port}`,
|
|
42
|
-
pathPrefix: reg.pathPrefix
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
public async discoverApi(apiName: string): Promise<string> {
|
|
48
|
-
await this.ensureConnected();
|
|
49
|
-
const res = await this.client.sendFrameworkRequest(GGDiscoveryIPC.discoveryServer.discoverApi, apiName);
|
|
50
|
-
if (res.success && res.url) {
|
|
51
|
-
return res.url;
|
|
52
|
-
} else {
|
|
53
|
-
throw new SERVER_ERROR({
|
|
54
|
-
displayMessage: res.error ?? "Unknown error",
|
|
55
|
-
debugData: {apiName}
|
|
56
|
-
})
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
public async register(): Promise<void> {
|
|
61
|
-
await this.ensureConnected();
|
|
62
|
-
if (this.entries.length > 0) {
|
|
63
|
-
await this.client.sendFrameworkRequest(GGDiscoveryIPC.discoveryServer.register, this.entries);
|
|
64
|
-
}
|
|
65
|
-
GGLog.info(this, `Registered ${this.entries.length} routes`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
public async unregister(): Promise<void> {
|
|
69
|
-
if (this.entries.length > 0 && this.client.isConnected()) {
|
|
70
|
-
try {
|
|
71
|
-
await this.client.sendFrameworkRequest(GGDiscoveryIPC.discoveryServer.unregister, this.entries);
|
|
72
|
-
} catch (err: any) {
|
|
73
|
-
GGLog.error(this, err);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
this.client.disconnect();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
protected async ensureConnected(): Promise<void> {
|
|
80
|
-
if (!this.client.isConnected()) {
|
|
81
|
-
await this.client.connect();
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* What consumers need to discover and connect to a service
|
|
88
|
-
*/
|
|
89
|
-
export interface GGServiceDiscoveryEntry {
|
|
90
|
-
api: string;
|
|
91
|
-
baseUrl: string; // Complete base URL: "http://localhost:8080" or "http://service-name:8080"
|
|
92
|
-
pathPrefix: string; // Path to append: "/api/users/"
|
|
93
|
-
}
|
|
94
|
-
|
|
1
|
+
import {GG_DISCOVERY, GGDiscoveryClient, GGServiceRegistration} from "@grest-ts/discovery";
|
|
2
|
+
import {GGLog} from "@grest-ts/logger";
|
|
3
|
+
import {IPCClient} from "@grest-ts/ipc";
|
|
4
|
+
import {GGLocator, GGLocatorServiceType} from "@grest-ts/locator";
|
|
5
|
+
import {GGDiscoveryIPC} from "./GGDiscoveryIPC";
|
|
6
|
+
import {SERVER_ERROR} from "@grest-ts/schema";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ENV variable key when running in tests, for example - this would be PORT of the service discovery instance.
|
|
10
|
+
* If running in tests, the test itself also acts as a service discovery of sorts, allowing multiple instances to run in parallel.
|
|
11
|
+
*/
|
|
12
|
+
export const GG_LOCAL_ROUTER_PORT = "GG_LOCAL_ROUTER_PORT"
|
|
13
|
+
|
|
14
|
+
export class GGLocalDiscoveryClient extends GGDiscoveryClient {
|
|
15
|
+
|
|
16
|
+
public override readonly isLocal = true;
|
|
17
|
+
public readonly port;
|
|
18
|
+
protected readonly entries: GGServiceDiscoveryEntry[] = [];
|
|
19
|
+
protected readonly client: IPCClient;
|
|
20
|
+
|
|
21
|
+
constructor(port?: number) {
|
|
22
|
+
if (port === undefined) {
|
|
23
|
+
throw new Error("No port specified for local router");
|
|
24
|
+
}
|
|
25
|
+
super();
|
|
26
|
+
this.port = port;
|
|
27
|
+
this.client = new IPCClient(port);
|
|
28
|
+
GGLog.info(this, "Started on port: " + this.port);
|
|
29
|
+
|
|
30
|
+
GGLocator.getScope().setWithLifecycle(GG_DISCOVERY, this, {
|
|
31
|
+
type: GGLocatorServiceType.SERVICE_DISCOVERY,
|
|
32
|
+
start: () => this.register(),
|
|
33
|
+
teardown: () => this.unregister()
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public registerRoutes(registrations: GGServiceRegistration[]): void {
|
|
38
|
+
for (const reg of registrations) {
|
|
39
|
+
this.entries.push({
|
|
40
|
+
api: reg.api,
|
|
41
|
+
baseUrl: `${reg.protocol}://localhost:${reg.port}`,
|
|
42
|
+
pathPrefix: reg.pathPrefix
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public async discoverApi(apiName: string): Promise<string> {
|
|
48
|
+
await this.ensureConnected();
|
|
49
|
+
const res = await this.client.sendFrameworkRequest(GGDiscoveryIPC.discoveryServer.discoverApi, apiName);
|
|
50
|
+
if (res.success && res.url) {
|
|
51
|
+
return res.url;
|
|
52
|
+
} else {
|
|
53
|
+
throw new SERVER_ERROR({
|
|
54
|
+
displayMessage: res.error ?? "Unknown error",
|
|
55
|
+
debugData: {apiName}
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public async register(): Promise<void> {
|
|
61
|
+
await this.ensureConnected();
|
|
62
|
+
if (this.entries.length > 0) {
|
|
63
|
+
await this.client.sendFrameworkRequest(GGDiscoveryIPC.discoveryServer.register, this.entries);
|
|
64
|
+
}
|
|
65
|
+
GGLog.info(this, `Registered ${this.entries.length} routes`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public async unregister(): Promise<void> {
|
|
69
|
+
if (this.entries.length > 0 && this.client.isConnected()) {
|
|
70
|
+
try {
|
|
71
|
+
await this.client.sendFrameworkRequest(GGDiscoveryIPC.discoveryServer.unregister, this.entries);
|
|
72
|
+
} catch (err: any) {
|
|
73
|
+
GGLog.error(this, err);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
this.client.disconnect();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
protected async ensureConnected(): Promise<void> {
|
|
80
|
+
if (!this.client.isConnected()) {
|
|
81
|
+
await this.client.connect();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* What consumers need to discover and connect to a service
|
|
88
|
+
*/
|
|
89
|
+
export interface GGServiceDiscoveryEntry {
|
|
90
|
+
api: string;
|
|
91
|
+
baseUrl: string; // Complete base URL: "http://localhost:8080" or "http://service-name:8080"
|
|
92
|
+
pathPrefix: string; // Path to append: "/api/users/"
|
|
93
|
+
}
|
|
94
|
+
|
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
import {GGLog} from "@grest-ts/logger";
|
|
2
|
-
import {IPCServer} from "@grest-ts/ipc";
|
|
3
|
-
import {GGLocalDiscoveryServer} from "./GGLocalDiscoveryServer";
|
|
4
|
-
import {GGLocalDiscoveryClient} from "./GGLocalDiscoveryClient";
|
|
5
|
-
|
|
6
|
-
export class GGLocalDiscoveryResilientClient extends GGLocalDiscoveryClient {
|
|
7
|
-
|
|
8
|
-
private discoveryServer?: GGLocalDiscoveryServer;
|
|
9
|
-
private isLeader = false;
|
|
10
|
-
private isShuttingDown = false;
|
|
11
|
-
|
|
12
|
-
constructor(port = 9000) {
|
|
13
|
-
super(port);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
public override async register(): Promise<void> {
|
|
17
|
-
if (!this.isLeader && !this.discoveryServer && !this.client.isConnected()) {
|
|
18
|
-
await this.becomeLeaderOrFollower();
|
|
19
|
-
}
|
|
20
|
-
await super.register();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
public override async unregister(): Promise<void> {
|
|
24
|
-
this.isShuttingDown = true;
|
|
25
|
-
await super.unregister();
|
|
26
|
-
|
|
27
|
-
if (this.discoveryServer) {
|
|
28
|
-
await this.discoveryServer.teardown();
|
|
29
|
-
this.discoveryServer = undefined;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
private async becomeLeaderOrFollower(): Promise<void> {
|
|
34
|
-
if (this.isShuttingDown) return;
|
|
35
|
-
|
|
36
|
-
const server = new IPCServer(this.port);
|
|
37
|
-
const router = new GGLocalDiscoveryServer(server);
|
|
38
|
-
if (await router.start()) {
|
|
39
|
-
this.discoveryServer = router;
|
|
40
|
-
this.isLeader = true;
|
|
41
|
-
GGLog.info(this, "This instance is LEADER");
|
|
42
|
-
} else {
|
|
43
|
-
this.isLeader = false;
|
|
44
|
-
GGLog.info(this, "This instance is FOLLOWER");
|
|
45
|
-
await this.connectToLeader();
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
private async connectToLeader(): Promise<void> {
|
|
50
|
-
if (this.isShuttingDown) return;
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
await this.client.connect();
|
|
54
|
-
|
|
55
|
-
this.client.onClose(async () => {
|
|
56
|
-
if (this.isShuttingDown) return;
|
|
57
|
-
GGLog.warn(this, "Leader died");
|
|
58
|
-
await this.becomeLeaderOrFollower();
|
|
59
|
-
if (this.isLeader && this.entries.length > 0) {
|
|
60
|
-
await super.register();
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
GGLog.debug(this, "Connected to leader");
|
|
65
|
-
} catch (err: any) {
|
|
66
|
-
GGLog.error(this, `Failed to connect to leader: ${err.message}`);
|
|
67
|
-
await this.delay(1000);
|
|
68
|
-
await this.connectToLeader();
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
protected override async ensureConnected(): Promise<void> {
|
|
73
|
-
const maxRetries = 20;
|
|
74
|
-
let retryCount = 0;
|
|
75
|
-
|
|
76
|
-
while (true) {
|
|
77
|
-
try {
|
|
78
|
-
await super.ensureConnected();
|
|
79
|
-
return;
|
|
80
|
-
} catch (err: any) {
|
|
81
|
-
if (retryCount < maxRetries && this.isConnectionError(err)) {
|
|
82
|
-
GGLog.debug(this, `Waiting for router...`);
|
|
83
|
-
await this.delay(Math.min(500 * Math.pow(1.5, retryCount), 5000));
|
|
84
|
-
retryCount++;
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
throw err;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
private delay(ms: number): Promise<void> {
|
|
93
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
private isConnectionError(err: any): boolean {
|
|
97
|
-
const codes = ['ECONNREFUSED', 'ETIMEDOUT', 'ECONNRESET'];
|
|
98
|
-
const hasCode = (code?: string) => code !== undefined && codes.includes(code);
|
|
99
|
-
return hasCode(err?.code) ||
|
|
100
|
-
hasCode(err?.cause?.code) ||
|
|
101
|
-
hasCode(err?.originalError?.code) ||
|
|
102
|
-
hasCode(err?.originalError?.cause?.code);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
1
|
+
import {GGLog} from "@grest-ts/logger";
|
|
2
|
+
import {IPCServer} from "@grest-ts/ipc";
|
|
3
|
+
import {GGLocalDiscoveryServer} from "./GGLocalDiscoveryServer";
|
|
4
|
+
import {GGLocalDiscoveryClient} from "./GGLocalDiscoveryClient";
|
|
5
|
+
|
|
6
|
+
export class GGLocalDiscoveryResilientClient extends GGLocalDiscoveryClient {
|
|
7
|
+
|
|
8
|
+
private discoveryServer?: GGLocalDiscoveryServer;
|
|
9
|
+
private isLeader = false;
|
|
10
|
+
private isShuttingDown = false;
|
|
11
|
+
|
|
12
|
+
constructor(port = 9000) {
|
|
13
|
+
super(port);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public override async register(): Promise<void> {
|
|
17
|
+
if (!this.isLeader && !this.discoveryServer && !this.client.isConnected()) {
|
|
18
|
+
await this.becomeLeaderOrFollower();
|
|
19
|
+
}
|
|
20
|
+
await super.register();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public override async unregister(): Promise<void> {
|
|
24
|
+
this.isShuttingDown = true;
|
|
25
|
+
await super.unregister();
|
|
26
|
+
|
|
27
|
+
if (this.discoveryServer) {
|
|
28
|
+
await this.discoveryServer.teardown();
|
|
29
|
+
this.discoveryServer = undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private async becomeLeaderOrFollower(): Promise<void> {
|
|
34
|
+
if (this.isShuttingDown) return;
|
|
35
|
+
|
|
36
|
+
const server = new IPCServer(this.port);
|
|
37
|
+
const router = new GGLocalDiscoveryServer(server);
|
|
38
|
+
if (await router.start()) {
|
|
39
|
+
this.discoveryServer = router;
|
|
40
|
+
this.isLeader = true;
|
|
41
|
+
GGLog.info(this, "This instance is LEADER");
|
|
42
|
+
} else {
|
|
43
|
+
this.isLeader = false;
|
|
44
|
+
GGLog.info(this, "This instance is FOLLOWER");
|
|
45
|
+
await this.connectToLeader();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private async connectToLeader(): Promise<void> {
|
|
50
|
+
if (this.isShuttingDown) return;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await this.client.connect();
|
|
54
|
+
|
|
55
|
+
this.client.onClose(async () => {
|
|
56
|
+
if (this.isShuttingDown) return;
|
|
57
|
+
GGLog.warn(this, "Leader died");
|
|
58
|
+
await this.becomeLeaderOrFollower();
|
|
59
|
+
if (this.isLeader && this.entries.length > 0) {
|
|
60
|
+
await super.register();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
GGLog.debug(this, "Connected to leader");
|
|
65
|
+
} catch (err: any) {
|
|
66
|
+
GGLog.error(this, `Failed to connect to leader: ${err.message}`);
|
|
67
|
+
await this.delay(1000);
|
|
68
|
+
await this.connectToLeader();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
protected override async ensureConnected(): Promise<void> {
|
|
73
|
+
const maxRetries = 20;
|
|
74
|
+
let retryCount = 0;
|
|
75
|
+
|
|
76
|
+
while (true) {
|
|
77
|
+
try {
|
|
78
|
+
await super.ensureConnected();
|
|
79
|
+
return;
|
|
80
|
+
} catch (err: any) {
|
|
81
|
+
if (retryCount < maxRetries && this.isConnectionError(err)) {
|
|
82
|
+
GGLog.debug(this, `Waiting for router...`);
|
|
83
|
+
await this.delay(Math.min(500 * Math.pow(1.5, retryCount), 5000));
|
|
84
|
+
retryCount++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private delay(ms: number): Promise<void> {
|
|
93
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private isConnectionError(err: any): boolean {
|
|
97
|
+
const codes = ['ECONNREFUSED', 'ETIMEDOUT', 'ECONNRESET'];
|
|
98
|
+
const hasCode = (code?: string) => code !== undefined && codes.includes(code);
|
|
99
|
+
return hasCode(err?.code) ||
|
|
100
|
+
hasCode(err?.cause?.code) ||
|
|
101
|
+
hasCode(err?.originalError?.code) ||
|
|
102
|
+
hasCode(err?.originalError?.cause?.code);
|
|
103
|
+
}
|
|
104
|
+
}
|