Skip to content

Commit

Permalink
fix: Transaction locking
Browse files Browse the repository at this point in the history
  • Loading branch information
NabinKawan committed May 8, 2024
1 parent b2c848d commit bc5cb0f
Show file tree
Hide file tree
Showing 18 changed files with 241 additions and 135 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ const environments = {
process.env.KUBER_API_URL || "https://sanchonet.kuber.cardanoapi.io",
apiKey: process.env.KUBER_API_KEY || "",
},
txTimeOut: parseInt(process.env.TX_TIMEOUT) || 120000,
txTimeOut: parseInt(process.env.TX_TIMEOUT) || 240000,
metadataBucketUrl:
process.env.METADATA_BUCKET_URL || "https://metadata.cardanoapi.io/data",
`${process.env.CARDANOAPI_METADATA_URL}/data` ||
"https://metadata.cardanoapi.io/data",
lockInterceptorUrl:
`${process.env.CARDANOAPI_METADATA_URL}/data` ||
"https://metadata.cardanoapi.io/lock",
};

export default environments;
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export const faucetWallet: StaticWallet = {

export const dRep01Wallet: StaticWallet = {
payment: {
private: "2f1053f22707b9881ea6112024027a660bd5508e22081cf5e4e95cc663802dd9",
public: "891ed5096ee248bc7f31a3094ea90f34485483eb1050c7ee368e64d04b90a009",
private: "2f1053f22707b9881ea6112024027a660bd5508e22081cf5e4e95cc663802dd9",
pkh: "5775ad2fb14ca1b45381a40e40f0c06081edaf2261e02bbcebcf8dc3",
},
stake: {
Expand Down
33 changes: 26 additions & 7 deletions tests/govtool-frontend/playwright/lib/helpers/transaction.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import environments from "@constants/environments";
import { Page, expect } from "@playwright/test";
import kuberService from "@services/kuberService";
import { LockInterceptor } from "lib/lockInterceptor";
import { LockInterceptor, LockInterceptorInfo } from "lib/lockInterceptor";
import { Logger } from "../../../cypress/lib/logger/logger";

/**
* Polls the transaction status until it's resolved or times out.
* address is used to release lock of that address
*/
export async function pollTransaction(txHash: string, address?: string) {
export async function pollTransaction(
txHash: string,
lockInfo?: LockInterceptorInfo
) {
try {
Logger.info(`Waiting for tx completion: ${txHash}`);
await expect
Expand All @@ -18,17 +21,33 @@ export async function pollTransaction(txHash: string, address?: string) {
const data = await response.json();
return data.length;
},
{ message: "Transaction failed", timeout: environments.txTimeOut }
{
timeout: environments.txTimeOut,
}
)
.toBeGreaterThan(0);

Logger.success("Tx completed");

if (!lockInfo) return;

await LockInterceptor.releaseLockForAddress(
lockInfo.address,
lockInfo.lockId,
`Task completed for:${lockInfo.lockId}`
);
} catch (err) {
throw err;
} finally {
if (!address) return;
if (lockInfo) {
const errorMessage = { lockInfo, error: JSON.stringify(err) };

await LockInterceptor.releaseLockForAddress(address);
await LockInterceptor.releaseLockForAddress(
lockInfo.address,
lockInfo.lockId,
`Task failure: \n${JSON.stringify(errorMessage)}`
);
}

throw err;
}
}

Expand Down
98 changes: 75 additions & 23 deletions tests/govtool-frontend/playwright/lib/lockInterceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,29 @@ import { Logger } from "../../cypress/lib/logger/logger";

import path = require("path");

export interface LockInterceptorInfo {
lockId: string;
address: string;
}

abstract class BaseLock {
abstract acquireLock(key: string, id?: string): Promise<boolean>;

abstract releaseLock(key: string, id?: string): Promise<boolean>;

abstract checkLock(key: string): Promise<boolean>;
}

export class LockInterceptor {
private static async acquireLock(
address: string,
message?: string
lockId: string
): Promise<void> {
const lockFilePath = path.resolve(__dirname, `../.lock-pool/${address}`);

try {
await log(
`${address} -> acquiring lock` + message && `\nMessage: ${message}`
`Initiator: ${address} \n---------------------> acquiring lock for:${lockId}`
);
await new Promise<void>((resolve, reject) => {
lockfile.lock(lockFilePath, (err) => {
Expand All @@ -25,21 +38,23 @@ export class LockInterceptor {
}
});
});
await log(`${address} -> acquired lock`);
await log(
`Initiator: ${address} \n---------------------> acquired lock for:${lockId}`
);
} catch (err) {
Logger.fail("Failed to write lock logs");
throw err;
}
}

private static async releaseLock(
address: string,
message?: string
lockId: string
): Promise<void> {
const lockFilePath = path.resolve(__dirname, `../.lock-pool/${address}`);

try {
await log(
`${address} -> releasing lock` + message && `\nMessage: ${message}`
`Initiator: ${address} \n---------------------> releasing lock for:${lockId}`
);
await new Promise<void>((resolve, reject) => {
lockfile.unlock(lockFilePath, async (err) => {
Expand All @@ -50,21 +65,23 @@ export class LockInterceptor {
}
});
});
await log(`${address} -> released lock\n`);
await log(
`Initiator: ${address} \n---------------------> released lock for:${lockId}\n`
);
} catch (err) {
Logger.fail("Failed to write lock logs");
throw err;
}
}

private static async waitForReleaseLock(
address: string,
message?: string
lockId: string
): Promise<void> {
const pollInterval = 200;
const pollInterval = 4000; // 4 secs

try {
await log(
`${address} -> waiting lock` + message && `\nMessage: ${message}`
`Initiator: ${address} \n ---------------------> waiting lock for:${lockId}`
);
return new Promise<void>((resolve, reject) => {
const pollFn = () => {
Expand All @@ -83,32 +100,57 @@ export class LockInterceptor {
pollFn();
});
} catch (err) {
Logger.fail("Failed to write lock logs");
throw err;
}
}

static async intercept(
address: string,
callbackFn: () => Promise<TxSubmitResponse>,
message?: string
lockId: string,
provider: "local" | "server" = "local"
): Promise<TxSubmitResponse> {
const isAddressLocked = checkAddressLock(address);
if (isAddressLocked) {
await LockInterceptor.waitForReleaseLock(address, message);
}
while (true) {
const isAddressLocked = checkAddressLock(address);
if (isAddressLocked) {
await LockInterceptor.waitForReleaseLock(address, lockId);
}

await LockInterceptor.acquireLock(address, message);
try {
await LockInterceptor.acquireLock(address, lockId);
break;
} catch (err) {
if (err.code === "EEXIST") {
await new Promise((resolve) => setTimeout(resolve, 1000)); // timeout for retry
continue;
} else {
throw err;
}
}
}
try {
const res = await callbackFn();
return { ...res, address };
return { ...res, lockInfo: { lockId, address } };
} catch (err) {
await LockInterceptor.releaseLock(address, "Tx failure");
const errorMessage = { lock_id: lockId, error: JSON.stringify(err) };
await log(`Task failure: \n${JSON.stringify(errorMessage)}`);
await LockInterceptor.releaseLock(address, lockId);
throw err;
}
}

static async releaseLockForAddress(address: string) {
await this.releaseLock(address);
static async releaseLockForAddress(
address: string,
lockId: string,
message?: string
) {
try {
message && (await log(message));

await this.releaseLock(address, lockId);
} catch {
Logger.fail("Failed to write lock logs");
}
}
}

Expand All @@ -118,8 +160,18 @@ function checkAddressLock(address: string): boolean {
}

function log(message: string): Promise<void> {
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
timeZone: "Asia/Kathmandu",
};
const logFilePath = path.resolve(__dirname, "../.logs/lock_logs.txt");
const logMessage = `[${new Date().toISOString()}] ${message}\n`;
const logMessage = `[${new Date().toLocaleString("en-US", options)}] ${message}\n`;
return new Promise((resolve, reject) => {
fs.appendFile(logFilePath, logMessage, (err) => {
if (err) {
Expand Down
28 changes: 17 additions & 11 deletions tests/govtool-frontend/playwright/lib/services/kuberService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@ import { ShelleyWallet } from "@helpers/crypto";
import { KuberValue } from "@types";
import * as blake from "blakejs";
import environments from "lib/constants/environments";
import { LockInterceptor } from "lib/lockInterceptor";
import { LockInterceptor, LockInterceptorInfo } from "lib/lockInterceptor";
import fetch, { BodyInit, RequestInit } from "node-fetch";
import { cborxDecoder, cborxEncoder } from "../helpers/cborEncodeDecode";
import convertBufferToHex from "../helpers/convertBufferToHex";
import { Logger } from "./../../../cypress/lib/logger/logger";

type CertificateType = "registerstake" | "registerdrep" | "deregisterdrep";

export type TxSubmitResponse = { cbor: string; txId: string; address?: string };
export type TxSubmitResponse = {
cbor: string;
txId: string;
lockInfo?: LockInterceptorInfo;
};

type KuberBalanceResponse = {
address: string;
txin: string;
value: KuberValue;
address?: string;
};

const config = {
Expand Down Expand Up @@ -71,18 +75,20 @@ class Kuber {

async signAndSubmitTx(tx: any) {
const signedTx = this.signTx(tx);
const signedTxBody = Uint8Array.from(cborxEncoder.encode(tx));
const lockId = Buffer.from(
blake.blake2b(signedTxBody, undefined, 32)
).toString("hex");
const submitTxCallback = async () => {
return this.submitTx(signedTx);
return this.submitTx(signedTx, lockId);
};
return LockInterceptor.intercept(
this.walletAddr,
submitTxCallback,
JSON.stringify(signedTx, null, 2)
);
return LockInterceptor.intercept(this.walletAddr, submitTxCallback, lockId);
}

async submitTx(signedTx: any) {
Logger.info(`Submitting tx: ${JSON.stringify(signedTx)}`);
async submitTx(signedTx: any, lockId?: string) {
Logger.info(
`Submitting tx: ${JSON.stringify({ lock_id: lockId, tx: signedTx })}`
);

const res = (await callKuber(
`/api/${this.version}/tx?submit=true`,
Expand Down
14 changes: 7 additions & 7 deletions tests/govtool-frontend/playwright/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion tests/govtool-frontend/playwright/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"format": "prettier . --write"
},
"dependencies": {
"@cardanoapi/cardano-test-wallet": "^1.0.0",
"@cardanoapi/cardano-test-wallet": "^1.0.2",
"@faker-js/faker": "^8.4.1",
"@noble/curves": "^1.3.0",
"@noble/ed25519": "^2.0.0",
Expand Down
Loading

0 comments on commit bc5cb0f

Please sign in to comment.