diff --git a/CHANGELOG.md b/CHANGELOG.md index f68d1aad8..cbfcfa2f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Reworked the portfolio calculator +- Improved the caching of the portfolio snapshot in the portfolio calculator by returning cached data and recalculating in the background when it expires - Exposed the log levels as an environment variable (`LOG_LEVELS`) - Exposed the maximum of chart data items as an environment variable (`MAX_CHART_ITEMS`) - Changed the data format of the environment variable `CACHE_QUOTES_TTL` from seconds to milliseconds diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 0fcb8b9d6..5384fd6d8 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -1,6 +1,7 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface'; +import { PortfolioSnapshotValue } from '@ghostfolio/api/app/portfolio/interfaces/snapshot-value.interface'; import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface'; import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; @@ -32,6 +33,7 @@ import { Logger } from '@nestjs/common'; import { Big } from 'big.js'; import { plainToClass } from 'class-transformer'; import { + addMilliseconds, differenceInDays, eachDayOfInterval, endOfDay, @@ -863,6 +865,29 @@ export abstract class PortfolioCalculator { return chartDateMap; } + private async computeAndCacheSnapshot() { + const snapshot = await this.computeSnapshot(); + + const expiration = addMilliseconds( + new Date(), + this.configurationService.get('CACHE_QUOTES_TTL') + ); + + this.redisCacheService.set( + this.redisCacheService.getPortfolioSnapshotKey({ + filters: this.filters, + userId: this.userId + }), + JSON.stringify(({ + expiration: expiration.getTime(), + portfolioSnapshot: snapshot + })), + 0 + ); + + return snapshot; + } + @LogPerformance private computeTransactionPoints() { this.transactionPoints = []; @@ -1006,19 +1031,33 @@ export abstract class PortfolioCalculator { private async initialize() { const startTimeTotal = performance.now(); - const cachedSnapshot = await this.redisCacheService.get( - this.redisCacheService.getPortfolioSnapshotKey({ - filters: this.filters, - userId: this.userId - }) - ); + let cachedPortfolioSnapshot: PortfolioSnapshot; + let isCachedPortfolioSnapshotExpired = false; + + try { + const cachedPortfolioSnapshotValue = await this.redisCacheService.get( + this.redisCacheService.getPortfolioSnapshotKey({ + filters: this.filters, + userId: this.userId + }) + ); + + const { expiration, portfolioSnapshot }: PortfolioSnapshotValue = + JSON.parse(cachedPortfolioSnapshotValue); - if (cachedSnapshot) { - this.snapshot = plainToClass( + cachedPortfolioSnapshot = plainToClass( PortfolioSnapshot, - JSON.parse(cachedSnapshot) + portfolioSnapshot ); + if (isAfter(new Date(), new Date(expiration))) { + isCachedPortfolioSnapshotExpired = true; + } + } catch {} + + if (cachedPortfolioSnapshot) { + this.snapshot = cachedPortfolioSnapshot; + Logger.debug( `Fetched portfolio snapshot from cache in ${( (performance.now() - startTimeTotal) / @@ -1026,17 +1065,14 @@ export abstract class PortfolioCalculator { ).toFixed(3)} seconds`, 'PortfolioCalculator' ); - } else { - this.snapshot = await this.computeSnapshot(); - this.redisCacheService.set( - this.redisCacheService.getPortfolioSnapshotKey({ - filters: this.filters, - userId: this.userId - }), - JSON.stringify(this.snapshot), - this.configurationService.get('CACHE_QUOTES_TTL') - ); + if (isCachedPortfolioSnapshotExpired) { + // Compute in the background + this.computeAndCacheSnapshot(); + } + } else { + // Wait for computation + this.snapshot = await this.computeAndCacheSnapshot(); } } } diff --git a/apps/api/src/app/portfolio/interfaces/snapshot-value.interface.ts b/apps/api/src/app/portfolio/interfaces/snapshot-value.interface.ts new file mode 100644 index 000000000..3d205416c --- /dev/null +++ b/apps/api/src/app/portfolio/interfaces/snapshot-value.interface.ts @@ -0,0 +1,4 @@ +export interface PortfolioSnapshotValue { + expiration: number; + portfolioSnapshot: string; +}