This is an automated email from the ASF dual-hosted git repository. panyuepeng pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/flink.git
commit 5ba9166f729d88cacdcdeca638094ce9dea8e655 Author: och5351 <[email protected]> AuthorDate: Mon Apr 13 22:40:14 2026 +0900 [FLINK-38898][runtime-web] Introduce the Rescales/Overview sub-page for streaming jobs with the adaptive scheduler enabled Co-authored-by: Yuepeng Pan <[email protected]> Co-authored-by: Matthias Pohl <[email protected]> --- .../src/app/interfaces/job-rescales.ts | 18 + .../pages/job/rescales/job-rescales.component.html | 402 +++++++++++++++++++++ .../pages/job/rescales/job-rescales.component.less | 78 +++- .../pages/job/rescales/job-rescales.component.ts | 49 ++- .../web-dashboard/src/app/services/job.service.ts | 5 + 5 files changed, 537 insertions(+), 15 deletions(-) diff --git a/flink-runtime-web/web-dashboard/src/app/interfaces/job-rescales.ts b/flink-runtime-web/web-dashboard/src/app/interfaces/job-rescales.ts index 85e90c98428..0cf7f521c36 100644 --- a/flink-runtime-web/web-dashboard/src/app/interfaces/job-rescales.ts +++ b/flink-runtime-web/web-dashboard/src/app/interfaces/job-rescales.ts @@ -16,6 +16,24 @@ * limitations under the License. */ +export interface RescalesOverview { + rescalesCounts: RescalesCounts; + latest: LatestRescales; +} + +export interface RescalesCounts { + ignored: number; + inProgress: number; + completed: number; + failed: number; +} + +export interface LatestRescales { + completed: BriefJobRescaleDetails | null; + failed: BriefJobRescaleDetails | null; + ignored: BriefJobRescaleDetails | null; +} + export type RescalesHistory = BriefJobRescaleDetails[]; export interface BriefJobRescaleDetails { diff --git a/flink-runtime-web/web-dashboard/src/app/pages/job/rescales/job-rescales.component.html b/flink-runtime-web/web-dashboard/src/app/pages/job/rescales/job-rescales.component.html index 08eb8f69a69..1b4342fac02 100644 --- a/flink-runtime-web/web-dashboard/src/app/pages/job/rescales/job-rescales.component.html +++ b/flink-runtime-web/web-dashboard/src/app/pages/job/rescales/job-rescales.component.html @@ -23,6 +23,408 @@ [nzTabBarExtraContent]="extraTemplate" (nzSelectedIndexChange)="refresh()" > + <nz-tab nzTitle="Overview"> + <!-- Reusable template for rescale details --> + <ng-template #rescaleDetailsTemplate let-rescale="rescale"> + <div class="overview-rescale-section"> + <h3><strong>Rescale Details</strong></h3> + <div> + <span> + <strong>Rescale UUID:</strong> + <span nz-tooltip [nzTooltipTitle]="rescale.rescaleUuid"> + {{ truncateUuid(rescale.rescaleUuid) }} + </span> + </span> + <nz-divider nzType="vertical"></nz-divider> + <span> + <strong>Requirements ID:</strong> + <span nz-tooltip [nzTooltipTitle]="rescale.resourceRequirementsUuid"> + {{ truncateUuid(rescale.resourceRequirementsUuid) }} + </span> + </span> + <nz-divider nzType="vertical"></nz-divider> + <span> + <strong>Attempt ID:</strong> + {{ rescale.rescaleAttemptId }} + </span> + <nz-divider nzType="vertical"></nz-divider> + <span> + <strong>Trigger Cause:</strong> + {{ rescale.triggerCause }} + </span> + <nz-divider nzType="vertical"></nz-divider> + <span> + <strong>Terminal State:</strong> + {{ rescale?.terminalState || '-' }} + </span> + <nz-divider nzType="vertical"></nz-divider> + <span> + <strong>Terminal Reason:</strong> + {{ rescale?.terminatedReason || '-' }} + </span> + </div> + <ng-container *ngIf="getDetail(rescale.rescaleUuid) as detail"> + <nz-table + class="small" + [nzData]="detail.vertices | keyvalue" + [nzSize]="'small'" + [nzFrontPagination]="false" + [nzShowPagination]="false" + [nzTitle]="verticesTitle" + [nzBordered]="true" + > + <thead> + <tr> + <th + nz-tooltip + nzTooltipTitle="The unique ID of target JobVertex consists of 32 hexadecimal characters" + > + <strong>ID</strong> + </th> + <th nz-tooltip nzTooltipTitle="The short name of target vertex"> + <strong>Name</strong> + </th> + <th + nz-tooltip + nzTooltipTitle="The previous parallelism of target vertex before the current rescale" + > + <strong>Previous Parallelism</strong> + </th> + <th + nz-tooltip + nzTooltipTitle="The acquired parallelism of target vertex after the current rescale" + > + <strong>Acquired Parallelism</strong> + </th> + <th nz-tooltip nzTooltipTitle="The desired parallelism of the target vertex"> + <strong>Desired Parallelism</strong> + </th> + <th nz-tooltip nzTooltipTitle="The minimal parallelism of target vertex to run"> + <strong>Sufficient Parallelism</strong> + </th> + <th + nz-tooltip + nzTooltipTitle="The unique ID of the slot sharing group consists of 32 hexadecimal characters" + > + <strong>Slot Sharing Group ID</strong> + </th> + </tr> + </thead> + <tbody> + <tr *ngFor="let vertex of detail.vertices | keyvalue"> + <td nz-tooltip [nzTooltipTitle]="vertex.value.jobVertexId"> + {{ truncateUuid(vertex.value.jobVertexId) }} + </td> + <td nz-tooltip [nzTooltipTitle]="vertex.value.jobVertexName"> + {{ truncateName(vertex.value.jobVertexName) }} + </td> + <td>{{ vertex.value.preRescaleParallelism }}</td> + <td>{{ vertex.value.postRescaleParallelism || '-' }}</td> + <td>{{ vertex.value.desiredParallelism }}</td> + <td>{{ vertex.value.sufficientParallelism }}</td> + <td nz-tooltip [nzTooltipTitle]="vertex.value.slotSharingGroupId"> + {{ truncateUuid(vertex.value.slotSharingGroupId) }} + </td> + </tr> + </tbody> + </nz-table> + + <nz-table + class="small" + [nzData]="detail.slots | keyvalue" + [nzSize]="'small'" + [nzFrontPagination]="false" + [nzShowPagination]="false" + [nzTitle]="slotsTitle" + [nzBordered]="true" + > + <thead> + <tr> + <th + nz-tooltip + nzTooltipTitle="The ID of the slot sharing group to which the slot belongs consists of 32 hexadecimal characters" + > + <strong>Slot Sharing Group ID</strong> + </th> + <th + nz-tooltip + nzTooltipTitle="The name of the slot sharing group to which the slot belongs" + > + <strong>Slot Sharing Group Name</strong> + </th> + <th nz-tooltip nzTooltipTitle="The previous number of slots before the rescale"> + <strong>Previous Slots</strong> + </th> + <th nz-tooltip nzTooltipTitle="The acquired number of slots after the rescale"> + <strong>Acquired Slots</strong> + </th> + <th nz-tooltip nzTooltipTitle="The desired number of slots of the rescale"> + <strong>Desired Slots</strong> + </th> + <th + nz-tooltip + nzTooltipTitle="The minimal number of slots to deploy tasks in the rescale" + > + <strong>Sufficient Slots</strong> + </th> + <th + nz-tooltip + nzTooltipTitle="The required resource profile of the slot sharing group in the rescale" + > + <strong>Required Profile</strong> + </th> + <th + nz-tooltip + nzTooltipTitle="The acquired resource profile of the slot sharing group in the rescale" + > + <strong>Acquired Profile</strong> + </th> + </tr> + </thead> + <tbody> + <tr *ngFor="let slot of detail.slots | keyvalue"> + <td nz-tooltip [nzTooltipTitle]="slot.value.slotSharingGroupId"> + {{ truncateUuid(slot.value.slotSharingGroupId) }} + </td> + <td>{{ slot.value.slotSharingGroupName }}</td> + <td>{{ slot.value.preRescaleSlots }}</td> + <td>{{ slot.value.postRescaleSlots || '-' }}</td> + <td>{{ slot.value.desiredSlots }}</td> + <td>{{ slot.value.minimalRequiredSlots }}</td> + <td> + <pre style="margin: 0">{{ slot.value.requestResourceProfile | json }}</pre> + </td> + <td> + <pre style="margin: 0">{{ slot.value.acquiredResourceProfile | json }}</pre> + </td> + </tr> + </tbody> + </nz-table> + + <nz-table + class="small" + [nzData]="detail.schedulerStates" + [nzSize]="'small'" + [nzFrontPagination]="false" + [nzShowPagination]="false" + [nzTitle]="schedulerStatesTitle" + [nzBordered]="true" + > + <thead> + <tr> + <th nz-tooltip nzTooltipTitle="The scheduler state name"> + <strong>State</strong> + </th> + <th nz-tooltip nzTooltipTitle="The time to enter the state"> + <strong>Enter Time</strong> + </th> + <th nz-tooltip nzTooltipTitle="The time to leave the state"> + <strong>Leave Time</strong> + </th> + <th + nz-tooltip + nzTooltipTitle="The duration time from enter time to leave time of the state" + > + <strong>Duration</strong> + </th> + <th + nz-tooltip + nzTooltipTitle="The exception information about current rescale during the state" + > + <strong>Exception</strong> + </th> + </tr> + </thead> + <tbody> + <tr *ngFor="let state of detail.schedulerStates"> + <td>{{ state.state }}</td> + <td>{{ state.enterTimestampInMillis | date: 'yyyy-MM-dd HH:mm:ss.SSS' }}</td> + <td>{{ state.leaveTimestampInMillis | date: 'yyyy-MM-dd HH:mm:ss.SSS' }}</td> + <td>{{ state.durationInMillis | humanizeDuration }}</td> + <td>{{ state.stringifiedException }}</td> + </tr> + </tbody> + </nz-table> + </ng-container> + + <div *ngIf="!getDetail(rescale.rescaleUuid)">Loading...</div> + </div> + </ng-template> + + <ng-container *ngIf="rescalesOverview"> + <div class="rescale-header rescale-counts-header"> + <h3><strong>Rescale Counts</strong></h3> + <span class="rescale-time-info"> + <span> + <strong>Total:</strong> + {{ getTotalRescaleCount() }} + </span> + <nz-divider nzType="vertical"></nz-divider> + <span> + <strong>In Progress:</strong> + {{ rescalesOverview.rescalesCounts.inProgress }} + </span> + <nz-divider nzType="vertical"></nz-divider> + <span> + <strong>Completed:</strong> + {{ rescalesOverview.rescalesCounts.completed }} + </span> + <nz-divider nzType="vertical"></nz-divider> + <span> + <strong>Failed:</strong> + {{ rescalesOverview.rescalesCounts.failed }} + </span> + <nz-divider nzType="vertical"></nz-divider> + <span> + <strong>Ignored:</strong> + {{ rescalesOverview.rescalesCounts.ignored }} + </span> + </span> + </div> + <nz-divider></nz-divider> + + <ng-container *ngIf="rescalesOverview.latest.completed"> + <div class="rescale-header"> + <h3><strong>Latest Completed Rescale</strong></h3> + <span class="rescale-time-info"> + <span> + <strong>Start Time:</strong> + {{ + rescalesOverview.latest.completed.startTimestampInMillis + | date: 'yyyy-MM-dd HH:mm:ss.SSS' + }} + </span> + <nz-divider nzType="vertical"></nz-divider> + <span> + <strong>End Time:</strong> + {{ + rescalesOverview.latest.completed.endTimestampInMillis + ? (rescalesOverview.latest.completed.endTimestampInMillis + | date: 'yyyy-MM-dd HH:mm:ss.SSS') + : '-' + }} + </span> + <nz-divider nzType="vertical"></nz-divider> + <span> + <strong>Duration:</strong> + {{ + (rescalesOverview.latest.completed.endTimestampInMillis || Date.now()) - + rescalesOverview.latest.completed.startTimestampInMillis | humanizeDuration + }} + </span> + </span> + </div> + <nz-divider></nz-divider> + <ng-container + *ngTemplateOutlet=" + rescaleDetailsTemplate; + context: { rescale: rescalesOverview.latest.completed } + " + ></ng-container> + </ng-container> + <ng-container *ngIf="!rescalesOverview.latest.completed"> + <div class="rescale-header"> + <h3><strong>Latest Completed Rescale</strong></h3> + <span class="rescale-time-info">None</span> + </div> + <nz-divider></nz-divider> + </ng-container> + + <ng-container *ngIf="rescalesOverview.latest.ignored"> + <div class="rescale-header"> + <h3><strong>Latest Ignored Rescale</strong></h3> + <span class="rescale-time-info"> + <span> + <strong>Start Time:</strong> + {{ + rescalesOverview.latest.ignored.startTimestampInMillis + | date: 'yyyy-MM-dd HH:mm:ss.SSS' + }} + </span> + <nz-divider nzType="vertical"></nz-divider> + <span> + <strong>End Time:</strong> + {{ + rescalesOverview.latest.ignored.endTimestampInMillis + ? (rescalesOverview.latest.ignored.endTimestampInMillis + | date: 'yyyy-MM-dd HH:mm:ss.SSS') + : '-' + }} + </span> + <nz-divider nzType="vertical"></nz-divider> + <span> + <strong>Duration:</strong> + {{ + (rescalesOverview.latest.ignored.endTimestampInMillis || Date.now()) - + rescalesOverview.latest.ignored.startTimestampInMillis | humanizeDuration + }} + </span> + </span> + </div> + <nz-divider></nz-divider> + <ng-container + *ngTemplateOutlet=" + rescaleDetailsTemplate; + context: { rescale: rescalesOverview.latest.ignored } + " + ></ng-container> + </ng-container> + <ng-container *ngIf="!rescalesOverview.latest.ignored"> + <div class="rescale-header"> + <h3><strong>Latest Ignored Rescale</strong></h3> + <span class="rescale-time-info">None</span> + </div> + <nz-divider></nz-divider> + </ng-container> + + <ng-container *ngIf="rescalesOverview.latest.failed"> + <div class="rescale-header"> + <h3><strong>Latest Failed Rescale</strong></h3> + <span class="rescale-time-info"> + <span> + <strong>Start Time:</strong> + {{ + rescalesOverview.latest.failed.startTimestampInMillis + | date: 'yyyy-MM-dd HH:mm:ss.SSS' + }} + </span> + <nz-divider nzType="vertical"></nz-divider> + <span> + <strong>End Time:</strong> + {{ + rescalesOverview.latest.failed.endTimestampInMillis + ? (rescalesOverview.latest.failed.endTimestampInMillis + | date: 'yyyy-MM-dd HH:mm:ss.SSS') + : '-' + }} + </span> + <nz-divider nzType="vertical"></nz-divider> + <span> + <strong>Duration:</strong> + {{ + (rescalesOverview.latest.failed.endTimestampInMillis || Date.now()) - + rescalesOverview.latest.failed.startTimestampInMillis | humanizeDuration + }} + </span> + </span> + </div> + <nz-divider></nz-divider> + <ng-container + *ngTemplateOutlet=" + rescaleDetailsTemplate; + context: { rescale: rescalesOverview.latest.failed } + " + ></ng-container> + </ng-container> + <ng-container *ngIf="!rescalesOverview.latest.failed"> + <div class="rescale-header"> + <h3><strong>Latest Failed Rescale</strong></h3> + <span class="rescale-time-info">None</span> + </div> + <nz-divider></nz-divider> + </ng-container> + </ng-container> + </nz-tab> <nz-tab nzTitle="History"> <nz-table class="no-border small" diff --git a/flink-runtime-web/web-dashboard/src/app/pages/job/rescales/job-rescales.component.less b/flink-runtime-web/web-dashboard/src/app/pages/job/rescales/job-rescales.component.less index bbca1186be4..a5ad73dde77 100644 --- a/flink-runtime-web/web-dashboard/src/app/pages/job/rescales/job-rescales.component.less +++ b/flink-runtime-web/web-dashboard/src/app/pages/job/rescales/job-rescales.component.less @@ -22,6 +22,37 @@ position: relative; top: -16px; padding: 24px; + + > h3 { + margin-top: 8px; + margin-bottom: 8px; + } + + > nz-divider { + margin: 8px 0; + } + + .rescale-header { + display: flex; + align-items: center; + margin-top: 8px; + margin-bottom: 8px; + + h3 { + flex-shrink: 0; + width: 280px; + margin: 0; + } + + .rescale-time-info { + color: rgba(0, 0, 0, 0.85); + font-size: 14px; + } + + &.rescale-counts-header { + margin-top: -2px; + } + } } .ant-tabs-nav-list { @@ -35,6 +66,24 @@ } } } + + .nz-disable-td { + width: 100% !important; + padding: 0 !important; + } + + .collapse-td { + width: 100% !important; + padding: 0 !important; + } + + tr.ant-table-expanded-row { + width: 100%; + + > td { + width: 100% !important; + } + } } } @@ -46,22 +95,22 @@ nz-empty { padding: 24px; } -::ng-deep { - .nz-disable-td { - width: 100% !important; - padding: 0 !important; - } +.overview-rescale-section { + margin: 16px 0; + padding: 16px; + border: 1px solid #f0f0f0; + background-color: #fff; - .collapse-td { - width: 100% !important; - padding: 0 !important; + h3 { + margin-top: 0; + margin-bottom: 8px; } - tr.ant-table-expanded-row { - width: 100%; + nz-table { + margin-bottom: 16px; - > td { - width: 100% !important; + &:last-child { + margin-bottom: 0; } } } @@ -79,6 +128,11 @@ nz-empty { border: 1px solid #f0f0f0; background-color: #fff; + h3 { + margin-top: 0; + margin-bottom: 8px; + } + ::ng-deep { nz-table.ant-table-wrapper { display: block !important; diff --git a/flink-runtime-web/web-dashboard/src/app/pages/job/rescales/job-rescales.component.ts b/flink-runtime-web/web-dashboard/src/app/pages/job/rescales/job-rescales.component.ts index 6f1a1ecc1c5..e25d8747d44 100644 --- a/flink-runtime-web/web-dashboard/src/app/pages/job/rescales/job-rescales.component.ts +++ b/flink-runtime-web/web-dashboard/src/app/pages/job/rescales/job-rescales.component.ts @@ -16,7 +16,7 @@ * limitations under the License. */ -import { NgFor, NgIf, JsonPipe, DatePipe, KeyValuePipe } from '@angular/common'; +import { NgFor, NgIf, JsonPipe, DatePipe, KeyValuePipe, NgTemplateOutlet } from '@angular/common'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { forkJoin, of, Subject } from 'rxjs'; import { catchError, distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators'; @@ -28,7 +28,8 @@ import { JobRescaleDetails, JobRescaleConfigInfo, JobDetail, - RescalesHistory + RescalesHistory, + RescalesOverview } from '@flink-runtime-web/interfaces'; import { JobService } from '@flink-runtime-web/services'; import { NzButtonModule } from 'ng-zorro-antd/button'; @@ -53,6 +54,7 @@ import { JobLocalService } from '../job-local.service'; JsonPipe, DatePipe, KeyValuePipe, + NgTemplateOutlet, NzTabsModule, NzDividerModule, JobBadgeComponent, @@ -66,6 +68,7 @@ import { JobLocalService } from '../job-local.service'; ] }) export class JobRescalesComponent implements OnInit, OnDestroy { + public rescalesOverview?: RescalesOverview; public rescalesHistory?: RescalesHistory; public rescaleDetailsMap = new Map<string, JobRescaleDetails>(); public rescalesConfig?: JobRescaleConfigInfo; @@ -87,6 +90,11 @@ export class JobRescalesComponent implements OnInit, OnDestroy { .pipe( switchMap(() => forkJoin([ + this.jobService.loadRescalesOverview(this.jobDetail.jid).pipe( + catchError(() => { + return of(undefined); + }) + ), this.jobService.loadRescalesHistory(this.jobDetail.jid).pipe( catchError(() => { return of(undefined); @@ -101,9 +109,24 @@ export class JobRescalesComponent implements OnInit, OnDestroy { ), takeUntil(this.destroy$) ) - .subscribe(([history, config]) => { + .subscribe(([overview, history, config]) => { + this.rescalesOverview = overview; this.rescalesHistory = history; this.rescalesConfig = config; + + // Load details for latest rescales in overview + if (overview?.latest) { + if (overview.latest.completed?.rescaleUuid) { + this.loadRescaleDetailIfNeeded(overview.latest.completed.rescaleUuid); + } + if (overview.latest.failed?.rescaleUuid) { + this.loadRescaleDetailIfNeeded(overview.latest.failed.rescaleUuid); + } + if (overview.latest.ignored?.rescaleUuid) { + this.loadRescaleDetailIfNeeded(overview.latest.ignored.rescaleUuid); + } + } + this.cdr.markForCheck(); }); @@ -178,4 +201,24 @@ export class JobRescalesComponent implements OnInit, OnDestroy { public truncateName(name: string, maxLength: number = 32): string { return name && name.length > maxLength ? `${name.substring(0, maxLength)}...` : name; } + + public getTotalRescaleCount(): number { + if (!this.rescalesOverview?.rescalesCounts) { + return 0; + } + const counts = this.rescalesOverview.rescalesCounts; + return counts.inProgress + counts.completed + counts.failed + counts.ignored; + } + + private loadRescaleDetailIfNeeded(rescaleUuid: string): void { + if (!this.rescaleDetailsMap.has(rescaleUuid)) { + this.jobService + .loadRescaleDetail(this.jobDetail.jid, rescaleUuid) + .pipe(takeUntil(this.destroy$)) + .subscribe(detail => { + this.rescaleDetailsMap.set(rescaleUuid, detail); + this.cdr.markForCheck(); + }); + } + } } diff --git a/flink-runtime-web/web-dashboard/src/app/services/job.service.ts b/flink-runtime-web/web-dashboard/src/app/services/job.service.ts index aff1e903b6d..a5bb8252e0b 100644 --- a/flink-runtime-web/web-dashboard/src/app/services/job.service.ts +++ b/flink-runtime-web/web-dashboard/src/app/services/job.service.ts @@ -26,6 +26,7 @@ import { CheckpointConfig, CheckpointDetail, CheckpointSubTask, + RescalesOverview, RescalesHistory, JobRescaleDetails, JobRescaleConfigInfo, @@ -181,6 +182,10 @@ export class JobService { ); } + public loadRescalesOverview(jobId: string): Observable<RescalesOverview> { + return this.httpClient.get<RescalesOverview>(`${this.configService.BASE_URL}/jobs/${jobId}/rescales/overview`); + } + public loadRescalesHistory(jobId: string): Observable<RescalesHistory> { return this.httpClient.get<RescalesHistory>(`${this.configService.BASE_URL}/jobs/${jobId}/rescales/history`); }
