Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check Tensorboard Instance #3189

Merged
merged 17 commits into from
Mar 10, 2020
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions frontend/src/components/viewers/Tensorboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('Tensorboard', () => {
let tree: ReactWrapper | ShallowWrapper;
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();
});

afterEach(async () => {
Expand Down Expand Up @@ -102,9 +103,14 @@ describe('Tensorboard', () => {
const config = { type: PlotType.TENSORBOARD, url: 'http://test/url' };
const getAppMock = () => Promise.resolve({ podAddress: 'test/address', tfVersion: '1.14.0' });
jest.spyOn(Apis, 'getTensorboardApp').mockImplementation(getAppMock);
jest.spyOn(Apis, 'isTensorboardPodReady').mockImplementation(() => Promise.resolve(true));
tree = shallow(<TensorboardViewer configs={[config]} />);

await TestUtils.flushPromises();
jest.runOnlyPendingTimers();
Bobgy marked this conversation as resolved.
Show resolved Hide resolved
await TestUtils.flushPromises();
expect(setInterval).toHaveBeenCalledTimes(1);
expect(setInterval).toHaveBeenCalledWith(expect.any(Function), 5000);
Bobgy marked this conversation as resolved.
Show resolved Hide resolved
expect(tree).toMatchSnapshot();
Bobgy marked this conversation as resolved.
Show resolved Hide resolved
});

Expand Down Expand Up @@ -266,4 +272,48 @@ describe('Tensorboard', () => {
expect(tree.findWhere(el => el.text() === 'Open Tensorboard').exists()).toBeTruthy();
expect(tree.findWhere(el => el.text() === 'Delete Tensorboard').exists()).toBeTruthy();
});

it('asks user to wait when Tensorboard status is not ready', async () => {
Bobgy marked this conversation as resolved.
Show resolved Hide resolved
const getAppMock = jest.fn(() =>
Promise.resolve({ podAddress: 'podaddress', tfVersion: '1.14.0' }),
);
jest.spyOn(Apis, 'getTensorboardApp').mockImplementation(getAppMock);
jest.spyOn(Apis, 'isTensorboardPodReady').mockImplementation(() => Promise.resolve(false));
jest.spyOn(Apis, 'deleteTensorboardApp').mockImplementation(jest.fn(() => Promise.resolve('')));
const config = { type: PlotType.TENSORBOARD, url: 'http://test/url' };
tree = mount(<TensorboardViewer configs={[config]} />);

await TestUtils.flushPromises();
jest.runOnlyPendingTimers();
await TestUtils.flushPromises();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we wrap these into a helper?
like flushPromisesAndTimers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

tree.update();
expect(setInterval).toHaveBeenCalledTimes(1);
expect(setInterval).toHaveBeenCalledWith(expect.any(Function), 5000);
expect(
tree
.findWhere(
el =>
el.text() === 'Open Tensorboard' &&
el.prop('title') ===
`Tensorboard is starting, and you may need to wait for a few minutes.`,
)
.exists(),
).toBeTruthy();
expect(tree.findWhere(el => el.text() === 'Delete Tensorboard').exists()).toBeTruthy();

// After a while, it is ready and wait message is not shwon any more
jest.spyOn(Apis, 'isTensorboardPodReady').mockImplementation(() => Promise.resolve(true));
Bobgy marked this conversation as resolved.
Show resolved Hide resolved
jest.runOnlyPendingTimers();
await TestUtils.flushPromises();
tree.update();
expect(
tree
.findWhere(
el =>
el.prop('title') ===
`Tensorboard is starting, and you may need to wait for a few minutes.`,
)
.exists(),
).toEqual(false);
});
});
34 changes: 33 additions & 1 deletion frontend/src/components/viewers/Tensorboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,25 @@ export interface TensorboardViewerConfig extends ViewerConfig {

interface TensorboardViewerProps {
configs: TensorboardViewerConfig[];
// Interval in ms. If not specified, default to 5000.
intervalOfCheckingTensorboardPodStatus?: number;
}

interface TensorboardViewerState {
busy: boolean;
deleteDialogOpen: boolean;
podAddress: string;
tensorflowVersion: string;
// When podAddress is not null, we need to further tell whether the TensorBoard pod is accessible or not
tensorboardReady: boolean;
}

// TODO(jingzhang36): we'll later parse Tensorboard version from mlpipeline-ui-metadata.json file.
const DEFAULT_TENSORBOARD_VERSION = '2.0.0';

class TensorboardViewer extends Viewer<TensorboardViewerProps, TensorboardViewerState> {
timerID: NodeJS.Timeout;

constructor(props: any) {
super(props);

Expand All @@ -76,6 +82,7 @@ class TensorboardViewer extends Viewer<TensorboardViewerProps, TensorboardViewer
deleteDialogOpen: false,
podAddress: '',
tensorflowVersion: DEFAULT_TENSORBOARD_VERSION,
tensorboardReady: false,
};
}

Expand All @@ -89,6 +96,14 @@ class TensorboardViewer extends Viewer<TensorboardViewerProps, TensorboardViewer

public componentDidMount(): void {
this._checkTensorboardApp();
this.timerID = setInterval(
() => this._checkTensorboardPodStatus(),
this.props.intervalOfCheckingTensorboardPodStatus || 5000,
);
}

public componentWillUnmount(): void {
clearInterval(this.timerID);
}

public handleVersionSelect = (e: React.ChangeEvent<{ name?: string; value: unknown }>): void => {
Expand Down Expand Up @@ -125,6 +140,11 @@ class TensorboardViewer extends Viewer<TensorboardViewerProps, TensorboardViewer
className={classes(commonCss.buttonAction, css.button)}
disabled={this.state.busy}
color={'primary'}
title={
this.state.tensorboardReady
? ``
: `Tensorboard is starting, and you may need to wait for a few minutes.`
}
>
Open Tensorboard
</Button>
Expand Down Expand Up @@ -235,6 +255,17 @@ class TensorboardViewer extends Viewer<TensorboardViewerProps, TensorboardViewer
return urls.length === 1 ? urls[0] : urls.map((c, i) => `Series${i + 1}:` + c).join(',');
}

private async _checkTensorboardPodStatus(): Promise<void> {
// If pod address is not null and tensorboard pod doesn't seem to be read, pull status again
if (this.state.podAddress && !this.state.tensorboardReady) {
Apis.isTensorboardPodReady(
'apis/v1beta1/_proxy/' + this.state.podAddress.replace(/(^\w+:|^)\/\//, ''),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can we have a comment explaining the regex here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

).then(ready => {
this.setState(({ tensorboardReady }) => ({ tensorboardReady: tensorboardReady || ready }));
});
}
}

private async _checkTensorboardApp(): Promise<void> {
this.setState({ busy: true }, async () => {
const { podAddress, tfVersion } = await Apis.getTensorboardApp(this._buildUrl());
Expand All @@ -253,7 +284,7 @@ class TensorboardViewer extends Viewer<TensorboardViewerProps, TensorboardViewer
encodeURIComponent(this._buildUrl()),
encodeURIComponent(this.state.tensorflowVersion),
);
this.setState({ busy: false }, () => {
this.setState({ busy: false, tensorboardReady: false }, () => {
this._checkTensorboardApp();
});
});
Expand All @@ -269,6 +300,7 @@ class TensorboardViewer extends Viewer<TensorboardViewerProps, TensorboardViewer
deleteDialogOpen: false,
podAddress: '',
tensorflowVersion: DEFAULT_TENSORBOARD_VERSION,
tensorboardReady: false,
Bobgy marked this conversation as resolved.
Show resolved Hide resolved
});
});
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ exports[`Tensorboard shows a link to the tensorboard instance if exists 1`] = `
className="buttonAction button"
color="primary"
disabled={false}
title=""
>
Open Tensorboard
</WithStyles(Button)>
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/lib/Apis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,16 @@ describe('Apis', () => {
},
);
});

it('checks if Tensorboard pod is ready', async () => {
Bobgy marked this conversation as resolved.
Show resolved Hide resolved
const spy = jest.fn(() =>
Bobgy marked this conversation as resolved.
Show resolved Hide resolved
Promise.resolve({
status: 200,
}),
);
window.fetch = spy;
const ready = await Apis.isTensorboardPodReady('apis/v1beta1/_proxy/pod_address');
expect(ready).toBe(true);
expect(spy).toHaveBeenCalledWith('apis/v1beta1/_proxy/pod_address', { method: 'HEAD' });
});
});
11 changes: 11 additions & 0 deletions frontend/src/lib/Apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,17 @@ export class Apis {
);
}

/**
* Check if the underlying Tensorboard pod is actually up, given
Bobgy marked this conversation as resolved.
Show resolved Hide resolved
*/
public static async isTensorboardPodReady(path: string): Promise<boolean> {
const resp = await fetch(path, { method: 'HEAD' });
if (resp.status === 200) {
return true;
}
return false;
}

/**
* Delete a deployment and its service of the Tensorboard given the URL
*/
Expand Down