Introduce prune scheduler to prune orphan instances and leases (#156)

This commit is contained in:
Chalith Desaman
2022-07-11 14:17:47 +05:30
committed by GitHub
parent 0a0cdcfa5b
commit 52b66dabb4
6 changed files with 154 additions and 14 deletions

View File

@@ -63,7 +63,7 @@ async function main() {
console.log('Data dir: ' + appenv.DATA_DIR);
console.log('Using Sashimono cli: ' + appenv.SASHI_CLI_PATH);
const mb = new MessageBoard(appenv.CONFIG_PATH, appenv.SECRET_CONFIG_PATH, appenv.DB_PATH, appenv.SASHI_CLI_PATH);
const mb = new MessageBoard(appenv.CONFIG_PATH, appenv.SECRET_CONFIG_PATH, appenv.DB_PATH, appenv.SASHI_CLI_PATH, appenv.SASHI_DB_PATH);
await mb.init();
}

View File

@@ -1,4 +1,5 @@
const process = require('process');
const path = require('path');
let appenv = {
IS_DEV_MODE: process.env.MB_DEV === "1",
@@ -16,9 +17,12 @@ appenv = {
DB_PATH: appenv.DATA_DIR + '/mb-xrpl.sqlite',
DB_TABLE_NAME: 'leases',
DB_UTIL_TABLE_NAME: 'util_data',
SASHI_DB_PATH: (appenv.IS_DEV_MODE ? "../build/" : path.join(appenv.DATA_DIR, '../')) + "sa.sqlite",
SASHI_TABLE_NAME: 'instances',
LAST_WATCHED_LEDGER: 'last_watched_ledger',
ACQUIRE_LEASE_TIMEOUT_THRESHOLD: 0.8,
ACQUIRE_LEASE_WAIT_TIMEOUT_THRESHOLD: 0.4,
ORPHAN_PRUNE_SCHEDULER_INTERVAL_HOURS: 4,
SASHI_CLI_PATH: appenv.IS_DEV_MODE ? "../build/sashi" : "/usr/bin/sashi",
MB_VERSION: '0.5.4',
TOS_HASH: '757A0237B44D8B2BBB04AE2BAD5813858E0AECD2F0B217075E27E0630BA74314' // This is the sha256 hash of TOS text.

View File

@@ -15,7 +15,7 @@ const LeaseStatus = {
}
class MessageBoard {
constructor(configPath, secretConfigPath, dbPath, sashiCliPath) {
constructor(configPath, secretConfigPath, dbPath, sashiCliPath, sashiDbPath) {
this.configPath = configPath;
this.secretConfigPath = secretConfigPath;
this.leaseTable = appenv.DB_TABLE_NAME;
@@ -28,6 +28,8 @@ class MessageBoard {
this.sashiCli = new SashiCLI(sashiCliPath);
this.db = new SqliteDatabase(dbPath);
this.sashiDb = new SqliteDatabase(sashiDbPath);
this.sashiTable = appenv.SASHI_TABLE_NAME
}
async init() {
@@ -81,7 +83,7 @@ class MessageBoard {
const currentMoment = await this.hostClient.getMoment(e.ledger_index);
// Sending heartbeat every CONF_HOST_HEARTBEAT_FREQ moments.
if ( ! ongoingHeartbeat &&
if (!ongoingHeartbeat &&
(this.lastHeartbeatMoment === 0 || (currentMoment % this.hostClient.config.hostHeartbeatFreq === 0 && currentMoment !== this.lastHeartbeatMoment))) {
ongoingHeartbeat = true;
console.log(`Reporting heartbeat at Moment ${this.lastHeartbeatMoment}...`);
@@ -117,7 +119,7 @@ class MessageBoard {
console.log(`Cannot find a NFT for ${x.containerName}`);
else {
const uriInfo = evernode.UtilHelpers.decodeLeaseNftUri(nft.URI);
await this.destroyInstance(x.containerName, x.tenant, uriInfo.leaseIndex, true);
await this.destroyInstance(x.containerName, x.tenant, uriInfo.leaseIndex);
}
this.activeInstanceCount--;
@@ -142,6 +144,9 @@ class MessageBoard {
this.hostClient.on(evernode.HostEvents.AcquireLease, r => this.handleAcquireLease(r));
this.hostClient.on(evernode.HostEvents.ExtendLease, r => this.handleExtendLease(r));
// Start a job to prune the orphan instances.
this.#startPruneScheduler();
}
// Connect the host and trying to reconnect in the event of account not found error.
@@ -172,6 +177,137 @@ class MessageBoard {
}
}
#startPruneScheduler() {
const timeout = appenv.ORPHAN_PRUNE_SCHEDULER_INTERVAL_HOURS * 3600000; // Hours to millisecs.
const scheduler = async () => {
console.log(`Starting the scheduled prune job...`);
await this.#pruneOrphanLeases().catch(console.error);
console.log(`Stopped the scheduled prune job.`);
setTimeout(async () => {
await scheduler();
}, timeout);
};
setTimeout(async () => {
await scheduler();
}, timeout);
}
async #pruneOrphanLeases() {
// Note: If this is soft deletion we need to handle the destroyed status and replace deleteLeaseRecord with changing the status.
// Get the records which are created before an acquire timeout x 2.
// Take the xrpl ledger time as 4 seconds.
const timeoutSecs = (this.hostClient.config.leaseAcquireWindow * 4 * appenv.ACQUIRE_LEASE_TIMEOUT_THRESHOLD) * 2;
const timeMargin = new Date(Date.now() - (1000 * timeoutSecs));
this.sashiDb.open();
const instances = (await this.sashiDb.getValues(this.sashiTable));
this.sashiDb.close();
this.db.open();
const leases = (await this.db.getValues(this.leaseTable));
this.db.close();
let activeInstanceCount = leases.filter(r => (r.status === LeaseStatus.ACQUIRED || r.status === LeaseStatus.EXTENDED)).length;
// Remove the instances which are orphan.
// Only consider the older ones.
for (const instance of instances.filter(i => i.time < timeMargin)) {
try {
const leaseIndex = leases.findIndex(l => l.container_name === instance.name);
const lease = leaseIndex >= 0 ? leases[leaseIndex] : null;
// If there's a lease record this is created from message board.
if (lease) {
leases.splice(leaseIndex, 1);
const nft = (await (new evernode.XrplAccount(lease.tenant_xrp_address)).getNfts())?.find(n => n.NFTokenID == instance.name);
// If lease is in ACQUIRING status acquire response is not received by the tenant and lease is not in expiry list.
// If the NFT is not owned by the tenant we destroy the instance since this is not a valid lease.
// In these cases, destroy the instance.
if (lease.status === LeaseStatus.ACQUIRING || !nft) {
console.log(`Pruning orphan instance ${instance.name}...`);
await this.sashiCli.destroyInstance(instance.name);
// After destroying, If the NFT is owned by the tenant, burn the NFT and recreate and refund the tenant.
if (nft) {
const uriInfo = evernode.UtilHelpers.decodeLeaseNftUri(nft.URI);
await this.recreateLeaseOffer(instance.name, lease.tenant_xrp_address, uriInfo.leaseIndex);
console.log(`Refunding tenant ${lease.tenant_xrp_address}...`);
await this.hostClient.refundTenant(lease.tx_hash, lease.tenant_xrp_address, uriInfo.leaseAmount.toString());
}
// Remove the lease record.
if (lease) {
this.db.open();
await this.deleteLeaseRecord(lease.tx_hash);
this.db.close();
if (lease.status === LeaseStatus.ACQUIRED || lease.status === LeaseStatus.EXTENDED)
activeInstanceCount--;
}
}
}
else {
// If there's no lease but the name matches with NFT pattern,
// This is created from the message board but lease record is missing.
const namePrefix = this.hostClient.getLeaseNFTokenIdPrefix();
if (instance.name.startsWith(namePrefix)) {
console.log(`Pruning orphan instance ${instance.name}...`);
await this.sashiCli.destroyInstance(instance.name);
}
}
}
catch (e) {
console.error(e);
}
}
// Remove the leases which are orphan (Does not have an instance).
// Only consider the older ones.
for (const lease of leases.filter(l => l.timestamp < timeMargin && (l.status === LeaseStatus.ACQUIRING || l.status === LeaseStatus.ACQUIRED || l.status === LeaseStatus.EXTENDED))) {
try {
// If lease does not have an instance.
this.sashiDb.open();
const instances = (await this.sashiDb.getValues(this.sashiTable, { name: lease.container_name }));
this.sashiDb.close();
if (!instances || instances.length === 0) {
console.log(`Pruning orphan lease ${lease.container_name}...`);
this.db.open();
await this.deleteLeaseRecord(lease.tx_hash);
this.db.close();
if (lease.status === LeaseStatus.ACQUIRED || lease.status === LeaseStatus.EXTENDED)
activeInstanceCount--;
const nft = (await (new evernode.XrplAccount(lease.tenant_xrp_address)).getNfts())?.find(n => n.NFTokenID == lease.container_name);
if (nft) {
const uriInfo = evernode.UtilHelpers.decodeLeaseNftUri(nft.URI);
await this.recreateLeaseOffer(lease.container_name, lease.tenant_xrp_address, uriInfo.leaseIndex);
// If lease is in ACQUIRING status acquire response is not received by the tenant and lease is not in expiry list.
if (lease.status === LeaseStatus.ACQUIRING) {
console.log(`Refunding tenant ${lease.tenant_xrp_address}...`);
await this.hostClient.refundTenant(lease.tx_hash, lease.tenant_xrp_address, uriInfo.leaseAmount.toString());
}
}
}
}
catch (e) {
console.error(e);
}
}
// If active instance count is updated, Send the update registration transaction.
if (this.activeInstanceCount !== activeInstanceCount) {
this.activeInstanceCount = activeInstanceCount;
await this.hostClient.updateRegInfo(this.activeInstanceCount);
}
}
async recreateLeaseOffer(nfTokenId, tenantAddress, leaseIndex) {
// Burn the NFTs and recreate the offer and send back the lease amount back to the tenant.
await this.hostClient.expireLease(nfTokenId, tenantAddress).catch(console.error);

View File

@@ -72,7 +72,7 @@ class SqliteDatabase {
});
}
getValues(tableName, filter = null) {
getValues(tableName, filter = null, op = '=') {
if (!this.db)
throw 'Database connection is not open.';
@@ -81,7 +81,7 @@ class SqliteDatabase {
if (filter) {
const columnNames = Object.keys(filter);
for (const columnName of columnNames) {
filterStr += `${columnName} = ? AND `;
filterStr += `${columnName} ${op} ? AND `;
values.push(filter[columnName] ? filter[columnName] : 'NULL');
}
}

View File

@@ -6,7 +6,7 @@
"": {
"name": "mb-xrpl",
"dependencies": {
"evernode-js-client": "0.4.39",
"evernode-js-client": "0.4.45",
"sqlite3": "5.0.2"
},
"devDependencies": {
@@ -996,9 +996,9 @@
}
},
"node_modules/evernode-js-client": {
"version": "0.4.39",
"resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.4.39.tgz",
"integrity": "sha512-CLGTGUuBiPsocUpIt0WZKkBRtGcol80uBKbTlvfz9StdAU8H6zhfUMOCU5KmiPxjOWf4Bcx69z04FIwOmqmW1A==",
"version": "0.4.45",
"resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.4.45.tgz",
"integrity": "sha512-gybY1TxytT+OMQsDj1g8F+mfeeZy4uwfaAOKx4yXI4fNdiaA9Ji23+WVB9mPyIxruunbGTdvmbdyBxIaYeZ+tQ==",
"dependencies": {
"elliptic": "6.5.4",
"ripple-address-codec": "4.2.0",
@@ -3899,9 +3899,9 @@
"dev": true
},
"evernode-js-client": {
"version": "0.4.39",
"resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.4.39.tgz",
"integrity": "sha512-CLGTGUuBiPsocUpIt0WZKkBRtGcol80uBKbTlvfz9StdAU8H6zhfUMOCU5KmiPxjOWf4Bcx69z04FIwOmqmW1A==",
"version": "0.4.45",
"resolved": "https://registry.npmjs.org/evernode-js-client/-/evernode-js-client-0.4.45.tgz",
"integrity": "sha512-gybY1TxytT+OMQsDj1g8F+mfeeZy4uwfaAOKx4yXI4fNdiaA9Ji23+WVB9mPyIxruunbGTdvmbdyBxIaYeZ+tQ==",
"requires": {
"elliptic": "6.5.4",
"ripple-address-codec": "4.2.0",

View File

@@ -5,7 +5,7 @@
"build": "npm run lint && ncc build app.js --minify -o dist"
},
"dependencies": {
"evernode-js-client": "0.4.39",
"evernode-js-client": "0.4.45",
"sqlite3": "5.0.2"
},
"devDependencies": {