Skip to content

[public-api] migrate PrebuildService #19062

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

Merged
merged 1 commit into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
80 changes: 45 additions & 35 deletions components/dashboard/src/components/PrebuildLogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import {
WorkspaceImageBuild,
HEADLESS_LOG_STREAM_STATUS_CODE_REGEX,
Disposable,
PrebuildWithStatus,
} from "@gitpod/gitpod-protocol";
import { getGitpodService } from "../service/service";
import { PrebuildStatus } from "../projects/Prebuilds";
import { workspaceClient } from "../service/public-api";
import { watchWorkspaceStatus } from "../data/workspaces/listen-to-workspace-ws-messages";
import { prebuildClient, watchPrebuild, workspaceClient } from "../service/public-api";
import { GetWorkspaceRequest, WorkspacePhase_Phase } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
import { disposableWatchWorkspaceStatus } from "../data/workspaces/listen-to-workspace-ws-messages";
import { Prebuild, PrebuildPhase_Phase } from "@gitpod/public-api/lib/gitpod/v1/prebuild_pb";

const WorkspaceLogs = React.lazy(() => import("./WorkspaceLogs"));

Expand All @@ -38,15 +38,19 @@ export default function PrebuildLogs(props: PrebuildLogsProps) {
>();
const [error, setError] = useState<Error | undefined>();
const [logsEmitter] = useState(new EventEmitter());
const [prebuild, setPrebuild] = useState<PrebuildWithStatus | undefined>();
const [prebuild, setPrebuild] = useState<Prebuild | undefined>();

const handlePrebuildUpdate = useCallback(
(prebuild: PrebuildWithStatus) => {
if (prebuild.info.buildWorkspaceId === props.workspaceId) {
(prebuild: Prebuild) => {
if (prebuild.workspaceId === props.workspaceId) {
setPrebuild(prebuild);

// In case the Prebuild got "aborted" or "time(d)out" we want to user to proceed anyway
if (props.onIgnorePrebuild && (prebuild.status === "aborted" || prebuild.status === "timeout")) {
if (
props.onIgnorePrebuild &&
(prebuild.status?.phase?.name === PrebuildPhase_Phase.ABORTED ||
prebuild.status?.phase?.name === PrebuildPhase_Phase.TIMEOUT)
) {
props.onIgnorePrebuild();
}
// TODO(gpl) We likely want to move the "happy path" logic (for status "available")
Expand Down Expand Up @@ -79,30 +83,17 @@ export default function PrebuildLogs(props: PrebuildLogsProps) {
setError(err);
}

// Try get hold of a recent Prebuild
try {
const pbws = await getGitpodService().server.findPrebuildByWorkspaceID(props.workspaceId);
if (pbws) {
const foundPrebuild = await getGitpodService().server.getPrebuild(pbws.id);
if (foundPrebuild) {
handlePrebuildUpdate(foundPrebuild);
}
}
} catch (err) {
console.error(err);
setError(err);
}

const watchDispose = disposableWatchWorkspaceStatus(props.workspaceId, (resp) => {
if (resp.status?.instanceId && resp.status?.phase?.name) {
setWorkspace({
instanceId: resp.status.instanceId,
phase: resp.status.phase.name,
});
}
});
// Register for future updates
disposables.push(watchDispose);
disposables.push(
watchWorkspaceStatus(props.workspaceId, (resp) => {
if (resp.status?.instanceId && resp.status?.phase?.name) {
setWorkspace({
instanceId: resp.status.instanceId,
phase: resp.status.phase.name,
});
}
}),
);
disposables.push(
getGitpodService().registerClient({
onWorkspaceImageBuildLogs: (
Expand All @@ -114,13 +105,32 @@ export default function PrebuildLogs(props: PrebuildLogsProps) {
}
logsEmitter.emit("logs", content.text);
},
onPrebuildUpdate(update: PrebuildWithStatus) {
if (update.info) {
handlePrebuildUpdate(update);
}
},
}),
);

try {
const response = await prebuildClient.listPrebuilds({ workspaceId: props.workspaceId });
const prebuild = response.prebuilds[0];
if (prebuild) {
handlePrebuildUpdate(prebuild);
disposables.push(
watchPrebuild(
{
scope: {
case: "prebuildId",
value: prebuild.id,
},
},
handlePrebuildUpdate,
),
);
} else {
setError(new Error("Prebuild not found"));
}
} catch (err) {
console.error(err);
setError(err);
}
})();
return function cleanup() {
disposables.dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,34 @@
* See License.AGPL.txt in the project root for license information.
*/

import { PrebuildWithStatus } from "@gitpod/gitpod-protocol";
import { useQuery } from "@tanstack/react-query";
import { getGitpodService } from "../../service/service";

export type LatestProjectPrebuildQueryResult = PrebuildWithStatus;
import { prebuildClient } from "../../service/public-api";
import { Prebuild } from "@gitpod/public-api/lib/gitpod/v1/prebuild_pb";
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";

type Args = {
projectId: string;
};
export const useLatestProjectPrebuildQuery = ({ projectId }: Args) => {
return useQuery<LatestProjectPrebuildQueryResult>({
return useQuery<Prebuild | null>({
queryKey: getLatestProjectPrebuildQueryKey(projectId),
// Prevent bursting for latest project prebuilds too frequently
staleTime: 1000 * 60 * 1, // 1 minute
queryFn: async () => {
const latestPrebuilds = await getGitpodService().server.findPrebuilds({
projectId,
latest: true,
});

return latestPrebuilds[0] || null;
try {
const response = await prebuildClient.listPrebuilds({
configurationId: projectId,
pagination: {
pageSize: 1,
},
});
return response.prebuilds[0] || null;
} catch (e) {
if (ApplicationError.hasErrorCode(e) && e.code === ErrorCodes.NOT_FOUND) {
return null;
}
throw e;
}
},
});
};
Expand Down
6 changes: 5 additions & 1 deletion components/dashboard/src/data/setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ import * as PaginationClasses from "@gitpod/public-api/lib/gitpod/v1/pagination_
import * as ConfigurationClasses from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
import * as AuthProviderClasses from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
import * as EnvVarClasses from "@gitpod/public-api/lib/gitpod/v1/envvar_pb";
import * as PrebuildClasses from "@gitpod/public-api/lib/gitpod/v1/prebuild_pb";
import * as SCMClasses from "@gitpod/public-api/lib/gitpod/v1/scm_pb";

// This is used to version the cache
// If data we cache changes in a non-backwards compatible way, increment this version
// That will bust any previous cache versions a client may have stored
const CACHE_VERSION = "6";
const CACHE_VERSION = "7";

export function noPersistence(queryKey: QueryKey): QueryKey {
return [...queryKey, "no-persistence"];
Expand Down Expand Up @@ -148,6 +150,8 @@ function initializeMessages() {
...Object.values(ConfigurationClasses),
...Object.values(AuthProviderClasses),
...Object.values(EnvVarClasses),
...Object.values(PrebuildClasses),
...Object.values(SCMClasses),
];
for (const c of constr) {
if ((c as any).prototype instanceof Message) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,19 @@ import { useQueryClient } from "@tanstack/react-query";
import { useEffect } from "react";
import { getListWorkspacesQueryKey, ListWorkspacesQueryResult } from "./list-workspaces-query";
import { useCurrentOrg } from "../organizations/orgs-query";
import { workspaceClient } from "../../service/public-api";
import { WatchWorkspaceStatusResponse, Workspace } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
import { stream, workspaceClient } from "../../service/public-api";
import {
WatchWorkspaceStatusRequest,
WatchWorkspaceStatusResponse,
Workspace,
} from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";

export const useListenToWorkspacesWSMessages = () => {
const queryClient = useQueryClient();
const organizationId = useCurrentOrg().data?.id;

useEffect(() => {
const disposable = disposableWatchWorkspaceStatus(undefined, (status) => {
const disposable = watchWorkspaceStatus(undefined, (status) => {
const queryKey = getListWorkspacesQueryKey(organizationId);
let foundWorkspaces = false;

Expand Down Expand Up @@ -47,38 +51,12 @@ export const useListenToWorkspacesWSMessages = () => {
}, [organizationId, queryClient]);
};

export const disposableWatchWorkspaceStatus = (
export function watchWorkspaceStatus(
workspaceId: string | undefined,
cb: (response: WatchWorkspaceStatusResponse) => void,
): Disposable => {
const MAX_BACKOFF = 60000;
const BASE_BACKOFF = 3000;
let backoff = BASE_BACKOFF;
const abortController = new AbortController();

(async () => {
while (!abortController.signal.aborted) {
try {
const it = workspaceClient.watchWorkspaceStatus(
{ workspaceId },
{
signal: abortController.signal,
},
);
for await (const response of it) {
cb(response);
backoff = BASE_BACKOFF;
}
} catch (e) {
backoff = Math.min(2 * backoff, MAX_BACKOFF);
console.error("failed to watch workspace status, retrying", e);
}
const jitter = Math.random() * 0.3 * backoff;
const delay = backoff + jitter;
await new Promise((resolve) => setTimeout(resolve, delay));
}
})();
return {
dispose: () => abortController.abort(),
};
};
): Disposable {
return stream<WatchWorkspaceStatusRequest>(
(options) => workspaceClient.watchWorkspaceStatus({ workspaceId }, options),
cb,
);
}
Loading