-
Notifications
You must be signed in to change notification settings - Fork 8
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
Changes from 35 commits
f66d2fb
c7b898e
97ea6f3
57b0b36
8a43573
511bd7e
0387143
7ba1178
3983453
9a1b7b4
3b578e4
a8ddb14
42b737b
c0a24f2
08f14b9
bd35b9a
f8a861e
db8d5a8
2727b81
c85dc11
3c3334f
4f61f33
d278a1a
e230055
a4eef71
b26b450
310d661
e3ad7bf
b8eeb87
5c8c6f9
ee8bdc8
a42fe6c
35fbd48
89cb1b8
09b7300
cb0cbbc
8376be1
1df9ef7
06772be
db0df5b
1f8c3e6
1f3638d
75708fd
ca70f8e
8c694a9
54f5d09
4e536b2
afae6d5
8a8848e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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"]; | ||
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, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from "./serviceMonitor/index"; | ||
export * from "./types"; |
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; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what about using setters and getters here instead? it would look better |
||
async isAlive(): Promise<ServiceStatus> { | ||
return resolveServiceStatus(sendRequest(this.url, { method: "Get" })); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you need here to make sure the URL is defined or not |
||
} | ||
} |
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. | ||||||
|
@@ -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[]; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we group all related rmb options into one object
|
||||||
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>> { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
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`); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Allow option There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. by default |
||||||
} | ||||||
|
||||||
/** | ||||||
* 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]), | ||||||
); | ||||||
} | ||||||
} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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), | ||||||
); | ||||||
} | ||||||
} |
There was a problem hiding this comment.
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