Browse Source

Task/recompute portfolio snapshot on portfolio change (#7403)

* Recompute portfolio snapshot on portfolio change

* Update changelog
pull/7367/head
Thomas Kaul 7 days ago
committed by GitHub
parent
commit
8169b2c723
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 15
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  3. 8
      apps/api/src/events/events.module.ts
  4. 53
      apps/api/src/events/portfolio-changed.listener.ts
  5. 16
      apps/api/src/services/api/api.service.ts

2
CHANGELOG.md

@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Recomputed the portfolio snapshot calculation in the background on a portfolio change
- Improved the deduplication of the portfolio snapshot calculation jobs by considering the filters
- Refactored the deprecated animation providers (`provideAnimations()` and `provideNoopAnimations()`) - Refactored the deprecated animation providers (`provideAnimations()` and `provideNoopAnimations()`)
- Improved the language localization for Polish (`pl`) - Improved the language localization for Polish (`pl`)

15
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -1097,16 +1097,19 @@ export abstract class PortfolioCalculator {
let cachedPortfolioSnapshot: PortfolioSnapshot; let cachedPortfolioSnapshot: PortfolioSnapshot;
let isCachedPortfolioSnapshotExpired = false; let isCachedPortfolioSnapshotExpired = false;
const jobId = this.userId; const portfolioSnapshotKey = this.redisCacheService.getPortfolioSnapshotKey(
{
try {
const cachedPortfolioSnapshotValue = await this.redisCacheService.get(
this.redisCacheService.getPortfolioSnapshotKey({
filters: this.filters, filters: this.filters,
userId: this.userId userId: this.userId
}) }
); );
const jobId = portfolioSnapshotKey;
try {
const cachedPortfolioSnapshotValue =
await this.redisCacheService.get(portfolioSnapshotKey);
const { expiration, portfolioSnapshot }: PortfolioSnapshotValue = const { expiration, portfolioSnapshot }: PortfolioSnapshotValue =
JSON.parse(cachedPortfolioSnapshotValue); JSON.parse(cachedPortfolioSnapshotValue);

8
apps/api/src/events/events.module.ts

@ -1,9 +1,12 @@
import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.module'; import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module';
import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
@ -13,11 +16,14 @@ import { PortfolioChangedListener } from './portfolio-changed.listener';
@Module({ @Module({
imports: [ imports: [
ActivitiesModule, ActivitiesModule,
ApiModule,
ConfigurationModule, ConfigurationModule,
DataGatheringQueueModule, DataGatheringQueueModule,
DataProviderModule, DataProviderModule,
ExchangeRateDataModule, ExchangeRateDataModule,
RedisCacheModule PortfolioSnapshotQueueModule,
RedisCacheModule,
UserModule
], ],
providers: [AssetProfileChangedListener, PortfolioChangedListener] providers: [AssetProfileChangedListener, PortfolioChangedListener]
}) })

53
apps/api/src/events/portfolio-changed.listener.ts

@ -1,4 +1,12 @@
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import {
PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_LOW,
PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME,
PORTFOLIO_SNAPSHOT_PROCESS_JOB_OPTIONS
} from '@ghostfolio/common/config';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter'; import { OnEvent } from '@nestjs/event-emitter';
@ -14,7 +22,12 @@ export class PortfolioChangedListener {
private debounceTimers = new Map<string, NodeJS.Timeout>(); private debounceTimers = new Map<string, NodeJS.Timeout>();
public constructor(private readonly redisCacheService: RedisCacheService) {} public constructor(
private readonly apiService: ApiService,
private readonly portfolioSnapshotService: PortfolioSnapshotService,
private readonly redisCacheService: RedisCacheService,
private readonly userService: UserService
) {}
@OnEvent(PortfolioChangedEvent.getName()) @OnEvent(PortfolioChangedEvent.getName())
handlePortfolioChangedEvent(event: PortfolioChangedEvent) { handlePortfolioChangedEvent(event: PortfolioChangedEvent) {
@ -39,6 +52,44 @@ export class PortfolioChangedListener {
private async processPortfolioChanged({ userId }: { userId: string }) { private async processPortfolioChanged({ userId }: { userId: string }) {
this.logger.log(`Portfolio of user '${userId}' has changed`); this.logger.log(`Portfolio of user '${userId}' has changed`);
try {
await this.redisCacheService.removePortfolioSnapshotsByUserId({ userId }); await this.redisCacheService.removePortfolioSnapshotsByUserId({ userId });
const user = await this.userService.user({ id: userId });
if (!user) {
return;
}
const userSettings = user.settings.settings;
const filters = this.apiService.buildFiltersFromUserSettings({
userSettings
});
// Recompute in the background to avoid a cold start on the next request
await this.portfolioSnapshotService.addJobToQueue({
data: {
filters,
userId,
calculationType: userSettings.performanceCalculationType,
userCurrency: userSettings.baseCurrency
},
name: PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME,
opts: {
...PORTFOLIO_SNAPSHOT_PROCESS_JOB_OPTIONS,
jobId: this.redisCacheService.getPortfolioSnapshotKey({
filters,
userId
}),
priority: PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_LOW
}
});
} catch (error) {
this.logger.error(
`Portfolio snapshot of user '${userId}' could not be recomputed`,
error
);
}
} }
} }

16
apps/api/src/services/api/api.service.ts

@ -1,4 +1,4 @@
import { Filter } from '@ghostfolio/common/interfaces'; import { Filter, UserSettings } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@ -89,4 +89,18 @@ export class ApiService {
return filters; return filters;
} }
public buildFiltersFromUserSettings({
userSettings
}: {
userSettings: UserSettings;
}): Filter[] {
return this.buildFiltersFromQueryParams({
filterByAccounts: userSettings?.['filters.accounts']?.[0],
filterByAssetClasses: userSettings?.['filters.assetClasses']?.[0],
filterByDataSource: userSettings?.['filters.dataSource'],
filterBySymbol: userSettings?.['filters.symbol'],
filterByTags: userSettings?.['filters.tags']?.[0]
});
}
} }

Loading…
Cancel
Save