Fix race conditions with BDC dashboard init (#8135)

* Fix race conditions with initialized

* Check for undefined status

* Disable no-floating-promises check

* Add catch instead of disabling tslint disable
This commit is contained in:
Charles Gagnon
2019-11-02 14:58:56 -07:00
committed by GitHub
parent 6864d39f85
commit a8eed6114b
5 changed files with 109 additions and 79 deletions

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
'use strict';
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
@@ -15,6 +13,7 @@ import { BdcDashboardOverviewPage } from './bdcDashboardOverviewPage';
import { BdcStatusModel, ServiceStatusModel } from '../controller/apiGenerated'; import { BdcStatusModel, ServiceStatusModel } from '../controller/apiGenerated';
import { getHealthStatusDot, getServiceNameDisplayText, showErrorMessage } from '../utils'; import { getHealthStatusDot, getServiceNameDisplayText, showErrorMessage } from '../utils';
import { HdfsDialogCancelledError } from './hdfsDialogBase'; import { HdfsDialogCancelledError } from './hdfsDialogBase';
import { BdcDashboardPage } from './bdcDashboardPage';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
@@ -25,12 +24,10 @@ const unselectedTabCss = { 'font-weight': '' };
type NavTab = { serviceName: string, div: azdata.DivContainer, dot: azdata.TextComponent, text: azdata.TextComponent }; type NavTab = { serviceName: string, div: azdata.DivContainer, dot: azdata.TextComponent, text: azdata.TextComponent };
export class BdcDashboard { export class BdcDashboard extends BdcDashboardPage {
private dashboard: azdata.workspace.ModelViewEditor; private dashboard: azdata.workspace.ModelViewEditor;
private initialized: boolean = false;
private modelView: azdata.ModelView; private modelView: azdata.ModelView;
private mainAreaContainer: azdata.FlexContainer; private mainAreaContainer: azdata.FlexContainer;
private navContainer: azdata.FlexContainer; private navContainer: azdata.FlexContainer;
@@ -44,8 +41,9 @@ export class BdcDashboard {
private serviceTabPageMapping = new Map<string, { navTab: NavTab, servicePage: azdata.FlexContainer }>(); private serviceTabPageMapping = new Map<string, { navTab: NavTab, servicePage: azdata.FlexContainer }>();
constructor(private title: string, private model: BdcDashboardModel) { constructor(private title: string, private model: BdcDashboardModel) {
this.model.onDidUpdateBdcStatus(bdcStatus => this.handleBdcStatusUpdate(bdcStatus)); super();
this.model.onBdcError(errorEvent => this.handleError(errorEvent)); this.model.onDidUpdateBdcStatus(bdcStatus => this.eventuallyRunOnInitialized(() => this.handleBdcStatusUpdate(bdcStatus)));
this.model.onBdcError(errorEvent => this.eventuallyRunOnInitialized(() => this.handleError(errorEvent)));
} }
public showDashboard(): void { public showDashboard(): void {
@@ -162,11 +160,10 @@ export class BdcDashboard {
}); });
} }
private handleBdcStatusUpdate(bdcStatus: BdcStatusModel): void { private handleBdcStatusUpdate(bdcStatus?: BdcStatusModel): void {
if (!this.initialized || !bdcStatus) { if (!bdcStatus) {
return; return;
} }
this.updateServiceNavTabs(bdcStatus.services); this.updateServiceNavTabs(bdcStatus.services);
} }
@@ -213,7 +210,7 @@ export class BdcDashboard {
* Helper to update the navigation tabs for the services when we get a status update * Helper to update the navigation tabs for the services when we get a status update
*/ */
private updateServiceNavTabs(services?: ServiceStatusModel[]): void { private updateServiceNavTabs(services?: ServiceStatusModel[]): void {
if (this.initialized && services) { if (services) {
// Add a nav item for each service // Add a nav item for each service
services.forEach(s => { services.forEach(s => {
const existingTabPage = this.serviceTabPageMapping[s.serviceName]; const existingTabPage = this.serviceTabPageMapping[s.serviceName];

View File

@@ -13,6 +13,7 @@ import { EndpointModel, ServiceStatusModel, BdcStatusModel } from '../controller
import { BdcDashboard } from './bdcDashboard'; import { BdcDashboard } from './bdcDashboard';
import { createViewDetailsButton } from './commonControls'; import { createViewDetailsButton } from './commonControls';
import { HdfsDialogCancelledError } from './hdfsDialogBase'; import { HdfsDialogCancelledError } from './hdfsDialogBase';
import { BdcDashboardPage } from './bdcDashboardPage';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
@@ -30,9 +31,8 @@ const serviceEndpointRowEndpointCellWidth = 350;
const hyperlinkedEndpoints = [Endpoint.metricsui, Endpoint.logsui, Endpoint.sparkHistory, Endpoint.yarnUi]; const hyperlinkedEndpoints = [Endpoint.metricsui, Endpoint.logsui, Endpoint.sparkHistory, Endpoint.yarnUi];
export class BdcDashboardOverviewPage { export class BdcDashboardOverviewPage extends BdcDashboardPage {
private initialized: boolean = false;
private modelBuilder: azdata.ModelBuilder; private modelBuilder: azdata.ModelBuilder;
private lastUpdatedLabel: azdata.TextComponent; private lastUpdatedLabel: azdata.TextComponent;
@@ -50,9 +50,10 @@ export class BdcDashboardOverviewPage {
private serviceStatusErrorMessage: azdata.TextComponent; private serviceStatusErrorMessage: azdata.TextComponent;
constructor(private dashboard: BdcDashboard, private model: BdcDashboardModel) { constructor(private dashboard: BdcDashboard, private model: BdcDashboardModel) {
this.model.onDidUpdateEndpoints(endpoints => this.handleEndpointsUpdate(endpoints)); super();
this.model.onDidUpdateBdcStatus(bdcStatus => this.handleBdcStatusUpdate(bdcStatus)); this.model.onDidUpdateEndpoints(endpoints => this.eventuallyRunOnInitialized(() => this.handleEndpointsUpdate(endpoints)));
this.model.onBdcError(error => this.handleBdcError(error)); this.model.onDidUpdateBdcStatus(bdcStatus => this.eventuallyRunOnInitialized(() => this.handleBdcStatusUpdate(bdcStatus)));
this.model.onBdcError(error => this.eventuallyRunOnInitialized(() => this.handleBdcError(error)));
} }
public create(view: azdata.ModelView): azdata.FlexContainer { public create(view: azdata.ModelView): azdata.FlexContainer {
@@ -209,14 +210,12 @@ export class BdcDashboardOverviewPage {
this.serviceStatusDisplayContainer.display = undefined; this.serviceStatusDisplayContainer.display = undefined;
this.propertiesContainer.display = undefined; this.propertiesContainer.display = undefined;
this.endpointsDisplayContainer.display = undefined; this.endpointsDisplayContainer.display = undefined;
} }
private handleBdcStatusUpdate(bdcStatus: BdcStatusModel): void {
if (!this.initialized || !bdcStatus) { private handleBdcStatusUpdate(bdcStatus?: BdcStatusModel): void {
if (!bdcStatus) {
return; return;
} }
this.lastUpdatedLabel.value = this.lastUpdatedLabel.value =
localize('bdc.dashboard.lastUpdated', "Last Updated : {0}", localize('bdc.dashboard.lastUpdated', "Last Updated : {0}",
this.model.bdcStatusLastUpdated ? this.model.bdcStatusLastUpdated ?
@@ -237,10 +236,6 @@ export class BdcDashboardOverviewPage {
} }
private handleEndpointsUpdate(endpoints: EndpointModel[]): void { private handleEndpointsUpdate(endpoints: EndpointModel[]): void {
if (!this.initialized || !endpoints) {
return;
}
this.endpointsRowContainer.clearItems(); this.endpointsRowContainer.clearItems();
// Sort the endpoints. The sort method is that SQL Server Master is first - followed by all // Sort the endpoints. The sort method is that SQL Server Master is first - followed by all

View File

@@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Deferred } from '../../common/promise';
export abstract class BdcDashboardPage {
private _initialized: boolean = false;
private onInitializedPromise: Deferred<void> = new Deferred();
constructor() { }
protected get initialized(): boolean {
return this._initialized;
}
protected set initialized(value: boolean) {
if (!this._initialized && value) {
this._initialized = true;
this.onInitializedPromise.resolve();
}
}
/**
* Runs the specified action when the component is initialized. If already initialized just runs
* the action immediately.
* @param action The action to be ran when the page is initialized
*/
protected eventuallyRunOnInitialized(action: () => void): void {
if (!this._initialized) {
this.onInitializedPromise.promise.then(() => action()).catch(error => console.error(`Unexpected error running onInitialized action for BDC Page : ${error}`));
} else {
action();
}
}
}

View File

@@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved. * Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
'use strict';
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
@@ -12,6 +11,7 @@ import { getHealthStatusDisplayText, getHealthStatusIcon, getStateDisplayText, S
import { cssStyles } from '../constants'; import { cssStyles } from '../constants';
import { isNullOrUndefined } from 'util'; import { isNullOrUndefined } from 'util';
import { createViewDetailsButton } from './commonControls'; import { createViewDetailsButton } from './commonControls';
import { BdcDashboardPage } from './bdcDashboardPage';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
@@ -39,17 +39,16 @@ const metricsAndLogsLogsColumnWidth = 75;
const viewText = localize('bdc.dashboard.viewHyperlink', "View"); const viewText = localize('bdc.dashboard.viewHyperlink', "View");
const notAvailableText = localize('bdc.dashboard.notAvailable', "N/A"); const notAvailableText = localize('bdc.dashboard.notAvailable', "N/A");
export class BdcDashboardResourceStatusPage { export class BdcDashboardResourceStatusPage extends BdcDashboardPage {
private rootContainer: azdata.FlexContainer; private rootContainer: azdata.FlexContainer;
private instanceHealthStatusRowsContainer: azdata.FlexContainer; private instanceHealthStatusRowsContainer: azdata.FlexContainer;
private metricsAndLogsRowsContainer: azdata.FlexContainer; private metricsAndLogsRowsContainer: azdata.FlexContainer;
private lastUpdatedLabel: azdata.TextComponent; private lastUpdatedLabel: azdata.TextComponent;
private sqlMetricsComponents: azdata.Component[];
private initialized: boolean = false;
constructor(private model: BdcDashboardModel, private modelView: azdata.ModelView, private serviceName: string, private resourceName: string) { constructor(private model: BdcDashboardModel, private modelView: azdata.ModelView, private serviceName: string, private resourceName: string) {
this.model.onDidUpdateBdcStatus(bdcStatus => this.handleBdcStatusUpdate(bdcStatus)); super();
this.model.onDidUpdateBdcStatus(bdcStatus => this.eventuallyRunOnInitialized(() => this.handleBdcStatusUpdate(bdcStatus)));
this.rootContainer = this.createContainer(modelView); this.rootContainer = this.createContainer(modelView);
} }
@@ -139,11 +138,14 @@ export class BdcDashboardResourceStatusPage {
return rootContainer; return rootContainer;
} }
private handleBdcStatusUpdate(bdcStatus: BdcStatusModel): void { private handleBdcStatusUpdate(bdcStatus?: BdcStatusModel): void {
if (!bdcStatus) {
return;
}
const service = bdcStatus.services ? bdcStatus.services.find(s => s.serviceName === this.serviceName) : undefined; const service = bdcStatus.services ? bdcStatus.services.find(s => s.serviceName === this.serviceName) : undefined;
const resource = service ? service.resources.find(r => r.resourceName === this.resourceName) : undefined; const resource = service ? service.resources.find(r => r.resourceName === this.resourceName) : undefined;
if (!this.initialized || !resource || isNullOrUndefined(resource.instances)) { if (!resource || isNullOrUndefined(resource.instances)) {
return; return;
} }

View File

@@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved. * Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
'use strict';
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import { BdcStatusModel, ResourceStatusModel } from '../controller/apiGenerated'; import { BdcStatusModel, ResourceStatusModel } from '../controller/apiGenerated';
@@ -10,12 +9,11 @@ import { BdcDashboardResourceStatusPage } from './bdcDashboardResourceStatusPage
import { BdcDashboardModel } from './bdcDashboardModel'; import { BdcDashboardModel } from './bdcDashboardModel';
import { getHealthStatusDot } from '../utils'; import { getHealthStatusDot } from '../utils';
import { cssStyles } from '../constants'; import { cssStyles } from '../constants';
import { BdcDashboardPage } from './bdcDashboardPage';
type ServiceTab = { div: azdata.DivContainer, dot: azdata.TextComponent, text: azdata.TextComponent }; type ServiceTab = { div: azdata.DivContainer, dot: azdata.TextComponent, text: azdata.TextComponent };
export class BdcServiceStatusPage { export class BdcServiceStatusPage extends BdcDashboardPage {
private initialized: boolean = false;
private currentTab: { tab: ServiceTab, index: number }; private currentTab: { tab: ServiceTab, index: number };
private currentTabPage: azdata.FlexContainer; private currentTabPage: azdata.FlexContainer;
@@ -25,7 +23,8 @@ export class BdcServiceStatusPage {
private createdTabs: Map<string, ServiceTab> = new Map<string, ServiceTab>(); private createdTabs: Map<string, ServiceTab> = new Map<string, ServiceTab>();
constructor(private serviceName: string, private model: BdcDashboardModel, private modelView: azdata.ModelView) { constructor(private serviceName: string, private model: BdcDashboardModel, private modelView: azdata.ModelView) {
this.model.onDidUpdateBdcStatus(bdcStatus => this.handleBdcStatusUpdate(bdcStatus)); super();
this.model.onDidUpdateBdcStatus(bdcStatus => this.eventuallyRunOnInitialized(() => this.handleBdcStatusUpdate(bdcStatus)));
this.createPage(); this.createPage();
} }
@@ -57,10 +56,9 @@ export class BdcServiceStatusPage {
} }
private handleBdcStatusUpdate(bdcStatus: BdcStatusModel): void { private handleBdcStatusUpdate(bdcStatus: BdcStatusModel): void {
if (!this.initialized || !bdcStatus) { if (!bdcStatus) {
return; return;
} }
const service = bdcStatus.services.find(s => s.serviceName === this.serviceName); const service = bdcStatus.services.find(s => s.serviceName === this.serviceName);
if (service && service.resources) { if (service && service.resources) {
this.createResourceNavTabs(service.resources); this.createResourceNavTabs(service.resources);
@@ -79,49 +77,47 @@ export class BdcServiceStatusPage {
* Helper to create the navigation tabs for the resources * Helper to create the navigation tabs for the resources
*/ */
private createResourceNavTabs(resources: ResourceStatusModel[]) { private createResourceNavTabs(resources: ResourceStatusModel[]) {
if (this.initialized) { let tabIndex = this.createdTabs.size;
let tabIndex = this.createdTabs.size; resources.forEach(resource => {
resources.forEach(resource => { const existingTab: ServiceTab = this.createdTabs[resource.resourceName];
const existingTab: ServiceTab = this.createdTabs[resource.resourceName]; if (existingTab) {
if (existingTab) { // We already created this tab so just update the status
// We already created this tab so just update the status existingTab.dot.value = getHealthStatusDot(resource.healthStatus);
existingTab.dot.value = getHealthStatusDot(resource.healthStatus); } else {
} else { // New tab - create and add to the end of the container
// New tab - create and add to the end of the container const currentIndex = tabIndex++;
const currentIndex = tabIndex++; const resourceHeaderTab = createResourceHeaderTab(this.modelView.modelBuilder, resource);
const resourceHeaderTab = createResourceHeaderTab(this.modelView.modelBuilder, resource); this.createdTabs[resource.resourceName] = resourceHeaderTab;
this.createdTabs[resource.resourceName] = resourceHeaderTab; const resourceStatusPage: azdata.FlexContainer = new BdcDashboardResourceStatusPage(this.model, this.modelView, this.serviceName, resource.resourceName).container;
const resourceStatusPage: azdata.FlexContainer = new BdcDashboardResourceStatusPage(this.model, this.modelView, this.serviceName, resource.resourceName).container; resourceHeaderTab.div.onDidClick(() => {
resourceHeaderTab.div.onDidClick(() => { // Don't need to do anything if this is already the currently selected tab
// Don't need to do anything if this is already the currently selected tab if (this.currentTab.index === currentIndex) {
if (this.currentTab.index === currentIndex) { return;
return; }
} if (this.currentTab) {
if (this.currentTab) { this.currentTab.tab.text.updateCssStyles(cssStyles.unselectedResourceHeaderTab);
this.currentTab.tab.text.updateCssStyles(cssStyles.unselectedResourceHeaderTab);
this.resourceHeader.removeItem(this.currentTab.tab.div);
this.resourceHeader.insertItem(this.currentTab.tab.div, this.currentTab.index, { flex: '0 0 auto', CSSStyles: cssStyles.unselectedTabDiv });
}
this.changeSelectedTabPage(resourceStatusPage);
this.currentTab = { tab: resourceHeaderTab, index: currentIndex };
this.currentTab.tab.text.updateCssStyles(cssStyles.selectedResourceHeaderTab);
this.resourceHeader.removeItem(this.currentTab.tab.div); this.resourceHeader.removeItem(this.currentTab.tab.div);
this.resourceHeader.insertItem(this.currentTab.tab.div, this.currentTab.index, { flex: '0 0 auto', CSSStyles: cssStyles.selectedTabDiv }); this.resourceHeader.insertItem(this.currentTab.tab.div, this.currentTab.index, { flex: '0 0 auto', CSSStyles: cssStyles.unselectedTabDiv });
});
// Set initial page
if (!this.currentTabPage) {
this.changeSelectedTabPage(resourceStatusPage);
this.currentTab = { tab: resourceHeaderTab, index: currentIndex };
this.currentTab.tab.text.updateCssStyles(cssStyles.selectedResourceHeaderTab);
this.resourceHeader.addItem(resourceHeaderTab.div, { flex: '0 0 auto', CSSStyles: cssStyles.selectedTabDiv });
}
else {
resourceHeaderTab.text.updateCssStyles(cssStyles.unselectedResourceHeaderTab);
this.resourceHeader.addItem(resourceHeaderTab.div, { flex: '0 0 auto', CSSStyles: cssStyles.unselectedTabDiv });
} }
this.changeSelectedTabPage(resourceStatusPage);
this.currentTab = { tab: resourceHeaderTab, index: currentIndex };
this.currentTab.tab.text.updateCssStyles(cssStyles.selectedResourceHeaderTab);
this.resourceHeader.removeItem(this.currentTab.tab.div);
this.resourceHeader.insertItem(this.currentTab.tab.div, this.currentTab.index, { flex: '0 0 auto', CSSStyles: cssStyles.selectedTabDiv });
});
// Set initial page
if (!this.currentTabPage) {
this.changeSelectedTabPage(resourceStatusPage);
this.currentTab = { tab: resourceHeaderTab, index: currentIndex };
this.currentTab.tab.text.updateCssStyles(cssStyles.selectedResourceHeaderTab);
this.resourceHeader.addItem(resourceHeaderTab.div, { flex: '0 0 auto', CSSStyles: cssStyles.selectedTabDiv });
} }
}); else {
} resourceHeaderTab.text.updateCssStyles(cssStyles.unselectedResourceHeaderTab);
this.resourceHeader.addItem(resourceHeaderTab.div, { flex: '0 0 auto', CSSStyles: cssStyles.unselectedTabDiv });
}
}
});
} }
} }