Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Monitoring: add logic to pick the available stack for a service #3105

Closed
wants to merge 49 commits into from
Closed
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
f66d2fb
fix: update main and types paths
0oM4R Jul 9, 2024
c7b898e
Feat: support log with colors
0oM4R Jul 9, 2024
97ea6f3
Chore: export monitroing types
0oM4R Jul 9, 2024
57b0b36
Chore: add new types that will be used in url picking class
0oM4R Jul 9, 2024
8a43573
Chore: support updating url in IServiceBase
0oM4R Jul 9, 2024
511bd7e
Chore: implement set url for tfchain monitor
0oM4R Jul 9, 2024
0387143
Merge branch 'development_fix_rmb_disconnect' of github.com:threefold…
0oM4R Jul 9, 2024
7ba1178
Chore: implement set url for rmb monitor
0oM4R Jul 9, 2024
3983453
Chore: add error handling in isAlive rmb monitor
0oM4R Jul 9, 2024
9a1b7b4
Chore: export ServiceUrlManager
0oM4R Jul 9, 2024
3b578e4
Chore: support updating url in gridpoxy and graphql monitors
0oM4R Jul 9, 2024
a8ddb14
Chore: rename setUrl to updateUrl
0oM4R Jul 9, 2024
42b737b
Feat: WIP introduce new class, that pick the available stack per service
0oM4R Jul 9, 2024
c0a24f2
Feat: add GetAvailableServices
0oM4R Jul 9, 2024
08f14b9
Refactor: enahnce code and add logs
0oM4R Jul 9, 2024
bd35b9a
Refactor: make sure that mnemonic is provided with rmb
0oM4R Jul 9, 2024
f8a861e
Docs: add example of useing SerivceUrlManager
0oM4R Jul 9, 2024
db8d5a8
Feat: Add Activation service aliveness checker
0oM4R Jul 14, 2024
2727b81
Feat: Add Stats service aliveness checker
0oM4R Jul 14, 2024
c85dc11
chore: push work
MohamedElmdary Jul 14, 2024
3c3334f
Refactor:
0oM4R Jul 15, 2024
4f61f33
Refactor: change the type, now it will be object of promices
0oM4R Jul 15, 2024
d278a1a
Refactor: update example
0oM4R Jul 15, 2024
e230055
Feat: add es6 build for browser
0oM4R Jul 15, 2024
a4eef71
remove tsconfig
0oM4R Jul 15, 2024
b26b450
merge new changes from base
0oM4R Jul 15, 2024
310d661
Refactor: replace axios with fetch
0oM4R Jul 15, 2024
e3ad7bf
Docs: add docs for url manger class
0oM4R Jul 15, 2024
b8eeb87
Refactor:
0oM4R Jul 15, 2024
5c8c6f9
Refactor: resolve warrnings
0oM4R Jul 15, 2024
ee8bdc8
Revert playground changes
0oM4R Jul 15, 2024
a42fe6c
Chore: include activation and stats services
0oM4R Jul 15, 2024
35fbd48
refactor:
0oM4R Jul 16, 2024
89cb1b8
refactor: handle empty urls arrya
0oM4R Jul 16, 2024
09b7300
fix typo
0oM4R Jul 16, 2024
cb0cbbc
refactor: - add setter and getters for ILivenessaChecke interface
0oM4R Jul 17, 2024
8376be1
chore: apply types changes
0oM4R Jul 17, 2024
1df9ef7
chore: update types
0oM4R Jul 17, 2024
06772be
refactor: make service url optional in conostracutor
0oM4R Jul 17, 2024
db0df5b
refactor: RMB: make service url optional in conostracutor
0oM4R Jul 17, 2024
1f8c3e6
refactor: expose serviceURlManager to seperet file
0oM4R Jul 17, 2024
1f3638d
chore: add docstring for some types
0oM4R Jul 17, 2024
75708fd
chore: update exports
0oM4R Jul 17, 2024
ca70f8e
chore: add update url method
0oM4R Jul 17, 2024
8c694a9
chore: make url setter private and add update method
0oM4R Jul 17, 2024
54f5d09
feat: Introduce service url manager
0oM4R Jul 17, 2024
4e536b2
Merge branch 'development' of github.com:threefoldtech/tfgrid-sdk-ts …
0oM4R Jul 18, 2024
afae6d5
Chore:
0oM4R Jul 21, 2024
8a8848e
Chore:
0oM4R Jul 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/monitoring/example/stackAvilablity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const gridproxy = ["test", "hello", "https://gridproxy.dev.grid.tf/"];
const rmb = ["wsefews://tfin.dev.grid.tf/ws", "wss://relay.dev.grid.tf"];
const graphql = ["https://graphql.dev.grid.tf/graphql"];
const tfChain = ["wss://tfchain.dev.grid.tf/ws", "wsss://tfchain.dev.grid.tf/ws"];
Comment on lines +1 to +4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that don't look like valid urls, you should use a valid url but not reachable

const mnemonic = "<MNEMONIC>";

import { ServiceUrlManager, StackPickerOptions } from "../src";

async function checkStacksAvailability(services: StackPickerOptions) {
try {
const pickedUrls = await new ServiceUrlManager(services).getAvailableServices();
await Promise.all(Object.values(pickedUrls));
console.log(pickedUrls);
process.exit(0);
} catch (err) {
console.log(err);
}
}

checkStacksAvailability({
tfChain,
rmb,
graphql,
gridproxy,
mnemonic,
});
17 changes: 13 additions & 4 deletions packages/monitoring/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,21 @@
"version": "2.5.0",
"description": "Threefold monitoring package",
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"main": "./dist/node/index.js",
"module": "./dist/es6/index.js",
"exports": {
"require": "./dist/node/index.js",
"import": "./dist/es6/index.js"
},
"types": "dist/es6/index.d.ts",
"files": [
"/dist"
],
"scripts": {
"example": "yarn run ts-node --project tsconfig.json example/index.ts",
"build": "tsc",
"build": "npm-run-all es6-build node-build",
"node-build": "tsc --build tsconfig-node.json",
"es6-build": "tsc --build tsconfig-es6.json",
"test": "jest "
},
"author": "Omar Kassem",
Expand All @@ -19,7 +29,6 @@
"@threefold/rmb_direct_client": "2.5.0",
"@threefold/tfchain_client": "2.5.0",
"@threefold/types": "2.5.0",
"axios": "^0.27.2",
"chalk": "4.1.2",
"ts-node": "^10.9.1",
"typescript": "^5.3.3"
Expand Down
14 changes: 9 additions & 5 deletions packages/monitoring/src/helpers/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,25 @@ class MonitorEventEmitter extends EventEmitter {
this.addListener(MonitorEvents.storeStatus, this.addToServiceSummary);
this.addListener(MonitorEvents.summarize, this.printStatusSummary);
}
public log(message: string) {
this.emit("MonitorLog", message);
public log(message: string, color?: string) {
this.emit("MonitorLog", message, color);
}
public summarize() {
this.emit("MonitorSummarize");
}
public storeStatus(serviceName: string, isAlive: boolean) {
this.emit("MonitorStoreStatus", serviceName, isAlive);
}
public serviceDown(serviceName: string, error: Error) {
public serviceDown(serviceName: string, error?: Error) {
this.emit("MonitorServiceDown", serviceName, error);
}

private monitorLogsHandler(msg) {
console.log(msg);
private monitorLogsHandler(msg: unknown, color?: string) {
if (color && chalk[color]) {
console.log(chalk[color](msg));
} else {
console.log(msg);
}
}
private serviceDownHandler(serviceName: string, error: Error) {
console.log(`${chalk.red.bold(serviceName + " is Down")}`);
Expand Down
14 changes: 6 additions & 8 deletions packages/monitoring/src/helpers/utils.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { RequestError } from "@threefold/types";
import axios, { AxiosError, AxiosRequestConfig } from "axios";
import { ServiceStatus } from "src/types";

export async function sendGetRequest(url: string, options: AxiosRequestConfig = {}) {
import { ServiceStatus } from "../types";

export async function sendRequest(url: string, options: RequestInit) {
try {
return await axios.get(url, options);
const res = await fetch(url, options);
if (!res?.ok) throw Error(`HTTP Response Code: ${res?.status}`);
} catch (e) {
const { response } = e as AxiosError;
const errorMessage = (response?.data as { error: string })?.error || (e as Error).message;

throw new RequestError(`HTTP request failed ${errorMessage ? "due to " + errorMessage : ""}.`);
throw new RequestError(`HTTP request failed due to ${e}.`);
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/monitoring/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./serviceMonitor/index";
export * from "./types";
22 changes: 22 additions & 0 deletions packages/monitoring/src/serviceMonitor/activation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { resolveServiceStatus, sendRequest } from "../helpers/utils";
import { ILivenessChecker, ServiceStatus } from "../types";

export class Activation implements ILivenessChecker {
private readonly name = "Activation";
private url: string;
constructor(activationServiceUrl: string) {
this.url = activationServiceUrl;
}
serviceName() {
return this.name;
}
serviceUrl() {
return this.url;
}
updateUrl(url: string) {
this.url = url;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about using setters and getters here instead?

it would look better service.url instead of service.serviceUrl and so on...

async isAlive(): Promise<ServiceStatus> {
return resolveServiceStatus(sendRequest(this.url, { method: "Get" }));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you need here to make sure the URL is defined or not

}
}
223 changes: 222 additions & 1 deletion packages/monitoring/src/serviceMonitor/alivenessChecker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import { KeypairType } from "@polkadot/util-crypto/types";

import { monitorEvents } from "../helpers/events";
import { IDisconnectHandler, ILivenessChecker } from "../types";
import {
IDisconnectHandler,
ILivenessChecker,
ServiceName,
ServicesUrls,
ServiceUrl,
StackPickerOptions,
} from "../types";
import { Activation } from "./activation";
import { GraphQLMonitor } from "./graphql";
import { GridProxyMonitor } from "./gridproxy";
import { RMBMonitor } from "./rmb";
import { Stats } from "./stats";
import { TFChainMonitor } from "./tfChain";

/**
* Represents a service monitor that periodically checks the liveness of multiple services.
Expand Down Expand Up @@ -77,3 +92,209 @@ export class ServiceMonitor {
await this.disconnect();
}
}

/**
* Manages service URLs, checking their availability and ensuring they are reachable.
*
* @property {number} retries - Number of retry attempts for each URL.
* @property {string[]} [ServiceName.tfChain] - URLs for tfChain service.
* @property {string[]} [ServiceName.GraphQl] - URLs for GraphQL service.
* @property {string[]} [ServiceName.RMB] - URLs for RMB service.
* @property {string[]} [ServiceName.GridPrixy] - URLs for GridProxy service.
* @property {string[]} [ServiceName.Stats] - URLs for stats service.
* @property {string[]} [ServiceName.Activation] - URLs for activation service.
* @property {string} [mnemonic] - Mnemonic required for RMB service.
* @property {KeypairType} keypairType - Type of keypair, default is "sr25519".
* @property {boolean} rmbValidatesChain - Indicates if RMB will validate the chain url.
* @property {string[]} rmbTFchainUrls - URLs of TFChain for RMB service, Required in case there is no TFChain urls for tfChain service.
* If there is a tfChain availability service provided, and it's result is valid, we will ignore the rmb-tfChain options
* @property {boolean} silent - To return null instead of throw an Error, Default is False
*/
export class ServiceUrlManager<N extends boolean = false> {
private result: ServicesUrls<N> = {};
private retries = 3;
private [ServiceName.tfChain]?: string[];
private [ServiceName.GraphQl]?: string[];
private [ServiceName.RMB]?: string[];
private [ServiceName.GridPrixy]?: string[];
private [ServiceName.Activation]?: string[];
private [ServiceName.Stats]?: string[];
private mnemonic?: string;
private keypairType: KeypairType = "sr25519";
private rmbValidatesChain = false;
private rmbTFchainUrls: string[];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we group all related rmb options into one object

rmb: {
urls: string[],
chainUrls?: string[],
validateChainUrl = false,
}

private silent: N = false as N;

constructor(options?: StackPickerOptions<N>) {
Object.assign(this, options);
}

/**
* Pings the given service to check if it is alive.
*
* This method checks the liveness of the provided service by calling its `isAlive` method.
* If the service supports disconnection (implements IDisconnectHandler), it calls the `disconnect` method after the liveness check.
*
* @param {ILivenessChecker} service - An instance of ILivenessChecker that provides methods to check the service's liveness.
*
* @returns {Promise<{alive: boolean, error?: Error}>} - A promise that resolves with the liveness status of the service.
*/
private async pingService(service: ILivenessChecker) {
const status = await service.isAlive();
if ("disconnect" in service) {
await (service as IDisconnectHandler).disconnect();
}
return status;
}

/**
* Attempts to find a reachable service URL from a list of provided URLs.
*
* This method iterates through the list of URLs, repeatedly pinging the service to
* check if it is alive. If a reachable URL is found, it is returned. If all URLs
* are exhausted without finding a reachable service, an error is thrown.
*
* @param {string[]} urls - An array of service URLs to check for reachability.
* @param {ILivenessChecker} service - An instance of ILivenessChecker that provides methods
* to check the service's liveness and manage service URLs.
*
* @returns {Promise<string>} - A promise that resolves with the reachable service URL.
*
* @throws {Error} - Throws an error if no reachable service URL is found after checking all URLs.
*
*/
async getAvailableServiceStack(urls: string[], service: ILivenessChecker): Promise<ServiceUrl<N>> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we should pass service instance, i don't like it but this will reduce code redundancy and handle all types of ILivenessChecker

let error: Error | string = "";
for (let i = 0; i < urls.length; i++) {
if (i != 0) await service.updateUrl(urls[i]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (i != 0) await service.updateUrl(urls[i]);
if (i != 0) service.updateUrl(urls[i]);

monitorEvents.log(`${service.serviceName()}: pinging ${service.serviceUrl()}`, "gray");
for (let retry = 0; retry < this.retries; retry++) {
const status = await this.pingService(service);
if (status.alive) {
monitorEvents.log(`${service.serviceName()} on ${service.serviceUrl()} Success!`, "green");
return service.serviceUrl();
}
error = status.error ?? "";
}
monitorEvents.log(
`${service.serviceName()}: failed to ping ${service.serviceUrl()} after ${this.retries} retries; ${error}`,
"red",
);
}
if (this.silent) return null as any;
throw new Error(` Failed to reach ${service.serviceName()} on all provided stacks`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allow option silent to return null instead of error

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by default silent: false

}

/**
* Fetches available service URLs for tfChain and RMB services.
*
* This method checks the availability of URLs for services by utilizing
* the `getAvailableServiceStack` method. It updates the `result` property with the reachable URLs.
*
* @returns {Promise<ServicesUrls>} - A promise that resolves with an object containing the available service URLs.
*/
async getAvailableServices(): Promise<ServicesUrls<N>> {
const result: any = {};

if (this[ServiceName.GraphQl]) {
if (this[ServiceName.GraphQl]?.length == 0)
result[ServiceName.GraphQl] = this.handleSilent(
"Can't validate GraphQL stacks; There is GraphQL urls provided",
);
else {
result[ServiceName.GraphQl] = this.getAvailableServiceStack(
this[ServiceName.GraphQl],
new GraphQLMonitor(this[ServiceName.GraphQl][0]),
);
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should I extract this logic to a separated function like RMB?


if (this[ServiceName.GridPrixy]) {
if (this[ServiceName.GridPrixy]?.length == 0) {
result[ServiceName.GridPrixy] = this.handleSilent(
"Can't validate GridProxy stacks; There is GridProxy urls provided",
);
} else {
result[ServiceName.GridPrixy] = this.getAvailableServiceStack(
this[ServiceName.GridPrixy],
new GridProxyMonitor(this[ServiceName.GridPrixy][0]),
);
}
}

if (this[ServiceName.Stats]) {
if (this[ServiceName.Stats]?.length == 0) {
result[ServiceName.Stats] = this.handleSilent("Can't validate Stats stacks; There is Stats urls provided");
} else {
result[ServiceName.Stats] = this.getAvailableServiceStack(
this[ServiceName.Stats],
new Stats(this[ServiceName.Stats][0]),
);
}
}

if (this[ServiceName.Activation]) {
if (this[ServiceName.Activation]?.length == 0) {
result[ServiceName.Activation] = this.handleSilent(
"Can't validate Activation stacks; There is Activation urls provided",
);
} else {
result[ServiceName.Activation] = this.getAvailableServiceStack(
this[ServiceName.Activation],
new Activation(this[ServiceName.Activation][0]),
);
}
}

if (this[ServiceName.tfChain]) {
if (this[ServiceName.tfChain]?.length == 0) {
result[ServiceName.tfChain] = this.handleSilent(
"Can't validate tfChain stacks; There is tfChain urls provided",
);
} else {
result[ServiceName.tfChain] = this.getAvailableServiceStack(
this[ServiceName.tfChain],
new TFChainMonitor(this[ServiceName.tfChain][0]),
);
}
}

if (this[ServiceName.RMB])
result[ServiceName.RMB] = this.validateRMBStacks(this[ServiceName.RMB], this.result?.tfChain);

const entries = Object.entries(result);
const values = await Promise.all(entries.map(r => r[1]));
for (let i = 0; i < entries.length; i++) this.result[entries[i][0]] = values[i];
return this.result;
}

private handleSilent(errorMsg: string) {
if (this.silent) {
console.log(errorMsg);
return null;
}
throw new Error(errorMsg);
}

private async validateRMBStacks(rmbUrls: string[], validatedChainUrl?: ServiceUrl<N> | undefined) {
if (!this.mnemonic) return this.handleSilent("Failed to validate RMB, Mnemonic is required");

let chainUrl: ServiceUrl<N> | undefined = undefined;
if (rmbUrls.length == 0) return this.handleSilent("Can't validate RMB stacks; There is RMB urls provided");
if (!validatedChainUrl && (!this.rmbTFchainUrls || this.rmbTFchainUrls.length == 0))
return this.handleSilent("Can't validate RMB urls; There is no Chain urls provided");

// chain Url
if (validatedChainUrl) chainUrl = validatedChainUrl;
else
chainUrl = this.rmbValidatesChain
? await this.getAvailableServiceStack(this.rmbTFchainUrls, new TFChainMonitor(this.rmbTFchainUrls[0]))
: this.rmbTFchainUrls[0];
if (!chainUrl) return this.handleSilent("Failed to validate RMB, there is no available valid Chain url");

return this.getAvailableServiceStack(
rmbUrls,
new RMBMonitor(rmbUrls[0], chainUrl, this.mnemonic!, this.keypairType),
);
}
}
Loading
Loading