mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-17 15:35:09 +00:00
Update transaction retry to use correct user
Also improves test coverage Signed-off-by: James Taylor <jamest@uk.ibm.com>
This commit is contained in:
parent
82b1249f4e
commit
bf91df7ef3
12 changed files with 776 additions and 230 deletions
|
|
@ -1,21 +0,0 @@
|
|||
import { RedisOptions } from 'ioredis';
|
||||
|
||||
class IORedis {
|
||||
redisOptions: RedisOptions;
|
||||
constructor(options: RedisOptions) {
|
||||
this.redisOptions = options;
|
||||
}
|
||||
|
||||
hincrby = jest.fn().mockReturnThis();
|
||||
multi = jest.fn().mockReturnThis();
|
||||
del = jest.fn().mockReturnThis();
|
||||
|
||||
zrem = jest.fn().mockReturnThis();
|
||||
|
||||
exec = jest.fn().mockReturnThis();
|
||||
|
||||
hset = jest.fn().mockReturnThis();
|
||||
zadd = jest.fn().mockReturnThis();
|
||||
}
|
||||
|
||||
export default IORedis;
|
||||
|
|
@ -144,7 +144,7 @@ mockBasicContract.createTransaction
|
|||
|
||||
const mockGetTransactionByIDTransaction = mock<Transaction>();
|
||||
mockGetTransactionByIDTransaction.evaluate
|
||||
.calledWith('mychannel', 'txn1')
|
||||
.calledWith('mychannel', 'txn2')
|
||||
.mockResolvedValue(processedTransactionBuffer);
|
||||
mockGetTransactionByIDTransaction.evaluate
|
||||
.calledWith('mychannel', 'txn3')
|
||||
|
|
|
|||
|
|
@ -592,7 +592,7 @@ describe('Asset Transfer Besic REST API', () => {
|
|||
|
||||
it('GET should respond with json details for the specified transaction ID', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/transactions/txn1')
|
||||
.get('/api/transactions/txn2')
|
||||
.set('X-Api-Key', 'ORG1MOCKAPIKEY');
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.header).toHaveProperty(
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ assetsRouter.post(
|
|||
const transactionId = await submitTransaction(
|
||||
contract,
|
||||
redis,
|
||||
mspId,
|
||||
'CreateAsset',
|
||||
assetId,
|
||||
req.body.color,
|
||||
|
|
@ -235,6 +236,7 @@ assetsRouter.put(
|
|||
const transactionId = await submitTransaction(
|
||||
contract,
|
||||
redis,
|
||||
mspId,
|
||||
'UpdateAsset',
|
||||
assetId,
|
||||
req.body.color,
|
||||
|
|
@ -306,6 +308,7 @@ assetsRouter.patch(
|
|||
const transactionId = await submitTransaction(
|
||||
contract,
|
||||
redis,
|
||||
mspId,
|
||||
'TransferAsset',
|
||||
assetId,
|
||||
newOwner
|
||||
|
|
@ -351,6 +354,7 @@ assetsRouter.delete('/:assetId', async (req: Request, res: Response) => {
|
|||
const transactionId = await submitTransaction(
|
||||
contract,
|
||||
redis,
|
||||
mspId,
|
||||
'DeleteAsset',
|
||||
assetId
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,12 +7,20 @@ import {
|
|||
createWallet,
|
||||
getContracts,
|
||||
getNetwork,
|
||||
retryTransaction,
|
||||
evatuateTransaction,
|
||||
submitTransaction,
|
||||
getBlockHeight,
|
||||
startRetryLoop,
|
||||
} from './fabric';
|
||||
import * as config from './config';
|
||||
|
||||
import IORedis from './__mocks__/IORedis';
|
||||
import { Redis } from 'ioredis';
|
||||
import {
|
||||
AssetExistsError,
|
||||
AssetNotFoundError,
|
||||
TransactionError,
|
||||
TransactionNotFoundError,
|
||||
} from './errors';
|
||||
|
||||
import {
|
||||
Contract,
|
||||
Gateway,
|
||||
|
|
@ -22,19 +30,14 @@ import {
|
|||
Wallet,
|
||||
} from 'fabric-network';
|
||||
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import * as fabricProtos from 'fabric-protos';
|
||||
|
||||
import { MockProxy, mock } from 'jest-mock-extended';
|
||||
import IORedis, { Redis } from 'ioredis';
|
||||
import Long from 'long';
|
||||
|
||||
jest.mock('./config');
|
||||
jest.mock('ioredis');
|
||||
|
||||
const redisOptions = {
|
||||
port: config.redisPort,
|
||||
host: config.redisHost,
|
||||
username: config.redisUsername,
|
||||
password: config.redisPassword,
|
||||
};
|
||||
|
||||
const redis = new IORedis(redisOptions) as unknown as Redis;
|
||||
jest.mock('ioredis', () => require('ioredis-mock/jest'));
|
||||
|
||||
describe('Fabric', () => {
|
||||
describe('createWallet', () => {
|
||||
|
|
@ -101,66 +104,393 @@ describe('Fabric', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Testing retryTransaction', () => {
|
||||
const transactionId =
|
||||
describe('startRetryLoop', () => {
|
||||
let redis: Redis;
|
||||
let mockTransaction: MockProxy<Transaction>;
|
||||
let mockContract: MockProxy<Contract>;
|
||||
let mockContracts: Map<string, Contract>;
|
||||
|
||||
const mockTransactionId =
|
||||
'0ae62c01e4c4b112c3f3954a2f11243da76778e46df9ad2783bcbafc79652b95';
|
||||
const state = `{"name":"CreateAsset","nonce":"damqinq8nrI4n4qY8lFVsZw7RwG2ufrv","transactionId":${transactionId}`;
|
||||
const args = '["test111","red",400,"Jean",101]';
|
||||
const timestamp = 1628078044362;
|
||||
const savedTransaction = {
|
||||
timestamp: timestamp.toString(),
|
||||
state: state,
|
||||
retries: '',
|
||||
args: args,
|
||||
const mockKey = `txn:${mockTransactionId}`;
|
||||
const mockMspId = 'Org1MSP';
|
||||
const mockState = Buffer.from(
|
||||
`{"name":"CreateAsset","nonce":"damqinq8nrI4n4qY8lFVsZw7RwG2ufrv","transactionId":${mockTransactionId}`
|
||||
);
|
||||
const mockArgs = '["test111","red",400,"Jean",101]';
|
||||
const mockTimestamp = 1628078044362;
|
||||
|
||||
const flushPromises = () => {
|
||||
jest.useRealTimers();
|
||||
return new Promise((resolve) => setImmediate(resolve));
|
||||
};
|
||||
|
||||
describe('Check retry increment ', () => {
|
||||
const transactionId =
|
||||
'0ae62c01e4c4b112c3f3954a2f11243da76778e46df9ad2783bcbafc79652b95';
|
||||
const state = `{"name":"CreateAsset","nonce":"damqinq8nrI4n4qY8lFVsZw7RwG2ufrv","transactionId":${transactionId}`;
|
||||
const args = '["test111","red",400,"Jean",101]';
|
||||
const timestamp = 1628078044362;
|
||||
const savedTransaction = {
|
||||
timestamp: timestamp.toString(),
|
||||
state: state,
|
||||
retries: '',
|
||||
args: args,
|
||||
const addMockTransationDetails = async (redis: Redis) => {
|
||||
await redis
|
||||
.multi()
|
||||
.hset(
|
||||
mockKey,
|
||||
'mspId',
|
||||
mockMspId,
|
||||
'state',
|
||||
mockState,
|
||||
'args',
|
||||
mockArgs,
|
||||
'timestamp',
|
||||
mockTimestamp,
|
||||
'retries',
|
||||
'0'
|
||||
)
|
||||
.zadd('index:txn:timestamp', mockTimestamp, mockTransactionId)
|
||||
.exec();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const redisOptions = {
|
||||
port: config.redisPort,
|
||||
host: config.redisHost,
|
||||
username: config.redisUsername,
|
||||
password: config.redisPassword,
|
||||
};
|
||||
|
||||
it('Transaction failure, check redis increment func call', async () => {
|
||||
const mockTransaction = mock<Transaction>();
|
||||
mockTransaction.submit.mockRejectedValue('MOCKERROR');
|
||||
const mockContract = mock<Contract>();
|
||||
mockContract.deserializeTransaction.mockReturnValue(mockTransaction);
|
||||
redis = new IORedis(redisOptions) as unknown as Redis;
|
||||
|
||||
savedTransaction.retries = '3';
|
||||
await retryTransaction(
|
||||
mockContract,
|
||||
redis,
|
||||
transactionId,
|
||||
savedTransaction
|
||||
);
|
||||
expect(redis.hincrby).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
mockTransaction = mock<Transaction>();
|
||||
mockTransaction.submit
|
||||
.mockResolvedValue(Buffer.from('MOCK PAYLOAD'))
|
||||
.mockName('submit');
|
||||
mockContract = mock<Contract>();
|
||||
mockContract.deserializeTransaction.mockReturnValue(mockTransaction);
|
||||
mockContracts = new Map<string, Contract>();
|
||||
mockContracts.set(mockMspId, mockContract);
|
||||
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
describe('Transaction successful, check redis delete key func call ', () => {
|
||||
it('call redis increment', async () => {
|
||||
const mockTransaction = mock<Transaction>();
|
||||
mockTransaction.submit.mockResolvedValue(Buffer.from('{}'));
|
||||
const mockContract = mock<Contract>();
|
||||
mockContract.deserializeTransaction.mockReturnValue(mockTransaction);
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
savedTransaction.retries = '3';
|
||||
await retryTransaction(
|
||||
it('starts a retry loop which does nothing if there are no saved transaction details', async () => {
|
||||
const getContractSpy = jest.spyOn(mockContracts, 'get');
|
||||
|
||||
startRetryLoop(mockContracts, redis);
|
||||
jest.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
|
||||
expect(getContractSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('starts a retry loop which clears the saved details after succesfully retrying a transaction', async () => {
|
||||
addMockTransationDetails(redis);
|
||||
|
||||
startRetryLoop(mockContracts, redis);
|
||||
jest.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
|
||||
expect(mockContract.deserializeTransaction).toBeCalledWith(mockState);
|
||||
expect(mockTransaction.submit).toBeCalledWith(
|
||||
'test111',
|
||||
'red',
|
||||
400,
|
||||
'Jean',
|
||||
101
|
||||
);
|
||||
|
||||
const index = await redis.zrange('index:txn:timestamp', 0, -1);
|
||||
expect(index).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('starts a retry loop which increments the retry count when a transaction fails', async () => {
|
||||
addMockTransationDetails(redis);
|
||||
mockTransaction.submit.mockRejectedValue(new Error('MOCK ERROR'));
|
||||
|
||||
startRetryLoop(mockContracts, redis);
|
||||
jest.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
|
||||
expect(mockContract.deserializeTransaction).toBeCalledWith(mockState);
|
||||
expect(mockTransaction.submit).toBeCalledWith(
|
||||
'test111',
|
||||
'red',
|
||||
400,
|
||||
'Jean',
|
||||
101
|
||||
);
|
||||
|
||||
const index = await redis.zrange('index:txn:timestamp', 0, -1);
|
||||
expect(index).toStrictEqual([
|
||||
'0ae62c01e4c4b112c3f3954a2f11243da76778e46df9ad2783bcbafc79652b95',
|
||||
]);
|
||||
|
||||
const savedTransaction = await (redis as Redis).hgetall(mockKey);
|
||||
expect(savedTransaction.retries).toBe('1');
|
||||
});
|
||||
|
||||
it('starts a retry loop which clears the saved details when a transaction fails as a duplicate', async () => {
|
||||
addMockTransationDetails(redis);
|
||||
const mockDuplicateTransactionError = new Error('MOCK ERROR');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(mockDuplicateTransactionError as any).errors = [
|
||||
{
|
||||
endorsements: [
|
||||
{
|
||||
details: 'duplicate transaction found',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
mockTransaction.submit.mockRejectedValue(mockDuplicateTransactionError);
|
||||
|
||||
startRetryLoop(mockContracts, redis);
|
||||
jest.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
|
||||
expect(mockContract.deserializeTransaction).toBeCalledWith(mockState);
|
||||
expect(mockTransaction.submit).toBeCalledWith(
|
||||
'test111',
|
||||
'red',
|
||||
400,
|
||||
'Jean',
|
||||
101
|
||||
);
|
||||
|
||||
const index = await redis.zrange('index:txn:timestamp', 0, -1);
|
||||
expect(index).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('starts a retry loop which clears the saved details when a transaction fails the final attempt', async () => {
|
||||
addMockTransationDetails(redis);
|
||||
await (redis as Redis).hincrby(mockKey, 'retries', 5);
|
||||
mockTransaction.submit.mockRejectedValue(new Error('MOCK ERROR'));
|
||||
|
||||
startRetryLoop(mockContracts, redis);
|
||||
jest.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
|
||||
expect(mockContract.deserializeTransaction).toBeCalledWith(mockState);
|
||||
expect(mockTransaction.submit).toBeCalledWith(
|
||||
'test111',
|
||||
'red',
|
||||
400,
|
||||
'Jean',
|
||||
101
|
||||
);
|
||||
|
||||
const index = await redis.zrange('index:txn:timestamp', 0, -1);
|
||||
expect(index).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('evatuateTransaction', () => {
|
||||
const mockPayload = Buffer.from('MOCK PAYLOAD');
|
||||
let mockTransaction: MockProxy<Transaction>;
|
||||
let mockContract: MockProxy<Contract>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockTransaction = mock<Transaction>();
|
||||
mockTransaction.evaluate.mockResolvedValue(mockPayload);
|
||||
mockContract = mock<Contract>();
|
||||
mockContract.createTransaction
|
||||
.calledWith('txn')
|
||||
.mockReturnValue(mockTransaction);
|
||||
});
|
||||
|
||||
it('gets the result of evaluating a transaction', async () => {
|
||||
const result = await evatuateTransaction(
|
||||
mockContract,
|
||||
'txn',
|
||||
'arga',
|
||||
'argb'
|
||||
);
|
||||
expect(result.toString()).toBe(mockPayload.toString());
|
||||
});
|
||||
|
||||
it.each([
|
||||
'the asset GOCHAINCODE already exists',
|
||||
'Asset JAVACHAINCODE already exists',
|
||||
'The asset JSCHAINCODE already exists',
|
||||
])(
|
||||
'throws an AssetExistsError an asset already exists error occurs: %s',
|
||||
async (msg) => {
|
||||
mockTransaction.evaluate.mockRejectedValue(new Error(msg));
|
||||
|
||||
await expect(async () => {
|
||||
await evatuateTransaction(mockContract, 'txn', 'arga', 'argb');
|
||||
}).rejects.toThrow(AssetExistsError);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
'the asset GOCHAINCODE does not exist',
|
||||
'Asset JAVACHAINCODE does not exist',
|
||||
'The asset JSCHAINCODE does not exist',
|
||||
])(
|
||||
'throws an AssetNotFoundError if an asset does not exist error occurs: %s',
|
||||
async (msg) => {
|
||||
mockTransaction.evaluate.mockRejectedValue(new Error(msg));
|
||||
|
||||
await expect(async () => {
|
||||
await evatuateTransaction(mockContract, 'txn', 'arga', 'argb');
|
||||
}).rejects.toThrow(AssetNotFoundError);
|
||||
}
|
||||
);
|
||||
|
||||
it('throws a TransactionNotFoundError if a transaction not found error occurs', async () => {
|
||||
mockTransaction.evaluate.mockRejectedValue(
|
||||
new Error(
|
||||
'Failed to get transaction with id txn, error Entry not found in index'
|
||||
)
|
||||
);
|
||||
|
||||
await expect(async () => {
|
||||
await evatuateTransaction(mockContract, 'txn', 'arga', 'argb');
|
||||
}).rejects.toThrow(TransactionNotFoundError);
|
||||
});
|
||||
|
||||
it('throws a TransactionError for other errors', async () => {
|
||||
mockTransaction.evaluate.mockRejectedValue(new Error('MOCK ERROR'));
|
||||
|
||||
await expect(async () => {
|
||||
await evatuateTransaction(mockContract, 'txn', 'arga', 'argb');
|
||||
}).rejects.toThrow(TransactionError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('submitTransaction', () => {
|
||||
let redis: Redis;
|
||||
const mockPayload = Buffer.from('MOCK PAYLOAD');
|
||||
let mockTransaction: MockProxy<Transaction>;
|
||||
let mockContract: MockProxy<Contract>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const redisOptions = {
|
||||
port: config.redisPort,
|
||||
host: config.redisHost,
|
||||
username: config.redisUsername,
|
||||
password: config.redisPassword,
|
||||
};
|
||||
|
||||
redis = new IORedis(redisOptions) as unknown as Redis;
|
||||
|
||||
mockTransaction = mock<Transaction>();
|
||||
mockTransaction.submit.mockResolvedValue(mockPayload);
|
||||
mockTransaction.getTransactionId.mockReturnValue('MOCK TXN ID');
|
||||
mockTransaction.serialize.mockReturnValue(Buffer.from('MOCK TXN STATE'));
|
||||
mockContract = mock<Contract>();
|
||||
mockContract.createTransaction
|
||||
.calledWith('txn')
|
||||
.mockReturnValue(mockTransaction);
|
||||
});
|
||||
|
||||
it('gets the transaction ID of the submitted transaction', async () => {
|
||||
const result = await submitTransaction(
|
||||
mockContract,
|
||||
redis,
|
||||
'mspid',
|
||||
'txn',
|
||||
'arga',
|
||||
'argb'
|
||||
);
|
||||
expect(result).toBe('MOCK TXN ID');
|
||||
});
|
||||
|
||||
it.each([
|
||||
'the asset GOCHAINCODE already exists',
|
||||
'Asset JAVACHAINCODE already exists',
|
||||
'The asset JSCHAINCODE already exists',
|
||||
])(
|
||||
'throws an AssetExistsError an asset already exists error occurs: %s',
|
||||
async (msg) => {
|
||||
mockTransaction.submit.mockRejectedValue(new Error(msg));
|
||||
|
||||
await expect(async () => {
|
||||
await submitTransaction(
|
||||
mockContract,
|
||||
redis,
|
||||
'mspid',
|
||||
'txn',
|
||||
'arga',
|
||||
'argb'
|
||||
);
|
||||
}).rejects.toThrow(AssetExistsError);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
'the asset GOCHAINCODE does not exist',
|
||||
'Asset JAVACHAINCODE does not exist',
|
||||
'The asset JSCHAINCODE does not exist',
|
||||
])(
|
||||
'throws an AssetNotFoundError if an asset does not exist error occurs: %s',
|
||||
async (msg) => {
|
||||
mockTransaction.submit.mockRejectedValue(new Error(msg));
|
||||
|
||||
await expect(async () => {
|
||||
await submitTransaction(
|
||||
mockContract,
|
||||
redis,
|
||||
'mspid',
|
||||
'txn',
|
||||
'arga',
|
||||
'argb'
|
||||
);
|
||||
}).rejects.toThrow(AssetNotFoundError);
|
||||
}
|
||||
);
|
||||
|
||||
it('throws a TransactionNotFoundError if a transaction not found error occurs', async () => {
|
||||
mockTransaction.submit.mockRejectedValue(
|
||||
new Error(
|
||||
'Failed to get transaction with id txn, error Entry not found in index'
|
||||
)
|
||||
);
|
||||
|
||||
await expect(async () => {
|
||||
await submitTransaction(
|
||||
mockContract,
|
||||
redis,
|
||||
transactionId,
|
||||
savedTransaction
|
||||
'mspid',
|
||||
'txn',
|
||||
'arga',
|
||||
'argb'
|
||||
);
|
||||
}).rejects.toThrow(TransactionNotFoundError);
|
||||
});
|
||||
|
||||
expect(redis.del).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('throws a TransactionError for other errors', async () => {
|
||||
mockTransaction.submit.mockRejectedValue(new Error('MOCK ERROR'));
|
||||
|
||||
await expect(async () => {
|
||||
await submitTransaction(
|
||||
mockContract,
|
||||
redis,
|
||||
'mspid',
|
||||
'txn',
|
||||
'arga',
|
||||
'argb'
|
||||
);
|
||||
}).rejects.toThrow(TransactionError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBlockHeight', () => {
|
||||
it('gets the current block height', async () => {
|
||||
const mockBlockchainInfoProto =
|
||||
fabricProtos.common.BlockchainInfo.create();
|
||||
mockBlockchainInfoProto.height = 42;
|
||||
const mockBlockchainInfoBuffer = Buffer.from(
|
||||
fabricProtos.common.BlockchainInfo.encode(
|
||||
mockBlockchainInfoProto
|
||||
).finish()
|
||||
);
|
||||
const mockContract = mock<Contract>();
|
||||
mockContract.evaluateTransaction
|
||||
.calledWith('GetChainInfo', 'mychannel')
|
||||
.mockResolvedValue(mockBlockchainInfoBuffer);
|
||||
|
||||
const result = (await getBlockHeight(mockContract)) as Long;
|
||||
expect(result.toInt()).toStrictEqual(42);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,8 +20,10 @@ import * as config from './config';
|
|||
import { logger } from './logger';
|
||||
import {
|
||||
storeTransactionDetails,
|
||||
getRetryTransactionDetails,
|
||||
clearTransactionDetails,
|
||||
incrementRetryCount,
|
||||
TransactionDetails,
|
||||
} from './redis';
|
||||
import {
|
||||
AssetExistsError,
|
||||
|
|
@ -115,52 +117,48 @@ export const getContracts = async (
|
|||
return { assetContract, qsccContract };
|
||||
};
|
||||
|
||||
export const startRetryLoop = (contract: Contract, redis: Redis): void => {
|
||||
setInterval(
|
||||
async (redis) => {
|
||||
try {
|
||||
const pendingTransactionCount = await (redis as Redis).zcard(
|
||||
'index:txn:timestamp'
|
||||
);
|
||||
logger.debug(
|
||||
'Transactions awaiting retry: %d',
|
||||
pendingTransactionCount
|
||||
);
|
||||
|
||||
// TODO pick a random transaction instead to reduce chances of
|
||||
// clashing with other instances? Currently no zrandmember
|
||||
// command though...
|
||||
// https://github.com/luin/ioredis/issues/1374
|
||||
const transactionIds = await (redis as Redis).zrange(
|
||||
'index:txn:timestamp',
|
||||
-1,
|
||||
-1
|
||||
);
|
||||
|
||||
if (transactionIds.length > 0) {
|
||||
const transactionId = transactionIds[0];
|
||||
const savedTransaction = await (redis as Redis).hgetall(
|
||||
`txn:${transactionId}`
|
||||
export const startRetryLoop = (
|
||||
contracts: Map<string, Contract>,
|
||||
redis: Redis
|
||||
): void => {
|
||||
const retryInterval = setInterval(
|
||||
async (contracts, redis) => {
|
||||
if (logger.isLevelEnabled('debug')) {
|
||||
try {
|
||||
const pendingTransactionCount = await (redis as Redis).zcard(
|
||||
'index:txn:timestamp'
|
||||
);
|
||||
logger.debug(
|
||||
'%d transactions awaiting retry',
|
||||
pendingTransactionCount
|
||||
);
|
||||
} catch (err) {
|
||||
logger.warn({ err }, 'Error getting pending transaction count');
|
||||
}
|
||||
}
|
||||
|
||||
const savedTransaction = await getRetryTransactionDetails(redis);
|
||||
|
||||
if (savedTransaction) {
|
||||
const contract = contracts.get(savedTransaction.mspId);
|
||||
|
||||
if (contract) {
|
||||
await retryTransaction(contract, redis, savedTransaction);
|
||||
} else {
|
||||
logger.error(
|
||||
'No contract found for %s to retry transaction %s',
|
||||
savedTransaction.mspId,
|
||||
savedTransaction.transactionId
|
||||
);
|
||||
if (parseInt(savedTransaction.retries) >= config.maxRetryCount) {
|
||||
await clearTransactionDetails(redis, transactionId);
|
||||
} else {
|
||||
await retryTransaction(
|
||||
contract,
|
||||
redis,
|
||||
transactionId,
|
||||
savedTransaction
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// TODO just log?
|
||||
logger.error(err, 'error getting saved transaction state');
|
||||
}
|
||||
},
|
||||
config.retryDelay,
|
||||
contracts,
|
||||
redis
|
||||
);
|
||||
|
||||
retryInterval.unref();
|
||||
};
|
||||
|
||||
export const evatuateTransaction = async (
|
||||
|
|
@ -173,7 +171,10 @@ export const evatuateTransaction = async (
|
|||
|
||||
try {
|
||||
const payload = await txn.evaluate(...transactionArgs);
|
||||
logger.debug({ payload }, 'Evaluate transaction response received');
|
||||
logger.debug(
|
||||
{ transactionId: txnId, payload: payload.toString() },
|
||||
'Evaluate transaction response received'
|
||||
);
|
||||
return payload;
|
||||
} catch (err) {
|
||||
throw handleError(txnId, err);
|
||||
|
|
@ -183,6 +184,7 @@ export const evatuateTransaction = async (
|
|||
export const submitTransaction = async (
|
||||
contract: Contract,
|
||||
redis: Redis,
|
||||
mspId: string,
|
||||
transactionName: string,
|
||||
...transactionArgs: string[]
|
||||
): Promise<string> => {
|
||||
|
|
@ -195,7 +197,14 @@ export const submitTransaction = async (
|
|||
try {
|
||||
// Store the transaction details and set the event handler in case there
|
||||
// are problems later with commiting the transaction
|
||||
await storeTransactionDetails(redis, txnId, txnState, txnArgs, timestamp);
|
||||
await storeTransactionDetails(
|
||||
redis,
|
||||
txnId,
|
||||
mspId,
|
||||
txnState,
|
||||
txnArgs,
|
||||
timestamp
|
||||
);
|
||||
txn.setEventHandler(DefaultEventHandlerStrategies.NONE);
|
||||
await txn.submit(...transactionArgs);
|
||||
} catch (err) {
|
||||
|
|
@ -212,6 +221,7 @@ export const submitTransaction = async (
|
|||
|
||||
// Unfortunately the chaincode samples do not use error codes, and the error
|
||||
// message text is not the same for each implementation
|
||||
// TODO move to errors.ts?
|
||||
const handleError = (transactionId: string, err: Error): Error => {
|
||||
// This regex needs to match the following error messages:
|
||||
// "the asset %s already exists"
|
||||
|
|
@ -266,40 +276,55 @@ const handleError = (transactionId: string, err: Error): Error => {
|
|||
return new TransactionError('Transaction error', transactionId);
|
||||
};
|
||||
|
||||
export const retryTransaction = async (
|
||||
const retryTransaction = async (
|
||||
contract: Contract,
|
||||
redis: Redis,
|
||||
transactionId: string,
|
||||
savedTransaction: Record<string, string>
|
||||
savedTransaction: TransactionDetails
|
||||
): Promise<void> => {
|
||||
logger.debug('Retrying transaction %s', transactionId);
|
||||
logger.debug('Retrying transaction %s', savedTransaction.transactionId);
|
||||
|
||||
try {
|
||||
const transaction = contract.deserializeTransaction(
|
||||
Buffer.from(savedTransaction.state)
|
||||
savedTransaction.transactionState
|
||||
);
|
||||
const args: string[] = JSON.parse(savedTransaction.args);
|
||||
const args: string[] = JSON.parse(savedTransaction.transactionArgs);
|
||||
|
||||
await transaction.submit(...args);
|
||||
await clearTransactionDetails(redis, transactionId);
|
||||
const payload = await transaction.submit(...args);
|
||||
logger.debug(
|
||||
{
|
||||
transactionId: savedTransaction.transactionId,
|
||||
payload: payload.toString(),
|
||||
},
|
||||
'Retry transaction response received'
|
||||
);
|
||||
|
||||
await clearTransactionDetails(redis, savedTransaction.transactionId);
|
||||
} catch (err) {
|
||||
if (isDuplicateTransaction(err)) {
|
||||
logger.warn('Transaction %s has already been committed', transactionId);
|
||||
await clearTransactionDetails(redis, transactionId);
|
||||
if (isDuplicateTransactionError(err)) {
|
||||
logger.warn(
|
||||
'Transaction %s has already been committed',
|
||||
savedTransaction.transactionId
|
||||
);
|
||||
await clearTransactionDetails(redis, savedTransaction.transactionId);
|
||||
} else {
|
||||
// TODO check for retry limit and update timestamp
|
||||
logger.warn(
|
||||
err,
|
||||
'Retry %d failed for transaction %s',
|
||||
savedTransaction.retries,
|
||||
transactionId
|
||||
savedTransaction.transactionId
|
||||
);
|
||||
await incrementRetryCount(redis, transactionId);
|
||||
|
||||
if (savedTransaction.retries < config.maxRetryCount) {
|
||||
await incrementRetryCount(redis, savedTransaction.transactionId);
|
||||
} else {
|
||||
await clearTransactionDetails(redis, savedTransaction.transactionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isDuplicateTransaction = (error: {
|
||||
// TODO move to errors.ts?
|
||||
const isDuplicateTransactionError = (error: {
|
||||
errors: { endorsements: { details: string }[] }[];
|
||||
}) => {
|
||||
// TODO this is horrible! Isn't it possible to check for TxValidationCode DUPLICATE_TXID somehow?
|
||||
|
|
|
|||
|
|
@ -2,24 +2,21 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Contract, Network } from 'fabric-network';
|
||||
import { Network } from 'fabric-network';
|
||||
import { Redis } from 'ioredis';
|
||||
import * as config from './config';
|
||||
import { startRetryLoop, blockEventHandler } from './fabric';
|
||||
import { blockEventHandler } from './fabric';
|
||||
import { logger } from './logger';
|
||||
import { createServer } from './server';
|
||||
|
||||
async function main() {
|
||||
const app = await createServer();
|
||||
|
||||
// TODO block listener and retry logic currently only handles a single org!!!
|
||||
// TODO should these be initialised here?
|
||||
const contract = app.get(config.mspIdOrg1).assetContract as Contract;
|
||||
// TODO block listener currently only handles a single org!!!
|
||||
// TODO should it be initialised here?
|
||||
const redis = app.get('redis') as Redis;
|
||||
const network = app.get('networkOrg1') as Network;
|
||||
|
||||
await network.addBlockListener(blockEventHandler(redis));
|
||||
startRetryLoop(contract, redis);
|
||||
|
||||
app.listen(config.port, () => {
|
||||
logger.info('Express server started on port: %d', config.port);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import pino from 'pino';
|
||||
import * as config from './config';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,75 +1,184 @@
|
|||
import IORedis from './__mocks__/IORedis';
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as config from './config';
|
||||
import { Redis } from 'ioredis';
|
||||
import IORedis, { Redis } from 'ioredis';
|
||||
import {
|
||||
clearTransactionDetails,
|
||||
incrementRetryCount,
|
||||
storeTransactionDetails,
|
||||
getTransactionDetails,
|
||||
getRetryTransactionDetails,
|
||||
} from './redis';
|
||||
|
||||
jest.mock('ioredis');
|
||||
jest.mock('ioredis', () => require('ioredis-mock/jest'));
|
||||
jest.mock('./config');
|
||||
|
||||
const redisOptions = {
|
||||
port: config.redisPort,
|
||||
host: config.redisHost,
|
||||
username: config.redisUsername,
|
||||
password: config.redisPassword,
|
||||
};
|
||||
const redis = new IORedis(redisOptions) as unknown as Redis;
|
||||
describe('Testing increment retries ', () => {
|
||||
const transactionId =
|
||||
describe('Redis', () => {
|
||||
let redis: Redis;
|
||||
|
||||
const mockTransactionId =
|
||||
'0ae62c01e4c4b112c3f3954a2f11243da76778e46df9ad2783bcbafc79652b95';
|
||||
it('Should increment retries for valid transction id', async () => {
|
||||
await incrementRetryCount(redis, transactionId);
|
||||
expect(redis.hincrby).toHaveBeenCalledTimes(1);
|
||||
const mockKey = `txn:${mockTransactionId}`;
|
||||
const mockMspId = 'Org1MSP';
|
||||
const mockState = Buffer.from(
|
||||
`{"name":"CreateAsset","nonce":"damqinq8nrI4n4qY8lFVsZw7RwG2ufrv","transactionId":${mockTransactionId}`
|
||||
);
|
||||
const mockArgs = '["test111","red",400,"Jean",101]';
|
||||
const mockTimestamp = 1628078044362;
|
||||
|
||||
const addMockTransationDetails = async (redis: Redis) => {
|
||||
await redis
|
||||
.multi()
|
||||
.hset(
|
||||
mockKey,
|
||||
'mspId',
|
||||
mockMspId,
|
||||
'state',
|
||||
mockState,
|
||||
'args',
|
||||
mockArgs,
|
||||
'timestamp',
|
||||
mockTimestamp,
|
||||
'retries',
|
||||
'0'
|
||||
)
|
||||
.zadd('index:txn:timestamp', mockTimestamp, mockTransactionId)
|
||||
.exec();
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const redisOptions = {
|
||||
port: config.redisPort,
|
||||
host: config.redisHost,
|
||||
username: config.redisUsername,
|
||||
password: config.redisPassword,
|
||||
};
|
||||
|
||||
redis = new IORedis(redisOptions) as unknown as Redis;
|
||||
});
|
||||
describe('storeTransactionDetails', () => {
|
||||
it('stores transaction details as a hash', async () => {
|
||||
await storeTransactionDetails(
|
||||
redis,
|
||||
mockTransactionId,
|
||||
mockMspId,
|
||||
mockState,
|
||||
mockArgs,
|
||||
mockTimestamp
|
||||
);
|
||||
|
||||
const storedTransaction = await redis.hgetall(mockKey);
|
||||
const expectedTransaction = {
|
||||
mspId: mockMspId,
|
||||
state: mockState,
|
||||
args: mockArgs,
|
||||
retries: '0',
|
||||
timestamp: mockTimestamp.toString(),
|
||||
};
|
||||
expect(storedTransaction).toStrictEqual(expectedTransaction);
|
||||
});
|
||||
|
||||
it('adds the transaction ID to the sorted set timestamp index', async () => {
|
||||
await storeTransactionDetails(
|
||||
redis,
|
||||
mockTransactionId,
|
||||
mockMspId,
|
||||
mockState,
|
||||
mockArgs,
|
||||
mockTimestamp
|
||||
);
|
||||
|
||||
const index = await redis.zrange('index:txn:timestamp', 0, -1);
|
||||
expect(index).toStrictEqual([mockTransactionId]);
|
||||
});
|
||||
|
||||
// TODO this seems to work for spying/mocking...
|
||||
// jest.spyOn(redis, 'multi').mock...
|
||||
// but haven't worked out how to spy on the hset, zadd, exec in that chain
|
||||
// Ask Mark?
|
||||
it.todo('handles an error from redis');
|
||||
});
|
||||
|
||||
it('Should not increment retries for empty transaction id ', async () => {
|
||||
await incrementRetryCount(redis, '');
|
||||
expect(redis.hincrby).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
describe('getTransactionDetails', () => {
|
||||
it('gets the transaction details from a hash', async () => {
|
||||
await addMockTransationDetails(redis);
|
||||
|
||||
describe('Testing storeTransactionDetails ', () => {
|
||||
const args = '["test111","red",400,"Jean",101]';
|
||||
const timestamp = 1628078044362;
|
||||
it('Should store details for valid transction Id', async () => {
|
||||
const transactionId =
|
||||
'0ae62c01e4c4b112c3f3954a2f11243da76778e46df9ad2783bcbafc79652b95';
|
||||
const state = `{"name":"CreateAsset","nonce":"damqinq8nrI4n4qY8lFVsZw7RwG2ufrv","transactionId":${transactionId}`;
|
||||
await storeTransactionDetails(
|
||||
redis,
|
||||
transactionId,
|
||||
Buffer.from(state),
|
||||
args,
|
||||
timestamp
|
||||
const details = await getTransactionDetails(redis, mockTransactionId);
|
||||
|
||||
expect(details).toStrictEqual({
|
||||
transactionId: mockTransactionId,
|
||||
mspId: mockMspId,
|
||||
transactionState: mockState,
|
||||
transactionArgs: mockArgs,
|
||||
retries: 0,
|
||||
timestamp: mockTimestamp,
|
||||
});
|
||||
});
|
||||
|
||||
it.todo('handles an error from redis');
|
||||
});
|
||||
|
||||
describe('getRetryTransactionDetails', () => {
|
||||
it('gets the oldest transaction details from a hash', async () => {
|
||||
await addMockTransationDetails(redis);
|
||||
|
||||
const details = await getRetryTransactionDetails(redis);
|
||||
|
||||
expect(details).toStrictEqual({
|
||||
transactionId: mockTransactionId,
|
||||
mspId: mockMspId,
|
||||
transactionState: mockState,
|
||||
transactionArgs: mockArgs,
|
||||
retries: 0,
|
||||
timestamp: mockTimestamp,
|
||||
});
|
||||
});
|
||||
|
||||
it('gets undefined if there are no transactions to retry', async () => {
|
||||
const details = await getRetryTransactionDetails(redis);
|
||||
|
||||
expect(details).toBeUndefined();
|
||||
});
|
||||
|
||||
it.todo('handles an error from redis');
|
||||
});
|
||||
|
||||
describe('clearTransactionDetails', () => {
|
||||
it('removes the transaction details hash', async () => {
|
||||
await addMockTransationDetails(redis);
|
||||
|
||||
await clearTransactionDetails(redis, mockTransactionId);
|
||||
|
||||
const storedTransaction = await redis.hgetall(mockKey);
|
||||
expect(storedTransaction).not.toHaveProperty('state');
|
||||
});
|
||||
|
||||
it('removes the transaction ID from the sorted set timestamp index', async () => {
|
||||
await addMockTransationDetails(redis);
|
||||
|
||||
await clearTransactionDetails(redis, mockTransactionId);
|
||||
|
||||
const index = await redis.zrange('index:txn:timestamp', 0, -1);
|
||||
expect(index).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('incrementRetryCount', () => {
|
||||
it('increments the retries value in the transction details hash', async () => {
|
||||
await addMockTransationDetails(redis);
|
||||
|
||||
await incrementRetryCount(redis, mockTransactionId);
|
||||
|
||||
const retries = await redis.hget(mockKey, 'retries');
|
||||
expect(retries).toBe('1');
|
||||
});
|
||||
|
||||
it.todo(
|
||||
'updates the position of the transaction ID in the sorted set timestamp index'
|
||||
);
|
||||
expect(redis.hset).toHaveBeenCalledTimes(1);
|
||||
expect(redis.zadd).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should not store details for empty transction Id', async () => {
|
||||
const transactionId = '';
|
||||
const state = `{"name":"CreateAsset","nonce":"damqinq8nrI4n4qY8lFVsZw7RwG2ufrv","transactionId":${transactionId}`;
|
||||
await storeTransactionDetails(
|
||||
redis,
|
||||
transactionId,
|
||||
Buffer.from(state),
|
||||
args,
|
||||
timestamp
|
||||
);
|
||||
expect(redis.hset).toHaveBeenCalledTimes(0);
|
||||
expect(redis.zadd).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Testing clearTransactionDetails ', () => {
|
||||
it('Should clear details ', async () => {
|
||||
const transactionId =
|
||||
'0ae62c01e4c4b112c3f3954a2f11243da76778e46df9ad2783bcbafc79652b95';
|
||||
await clearTransactionDetails(redis, transactionId);
|
||||
expect(redis.del).toHaveBeenCalledTimes(1);
|
||||
expect(redis.zrem).toHaveBeenCalledTimes(1);
|
||||
it.todo('handles an error from redis');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* This sample includes basic retry logic so it needs somewhere to store
|
||||
* transaction details in case the app restarts for any reason, and Redis is
|
||||
* just one of the options available
|
||||
*
|
||||
* Note: This implementation is not designed with multiple instances of the
|
||||
* REST app in mind, which is likely to be required in a production environment
|
||||
*/
|
||||
|
||||
import IORedis, { Redis, RedisOptions } from 'ioredis';
|
||||
|
|
@ -16,29 +23,44 @@ const redisOptions: RedisOptions = {
|
|||
|
||||
export const redis = new IORedis(redisOptions);
|
||||
|
||||
export type TransactionDetails = {
|
||||
transactionId: string;
|
||||
mspId: string;
|
||||
transactionState: Buffer;
|
||||
transactionArgs: string;
|
||||
timestamp: number;
|
||||
retries: number;
|
||||
};
|
||||
|
||||
/*
|
||||
* Store enough information in order to resubmit a transaction
|
||||
*/
|
||||
export const storeTransactionDetails = async (
|
||||
redis: Redis,
|
||||
transactionId: string,
|
||||
mspId: string,
|
||||
transactionState: Buffer,
|
||||
transactionArgs: string,
|
||||
timestamp: number
|
||||
): Promise<void> => {
|
||||
try {
|
||||
if (transactionId.length === 0) {
|
||||
throw new Error('Empty transaction Id found');
|
||||
}
|
||||
const key = `txn:${transactionId}`;
|
||||
logger.debug(
|
||||
'Storing transaction details. Key: %s State: %s Args: %s Timestamp: %d',
|
||||
key,
|
||||
transactionState,
|
||||
transactionArgs,
|
||||
timestamp
|
||||
{
|
||||
key,
|
||||
mspId,
|
||||
transactionState,
|
||||
transactionArgs,
|
||||
timestamp,
|
||||
},
|
||||
'Storing transaction details'
|
||||
);
|
||||
await redis
|
||||
.multi()
|
||||
.hset(
|
||||
key,
|
||||
'mspId',
|
||||
mspId,
|
||||
'state',
|
||||
transactionState,
|
||||
'args',
|
||||
|
|
@ -51,14 +73,84 @@ export const storeTransactionDetails = async (
|
|||
.zadd('index:txn:timestamp', timestamp, transactionId)
|
||||
.exec();
|
||||
} catch (err) {
|
||||
// TODO just log?!
|
||||
logger.error(
|
||||
err,
|
||||
'Error storing transaction details. ID %s',
|
||||
{ err },
|
||||
'Error storing details for transaction ID %s',
|
||||
transactionId
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Get the information required to resubmit a transaction
|
||||
*/
|
||||
export const getTransactionDetails = async (
|
||||
redis: Redis,
|
||||
transactionId: string
|
||||
): Promise<TransactionDetails | undefined> => {
|
||||
try {
|
||||
const savedTransaction = await (redis as Redis).hgetall(
|
||||
`txn:${transactionId}`
|
||||
);
|
||||
logger.debug(
|
||||
{ transactionId: transactionId, state: savedTransaction },
|
||||
'Got transaction details'
|
||||
);
|
||||
|
||||
const transactionDetails = {
|
||||
transactionId: transactionId,
|
||||
mspId: savedTransaction.mspId,
|
||||
transactionState: Buffer.from(savedTransaction.state),
|
||||
transactionArgs: savedTransaction.args,
|
||||
timestamp: parseInt(savedTransaction.timestamp),
|
||||
retries: parseInt(savedTransaction.retries),
|
||||
};
|
||||
return transactionDetails;
|
||||
} catch (err) {
|
||||
// TODO just log?!
|
||||
logger.error(
|
||||
{ err },
|
||||
'Error getting details for transaction ID %s',
|
||||
transactionId
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Get the oldest transaction details
|
||||
*/
|
||||
export const getRetryTransactionDetails = async (
|
||||
redis: Redis
|
||||
): Promise<TransactionDetails | undefined> => {
|
||||
try {
|
||||
const transactionIds = await (redis as Redis).zrange(
|
||||
'index:txn:timestamp',
|
||||
-1,
|
||||
-1
|
||||
);
|
||||
|
||||
if (transactionIds.length > 0) {
|
||||
const transactionId = transactionIds[0];
|
||||
|
||||
const savedTransaction = await getTransactionDetails(
|
||||
redis,
|
||||
transactionId
|
||||
);
|
||||
return savedTransaction;
|
||||
}
|
||||
} catch (err) {
|
||||
// TODO just log?!
|
||||
logger.error(
|
||||
{ err },
|
||||
'Error getting details for next transaction to retry'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Delete transaction details
|
||||
*/
|
||||
export const clearTransactionDetails = async (
|
||||
redis: Redis,
|
||||
transactionId: string
|
||||
|
|
@ -72,16 +164,20 @@ export const clearTransactionDetails = async (
|
|||
.zrem('index:txn:timestamp', transactionId)
|
||||
.exec();
|
||||
} catch (err) {
|
||||
// TODO just log?!
|
||||
logger.error(
|
||||
err,
|
||||
'Error remove saved transaction state for transaction ID %s',
|
||||
{ err },
|
||||
'Error remove details for transaction ID %s',
|
||||
transactionId
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO add getTransaction etc. helpers?
|
||||
/*
|
||||
* Increment the number of times the transaction has been retried
|
||||
|
||||
* TODO needs to update the timestamp and index as well
|
||||
*/
|
||||
export const incrementRetryCount = async (
|
||||
redis: Redis,
|
||||
transactionId: string
|
||||
|
|
@ -89,11 +185,9 @@ export const incrementRetryCount = async (
|
|||
const key = `txn:${transactionId}`;
|
||||
logger.debug('Incrementing retries fortransaction Key: %s', key);
|
||||
try {
|
||||
if (transactionId.length === 0) {
|
||||
throw new Error('Empty transaction Id found');
|
||||
}
|
||||
await (redis as Redis).hincrby(`txn:${transactionId}`, 'retries', 1);
|
||||
} catch (err) {
|
||||
// TODO just log?!
|
||||
logger.error(
|
||||
err,
|
||||
'Error incrementing retries for transaction ID %s',
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import helmet from 'helmet';
|
|||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
import express, { Application, NextFunction, Request, Response } from 'express';
|
||||
import pinoMiddleware from 'pino-http';
|
||||
import { Contract } from 'fabric-network';
|
||||
|
||||
import { logger } from './logger';
|
||||
import { assetsRouter } from './assets.router';
|
||||
|
|
@ -16,6 +17,7 @@ import {
|
|||
getNetwork,
|
||||
createGateway,
|
||||
createWallet,
|
||||
startRetryLoop,
|
||||
} from './fabric';
|
||||
import { redis } from './redis';
|
||||
import * as config from './config';
|
||||
|
|
@ -91,6 +93,11 @@ export const createServer = async (): Promise<Application> => {
|
|||
const contractsOrg2 = await getContracts(networkOrg2);
|
||||
app.set(config.mspIdOrg2, contractsOrg2);
|
||||
|
||||
const assetContracts = new Map<string, Contract>();
|
||||
assetContracts.set(config.mspIdOrg1, contractsOrg1.assetContract);
|
||||
assetContracts.set(config.mspIdOrg2, contractsOrg2.assetContract);
|
||||
startRetryLoop(assetContracts, redis);
|
||||
|
||||
app.set('redis', redis);
|
||||
|
||||
app.use('/', healthRouter);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Contract } from 'fabric-network';
|
|||
import { protos } from 'fabric-protos';
|
||||
import { getReasonPhrase, StatusCodes } from 'http-status-codes';
|
||||
import { Redis } from 'ioredis';
|
||||
import { getTransactionDetails } from './redis';
|
||||
import { evatuateTransaction } from './fabric';
|
||||
import { logger } from './logger';
|
||||
import * as config from './config';
|
||||
|
|
@ -33,18 +34,14 @@ transactionsRouter.get(
|
|||
const redis = req.app.get('redis') as Redis;
|
||||
|
||||
try {
|
||||
const savedTransaction = await (redis as Redis).hgetall(
|
||||
`txn:${transactionId}`
|
||||
);
|
||||
logger.debug(
|
||||
{ transactionId: transactionId, state: savedTransaction },
|
||||
'Saved transaction state'
|
||||
const savedTransaction = await getTransactionDetails(
|
||||
redis,
|
||||
transactionId
|
||||
);
|
||||
|
||||
if (savedTransaction.state) {
|
||||
if (savedTransaction?.transactionState) {
|
||||
foundTransaction = true;
|
||||
const retries = parseInt(savedTransaction.retries);
|
||||
if (retries > 0) {
|
||||
if (savedTransaction.retries > 0) {
|
||||
progress = 'RETRYING';
|
||||
} else {
|
||||
progress = 'ACCEPTED';
|
||||
|
|
|
|||
Loading…
Reference in a new issue