Skip to content

Commit

Permalink
Add support for timeout for fail to launch
Browse files Browse the repository at this point in the history
  • Loading branch information
craigrbarnes committed Dec 19, 2024
1 parent 1f76f2e commit 071e940
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 262 deletions.
10 changes: 10 additions & 0 deletions packages/frontend/__mocks__/createWebStorageMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* global jest */

const mockStorage = {
getItem: jest.fn((_key) => Promise.resolve(null)),
setItem: jest.fn((_key, item) => Promise.resolve(item)),
removeItem: jest.fn((_key) => Promise.resolve()),
};

const createWebStorage = (_arg) => mockStorage;
module.exports = createWebStorage;
12 changes: 10 additions & 2 deletions packages/frontend/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@ const jestConfig: JestConfigWithTsJest = {
roots: ['<rootDir>/src'],
setupFilesAfterEnv: ['<rootDir>/setupTests.ts'],
testEnvironment: 'jsdom',
moduleNameMapper: {
'^@/app(.*)$': '<rootDir>/src/app/$1',
'^@/components(.*)$': '<rootDir>/src/components/$1',
'^@/features/(.*)$': '<rootDir>/src/features/$1',
'^@/utils/(.*)$': '<rootDir>/src/utils/$1',
'^redux-persist/lib/storage/createWebStorage$':
'<rootDir>/__mocks__/createWebStorageMock.js',
},
transform: {
'^.+\\.(ts|tsx)?$':[
'^.+\\.(ts|tsx)?$': [
'ts-jest',
{
tsconfig: 'tsconfig.test.json',
Expand All @@ -14,7 +22,7 @@ const jestConfig: JestConfigWithTsJest = {
],
'node_modules/(flat|jsonpath-plus)/.+\\.(j|t)sx?$': ['ts-jest', {}],
},
'transformIgnorePatterns': ['/node_modules/(?!(flat|jsonpath-plus))']
transformIgnorePatterns: ['/node_modules/(?!(flat|jsonpath-plus))'],
};

export default jestConfig;
111 changes: 78 additions & 33 deletions packages/frontend/src/components/Providers/ResourceMonitor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,48 @@ import {
useGetWorkspaceStatusQuery,
useTerminateWorkspaceMutation,
WorkspaceStatus,
isTimeGreaterThan,
selectRequestedWorkspaceStatusTimestamp,
} from '@gen3/core';
import { notifications } from '@mantine/notifications';
import { useDeepCompareEffect } from 'use-deep-compare';

const WORKSPACE_SHUTDOWN_ALERT_LIMIT = 30000; // TODO add to config

enum NotificationStatus {
Info,
Warn,
Error,
}

const notifyUser = (
title: string,
message: string,
status = NotificationStatus.Info,
) => {
const NotificationMap: Record<NotificationStatus, string> = {
[NotificationStatus.Info]: 'utility.1',
[NotificationStatus.Warn]: 'utility.2',
[NotificationStatus.Error]: 'utility.4',
};

notifications.show({
title,
message,
color: NotificationMap[status],
position: 'top-center',
});
};

// TODO: convert to seconds/minutes for readability
const WorkspacePollingInterval: Record<WorkspaceStatus, number> = {
'Not Found': 0,
Launching: 5000,
Terminating: 5000,
Running: 300000,
Stopped: 5000,
Errored: 10000,
'Status Error': 0,
[WorkspaceStatus.NotFound]: 0,
[WorkspaceStatus.Launching]: 5000,
[WorkspaceStatus.Terminating]: 5000,
[WorkspaceStatus.Running]: 300000,
[WorkspaceStatus.Stopped]: 5000,
[WorkspaceStatus.Errored]: 10000,
[WorkspaceStatus.StatusError]: 0,
};

// TODO: convert to seconds/minutes for readability
Expand Down Expand Up @@ -69,6 +96,9 @@ export const useWorkspaceResourceMonitor = () => {
});
const [terminateWorkspace] = useTerminateWorkspaceMutation();
const requestedStatus = useCoreSelector(selectRequestedWorkspaceStatus); // trigger to start/stop workspaces
const requestedStatusTimestamp = useCoreSelector(
selectRequestedWorkspaceStatusTimestamp,
); // last time requested status changed
const dispatch = useCoreDispatch();

useEffect(() => {
Expand Down Expand Up @@ -134,47 +164,51 @@ export const useWorkspaceResourceMonitor = () => {
if (idleTimeLimit && idleTimeLimit > 0 && lastActivityTime > 0) {
const remainingWorkspaceKernelLife =
idleTimeLimit - (Date.now() - lastActivityTime);

if (remainingWorkspaceKernelLife <= 0) {
// kernel has died due to inactivity
// so terminate
try {
terminateWorkspace().unwrap(); // Unwrap mutation response
} catch (error) {
console.error('Workspace termination failed: ', error);
notifications.show({
title: 'Error',
message: 'Failed to terminate workspace',
color: 'red',
position: 'top-center',
});
const errorMessage =
(error as Error).message || 'Unknown error occurred';
console.error('Workspace termination failed: ', errorMessage);
notifyUser(
'Workspace Error',
`Failed to terminate workspace: ${errorMessage}`,
NotificationStatus.Error,
);
}

dispatch(
setRequestedWorkspaceStatus(RequestedWorkspaceStatus.Terminate),
);
setPollingInterval(
WorkspacePollingInterval[WorkspaceStatus.Terminating],
);
dispatch(setActiveWorkspaceStatus(WorkspaceStatus.Terminating));
notifications.show({
title: 'Workspace Shutdown',
message:
'Workspace has been idle for too long. Shutting workspace down',
position: 'top-center',
});
notifyUser(
'Workspace Shutdown',
'Workspace has been idle for too long. Shutting workspace down',
NotificationStatus.Error,
);
return;
}
if (remainingWorkspaceKernelLife <= workspaceShutdownAlertLimit) {
notifications.show({
title: 'Workspace Warning',
message: 'Workspace has been idle for too long. Will shutdown soon',
position: 'top-center',
});
notifyUser(
'Workspace Warning',
'Workspace has been idle for too long. Will shutdown soon',
NotificationStatus.Warn,
);
}
}
if (requestedStatus === 'Launch') {

if (requestedStatus === RequestedWorkspaceStatus.Launch) {
// if the workspace is running then requested status has been met
dispatch(setRequestedWorkspaceStatus(RequestedWorkspaceStatus.Unset));
}
if (requestedStatus === 'Terminate') {
if (requestedStatus === RequestedWorkspaceStatus.Terminate) {
return;
}

Expand All @@ -184,12 +218,6 @@ export const useWorkspaceResourceMonitor = () => {
}

if (workspaceStatusData.status === WorkspaceStatus.NotFound) {
console.log(
'workspaceStatusData.status',
workspaceStatusData.status,
' requested status',
requestedStatus,
);
// NotFound means pod is not running
// either starting up
// or finally terminated.
Expand All @@ -201,6 +229,10 @@ export const useWorkspaceResourceMonitor = () => {
// both requested status and workspace pod status are the same so stop all polling
setPollingInterval(WorkspacePollingInterval[WorkspaceStatus.NotFound]);
dispatch(setActiveWorkspaceStatus(WorkspaceStatus.NotFound));
if (requestedStatus === RequestedWorkspaceStatus.Terminate) {
// Clean up termination after terminated
dispatch(setRequestedWorkspaceStatus(RequestedWorkspaceStatus.Unset));
}
}
return;
}
Expand All @@ -209,4 +241,17 @@ export const useWorkspaceResourceMonitor = () => {
dispatch(setActiveWorkspaceStatus(workspaceStatusData.status));
setPollingInterval(WorkspacePollingInterval[workspaceStatusData.status]);
}, [dispatch, workspaceStatusData, requestedStatus]);

if (
requestedStatus === RequestedWorkspaceStatus.Launch &&
isTimeGreaterThan(requestedStatusTimestamp, 1)
) {
terminateWorkspace();
dispatch(setRequestedWorkspaceStatus(RequestedWorkspaceStatus.Terminate));
notifyUser(
'Workspace Startup',
'Workspace failed to start. Shutting down',
NotificationStatus.Error,
);
}
};
Loading

0 comments on commit 071e940

Please sign in to comment.