Skip to content

Commit

Permalink
NAS-130767 / 25.04 / Optimize app.query calls (#10569)
Browse files Browse the repository at this point in the history
* NAS-130767: Optimize app.query calls

* NAS-130767: Use filesystem.statfs for resources

* NAS-130767: Added back subscribeToInstalledAppsUpdates

* NAS-130767: Optimize app.query calls

* NAS-130767: Use `app.available_space`  endpoint
  • Loading branch information
denysbutenko authored Sep 10, 2024
1 parent 3447628 commit 4598a0c
Show file tree
Hide file tree
Showing 11 changed files with 68 additions and 95 deletions.
2 changes: 1 addition & 1 deletion src/app/enums/docker-config.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface DockerConfigUpdate {
nvidia?: boolean;
}

export interface DockerStatusResponse {
export interface DockerStatusData {
status: DockerStatus;
description: string;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { App } from 'app/interfaces/app.interface';
import { ContainerImage } from 'app/interfaces/container-image.interface';
import { Group } from 'app/interfaces/group.interface';
import { Pool } from 'app/interfaces/pool.interface';
Expand All @@ -13,6 +14,7 @@ export interface ApiCallAndSubscribeEventDirectory {
'pool.query': { response: Pool };
'group.query': { response: Group };
'app.image.query': { response: ContainerImage };
'app.query': { response: App };
}

export type ApiCallAndSubscribeMethod = keyof ApiCallAndSubscribeEventDirectory;
Expand Down
5 changes: 3 additions & 2 deletions src/app/interfaces/api/api-call-directory.interface.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AlertPolicy } from 'app/enums/alert-policy.enum';
import { DatasetRecordSize, DatasetType } from 'app/enums/dataset.enum';
import { DeviceType } from 'app/enums/device-type.enum';
import { DockerConfig, DockerStatusResponse } from 'app/enums/docker-config.interface';
import { DockerConfig, DockerStatusData } from 'app/enums/docker-config.interface';
import { EnclosureSlotStatus } from 'app/enums/enclosure-slot-status.enum';
import { FailoverDisabledReason } from 'app/enums/failover-disabled-reason.enum';
import { FailoverStatus } from 'app/enums/failover-status.enum';
Expand Down Expand Up @@ -314,6 +314,7 @@ export interface ApiCallDirectory {
'app.query': { params: AppQueryParams; response: App[] };
'app.upgrade_summary': { params: AppUpgradeParams; response: AppUpgradeSummary };
'app.available': { params: QueryParams<AvailableApp>; response: AvailableApp[] };
'app.available_space': { params: void; response: number };
'app.categories': { params: void; response: string[] };
'app.latest': { params: QueryParams<AvailableApp>; response: AvailableApp[] };
'app.similar': { params: [app_name: string, train: string]; response: AvailableApp[] };
Expand Down Expand Up @@ -605,7 +606,7 @@ export interface ApiCallDirectory {

// Docker
'docker.config': { params: void; response: DockerConfig };
'docker.status': { params: void; response: DockerStatusResponse };
'docker.status': { params: void; response: DockerStatusData };
'docker.lacks_nvidia_drivers': { params: void; response: boolean };

// LDAP
Expand Down
4 changes: 2 additions & 2 deletions src/app/interfaces/api/api-event-directory.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DockerStatusResponse } from 'app/enums/docker-config.interface';
import { DockerStatusData } from 'app/enums/docker-config.interface';
import { FailoverStatus } from 'app/enums/failover-status.enum';
import { Alert } from 'app/interfaces/alert.interface';
import { App, AppStats } from 'app/interfaces/app.interface';
Expand Down Expand Up @@ -26,7 +26,7 @@ export interface ApiEventDirectory {
'core.get_jobs': { response: Job };
'directoryservices.status': { response: DirectoryServicesState };
'disk.query': { response: Disk };
'docker.state': { response: DockerStatusResponse };
'docker.state': { response: DockerStatusData };
'failover.disabled.reasons': { response: FailoverDisabledReasonEvent };
'failover.status': { response: { status: FailoverStatus } };
'group.query': { response: Group };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ <h2>{{ 'Screenshots' | translate }}</h2>
<section class="app-info-cards">
<ix-app-resources-card
class="app-info-card"
[isLoading$]="isLoading$"
[isLoading]="isLoading$ | async"
></ix-app-resources-card>
<ix-app-available-info-card
class="app-info-card"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ <h3>{{ 'Available Resources' | translate }}</h3>

<div class="app-list-item">
<span class="label">{{ 'CPU Usage' | translate }}:</span>
@if (isLoading$ | async) {
@if (isLoading()) {
<ngx-skeleton-loader></ngx-skeleton-loader>
} @else {
<div>{{ "{cpuPercentage}% Avg. Usage" | translate: { cpuPercentage } }}</div>
}
</div>
<div class="app-list-item">
<span class="label">{{ 'Memory Usage' | translate }}:</span>
@if (isLoading$ | async) {
@if (isLoading()) {
<ngx-skeleton-loader></ngx-skeleton-loader>
} @else {
@if (memoryUsed && memoryTotal) {
Expand All @@ -22,22 +22,25 @@ <h3>{{ 'Available Resources' | translate }}</h3>
</div>
<div class="app-list-item">
<span class="label">{{ 'Pool' | translate }}:</span>
@if (isLoading$ | async) {
@if (isLoading()) {
<ngx-skeleton-loader></ngx-skeleton-loader>
} @else {
@if ((dockerStore.selectedPool$| async); as selectedPool) {
{{ selectedPool }}
@if (selectedPool()) {
{{ selectedPool() }}
} @else {
{{ 'N/A' | translate }}
}
}
</div>
<div class="app-list-item">
<span class="label">{{ 'Available Space' | translate }}:</span>
@if (isLoading()) {
<ngx-skeleton-loader></ngx-skeleton-loader>
} @else {
@if (selectedPool()) {
{{ availableSpace() | ixFileSize }}
} @else {
{{ 'N/A' | translate }}
}
}
</div>

@if (dockerStore.selectedPool$| async) {
<div class="app-list-item">
<span class="label">{{ 'Available Space' | translate }}:</span>
<span *ixWithLoadingState="availableSpace$ as availableSpace">
{{ +availableSpace | ixFileSize }}
</span>
</div>
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Spectator } from '@ngneat/spectator';
import { createComponentFactory, mockProvider } from '@ngneat/spectator/jest';
import { BehaviorSubject, of } from 'rxjs';
import { of } from 'rxjs';
import { mockCall, mockWebSocket } from 'app/core/testing/utils/mock-websocket.utils';
import { DatasetDetails } from 'app/interfaces/dataset.interface';
import { FileSizePipe } from 'app/modules/pipes/file-size/file-size.pipe';
import { AppResourcesCardComponent } from 'app/pages/apps/components/app-detail-view/app-resources-card/app-resources-card.component';
import { DockerStore } from 'app/pages/apps/store/docker.store';
Expand All @@ -12,20 +11,14 @@ describe('AppResourcesCardComponent', () => {
let spectator: Spectator<AppResourcesCardComponent>;
let websocket: WebSocketService;

const isLoading$ = new BehaviorSubject(false);

const createComponent = createComponentFactory({
component: AppResourcesCardComponent,
imports: [
FileSizePipe,
],
providers: [
mockWebSocket([
mockCall('pool.dataset.get_instance', {
available: {
rawvalue: '2500',
},
} as DatasetDetails),
mockCall('app.available_space', 2500),
]),
mockProvider(DockerStore, {
selectedPool$: of('pool'),
Expand All @@ -36,7 +29,7 @@ describe('AppResourcesCardComponent', () => {
beforeEach(() => {
spectator = createComponent({
props: {
isLoading$,
isLoading: false,
},
});
websocket = spectator.inject(WebSocketService);
Expand All @@ -55,7 +48,7 @@ describe('AppResourcesCardComponent', () => {
});

it('loads and reports available space on apps dataset', () => {
expect(websocket.call).toHaveBeenCalledWith('pool.dataset.get_instance', ['pool/ix-apps']);
expect(websocket.call).toHaveBeenCalledWith('app.available_space');
expect(spectator.queryAll('.app-list-item')[3]).toHaveText('Available Space: 2.44 KiB');
});
});
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import {
ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit,
ChangeDetectionStrategy, ChangeDetectorRef, Component, input, OnInit,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import {
Observable, filter, map, switchMap, throttleTime,
} from 'rxjs';
import { toLoadingState } from 'app/helpers/operators/to-loading-state.helper';
import { map, throttleTime } from 'rxjs';
import { MemoryStatsEventData } from 'app/interfaces/events/memory-stats-event.interface';
import { DockerStore } from 'app/pages/apps/store/docker.store';
import { ixAppsDataset } from 'app/pages/datasets/utils/dataset.utils';
import { WebSocketService } from 'app/services/ws.service';

@UntilDestroy()
Expand All @@ -19,24 +16,18 @@ import { WebSocketService } from 'app/services/ws.service';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppResourcesCardComponent implements OnInit {
@Input() isLoading$: Observable<boolean>;
isLoading = input<boolean>(true);
cpuPercentage = 0;
memoryUsed: number;
memoryTotal: number;
pool: string;

availableSpace$ = this.dockerStore.selectedPool$.pipe(
filter((pool) => !!pool),
switchMap((pool) => this.ws.call('pool.dataset.get_instance', [`${pool}/${ixAppsDataset}`])),
map((dataset) => dataset.available.rawvalue),
).pipe(
toLoadingState(),
);
availableSpace = toSignal(this.ws.call('app.available_space'));
selectedPool = toSignal(this.dockerStore.selectedPool$);

constructor(
private ws: WebSocketService,
private cdr: ChangeDetectorRef,
protected dockerStore: DockerStore,
private dockerStore: DockerStore,
) {}

ngOnInit(): void {
Expand Down
5 changes: 4 additions & 1 deletion src/app/pages/apps/services/applications.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { TranslateService } from '@ngx-translate/core';
import {
EMPTY,
Observable, OperatorFunction, filter, map, pipe,
shareReplay,
switchMap,
} from 'rxjs';
import { customApp } from 'app/constants/catalog.constants';
Expand Down Expand Up @@ -64,7 +65,9 @@ export class ApplicationsService {
}

getAllApps(): Observable<App[]> {
return this.ws.call('app.query', [[], { extra: { retrieve_config: true } }]);
return this.ws.call('app.query', [[], { extra: { retrieve_config: true } }]).pipe(
shareReplay({ bufferSize: 1, refCount: true }),
);
}

getApp(name: string): Observable<App[]> {
Expand Down
39 changes: 11 additions & 28 deletions src/app/pages/apps/store/docker.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { TranslateService } from '@ngx-translate/core';
import {
forkJoin, Observable, switchMap, tap,
forkJoin, map, Observable, switchMap, tap,
} from 'rxjs';
import { DockerConfig, DockerStatusResponse } from 'app/enums/docker-config.interface';
import { DockerConfig, DockerStatusData } from 'app/enums/docker-config.interface';
import { DockerStatus } from 'app/enums/docker-status.enum';
import { JobState } from 'app/enums/job-state.enum';
import { ApiEvent } from 'app/interfaces/api-message.interface';
import { Job } from 'app/interfaces/job.interface';
import { DialogService } from 'app/modules/dialog/dialog.service';
import { ErrorHandlerService } from 'app/services/error-handler.service';
Expand All @@ -18,10 +17,7 @@ export interface DockerConfigState {
pool: string;
nvidiaDriversInstalled: boolean;
lacksNvidiaDrivers: boolean;
statusData: {
status: string;
description: string;
};
statusData: DockerStatusData;
}

const initialState: DockerConfigState = {
Expand All @@ -41,13 +37,7 @@ export class DockerStore extends ComponentStore<DockerConfigState> {
readonly selectedPool$ = this.select((state) => state.pool);
readonly nvidiaDriversInstalled$ = this.select((state) => state.nvidiaDriversInstalled);
readonly lacksNvidiaDrivers$ = this.select((state) => state.lacksNvidiaDrivers);
readonly isDockerStarted$ = this.select((state) => {
return [
DockerStatus.Initializing,
DockerStatus.Pending,
DockerStatus.Running,
].includes(state.statusData.status as DockerStatus);
});
readonly isDockerStarted$ = this.select((state) => DockerStatus.Running === state.statusData.status);
readonly status$ = this.select((state) => state.statusData.status);
readonly statusDescription$ = this.select((state) => state.statusData.description);

Expand All @@ -73,15 +63,12 @@ export class DockerStore extends ComponentStore<DockerConfigState> {
this.getLacksNvidiaDrivers(),
])),
tap(
([dockerConfig, dockerStatus, lacksNvidiaDrivers]: [DockerConfig, DockerStatusResponse, boolean]) => {
([dockerConfig, statusData, lacksNvidiaDrivers]: [DockerConfig, DockerStatusData, boolean]) => {
this.patchState({
pool: dockerConfig.pool,
nvidiaDriversInstalled: dockerConfig.nvidia,
lacksNvidiaDrivers,
statusData: {
status: dockerStatus.status,
description: dockerStatus.description,
},
statusData,
isLoading: false,
});
},
Expand All @@ -97,7 +84,7 @@ export class DockerStore extends ComponentStore<DockerConfigState> {
return this.ws.call('docker.lacks_nvidia_drivers');
}

private getDockerStatus(): Observable<DockerStatusResponse> {
private getDockerStatus(): Observable<DockerStatusData> {
return this.ws.call('docker.status');
}

Expand Down Expand Up @@ -146,15 +133,11 @@ export class DockerStore extends ComponentStore<DockerConfigState> {
* @returns An observable that should be subscribed to at component level. This event subscription should only
* stay alive until the component subscription stays alive i.e., until the component is destroyed
*/
dockerStatusEventUpdates(): Observable<ApiEvent<DockerStatusResponse>> {
dockerStatusEventUpdates(): Observable<DockerStatusData> {
return this.ws.subscribe('docker.state').pipe(
tap((dockerState) => {
this.patchState({
statusData: {
status: dockerState.fields.status,
description: dockerState.fields.description,
},
});
map((event) => event.fields),
tap((statusData) => {
this.patchState({ statusData });
}),
);
}
Expand Down
Loading

0 comments on commit 4598a0c

Please sign in to comment.