Skip to content

Commit

Permalink
feat: always pull session status but ignore change if the pathname is…
Browse files Browse the repository at this point in the history
… a session url
  • Loading branch information
andre-code committed Nov 29, 2022
1 parent 63deb7a commit 80df1ee
Show file tree
Hide file tree
Showing 9 changed files with 78 additions and 149 deletions.
7 changes: 6 additions & 1 deletion client/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { AddDataset } from "./dataset/addtoproject/DatasetAdd.container";
import { DatasetCoordinator } from "./dataset/Dataset.state";
import AppContext from "./utils/context/appContext";
import { setupWebSocket } from "./websocket";
import { WsMessage } from "./websocket/WsMessages";

export const ContainerWrap = ({ children, fullSize = false }) => {
const classContainer = !fullSize ? "container-xxl py-4 mt-2 renku-container" : "w-100";
Expand Down Expand Up @@ -213,7 +214,11 @@ class App extends Component {
webSocketUrl = "ws" + webSocketUrl.substring(4);
// ? adding a small delay to allow session cookie to be saved to local browser before sending requests
setTimeout(
() => { this.webSocket = setupWebSocket(webSocketUrl, this.props.model); return; },
() => {
this.webSocket = setupWebSocket(webSocketUrl, this.props.model, getLocation, this.props.client);
this.webSocket?.send(JSON.stringify(new WsMessage({}, "pullSessionStatus")));
return;
},
1000
);
}
Expand Down
17 changes: 7 additions & 10 deletions client/src/project/Project.present.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ import EntityHeader from "../utils/components/entityHeader/EntityHeader";
import { useProjectJsonLdQuery } from "../features/projects/ProjectKgApi";

import "./Project.css";
import PullSessionStatus from "../utils/components/PullSessionStatus";

function filterPaths(paths, blacklist) {
// Return paths to do not match the blacklist of regexps.
Expand Down Expand Up @@ -1381,7 +1380,6 @@ function ProjectView(props) {
<script type="application/ld+json">{JSON.stringify(data)}</script> : null;
const cleanSessionUrl = props.location.pathname.split("/").slice(0, -1).join("/") + "/:server";
const isShowSession = cleanSessionUrl === props.sessionShowUrl;
const pullingSession = <PullSessionStatus key="pullingSessions" fetchSessions={true} socket={props.socket} />;
return [
<Helmet key="page-title">
<title>{pageTitle}</title>
Expand Down Expand Up @@ -1412,20 +1410,19 @@ function ProjectView(props) {
<Row key="content">
<Switch>
<Route exact path={props.baseUrl}
render={() =>
[<ProjectViewOverview key="overview" {...props} />, pullingSession]} />
render={() => <ProjectViewOverview key="overview" {...props} />} />
<Route path={props.overviewUrl}
render={() => [<ProjectViewOverview key="overview" {...props} />, pullingSession]} />
render={() => <ProjectViewOverview key="overview" {...props} />} />
<Route path={props.collaborationUrl}
render={() => [<ProjectViewCollaboration key="collaboration" {...props} />, pullingSession]} />
render={() => <ProjectViewCollaboration key="collaboration" {...props} />} />
<Route path={props.filesUrl}
render={() => [<ProjectViewFiles key="files" {...props} />, pullingSession]} />
render={() => <ProjectViewFiles key="files" {...props} />} />
<Route path={props.datasetsUrl}
render={() => [<ProjectViewDatasets key="datasets" {...props} />, pullingSession]} />
render={() => <ProjectViewDatasets key="datasets" {...props} />} />
<Route path={[props.workflowUrl, props.workflowsUrl]}
render={() => [<ProjectViewWorkflows key="workflows" {...props} />, pullingSession]} />
render={() => <ProjectViewWorkflows key="workflows" {...props} />} />
<Route path={props.settingsUrl}
render={() => [<ProjectSettings key="settings" {...props} />, pullingSession]} />
render={() => <ProjectSettings key="settings" {...props} />} />
<Route path={props.notebookServersUrl}
render={() => <ProjectSessions key="sessions" {...props} />} />
<Route component={NotFoundInsideProject} />
Expand Down
42 changes: 0 additions & 42 deletions client/src/utils/components/PullSessionStatus.tsx

This file was deleted.

15 changes: 14 additions & 1 deletion client/src/utils/helpers/url/Url.js
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,20 @@ function getSearchParams(expectedParams = null, convertParams = null, convertTyp
return parameters;
}

export { Url, getSearchParams };
/**
* Calculate if the url is a session URL
*
* @param {string} [url] - url
* @returns {boolean} if match with a session url
*/
function isSessionUrl(url) {
const isNewSessionUrl = /\/sessions\/new/g.exec(url);
const isShowSessionUrl = /\/sessions\/show\//g.exec(url);
const endUrl = /\w+$/g.exec(url);
return isNewSessionUrl?.length || isShowSessionUrl?.length || (endUrl && endUrl[0] === "sessions");
}

export { Url, getSearchParams, isSessionUrl };

// testing only
export { UrlRule };
51 changes: 14 additions & 37 deletions client/src/websocket/handlers/sessionStatusHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,47 +15,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
interface Session {
annotations: Record<string, string>;
cloudStorage: Record<string, string|number> | null;
image: string;
name: string;
resources: {
requests: Record<string, string|number>;
usage: Record<string, string|number> | null;
started: string;
};
state: {
"pod_name": string;
};
status: {
details: {
status: string;
step: string;
}[];
message?: string;
readyNumContainers: number;
state: string;
totalNumContainers: number;
};
url: string;
}

interface ServersResult {
fetching: boolean;
fetched: Date;
all: Record<string, Session>;
}
import { isSessionUrl } from "../../utils/helpers/url/Url";
import { NotebooksCoordinator } from "../../notebooks";
import APIClient from "../../api-client";
import { StateModel } from "../../model";

function handleSessionsStatus(data: Record<string, unknown>, webSocket: WebSocket, model: any, notifications: any) {
if (data.message) {
const statuses = JSON.parse(data.message as string);
function handleSessionsStatus(
data: Record<string, unknown>, webSocket: WebSocket, model: StateModel, getLocation: Function, client: APIClient) {
if (data.message as boolean && client && model) {
const location = getLocation();

let updatedNotebooks: ServersResult = { fetching: false, fetched: new Date(), all: {} };
updatedNotebooks.all = statuses;
model.set("notebooks.notebooks", updatedNotebooks);
if (!isSessionUrl(location?.pathname)) {
const notebooksModel = model.subModel("notebooks");
const userModel = model.subModel("user");
const notebookCoordinator = new NotebooksCoordinator(client, notebooksModel, userModel);
notebookCoordinator.fetchNotebooks();
}
}
return null;
}

export { handleSessionsStatus };
9 changes: 5 additions & 4 deletions client/src/websocket/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ import { StateModel, globalSchema } from "../model";
import { getWsServerMessageHandler, retryConnection, setupWebSocket, MessageData } from "./index";
import { WsServerMessage } from "./WsMessages";
import { sleep } from "../utils/helpers/HelperFunctions";
import APIClient, { testClient as client } from "../api-client";


const fakeLocation = () => { };
const messageHandlers: Record<string, Record<string, Array<MessageData>>> = {
"user": {
"init": [
Expand Down Expand Up @@ -124,7 +125,7 @@ describe("Test WebSocket functions", () => {
const reconnectModel = model.subModel("webSocket.reconnect");
expect(reconnectModel.get("attempts")).toBe(0);
expect(reconnectModel.get("retrying")).toBe(false);
retryConnection("fakeUrl", model);
retryConnection("fakeUrl", model, fakeLocation, client as APIClient);
expect(reconnectModel.get("attempts")).toBe(1);
expect(reconnectModel.get("retrying")).toBe(true);
});
Expand All @@ -144,12 +145,12 @@ describe("Test WebSocket server", () => {

// using a wrong URL shouldn't work
expect(localModel.get("open")).toBe(false);
setupWebSocket(webSocketURL.replace("localhost", "fake_host"), fullModel);
setupWebSocket(webSocketURL.replace("localhost", "fake_host"), fullModel, fakeLocation, client as APIClient);
await sleep(0.01); // ? It's ugly, but it's needed when using the fake WebSocket server...
expect(localModel.get("open")).toBe(false);

// using the correct URL opens the connection
setupWebSocket(webSocketURL, fullModel);
setupWebSocket(webSocketURL, fullModel, fakeLocation, client as APIClient);
await sleep(0.01);
expect(localModel.get("open")).toBe(true);

Expand Down
16 changes: 11 additions & 5 deletions client/src/websocket/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import { checkWsServerMessage, WsMessage, WsServerMessage } from "./WsMessages";
import { handleUserInit, handleUserUiVersion, handleUserError } from "./handlers/userHandlers";
import { handleSessionsStatus } from "./handlers/sessionStatusHandler";
import { StateModel } from "../model";
import APIClient from "../api-client";


const timeoutIntervalMs = 45 * 1000; // ? set to 0 to disable
Expand Down Expand Up @@ -88,8 +90,10 @@ const messageHandlers: Record<string, Record<string, Array<MessageData>>> = {
* Setup WebSocket channel.
* @param webSocketUrl - target URL
* @param fullModel - global model
* @param getLocation - function to get location
* @param client - api client
*/
function setupWebSocket(webSocketUrl: string, fullModel: any) {
function setupWebSocket(webSocketUrl: string, fullModel: StateModel, getLocation: Function, client: APIClient) {
const model = fullModel.subModel("webSocket");
const webSocket = new WebSocket(webSocketUrl);
model.setObject({ open: false, reconnect: { retrying: false } });
Expand Down Expand Up @@ -130,7 +134,7 @@ function setupWebSocket(webSocketUrl: string, fullModel: any) {
model.setObject(wsData);

if (data.code === 1006 || data.code === 4000)
retryConnection(webSocketUrl, fullModel);
retryConnection(webSocketUrl, fullModel, getLocation, client);
};

webSocket.onmessage = (message) => {
Expand Down Expand Up @@ -164,7 +168,7 @@ function setupWebSocket(webSocketUrl: string, fullModel: any) {
// execute the command
try {
// ? Mind we are passing the full model, not just model
const outcome = handler(serverMessage.data, webSocket, fullModel);
const outcome = handler(serverMessage.data, webSocket, fullModel, getLocation, client);
if (outcome && model.get("error"))
model.set("error", false);
else if (!outcome && !model.get("error"))
Expand Down Expand Up @@ -241,8 +245,10 @@ function getWsServerMessageHandler(
* Retry connection when it fails, keeping track of the attempts.
* @param webSocketUrl - target URL
* @param fullModel - global model
* @param getLocation - function to get location
* @param client - api client
*/
function retryConnection(webSocketUrl: string, fullModel: any) {
function retryConnection(webSocketUrl: string, fullModel: StateModel, getLocation: Function, client: APIClient) {
const reconnectModel = fullModel.subModel("webSocket.reconnect");
const reconnectData = reconnectModel.get("");
// reset timer after 1 hour
Expand All @@ -254,7 +260,7 @@ function retryConnection(webSocketUrl: string, fullModel: any) {
reconnectData.retrying = true;
const delay = (reconnectPenaltyFactor ** reconnectData.attempts) * reconnectIntervalMs;
reconnectModel.setObject(reconnectData);
setTimeout(() => setupWebSocket(webSocketUrl, fullModel), delay);
setTimeout(() => setupWebSocket(webSocketUrl, fullModel, getLocation, client), delay);
}


Expand Down
62 changes: 21 additions & 41 deletions server/src/websocket/handlers/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,6 @@ interface SessionsResult {
servers: Record<string, Session>;
}
interface Session {
annotations: Record<string, string>;
cloudStorage: Record<string, string|number> | null;
image: string;
name: string;
resources: {
requests: Record<string, string|number>;
usage: Record<string, string|number> | null;
started: string;
};
state: {
"pod_name": string;
};
status: {
details: {
status: string;
Expand All @@ -48,17 +36,10 @@ interface Session {
state: string;
totalNumContainers: number;
};
url: string;
}

function handlerRequestSessionStatus(
data: Record<string, unknown>, channel: Channel): void {
channel.data.set("requestSessionStatus", true);
}

function handlerRequestStopSessionStatus(
data: Record<string, unknown>, channel: Channel): void {
channel.data.set("requestSessionStatus", false);
channel.data.set("sessionStatus", null);
}

Expand All @@ -69,28 +50,27 @@ function sendMessage(data: string, channel: Channel) {

function heartbeatRequestSessionStatus
(channel: Channel, apiClient: APIClient, authHeathers: Record<string, string>): void {
const requestSession = channel.data.get("requestSessionStatus") as boolean;
if (requestSession) {
const previousStatuses = channel.data.get("sessionStatus") as SessionsResult;
apiClient.sessionStatus(authHeathers)
.then((response) => {
const statusFetched = response as unknown as SessionsResult;
const servers = statusFetched?.servers ?? {};
const cleanStatus: Record<string, Session> = servers;
// remove usage information, it is not necessary at this time.
Object.keys(servers).map( key => {
cleanStatus[key].resources.usage = null;
});
if (!util.isDeepStrictEqual(previousStatuses, cleanStatus)) {
sendMessage(JSON.stringify(cleanStatus), channel);
channel.data.set("sessionStatus", cleanStatus);
}
})
.catch((e) => {
logger.warn("There was a problem while trying to fetch sessions");
logger.warn(e);
const previousStatuses = channel.data.get("sessionStatus") as SessionsResult;
apiClient.sessionStatus(authHeathers)
.then((response) => {
const statusFetched = response as unknown as SessionsResult;
const servers = statusFetched?.servers ?? {};
const cleanStatus: Record<string, Session> = {};
// only keep status information
Object.keys(servers).map( key => {
cleanStatus[key] = { status: servers[key].status };
});
}

// only send message when something change
if (!util.isDeepStrictEqual(previousStatuses, cleanStatus)) {
sendMessage("true", channel);
channel.data.set("sessionStatus", cleanStatus);
}
})
.catch((e) => {
logger.warn("There was a problem while trying to fetch sessions");
logger.warn(e);
});
}

export { handlerRequestSessionStatus, heartbeatRequestSessionStatus, handlerRequestStopSessionStatus };
export { handlerRequestSessionStatus, heartbeatRequestSessionStatus };
8 changes: 0 additions & 8 deletions server/src/websocket/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import { handlerRequestServerVersion, heartbeatRequestServerVersion } from "./ha
import APIClient from "../api-client";
import {
handlerRequestSessionStatus,
handlerRequestStopSessionStatus,
heartbeatRequestSessionStatus
} from "./handlers/sessions";

Expand Down Expand Up @@ -69,13 +68,6 @@ const acceptedMessages: Record<string, Array<MessageData>> = {
handler: handlerRequestSessionStatus
} as MessageData,
],
"stopPullSessionStatus": [
{
required: null,
optional: null,
handler: handlerRequestStopSessionStatus
} as MessageData,
],
"ping": [
{
required: null,
Expand Down

0 comments on commit 80df1ee

Please sign in to comment.