Feature/improve portfolio snapshot caching (#3717)

* Improve portfolio snapshot caching

* Update changelog
pull/3719/head
Thomas Kaul 5 months ago committed by GitHub
parent c6f804f68c
commit 71a568264d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Reworked the portfolio calculator - 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 log levels as an environment variable (`LOG_LEVELS`)
- Exposed the maximum of chart data items as an environment variable (`MAX_CHART_ITEMS`) - 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 - Changed the data format of the environment variable `CACHE_QUOTES_TTL` from seconds to milliseconds

@ -1,6 +1,7 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface'; 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 { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface';
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; 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 { Big } from 'big.js';
import { plainToClass } from 'class-transformer'; import { plainToClass } from 'class-transformer';
import { import {
addMilliseconds,
differenceInDays, differenceInDays,
eachDayOfInterval, eachDayOfInterval,
endOfDay, endOfDay,
@ -863,6 +865,29 @@ export abstract class PortfolioCalculator {
return chartDateMap; 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(<PortfolioSnapshotValue>(<unknown>{
expiration: expiration.getTime(),
portfolioSnapshot: snapshot
})),
0
);
return snapshot;
}
@LogPerformance @LogPerformance
private computeTransactionPoints() { private computeTransactionPoints() {
this.transactionPoints = []; this.transactionPoints = [];
@ -1006,19 +1031,33 @@ export abstract class PortfolioCalculator {
private async initialize() { private async initialize() {
const startTimeTotal = performance.now(); const startTimeTotal = performance.now();
const cachedSnapshot = await this.redisCacheService.get( let cachedPortfolioSnapshot: PortfolioSnapshot;
this.redisCacheService.getPortfolioSnapshotKey({ let isCachedPortfolioSnapshotExpired = false;
filters: this.filters,
userId: this.userId 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) { cachedPortfolioSnapshot = plainToClass(
this.snapshot = plainToClass(
PortfolioSnapshot, PortfolioSnapshot,
JSON.parse(cachedSnapshot) portfolioSnapshot
); );
if (isAfter(new Date(), new Date(expiration))) {
isCachedPortfolioSnapshotExpired = true;
}
} catch {}
if (cachedPortfolioSnapshot) {
this.snapshot = cachedPortfolioSnapshot;
Logger.debug( Logger.debug(
`Fetched portfolio snapshot from cache in ${( `Fetched portfolio snapshot from cache in ${(
(performance.now() - startTimeTotal) / (performance.now() - startTimeTotal) /
@ -1026,17 +1065,14 @@ export abstract class PortfolioCalculator {
).toFixed(3)} seconds`, ).toFixed(3)} seconds`,
'PortfolioCalculator' 'PortfolioCalculator'
); );
} else {
this.snapshot = await this.computeSnapshot();
this.redisCacheService.set( if (isCachedPortfolioSnapshotExpired) {
this.redisCacheService.getPortfolioSnapshotKey({ // Compute in the background
filters: this.filters, this.computeAndCacheSnapshot();
userId: this.userId }
}), } else {
JSON.stringify(this.snapshot), // Wait for computation
this.configurationService.get('CACHE_QUOTES_TTL') this.snapshot = await this.computeAndCacheSnapshot();
);
} }
} }
} }

@ -0,0 +1,4 @@
export interface PortfolioSnapshotValue {
expiration: number;
portfolioSnapshot: string;
}
Loading…
Cancel
Save