)}
Global environment
- {description ? description
: null}
+ {description ? {description}
: null}
NotebooksHelper.cleanAnnotations(annotations) as NotebookAnnotations,
- [annotations]
- );
+ const { status, image } = session;
const state = status.state;
- const defaultImage = cleanAnnotations.default_image_used;
const badge =
- state === "running" && defaultImage ? (
+ state === "running" && !image ? (
Running Session
@@ -91,7 +91,7 @@ export function SessionStatusV2Label({ session }: ActiveSessionV2Props) {
className={cx("me-1", "text-warning-emphasis")}
inline
/>
- Deleting Session
+ Shutting down session
) : state === "hibernated" ? (
@@ -123,15 +123,7 @@ export function SessionStatusV2Description({
session,
showInfoDetails = true,
}: ActiveSessionDescV2Props) {
- const { annotations, started, status } = session;
-
- const cleanAnnotations = useMemo(
- () => NotebooksHelper.cleanAnnotations(annotations) as NotebookAnnotations,
- [annotations]
- );
-
- const details = { message: session.status.message };
-
+ const { started, status, name } = session;
return (
-
+
{showInfoDetails && (
-
+
)}
);
}
+
+interface StatusExtraDetailsV2Props {
+ status: SessionStatus;
+ uid: string;
+}
+export function SessionListRowStatusExtraDetailsV2({
+ status,
+ uid,
+}: StatusExtraDetailsV2Props) {
+ if (!status.message) return null;
+
+ const popover = (
+
+ Kubernetes pod status
+
+
+
+
+ );
+
+ if (status.state == "failed")
+ return (
+ <>
+ {" "}
+
+ (Click here for details.)
+
+ {popover}
+ >
+ );
+ return (
+ <>
+ {" "}
+
+ {popover}
+ >
+ );
+}
export function SessionStatusV2Title({
session,
launcher,
@@ -182,66 +204,46 @@ export function SessionStatusV2Title({
return text ? {text}
: null;
}
interface SessionStatusV2TextProps {
- annotations: NotebookAnnotations;
startTimestamp: string;
- status: SessionStatusState;
+ status: SessionStatus;
}
function SessionStatusV2Text({
- annotations,
startTimestamp,
status,
}: SessionStatusV2TextProps) {
+ const { state, will_hibernate_at, will_delete_at } = status;
const startTimeText = (
);
const hibernationTimestamp =
- status === "hibernated" ? annotations["hibernationDate"] ?? "" : null;
- const hibernationDateTime = hibernationTimestamp
- ? ensureDateTime(hibernationTimestamp)
- : null;
-
- const hibernatedSecondsThreshold =
- status === "hibernated"
- ? parseInt(annotations?.hibernatedSecondsThreshold ?? "", 10)
- : null;
- const hibernationThresholdDuration =
- !hibernatedSecondsThreshold || isNaN(hibernatedSecondsThreshold)
- ? Duration.fromISO("")
- : Duration.fromObject({ seconds: hibernatedSecondsThreshold });
- const hibernationCullTimestamp =
- hibernationDateTime &&
- hibernationThresholdDuration &&
- hibernationThresholdDuration.isValid &&
- hibernationThresholdDuration.valueOf() > 0
- ? hibernationDateTime.plus(hibernationThresholdDuration)
- : null;
+ state === "hibernated" ? will_hibernate_at ?? "" : null;
- return status === "running" ? (
+ return state === "running" ? (
Launched {startTimeText}
- ) : status === "starting" ? (
+ ) : state === "starting" ? (
Created {startTimeText}
- ) : status === "stopping" ? (
- <>Deleting Session...>
- ) : status === "hibernated" && hibernationCullTimestamp ? (
+ ) : state === "stopping" ? (
+ <>Shutting down session...>
+ ) : state === "hibernated" && will_delete_at ? (
Session will be deleted in{" "}
- ) : status === "hibernated" && hibernationTimestamp ? (
+ ) : state === "hibernated" && hibernationTimestamp ? (
@@ -249,7 +251,7 @@ function SessionStatusV2Text({
- ) : status === "hibernated" ? (
+ ) : state === "hibernated" ? (
@@ -257,7 +259,7 @@ function SessionStatusV2Text({
- ) : status === "failed" ? (
+ ) : state === "failed" ? (
diff --git a/client/src/features/sessionsV2/session.utils.ts b/client/src/features/sessionsV2/session.utils.ts
index 3d7417e907..75bedee9c3 100644
--- a/client/src/features/sessionsV2/session.utils.ts
+++ b/client/src/features/sessionsV2/session.utils.ts
@@ -20,11 +20,13 @@ import { FaviconStatus } from "../display/display.types";
import { SessionStatusState } from "../session/sessions.types";
import { DEFAULT_URL } from "./session.constants";
import {
+ SessionCloudStorageV2,
SessionEnvironmentList,
SessionLauncher,
SessionLauncherEnvironmentParams,
SessionLauncherForm,
} from "./sessionsV2.types";
+import { SessionStartCloudStorageConfiguration } from "./startSessionOptionsV2.types";
export function getSessionFavicon(
sessionState?: SessionStatusState,
@@ -222,3 +224,58 @@ export function isValidJSONStringArray(
if (parseString.parsed) return true;
return parseString.error ?? "Is not a valid JSON array string";
}
+
+export function storageDefinitionFromConfigV2(
+ config: SessionStartCloudStorageConfiguration
+): SessionCloudStorageV2 {
+ const { storage: storageDefinition } = config.cloudStorage;
+ const { sensitiveFieldValues } = config;
+
+ // Merge configuration with sensitive field values, excluding empty ones
+ const configuration = {
+ ...storageDefinition.configuration,
+ ...Object.fromEntries(
+ Object.entries(sensitiveFieldValues).filter(
+ ([, value]) => value != null && value !== ""
+ )
+ ),
+ };
+
+ return {
+ readonly: !!storageDefinition.readonly,
+ source_path: storageDefinition.source_path,
+ target_path: storageDefinition.target_path,
+ storage_id: storageDefinition.storage_id,
+ configuration,
+ };
+}
+
+/**
+ * Ensure a given URL uses the HTTPS protocol.
+ * If the URL already starts with "https://", it is returned unchanged.
+ * If the URL starts with "http://", it is replaced with "https://".
+ * If the URL has no protocol, "https://" is prepended.
+ *
+ * @param url - The URL to be ensured with HTTPS protocol.
+ * @returns A URL string guaranteed to start with "https://".
+ * @throws Error if the input is not a valid URL.
+ */
+export function ensureHTTPS(url: string): string {
+ try {
+ const parsedUrl = new URL(url);
+ if (parsedUrl.protocol === "https:") {
+ return url;
+ }
+
+ if (parsedUrl.protocol === "http:") {
+ return url.replace("http:", "https:");
+ }
+
+ throw new Error("Unsupported protocol");
+ } catch (error) {
+ if (url && !url.includes("://")) {
+ return `https://${url}`;
+ }
+ throw new Error(`Invalid URL: ${url}`);
+ }
+}
diff --git a/client/src/features/sessionsV2/sessionsV2.api.ts b/client/src/features/sessionsV2/sessionsV2.api.ts
index 5901963350..ef2316b386 100644
--- a/client/src/features/sessionsV2/sessionsV2.api.ts
+++ b/client/src/features/sessionsV2/sessionsV2.api.ts
@@ -21,10 +21,18 @@ import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import {
AddSessionLauncherParams,
DeleteSessionLauncherParams,
+ DockerImage,
+ GetLogsParams,
GetProjectSessionLaunchersParams,
+ LaunchSessionParams,
+ PatchSessionParams,
SessionEnvironmentList,
+ SessionImageParams,
SessionLauncher,
SessionLauncherList,
+ SessionList,
+ SessionV2,
+ StopSessionParams,
UpdateSessionLauncherParams,
} from "./sessionsV2.types";
@@ -33,7 +41,7 @@ const sessionsV2Api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: "/ui-server/api/data",
}),
- tagTypes: ["Environment", "Launcher"],
+ tagTypes: ["Environment", "Launcher", "SessionsV2"],
endpoints: (builder) => ({
getSessionEnvironments: builder.query({
query: () => {
@@ -110,15 +118,101 @@ const sessionsV2Api = createApi({
},
invalidatesTags: ["Launcher"],
}),
+ getSessions: builder.query({
+ query: () => ({ url: "sessions" }),
+ providesTags: (result) =>
+ result
+ ? [
+ ...result.map(({ name }) => ({
+ type: "SessionsV2" as const,
+ name,
+ })),
+ "SessionsV2",
+ ]
+ : ["SessionsV2"],
+ }),
+ launchSession: builder.mutation({
+ query: ({
+ launcher_id,
+ disk_storage,
+ resource_class_id,
+ cloudstorage,
+ }) => {
+ const body = {
+ launcher_id,
+ disk_storage,
+ resource_class_id,
+ cloudstorage,
+ };
+ return {
+ body,
+ method: "POST",
+ url: "/sessions",
+ };
+ },
+ }),
+ patchSession: builder.mutation({
+ query: ({ session_id, state, resource_class_id }) => ({
+ method: "PATCH",
+ url: `sessions/${session_id}`,
+ body: {
+ ...(state ? { state } : {}),
+ ...(resource_class_id ? { resource_class_id } : {}),
+ },
+ }),
+ transformResponse: () => null,
+ invalidatesTags: (_result, _error, { session_id }) => [
+ { id: session_id, type: "SessionsV2" },
+ ],
+ }),
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ getLogs: builder.query({
+ query: ({ session_id, max_lines }) => {
+ return {
+ url: `sessions/${session_id}/logs`,
+ params: { max_lines },
+ };
+ },
+ transformResponse: (result: unknown) => {
+ return result && typeof result == "string"
+ ? JSON.parse(result)
+ : result;
+ },
+ keepUnusedDataFor: 0,
+ }),
+ stopSession: builder.mutation({
+ query: ({ session_id }) => ({
+ method: "DELETE",
+ url: `sessions/${session_id}`,
+ }),
+ invalidatesTags: ["SessionsV2"],
+ }),
+ invalidateSessions: builder.mutation({
+ queryFn: () => ({ data: null }),
+ invalidatesTags: ["SessionsV2"],
+ }),
+ getDockerImage: builder.query({
+ query: ({ image_url }) => ({
+ method: "GET",
+ url: "sessions/images",
+ params: { image_url },
+ }),
+ }),
}),
});
export default sessionsV2Api;
export const {
+ useGetSessionsQuery,
useGetSessionEnvironmentsQuery,
useGetSessionLaunchersQuery,
useGetProjectSessionLaunchersQuery,
useAddSessionLauncherMutation,
useUpdateSessionLauncherMutation,
useDeleteSessionLauncherMutation,
+ useLaunchSessionMutation,
+ usePatchSessionMutation,
+ useStopSessionMutation,
+ useGetLogsQuery,
+ useGetDockerImageQuery,
} = sessionsV2Api;
diff --git a/client/src/features/sessionsV2/sessionsV2.types.ts b/client/src/features/sessionsV2/sessionsV2.types.ts
index 8b3096588c..52bd3b637c 100644
--- a/client/src/features/sessionsV2/sessionsV2.types.ts
+++ b/client/src/features/sessionsV2/sessionsV2.types.ts
@@ -17,6 +17,7 @@
*/
import { ResourceClass } from "../dataServices/dataServices.types";
+import { CloudStorageDetailsOptions } from "../project/components/cloudStorage/projectCloudStorage.types";
export interface SessionEnvironment {
container_image: string;
@@ -126,3 +127,70 @@ export interface SessionLauncherForm {
command: string;
args: string;
}
+
+export interface SessionResources {
+ cpu: number;
+ gpu: number;
+ memory: number;
+ storage: number;
+}
+
+export interface SessionStatus {
+ message?: string;
+ state: "running" | "starting" | "stopping" | "failed" | "hibernated";
+ will_hibernate_at?: string;
+ will_delete_at?: string;
+ ready_containers: number;
+ total_containers: number;
+}
+
+export type SessionList = SessionV2[];
+export interface SessionV2 {
+ image: string;
+ name: string;
+ resources: SessionResources;
+ started: string;
+ status: SessionStatus;
+ url: string;
+ project_id: string;
+ launcher_id: string;
+ resource_class_id: string;
+}
+
+export interface SessionCloudStorageV2 {
+ configuration: CloudStorageDetailsOptions;
+ readonly: boolean;
+ source_path: string;
+ storage_id: string;
+ target_path: string;
+}
+
+export interface LaunchSessionParams {
+ launcher_id: string;
+ disk_storage?: number;
+ cloudstorage?: SessionCloudStorageV2[];
+ resource_class_id?: number;
+}
+
+export interface PatchSessionParams {
+ session_id: string;
+ state?: Extract<"running" | "hibernated", SessionStatus["state"]>;
+ resource_class_id?: number;
+}
+
+export interface GetLogsParams {
+ session_id: string;
+ max_lines: number;
+}
+
+export interface StopSessionParams {
+ session_id: string;
+}
+export interface SessionImageParams {
+ image_url: string;
+}
+
+export interface DockerImage {
+ image: string;
+ available: boolean;
+}
diff --git a/client/src/features/sessionsV2/useSessionLaunchState.hook.ts b/client/src/features/sessionsV2/useSessionLaunchState.hook.ts
index 95eebd8f48..1b2513f645 100644
--- a/client/src/features/sessionsV2/useSessionLaunchState.hook.ts
+++ b/client/src/features/sessionsV2/useSessionLaunchState.hook.ts
@@ -16,7 +16,6 @@
* limitations under the License
*/
-import { skipToken } from "@reduxjs/toolkit/query";
import { useEffect, useMemo } from "react";
import useAppDispatch from "../../utils/customHooks/useAppDispatch.hook";
import useAppSelector from "../../utils/customHooks/useAppSelector.hook";
@@ -24,13 +23,14 @@ import { useGetResourcePoolsQuery } from "../dataServices/computeResources.api";
import useDataSourceConfiguration from "../ProjectPageV2/ProjectPageContent/DataSources/useDataSourceConfiguration.hook";
import type { Project } from "../projectsV2/api/projectV2.api";
import { useGetStoragesV2Query } from "../projectsV2/api/storagesV2.api";
-import { useGetDockerImageQuery } from "../session/sessions.api";
-import { SESSION_CI_PIPELINE_POLLING_INTERVAL_MS } from "../session/startSessionOptions.constants";
-import { DockerImageStatus } from "../session/startSessionOptions.types";
+import { DEFAULT_URL } from "./session.constants";
import { SessionLauncher } from "./sessionsV2.types";
import startSessionOptionsV2Slice from "./startSessionOptionsV2.slice";
import useSessionResourceClass from "./useSessionResourceClass.hook";
-import { DEFAULT_URL } from "./session.constants";
+import { SESSION_CI_PIPELINE_POLLING_INTERVAL_MS } from "../session/startSessionOptions.constants";
+import { skipToken } from "@reduxjs/toolkit/query";
+import { useGetDockerImageQuery } from "./sessionsV2.api";
+import { DockerImageStatus } from "../session/startSessionOptions.types";
interface StartSessionFromLauncherProps {
launcher: SessionLauncher;
@@ -71,7 +71,7 @@ export default function useSessionLauncherState({
useGetDockerImageQuery(
containerImage !== "unknown"
? {
- image: containerImage,
+ image_url: containerImage,
}
: skipToken,
{
@@ -124,6 +124,9 @@ export default function useSessionLauncherState({
startSessionOptionsV2Slice.actions.setDockerImageStatus(newStatus)
);
}
+ dispatch(
+ startSessionOptionsV2Slice.actions.setDockerImageStatus("available")
+ );
}, [
dispatch,
dockerImageStatus,
diff --git a/client/src/notebooks/components/SessionListStatus.tsx b/client/src/notebooks/components/SessionListStatus.tsx
index 72bec43556..d04d3ba1e3 100644
--- a/client/src/notebooks/components/SessionListStatus.tsx
+++ b/client/src/notebooks/components/SessionListStatus.tsx
@@ -66,7 +66,7 @@ export function SessionListRowStatusExtraDetails({
return (
<>
{" "}
-
+
(Click here for details.)
{popover}
diff --git a/client/src/utils/customHooks/UseGetSessionLogs.ts b/client/src/utils/customHooks/UseGetSessionLogs.ts
index af0b8ec0df..f303d99b83 100644
--- a/client/src/utils/customHooks/UseGetSessionLogs.ts
+++ b/client/src/utils/customHooks/UseGetSessionLogs.ts
@@ -17,6 +17,7 @@
*/
import { useEffect, useState } from "react";
import { useGetLogsQuery } from "../../features/session/sessions.api";
+import { useGetLogsQuery as useGetLogsQueryV2 } from "../../features/sessionsV2/sessionsV2.api";
import { ILogs } from "../../components/Logs";
/**
@@ -51,4 +52,33 @@ function useGetSessionLogs(serverName: string, show: boolean | string) {
return { logs, fetchLogs };
}
+export function useGetSessionLogsV2(
+ serverName: string,
+ show: boolean | string
+) {
+ const { data, isFetching, isLoading, error, refetch } = useGetLogsQueryV2(
+ { session_id: serverName, max_lines: 250 },
+ { skip: !serverName }
+ );
+ const [logs, setLogs] = useState(undefined);
+ const fetchLogs = () => {
+ return refetch().then((result) => {
+ if (result.isSuccess)
+ return Promise.resolve(result.data as ILogs["data"]);
+ return Promise.reject({} as ILogs["data"]);
+ }) as Promise;
+ };
+
+ useEffect(() => {
+ setLogs({
+ data,
+ fetched: !isLoading && !error && data,
+ fetching: isFetching,
+ show: show ? serverName : false,
+ });
+ }, [data, error, show, isFetching, isLoading, serverName]);
+
+ return { logs, fetchLogs };
+}
+
export default useGetSessionLogs;
diff --git a/client/src/websocket/handlers/sessionStatusHandlerV2.ts b/client/src/websocket/handlers/sessionStatusHandlerV2.ts
new file mode 100644
index 0000000000..5462accf3c
--- /dev/null
+++ b/client/src/websocket/handlers/sessionStatusHandlerV2.ts
@@ -0,0 +1,34 @@
+/*!
+ * Copyright 2024 - Swiss Data Science Center (SDSC)
+ * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
+ * Eidgenössische Technische Hochschule Zürich (ETHZ).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import sessionsV2Api from "../../features/sessionsV2/sessionsV2.api";
+import { StateModel } from "../../model";
+
+function handleSessionsStatusV2(
+ data: Record,
+ _webSocket: WebSocket,
+ model: StateModel
+) {
+ if ((data.message as boolean) && model) {
+ model.reduxStore.dispatch(
+ sessionsV2Api.endpoints.invalidateSessions.initiate()
+ );
+ }
+}
+
+export { handleSessionsStatusV2 };
diff --git a/client/src/websocket/index.ts b/client/src/websocket/index.ts
index 5071d9271a..4431231172 100644
--- a/client/src/websocket/index.ts
+++ b/client/src/websocket/index.ts
@@ -38,6 +38,7 @@ import type { KgInactiveProjectsState } from "../features/inactiveKgProjects/";
import { ActivationStatusProgressError } from "../features/inactiveKgProjects/";
import { StateModel } from "../model";
import APIClient from "../api-client";
+import { handleSessionsStatusV2 } from "./handlers/sessionStatusHandlerV2";
const timeoutIntervalMs = 45 * 1000; // ? set to 0 to disable
const reconnectIntervalMs = 10 * 1000;
@@ -107,6 +108,13 @@ const messageHandlers: Record>> = {
handler: handleSessionsStatus,
},
],
+ sessionStatusV2: [
+ {
+ required: null,
+ optional: ["message"],
+ handler: handleSessionsStatusV2,
+ },
+ ],
},
};
@@ -151,6 +159,12 @@ function setupWebSocket(
);
}
+ function startPullingSessionStatusV2(targetWebSocket: WebSocket) {
+ targetWebSocket.send(
+ JSON.stringify(new WsMessage({}, "pullSessionStatusV2"))
+ );
+ }
+
function resumePendingKgActivation(model: any, socket: any) {
const state = model?.reduxStore?.getState();
if (state == null) return;
@@ -195,6 +209,8 @@ function setupWebSocket(
model.setObject({ open: true, error: false, lastReceived: null });
// request session status
startPullingSessionStatus(webSocket);
+ // request session status V2
+ startPullingSessionStatusV2(webSocket);
// resume running processes
resumePendingProcesses(fullModel, webSocket);
}
diff --git a/server/src/api-client/index.ts b/server/src/api-client/index.ts
index 3beae1a12f..d537a5c178 100644
--- a/server/src/api-client/index.ts
+++ b/server/src/api-client/index.ts
@@ -45,6 +45,19 @@ class APIClient {
return this.clientFetch(sessionsUrl, options, RETURN_TYPES.json);
}
+ /**
+ * Fetch session status
+ *
+ */
+ async getSessionStatusV2(authHeathers: HeadersInit): Promise {
+ const sessionsUrl = `${this.gatewayUrl}/data/sessions`;
+ logger.debug(`Fetching session status.`);
+ const options = {
+ headers: new Headers(authHeathers),
+ };
+ return this.clientFetch(sessionsUrl, options, RETURN_TYPES.json);
+ }
+
/**
* Fetch kg activation status by projectId
*
diff --git a/server/src/utils/index.ts b/server/src/utils/index.ts
index a55de1a26c..e35df1857f 100644
--- a/server/src/utils/index.ts
+++ b/server/src/utils/index.ts
@@ -16,6 +16,7 @@
* limitations under the License.
*/
+import { SessionV2 } from "src/websocket/handlers/sessionsV2";
import urlJoin from "./url-join";
/**
@@ -156,9 +157,59 @@ function sortObjectProperties(
);
}
+interface SessionFlattedV2 {
+ image: string;
+ name: string;
+ started: string;
+ url: string;
+ project_id: string;
+ launcher_id: string;
+ resource_class_id: string;
+ "resources.cpu": SessionV2["resources"]["cpu"];
+ "resources.gpu": SessionV2["resources"]["gpu"];
+ "resources.memory": SessionV2["resources"]["memory"];
+ "resources.storage": SessionV2["resources"]["storage"];
+
+ // Flatten the nested status object
+ "status.message": SessionV2["status"]["message"];
+ "status.state": SessionV2["status"]["state"];
+ "status.will_hibernate_at": SessionV2["status"]["will_hibernate_at"];
+ "status.will_delete_at": SessionV2["status"]["will_delete_at"];
+ "status.ready_containers": SessionV2["status"]["ready_containers"];
+ "status.total_containers": SessionV2["status"]["total_containers"];
+}
+
+function flattenSessionV2(session: SessionV2): SessionFlattedV2 {
+ return {
+ // Flatten the top-level properties
+ image: session.image,
+ name: session.name,
+ started: session.started,
+ url: session.url,
+ project_id: session.project_id,
+ launcher_id: session.launcher_id,
+ resource_class_id: session.resource_class_id,
+
+ // Flatten the nested resources object
+ "resources.cpu": session.resources.cpu,
+ "resources.gpu": session.resources.gpu,
+ "resources.memory": session.resources.memory,
+ "resources.storage": session.resources.storage,
+
+ // Flatten the nested status object
+ "status.message": session.status.message,
+ "status.state": session.status.state,
+ "status.will_hibernate_at": session.status.will_hibernate_at,
+ "status.will_delete_at": session.status.will_delete_at,
+ "status.ready_containers": session.status.ready_containers,
+ "status.total_containers": session.status.total_containers,
+ };
+}
+
export {
clamp,
convertType,
+ flattenSessionV2,
getCookieValueByName,
getRelease,
serializeCookie,
@@ -166,4 +217,5 @@ export {
simpleHash,
sleep,
urlJoin,
+ SessionFlattedV2,
};
diff --git a/server/src/websocket/handlers/sessionsV2.ts b/server/src/websocket/handlers/sessionsV2.ts
new file mode 100644
index 0000000000..e0ced702fc
--- /dev/null
+++ b/server/src/websocket/handlers/sessionsV2.ts
@@ -0,0 +1,98 @@
+/*!
+ * Copyright 2024 - Swiss Data Science Center (SDSC)
+ * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
+ * Eidgenössische Technische Hochschule Zürich (ETHZ).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import logger from "../../logger";
+import { Channel } from "../index";
+import * as util from "util";
+import { WsMessage } from "../WsMessages";
+import { flattenSessionV2, simpleHash } from "../../utils";
+import { WebSocketHandlerArgs } from "./handlers.types";
+
+export interface SessionResources {
+ cpu: number;
+ gpu: number;
+ memory: number;
+ storage: number;
+}
+
+export interface SessionStatus {
+ message?: string;
+ state: "running" | "starting" | "stopping" | "failed" | "hibernated";
+ will_hibernate_at?: string;
+ will_delete_at?: string;
+ ready_containers: number;
+ total_containers: number;
+}
+
+export interface SessionV2 {
+ image: string;
+ name: string;
+ resources: SessionResources;
+ started: string;
+ status: SessionStatus;
+ url: string;
+ project_id: string;
+ launcher_id: string;
+ resource_class_id: string;
+}
+
+function handlerRequestSessionStatusV2(
+ data: Record,
+ channel: Channel
+): void {
+ channel.data.set("sessionStatusV2", null);
+}
+
+function sendMessage(data: string, channel: Channel) {
+ const info = new WsMessage({ message: data }, "user", "sessionStatusV2");
+ channel.sockets.forEach((socket) => socket.send(info.toString()));
+}
+
+function heartbeatRequestSessionStatusV2({
+ channel,
+ apiClient,
+ headers,
+}: WebSocketHandlerArgs): void {
+ const previousStatuses = channel.data.get("sessionStatusV2") as string;
+ apiClient
+ .getSessionStatusV2(headers)
+ .then((response) => {
+ if (!Array.isArray(response)) {
+ logger.warn("Response is not an array");
+ return;
+ }
+ const sessions = response.map((session) => flattenSessionV2(session));
+ const sortedSessions = sessions.sort((a, b) =>
+ a.name.localeCompare(b.name)
+ );
+ const currentHashedSessions = simpleHash(
+ JSON.stringify(sortedSessions)
+ ).toString();
+ // only send message when something change
+ if (!util.isDeepStrictEqual(previousStatuses, currentHashedSessions)) {
+ sendMessage("true", channel);
+ channel.data.set("sessionStatusV2", currentHashedSessions);
+ }
+ })
+ .catch((error) => {
+ logger.warn("There was a problem while trying to fetch sessions");
+ if (error.message) logger.warn(error.message);
+ });
+}
+
+export { handlerRequestSessionStatusV2, heartbeatRequestSessionStatusV2 };
diff --git a/server/src/websocket/index.ts b/server/src/websocket/index.ts
index fc117d64e8..ec905afc48 100644
--- a/server/src/websocket/index.ts
+++ b/server/src/websocket/index.ts
@@ -40,6 +40,10 @@ import {
heartbeatRequestSessionStatus,
} from "./handlers/sessions";
import type { Channel, WebSocketHandler } from "./handlers/handlers.types";
+import {
+ handlerRequestSessionStatusV2,
+ heartbeatRequestSessionStatusV2,
+} from "./handlers/sessionsV2";
// *** Channels ***
// No need to store data in Redis since it's used only locally. We can modify this if necessary.
@@ -76,6 +80,13 @@ const acceptedMessages: Record> = {
handler: handlerRequestSessionStatus,
} as MessageData,
],
+ pullSessionStatusV2: [
+ {
+ required: null,
+ optional: null,
+ handler: handlerRequestSessionStatusV2,
+ } as MessageData,
+ ],
ping: [
{
required: null,
@@ -99,6 +110,7 @@ const longLoopFunctions: Array = [
const shortLoopFunctions: Array = [
heartbeatRequestSessionStatus,
heartbeatRequestActivationKgStatus,
+ heartbeatRequestSessionStatusV2,
];
/**