diff --git a/src/contexts/instance-context.js b/src/contexts/instance-context.js index df501942..94e4ceaa 100644 --- a/src/contexts/instance-context.js +++ b/src/contexts/instance-context.js @@ -32,7 +32,7 @@ export const InstanceProvider = ({ children }) => { let isAppReady = false; try { - isAppReady = await api.getAppReady(decoded_url); + isAppReady = await api.getAppReady(sid); } catch(e) {} // just absorb the exception, the default false is correct here if (isAppReady) { let newActivity = { diff --git a/src/contexts/workspaces-context/api.ts b/src/contexts/workspaces-context/api.ts index 35e93278..135297c6 100644 --- a/src/contexts/workspaces-context/api.ts +++ b/src/contexts/workspaces-context/api.ts @@ -357,17 +357,22 @@ export class WorkspacesAPI implements IWorkspacesAPI { } @APIRequest() - async getAppReady(decodedURL: string, fetchOptions: AxiosRequestConfig={}): Promise { - const parts = decodedURL.split('/'); - const sid = (parts.length >= 2)?parts[parts.length - 2]:""; - - if(sid.length == 0) return false; + async getAppReady(sid: string, fetchOptions: AxiosRequestConfig={}): Promise { const res = await this.axios.get(`/instances/${ sid }/is_ready/`, { ...fetchOptions }); if(res.data) return res.data.is_ready; return false; } + + @APIRequest() + async getAppReadyByURL(decodedURL: string, fetchOptions: AxiosRequestConfig={}): Promise { + const parts = decodedURL.split('/'); + const sid = (parts.length >= 2)?parts[parts.length - 2]:""; + + if(sid.length == 0) return false; + return await this.getAppReady(sid, fetchOptions) + } } /** diff --git a/src/contexts/workspaces-context/api.types.ts b/src/contexts/workspaces-context/api.types.ts index caf75fd1..1803c91b 100644 --- a/src/contexts/workspaces-context/api.types.ts +++ b/src/contexts/workspaces-context/api.types.ts @@ -233,5 +233,6 @@ export interface IWorkspacesAPI { stopAppInstance(sid: string, fetchOptions?: AxiosRequestConfig): Promise updateAppInstance(sid: string, workspace: string, cpu: string, gpu: string, memory: string, fetchOptions?: AxiosRequestConfig): Promise launchApp(appId: string, cpus: number, gpus: number, memory: string, fetchOptions?: AxiosRequestConfig): Promise - getAppReady(appUrl: string, fetchOptions?: AxiosRequestConfig): Promise + getAppReady(sid: string, fetchOptions?: AxiosRequestConfig): Promise + getAppReadyByURL(decodedURL: string, fetchOptions?: AxiosRequestConfig): Promise } diff --git a/src/views/splash-screen.js b/src/views/splash-screen.js index f30dceaa..94279066 100644 --- a/src/views/splash-screen.js +++ b/src/views/splash-screen.js @@ -39,7 +39,7 @@ export const SplashScreenView = withWorkspaceAuthentication((props) => { (async () => { try { await callWithRetry(async () => { - const isReady = await api.getAppReady(decoded_url); + const isReady = await api.getAppReadyByURL(decoded_url); if (isReady && shouldCancel === false) { setLoading(false) } else { diff --git a/src/views/workspaces/active.js b/src/views/workspaces/active.js index 0078a292..cd125e00 100644 --- a/src/views/workspaces/active.js +++ b/src/views/workspaces/active.js @@ -1,4 +1,4 @@ -import React, { Fragment, useEffect, useState } from 'react'; +import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react'; import { Button, Col, Form, Input, Layout, Modal, Table, Typography, Slider, Spin, Row, Progress, Space, Tooltip } from 'antd'; import { DeleteOutlined, RightCircleOutlined, LoadingOutlined, CloseOutlined, ExclamationOutlined, QuestionOutlined } from '@ant-design/icons'; import { NavigationTabGroup } from '../../components/workspaces/navigation-tab-group'; @@ -20,7 +20,6 @@ export const ActiveView = withWorkspaceAuthentication(() => { const [instances, setInstances] = useState(); const [apps, setApps] = useState(); const [refresh, setRefresh] = useState(false); - const [isLoading, setLoading] = useState(false); const { api } = useWorkspacesAPI() const { appSpecs, appActivityCache, getLatestActivity } = useActivity(); const { analyticsEvents } = useAnalytics(); @@ -44,19 +43,29 @@ export const ActiveView = withWorkspaceAuthentication(() => { useTitle("Active Workspaces") + const isLoading = useMemo(() => instances === undefined, [instances]) + useEffect(() => { - const renderInstance = async () => { - setLoading(true); + let stale = false + // We poll the instances endpoint to periodically update readiness probe values. + // This approach is simpler than individually polling each app's readiness. + const pollInstances = async () => { try { const instances = await api.getAppInstances() + if (stale) return setInstances(instances) + setTimeout(pollInstances, 5000) } catch (e) { + if (stale) return setInstances([]) openNotificationWithIcon('error', 'Error', 'An error has occurred while loading instances.') + setTimeout(pollInstances, 2500) } - setLoading(false); } - renderInstance(); + pollInstances() + return () => { + stale = true + } }, [refresh, api]) useEffect(() => { @@ -70,10 +79,12 @@ export const ActiveView = withWorkspaceAuthentication(() => { openNotificationWithIcon('error', 'Error', 'An error has occurred while loading app configuration.') } } - if (instances && instances.length === 0) setTimeout(() => navigate('/helx/workspaces/available'), 1000) - else loadAppsConfig(); + loadAppsConfig(); + }, [api]) - }, [instances, api]) + useEffect(() => { + if (instances && instances.length === 0) setTimeout(() => navigate('/helx/workspaces/available'), 1000) + }, [instances]) const stopInstanceHandler = async () => { // besides making requests to delete the instance, close its browser tab and stop polling service @@ -202,7 +213,14 @@ export const ActiveView = withWorkspaceAuthentication(() => { let activity = getLatestActivity(record.sid) let indicator = null let statusText = null - switch (activity?.data.status) { + + let status = activity?.data.status + if (status === "LAUNCHED") { + // Containers may be launched but also need to ensure the pod readiness probe has passed, + // otherwise display as still launching. + if (record.status !== "ready") status = "LAUNCHING" + } + switch (status) { case "LAUNCHING": indicator = } /> statusText = "Launching"