mirror of
https://github.com/EvernodeXRPL/sashimono.git
synced 2026-06-07 02:36:49 +00:00
Introduce prune scheduler to prune orphan instances and leases (#156)
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
14
mb-xrpl/package-lock.json
generated
14
mb-xrpl/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user