diff --git a/src/common/gateway/gateway.service.ts b/src/common/gateway/gateway.service.ts index 735def27e..80e5bd2fa 100644 --- a/src/common/gateway/gateway.service.ts +++ b/src/common/gateway/gateway.service.ts @@ -182,8 +182,22 @@ export class GatewayService { return new NftData(result.tokenData); } - async getTransactionPool(): Promise { - return await this.get(`transaction/pool?fields=*`, GatewayComponentRequest.transactionPool); + async getTransactionPool(): Promise { + try { + return await this.get(`transaction/pool?fields=*`, GatewayComponentRequest.transactionPool); + } catch (error: any) { + if (error?.message?.startsWith('maxContentLength size of')) { + return null; + } + + throw error; + } + } + + async getTransactionPoolCount(): Promise { + const result = await this.get('transaction/pool/count', GatewayComponentRequest.transactionPool); + + return Object.values(result.txPoolCounts).reduce((total, count) => total + count, 0); } async getTransaction(txHash: string): Promise { diff --git a/src/crons/cache.warmer/cache.warmer.service.ts b/src/crons/cache.warmer/cache.warmer.service.ts index e50c8b391..2a123873f 100644 --- a/src/crons/cache.warmer/cache.warmer.service.ts +++ b/src/crons/cache.warmer/cache.warmer.service.ts @@ -151,9 +151,19 @@ export class CacheWarmerService { @Lock({ name: 'Transaction pool invalidation', verbose: true }) async handleTxPoolInvalidations() { + const ttl = this.apiConfigService.getTransactionPoolCacheWarmerTtlInSeconds(); const pool = await this.poolService.getTxPoolRaw(); - await this.invalidateKey(CacheInfo.TransactionPool.key, pool, this.apiConfigService.getTransactionPoolCacheWarmerTtlInSeconds()); + const invalidations = [ + this.invalidateKey(CacheInfo.TransactionPool.key, pool, ttl), + ]; + + if (pool == null) { + const count = await this.gatewayService.getTransactionPoolCount(); + invalidations.push(this.invalidateKey(CacheInfo.TransactionPoolCount.key, count, ttl)); + } + + await Promise.all(invalidations); } @Cron('*/2 * * * *') diff --git a/src/crons/websocket/pool.gateway.ts b/src/crons/websocket/pool.gateway.ts index 722466a9e..3b2cb29d3 100644 --- a/src/crons/websocket/pool.gateway.ts +++ b/src/crons/websocket/pool.gateway.ts @@ -10,6 +10,7 @@ import { QueryPagination } from 'src/common/entities/query.pagination'; import { PoolSubscribePayload } from '../../endpoints/pool/entities/pool.subscribe'; import { RoomKeyGenerator } from './room.key.generator'; import { LockingGuardInterceptor } from 'src/utils/locking.guard.interceptor'; +import { PoolUpdateStatus } from '../../endpoints/pool/entities/pool.update.status'; @UseFilters(WebsocketExceptionsFilter) @WebSocketGateway({ cors: { origin: '*' }, path: '/ws/subscription' }) @@ -63,6 +64,8 @@ export class PoolGateway { type: filter.type, }); + let status = PoolUpdateStatus.success; + const [pool, poolCount] = await Promise.all([ this.poolService.getPool( new QueryPagination({ @@ -70,13 +73,22 @@ export class PoolGateway { size: filter.size, }), poolFilter, - ), + ).catch((error) => { + this.logger.error(error); + status = PoolUpdateStatus.internalServerError; + return null; + }), this.poolService.getPoolCount(poolFilter), ]); - this.server.to(roomName).emit("poolUpdate", { pool, poolCount }); + if (pool == null && status === PoolUpdateStatus.success) { + status = PoolUpdateStatus.tooLarge; + } + + this.server.to(roomName).emit("poolUpdate", { status, pool, poolCount }); } catch (error) { this.logger.error(error); + this.server.to(roomName).emit("poolUpdate", { status: PoolUpdateStatus.internalServerError, pool: null, poolCount: null }); } } diff --git a/src/endpoints/pool/entities/pool.update.status.ts b/src/endpoints/pool/entities/pool.update.status.ts new file mode 100644 index 000000000..8b56289f4 --- /dev/null +++ b/src/endpoints/pool/entities/pool.update.status.ts @@ -0,0 +1,5 @@ +export enum PoolUpdateStatus { + success = 'success', + tooLarge = 'tooLarge', + internalServerError = 'internalServerError', +} diff --git a/src/endpoints/pool/entities/transaction.pool.too.large.exception.ts b/src/endpoints/pool/entities/transaction.pool.too.large.exception.ts new file mode 100644 index 000000000..67342075a --- /dev/null +++ b/src/endpoints/pool/entities/transaction.pool.too.large.exception.ts @@ -0,0 +1,13 @@ +import { HttpException, HttpStatus } from "@nestjs/common"; + +export class TransactionPoolTooLargeException extends HttpException { + static readonly code = 'transaction_pool_too_large'; + + constructor() { + super({ + statusCode: HttpStatus.SERVICE_UNAVAILABLE, + code: TransactionPoolTooLargeException.code, + message: 'The transaction pool is too large to be displayed', + }, HttpStatus.SERVICE_UNAVAILABLE); + } +} diff --git a/src/endpoints/pool/pool.controller.ts b/src/endpoints/pool/pool.controller.ts index f6948e4f9..80d987295 100644 --- a/src/endpoints/pool/pool.controller.ts +++ b/src/endpoints/pool/pool.controller.ts @@ -1,12 +1,13 @@ import { ParseAddressAndMetachainPipe, ParseAddressPipe, ParseEnumPipe, ParseIntPipe, ParseTransactionHashPipe, ParseArrayPipe } from "@multiversx/sdk-nestjs-common"; import { Controller, DefaultValuePipe, Get, NotFoundException, Param, Query } from "@nestjs/common"; -import { ApiExcludeEndpoint, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiQuery, ApiTags } from "@nestjs/swagger"; +import { ApiExcludeEndpoint, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiQuery, ApiServiceUnavailableResponse, ApiTags } from "@nestjs/swagger"; import { PoolService } from "./pool.service"; import { QueryPagination } from "src/common/entities/query.pagination"; import { TransactionInPool } from "./entities/transaction.in.pool.dto"; import { TransactionType } from "../transactions/entities/transaction.type"; import { PoolFilter } from "./entities/pool.filter"; import { ParseArrayPipeOptions } from "@multiversx/sdk-nestjs-common/lib/pipes/entities/parse.array.options"; +import { TransactionPoolTooLargeException } from "./entities/transaction.pool.too.large.exception"; @Controller() @ApiTags('pool') @@ -18,6 +19,7 @@ export class PoolController { @Get("/pool") @ApiOperation({ summary: 'Transactions pool', description: 'Returns the transactions that are currently in the memory pool.' }) @ApiOkResponse({ type: TransactionInPool, isArray: true }) + @ApiServiceUnavailableResponse({ description: 'The transaction pool is too large to be displayed' }) @ApiQuery({ name: 'from', description: 'Number of items to skip for the result set', required: false }) @ApiQuery({ name: 'size', description: 'Number of items to retrieve', required: false }) @ApiQuery({ name: 'sender', description: 'Search in transaction pool by a specific sender', required: false }) @@ -37,7 +39,7 @@ export class PoolController { @Query('type', new ParseEnumPipe(TransactionType)) type?: TransactionType, @Query('function', new ParseArrayPipe(new ParseArrayPipeOptions({ allowEmptyString: true }))) functions?: string[], ): Promise { - return await this.poolService.getPool(new QueryPagination({ from, size }), new PoolFilter({ + const pool = await this.poolService.getPool(new QueryPagination({ from, size }), new PoolFilter({ sender: sender, receiver: receiver, senderShard: senderShard, @@ -45,6 +47,12 @@ export class PoolController { type: type, functions: functions, })); + + if (pool == null) { + throw new TransactionPoolTooLargeException(); + } + + return pool; } @Get("/pool/count") @@ -85,10 +93,15 @@ export class PoolController { @ApiOperation({ summary: 'Transaction from pool', description: 'Returns a transaction from the memory pool.' }) @ApiOkResponse({ type: TransactionInPool }) @ApiNotFoundResponse({ description: 'Transaction not found' }) + @ApiServiceUnavailableResponse({ description: 'The transaction pool is too large to be displayed' }) async getTransactionFromPool( @Param('txhash', ParseTransactionHashPipe) txHash: string, ): Promise { const transaction = await this.poolService.getTransactionFromPool(txHash); + if (transaction === null) { + throw new TransactionPoolTooLargeException(); + } + if (transaction === undefined) { throw new NotFoundException('Transaction not found'); } diff --git a/src/endpoints/pool/pool.service.ts b/src/endpoints/pool/pool.service.ts index 501261fdf..3e667ebbd 100644 --- a/src/endpoints/pool/pool.service.ts +++ b/src/endpoints/pool/pool.service.ts @@ -12,6 +12,7 @@ import { TxInPoolFields } from "src/common/gateway/entities/tx.in.pool.fields"; import { TransactionActionService } from "../transactions/transaction-action/transaction.action.service"; import { Transaction } from "../transactions/entities/transaction"; import { ApiUtils } from "@multiversx/sdk-nestjs-http"; +import { TransactionPoolTooLargeException } from "./entities/transaction.pool.too.large.exception"; @Injectable() export class PoolService { @@ -22,43 +23,72 @@ export class PoolService { private readonly transactionActionService: TransactionActionService, ) { } - async getTransactionFromPool(txHash: string): Promise { - const pool = await this.getPoolWithFilters(); + async getTransactionFromPool(txHash: string): Promise { + const pool = await this.getTxPool(); + if (pool == null) { + return null; + } + return pool.find(tx => tx.txHash === txHash); } async getPoolCount(filter: PoolFilter): Promise { - const pool = await this.getPoolWithFilters(filter); - return pool.length; + const pool = await this.getTxPool().catch(() => null); + if (pool != null) { + return this.applyFilters(pool, filter).length; + } + + return await this.cacheService.getOrSet( + CacheInfo.TransactionPoolCount.key, + async () => await this.gatewayService.getTransactionPoolCount(), + CacheInfo.TransactionPoolCount.ttl, + ); } async getPool( queryPagination: QueryPagination, filter?: PoolFilter, - ): Promise { + ): Promise { if (!this.apiConfigService.isTransactionPoolEnabled()) { return []; } const { from, size } = queryPagination; - const pool = await this.getPoolWithFilters(filter); - return pool.slice(from, from + size); + const pool = await this.getTxPool(); + if (pool == null) { + return null; + } + + return this.applyFilters(pool, filter).slice(from, from + size); } async getPoolWithFilters( filter?: PoolFilter, ): Promise { - const pool = await this.cacheService.getOrSet( + const pool = await this.getTxPool(); + if (pool == null) { + throw new TransactionPoolTooLargeException(); + } + + return this.applyFilters(pool, filter); + } + + private async getTxPool(): Promise { + return await this.cacheService.getOrSet( CacheInfo.TransactionPool.key, async () => await this.getTxPoolRaw(), CacheInfo.TransactionPool.ttl, + CacheInfo.TransactionPool.ttl, + true, ); - - return this.applyFilters(pool, filter); } - async getTxPoolRaw(): Promise { + async getTxPoolRaw(): Promise { const pool = await this.gatewayService.getTransactionPool(); + if (pool == null) { + return null; + } + return this.parseTransactions(pool); } diff --git a/src/test/unit/services/cache.warmer.pool.spec.ts b/src/test/unit/services/cache.warmer.pool.spec.ts new file mode 100644 index 000000000..c16847a41 --- /dev/null +++ b/src/test/unit/services/cache.warmer.pool.spec.ts @@ -0,0 +1,51 @@ +import { Locker, LockResult } from "@multiversx/sdk-nestjs-common"; +import { CacheWarmerService } from "src/crons/cache.warmer/cache.warmer.service"; +import { TransactionType } from "src/endpoints/transactions/entities/transaction.type"; +import { CacheInfo } from "src/utils/cache.info"; + +describe('CacheWarmerService transaction pool', () => { + const ttl = 10; + const pool = [{ txHash: 'a', type: TransactionType.Transaction }]; + + let warmer: CacheWarmerService; + let poolService: any; + let gatewayService: any; + let cachingService: any; + + beforeEach(() => { + jest.spyOn(Locker, 'lock').mockImplementation(async (_key: string, func: () => Promise) => { + await func(); + return LockResult.SUCCESS; + }); + + poolService = { getTxPoolRaw: jest.fn().mockResolvedValue(pool) }; + gatewayService = { getTransactionPoolCount: jest.fn().mockResolvedValue(12) }; + cachingService = { set: jest.fn() }; + + warmer = Object.assign(Object.create(CacheWarmerService.prototype), { + poolService, + gatewayService, + cachingService, + apiConfigService: { getTransactionPoolCacheWarmerTtlInSeconds: () => ttl }, + clientProxy: { emit: jest.fn() }, + }); + }); + + it('should warm only the pool when it can be read', async () => { + await warmer.handleTxPoolInvalidations(); + + expect(cachingService.set).toHaveBeenCalledTimes(1); + expect(cachingService.set).toHaveBeenCalledWith(CacheInfo.TransactionPool.key, pool, ttl); + expect(gatewayService.getTransactionPoolCount).not.toHaveBeenCalled(); + }); + + it('should warm the pool as null and refresh the count from the gateway when the pool is too large', async () => { + poolService.getTxPoolRaw.mockResolvedValue(null); + + await warmer.handleTxPoolInvalidations(); + + expect(cachingService.set).toHaveBeenCalledTimes(2); + expect(cachingService.set).toHaveBeenCalledWith(CacheInfo.TransactionPool.key, null, ttl); + expect(cachingService.set).toHaveBeenCalledWith(CacheInfo.TransactionPoolCount.key, 12, ttl); + }); +}); diff --git a/src/test/unit/services/gateway.pool.count.spec.ts b/src/test/unit/services/gateway.pool.count.spec.ts new file mode 100644 index 000000000..5d94938f6 --- /dev/null +++ b/src/test/unit/services/gateway.pool.count.spec.ts @@ -0,0 +1,35 @@ +import { GatewayService } from "src/common/gateway/gateway.service"; + +describe('GatewayService transaction pool', () => { + let gatewayService: GatewayService; + let apiService: any; + + beforeEach(() => { + apiService = { + get: jest.fn().mockResolvedValue({ data: { data: { txPoolCounts: { '0': 1, '1': 2, '2': 3, '4294967295': 4 } } } }), + }; + + gatewayService = new GatewayService( + { getGatewayUrl: () => 'https://gateway', getSnapshotlessGatewayUrl: () => undefined } as any, + apiService, + ); + Object.assign(gatewayService, { eventEmitter: { emit: jest.fn() } }); + }); + + it('should read a pool over the response size limit as null', async () => { + apiService.get.mockRejectedValue({ message: 'maxContentLength size of 2097152 exceeded' }); + + expect(await gatewayService.getTransactionPool()).toBeNull(); + }); + + it('should keep throwing other failures of the pool', async () => { + apiService.get.mockRejectedValue({ message: 'connect ECONNREFUSED' }); + + await expect(gatewayService.getTransactionPool()).rejects.toEqual({ message: 'connect ECONNREFUSED' }); + }); + + it('should sum the counts of every shard', async () => { + expect(await gatewayService.getTransactionPoolCount()).toStrictEqual(10); + expect(apiService.get).toHaveBeenCalledWith('https://gateway/transaction/pool/count', expect.anything(), undefined); + }); +}); diff --git a/src/test/unit/services/pool.spec.ts b/src/test/unit/services/pool.spec.ts index a276ca2e3..43187ebe7 100644 --- a/src/test/unit/services/pool.spec.ts +++ b/src/test/unit/services/pool.spec.ts @@ -8,6 +8,8 @@ import { PoolService } from "src/endpoints/pool/pool.service"; import { TransactionType } from "src/endpoints/transactions/entities/transaction.type"; import { ProtocolService } from "../../../common/protocol/protocol.service"; import { TransactionActionService } from "../../../endpoints/transactions/transaction-action/transaction.action.service"; +import { CacheInfo } from "src/utils/cache.info"; +import { TransactionPoolTooLargeException } from "src/endpoints/pool/entities/transaction.pool.too.large.exception"; describe('PoolService', () => { let service: PoolService; @@ -22,6 +24,7 @@ describe('PoolService', () => { provide: GatewayService, useValue: { getTransactionPool: jest.fn(), + getTransactionPoolCount: jest.fn(), }, }, { @@ -60,7 +63,9 @@ describe('PoolService', () => { gatewayService.getTransactionPool = jest.fn().mockResolvedValue(data); const txPoolRaw = await service.getTxPoolRaw(); - cacheService.getOrSet = jest.fn().mockResolvedValue(txPoolRaw); + cacheService.getOrSet = jest.fn().mockImplementation(async (key: string, createValueFunc: () => Promise) => { + return key === CacheInfo.TransactionPool.key ? txPoolRaw : await createValueFunc(); + }); }); @@ -72,7 +77,7 @@ describe('PoolService', () => { it('should work and return the pool', async () => { const pool = await service.getPool(new QueryPagination(), new PoolFilter()); expect(pool).toHaveLength(7); - expect(pool[0].type).toStrictEqual(TransactionType.Transaction); + expect(pool?.[0].type).toStrictEqual(TransactionType.Transaction); }); it('should work and return the pool with filters', async () => { @@ -89,14 +94,28 @@ describe('PoolService', () => { it('should work and return the pool with query pagination', async () => { const pool = await service.getPool(new QueryPagination({ from: 0, size: 2 }), new PoolFilter({ type: TransactionType.Reward })); expect(pool).toHaveLength(2); - expect(pool[0].type).toStrictEqual(TransactionType.Reward); + expect(pool?.[0].type).toStrictEqual(TransactionType.Reward); }); }); describe('getPoolCount', () => { - it('should work and return the pool count', async () => { - const poolCount = await service.getPoolCount(new PoolFilter()); - expect(poolCount).toStrictEqual(7); + it('should count the total from the pool, so that it matches what the pool lists', async () => { + gatewayService.getTransactionPoolCount = jest.fn(); + + expect(await service.getPoolCount(new PoolFilter())).toStrictEqual(7); + expect(gatewayService.getTransactionPoolCount).not.toHaveBeenCalled(); + }); + + it('should count the pool, without caching the count, for filters', async () => { + const filter = new PoolFilter({ type: TransactionType.Transaction, senderShard: 0 }); + + const poolCount = await service.getPoolCount(filter); + expect(cacheService.getOrSet).toHaveBeenCalledTimes(1); + expect(cacheService.getOrSet).toHaveBeenCalledWith(CacheInfo.TransactionPool.key, expect.any(Function), CacheInfo.TransactionPool.ttl, CacheInfo.TransactionPool.ttl, true); + + const pool = await service.getPool(new QueryPagination({ from: 0, size: 100 }), filter); + expect(pool?.length).toBeGreaterThan(0); + expect(poolCount).toStrictEqual(pool?.length); }); it('should work and return the pool count with filters', async () => { @@ -111,6 +130,56 @@ describe('PoolService', () => { }); }); + describe('pool too large', () => { + it('should read a pool too large for the gateway response limit as null', async () => { + gatewayService.getTransactionPool = jest.fn().mockResolvedValue(null); + + expect(await service.getTxPoolRaw()).toBeNull(); + }); + + it('should answer null from the cached null, without downloading the pool again', async () => { + cacheService.getOrSet = jest.fn().mockImplementation(async (key: string, createValueFunc: () => Promise) => { + return key === CacheInfo.TransactionPool.key ? null : await createValueFunc(); + }); + gatewayService.getTransactionPool = jest.fn(); + + expect(await service.getPool(new QueryPagination(), new PoolFilter())).toBeNull(); + expect(await service.getTransactionFromPool('e07af9835b6da5740d0f791cfe65491a562852c57d44af63fdc14be5d73f01da')).toBeNull(); + expect(gatewayService.getTransactionPool).not.toHaveBeenCalled(); + }); + + it('should fail the pool with filters, like any other failure, while the pool is too large', async () => { + cacheService.getOrSet = jest.fn().mockResolvedValue(null); + + await expect(service.getPoolWithFilters({ senderShard: 0 })).rejects.toBeInstanceOf(TransactionPoolTooLargeException); + }); + + it('should count the total through the gateway, cached, whatever the filters', async () => { + gatewayService.getTransactionPoolCount = jest.fn().mockResolvedValue(42); + cacheService.getOrSet = jest.fn().mockImplementation(async (key: string, createValueFunc: () => Promise) => { + return key === CacheInfo.TransactionPool.key ? null : await createValueFunc(); + }); + + expect(await service.getPoolCount(new PoolFilter())).toStrictEqual(42); + expect(await service.getPoolCount(new PoolFilter({ type: TransactionType.Reward }))).toStrictEqual(42); + expect(await service.getPoolCount(new PoolFilter({ type: TransactionType.Reward, sender: 'erd1qqqqqqqqqqqqqpgqp699jngundfqw07d8jzkepucvpzush6k3wvqyc44rx' }))).toStrictEqual(42); + expect(cacheService.getOrSet).toHaveBeenCalledWith(CacheInfo.TransactionPoolCount.key, expect.any(Function), CacheInfo.TransactionPoolCount.ttl); + expect(gatewayService.getTransactionPoolCount).toHaveBeenCalledTimes(3); + }); + }); + + describe('pool failing for another reason', () => { + it('should still count the total through the gateway', async () => { + gatewayService.getTransactionPoolCount = jest.fn().mockResolvedValue(42); + cacheService.getOrSet = jest.fn().mockImplementation(async (key: string, createValueFunc: () => Promise) => { + return key === CacheInfo.TransactionPool.key ? await Promise.reject(new Error('gateway unreachable')) : await createValueFunc(); + }); + + expect(await service.getPoolCount(new PoolFilter({ type: TransactionType.Reward }))).toStrictEqual(42); + await expect(service.getPool(new QueryPagination(), new PoolFilter())).rejects.toThrow('gateway unreachable'); + }); + }); + describe('getTransactionFromPool', () => { it('should work and return the transaction', async () => { const tx = await service.getTransactionFromPool("e07af9835b6da5740d0f791cfe65491a562852c57d44af63fdc14be5d73f01da"); diff --git a/src/test/unit/services/pool.too.large.spec.ts b/src/test/unit/services/pool.too.large.spec.ts new file mode 100644 index 000000000..df8e9eb65 --- /dev/null +++ b/src/test/unit/services/pool.too.large.spec.ts @@ -0,0 +1,116 @@ +import { NotFoundException } from "@nestjs/common"; +import { PoolGateway } from "src/crons/websocket/pool.gateway"; +import { TransactionPoolTooLargeException } from "src/endpoints/pool/entities/transaction.pool.too.large.exception"; +import { PoolUpdateStatus } from "src/endpoints/pool/entities/pool.update.status"; +import { PoolController } from "src/endpoints/pool/pool.controller"; +import { TransactionType } from "src/endpoints/transactions/entities/transaction.type"; +import { TransactionService } from "src/endpoints/transactions/transaction.service"; + +describe('Transaction pool too large', () => { + const txHash = 'e07af9835b6da5740d0f791cfe65491a562852c57d44af63fdc14be5d73f01da'; + + let poolService: any; + + beforeEach(() => { + poolService = { + getPool: jest.fn().mockResolvedValue(null), + getTransactionFromPool: jest.fn().mockResolvedValue(null), + getPoolCount: jest.fn().mockResolvedValue(42), + }; + }); + + describe('PoolController', () => { + let controller: PoolController; + + beforeEach(() => { + controller = new PoolController(poolService); + }); + + it('should answer the pool and the transaction with the too large exception', async () => { + await expect(controller.getTransactionPool(0, 25)).rejects.toBeInstanceOf(TransactionPoolTooLargeException); + await expect(controller.getTransactionFromPool(txHash)).rejects.toBeInstanceOf(TransactionPoolTooLargeException); + }); + + it('should answer the count, whatever the filters', async () => { + expect(await controller.getTransactionPoolCount()).toStrictEqual(42); + expect(await controller.getTransactionPoolCount(undefined, undefined, undefined, undefined, TransactionType.Reward)).toStrictEqual(42); + expect(await controller.getTransactionPoolCount('erd1qqqqqqqqqqqqqpgqp699jngundfqw07d8jzkepucvpzush6k3wvqyc44rx')).toStrictEqual(42); + }); + + it('should answer the too large exception as unavailable, with a code and a message', () => { + const exception = new TransactionPoolTooLargeException(); + + expect(exception.getStatus()).toStrictEqual(503); + expect(exception.getResponse()).toEqual({ statusCode: 503, code: 'transaction_pool_too_large', message: 'The transaction pool is too large to be displayed' }); + }); + + it('should keep answering other failures as errors', async () => { + poolService.getPool.mockRejectedValue(new Error('gateway unreachable')); + poolService.getTransactionFromPool.mockResolvedValue(undefined); + + await expect(controller.getTransactionPool(0, 25)).rejects.toThrow('gateway unreachable'); + await expect(controller.getTransactionFromPool(txHash)).rejects.toBeInstanceOf(NotFoundException); + }); + }); + + describe('TransactionService price per unit', () => { + it('should handle a pool too large like any other failure of the pool', async () => { + const transactionService: TransactionService = Object.assign(Object.create(TransactionService.prototype), { + blockService: { getBlocks: jest.fn().mockResolvedValue([{ nonce: 100 }]) }, + networkService: { getConstants: jest.fn().mockResolvedValue({ minGasLimit: 50000, gasPerDataByte: 1500, gasPriceModifier: '0.01' }) }, + poolService: { getPoolWithFilters: jest.fn().mockRejectedValue(new TransactionPoolTooLargeException()) }, + logger: { error: jest.fn() }, + }); + + expect(await transactionService.getPpuByShardIdRaw(1)).toBeNull(); + }); + }); + + describe('PoolGateway', () => { + let gateway: PoolGateway; + let emit: jest.Mock; + + beforeEach(() => { + emit = jest.fn(); + gateway = new PoolGateway(poolService); + gateway.server = { to: jest.fn().mockReturnValue({ emit }) } as any; + }); + + it('should send a null pool with the total count', async () => { + await gateway.pushPoolForRoom('pool-{"from":0,"size":25}'); + + expect(emit).toHaveBeenCalledWith('poolUpdate', { status: PoolUpdateStatus.tooLarge, pool: null, poolCount: 42 }); + }); + + it('should send a null pool with the total count to a room filtered by type', async () => { + await gateway.pushPoolForRoom(`pool-{"from":0,"size":25,"type":"${TransactionType.Reward}"}`); + + expect(emit).toHaveBeenCalledWith('poolUpdate', { status: PoolUpdateStatus.tooLarge, pool: null, poolCount: 42 }); + }); + + it('should send the pool when it can be read', async () => { + poolService.getPool.mockResolvedValue([{ txHash }]); + + await gateway.pushPoolForRoom('pool-{"from":0,"size":25}'); + + expect(emit).toHaveBeenCalledWith('poolUpdate', { status: PoolUpdateStatus.success, pool: [{ txHash }], poolCount: 42 }); + }); + + it('should send the internal server error status with the count when the pool fails for another reason', async () => { + poolService.getPool.mockRejectedValue(new Error('gateway unreachable')); + + await gateway.pushPoolForRoom('pool-{"from":0,"size":25}'); + + expect(emit).toHaveBeenCalledWith('poolUpdate', { status: PoolUpdateStatus.internalServerError, pool: null, poolCount: 42 }); + }); + + it('should send the internal server error status without a count when the count fails as well', async () => { + poolService.getPool.mockRejectedValue(new Error('gateway unreachable')); + poolService.getPoolCount.mockRejectedValue(new Error('gateway unreachable')); + + await gateway.pushPoolForRoom('pool-{"from":0,"size":25}'); + + expect(emit).toHaveBeenCalledWith('poolUpdate', { status: PoolUpdateStatus.internalServerError, pool: null, poolCount: null }); + }); + }); +}); diff --git a/src/utils/cache.info.ts b/src/utils/cache.info.ts index e88079edf..cbcd880de 100644 --- a/src/utils/cache.info.ts +++ b/src/utils/cache.info.ts @@ -57,6 +57,11 @@ export class CacheInfo { ttl: Constants.oneSecond(), }; + static TransactionPoolCount: CacheInfo = { + key: 'txpool:count', + ttl: Constants.oneSecond(), + }; + static ApplicationMostUsed: CacheInfo = { key: 'applicationMostUsed', ttl: Constants.oneHour(),