Skip to content

[server] incremental workspaces (1 of 2) #14303

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

Closed
wants to merge 1 commit into from
Closed
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
12 changes: 1 addition & 11 deletions components/dashboard/src/projects/Project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -378,17 +378,7 @@ export default function () {
)}
</a>
<span className="flex-grow" />
<a
href={
prebuild
? gitpodHostUrl
.withContext(
`open-prebuild/${prebuild.info.id}/${prebuild.info.changeUrl}`,
)
.toString()
: gitpodHostUrl.withContext(`${branch.url}`).toString()
}
>
<a href={gitpodHostUrl.withContext(`${branch.url}`).toString()}>
<button
className={`primary mr-2 py-2 opacity-0 group-hover:opacity-100`}
>
Expand Down
12 changes: 12 additions & 0 deletions components/dashboard/src/projects/ProjectSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,18 @@ export default function () {
checked={!project.settings?.keepOutdatedPrebuildsRunning}
onChange={({ target }) => updateProjectSettings({ keepOutdatedPrebuildsRunning: !target.checked })}
/>
<h3 className="mt-12">Workspace Starts</h3>
<CheckBox
title={<span>Incrementally update from old prebuilds</span>}
desc={
<span>
Whether new workspaces can be started based on prebuilds that ran on older Git commits and get
incrementally updated.
</span>
}
checked={!!project.settings?.allowUsingPreviousPrebuilds}
onChange={({ target }) => updateProjectSettings({ allowUsingPreviousPrebuilds: target.checked })}
/>
{showPersistentVolumeClaimUI && (
<>
<br></br>
Expand Down
19 changes: 10 additions & 9 deletions components/dashboard/src/start/CreateWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@

import React, { useEffect, useContext, useState } from "react";
import {
CreateWorkspaceMode,
WorkspaceCreationResult,
RunningWorkspacePrebuildStarting,
ContextURL,
DisposableCollection,
Team,
GitpodServer,
} from "@gitpod/gitpod-protocol";
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
import Modal from "../components/Modal";
Expand Down Expand Up @@ -52,7 +52,7 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
this.createWorkspace();
}

async createWorkspace(mode = CreateWorkspaceMode.SelectIfRunning, forceDefaultConfig = false) {
async createWorkspace(options?: Omit<GitpodServer.CreateWorkspaceOptions, "contextUrl">) {
// Invalidate any previous result.
this.setState({ result: undefined, stillParsing: true });

Expand All @@ -62,8 +62,7 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
try {
const result = await getGitpodService().server.createWorkspace({
contextUrl: this.props.contextUrl,
mode,
forceDefaultConfig,
...options,
});
if (result.workspaceURL) {
window.location.href = result.workspaceURL;
Expand Down Expand Up @@ -148,7 +147,7 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
<button
className=""
onClick={() => {
this.createWorkspace(CreateWorkspaceMode.Default, true);
this.createWorkspace({ forceDefaultConfig: true });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}}
>
Continue with default configuration
Expand Down Expand Up @@ -262,7 +261,9 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
</>
</div>
<div className="flex justify-end mt-6">
<button onClick={() => this.createWorkspace(CreateWorkspaceMode.Default)}>New Workspace</button>
<button onClick={() => this.createWorkspace({ ignoreRunningWorkspaceOnSameCommit: true })}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

New Workspace
</button>
</div>
</Modal>
);
Expand All @@ -271,10 +272,10 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
<RunningPrebuildView
runningPrebuild={result.runningWorkspacePrebuild}
onUseLastSuccessfulPrebuild={() =>
this.createWorkspace(CreateWorkspaceMode.UseLastSuccessfulPrebuild)
this.createWorkspace({ allowUsingPreviousPrebuilds: true, ignoreRunningPrebuild: true })
}
onIgnorePrebuild={() => this.createWorkspace(CreateWorkspaceMode.ForceNew)}
onPrebuildSucceeded={() => this.createWorkspace(CreateWorkspaceMode.UsePrebuild)}
onIgnorePrebuild={() => this.createWorkspace({ ignoreRunningPrebuild: true })}
onPrebuildSucceeded={() => this.createWorkspace()}
/>
);
}
Expand Down
6 changes: 4 additions & 2 deletions components/gitpod-protocol/src/gitpod-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
WhitelistedRepository,
WorkspaceImageBuild,
AuthProviderInfo,
CreateWorkspaceMode,
Token,
UserEnvVarValue,
Terms,
Expand Down Expand Up @@ -422,7 +421,10 @@ export namespace GitpodServer {
}
export interface CreateWorkspaceOptions {
contextUrl: string;
mode?: CreateWorkspaceMode;
// whether running workspaces on the same context should be ignored. If false (default) users will be asked.
ignoreRunningWorkspaceOnSameCommit?: boolean;
ignoreRunningPrebuild?: boolean;
allowUsingPreviousPrebuilds?: boolean;
forceDefaultConfig?: boolean;
}
export interface StartWorkspaceOptions {
Expand Down
13 changes: 0 additions & 13 deletions components/gitpod-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1381,19 +1381,6 @@ export interface WorkspaceCreationResult {
}
export type RunningWorkspacePrebuildStarting = "queued" | "starting" | "running";

export enum CreateWorkspaceMode {
// Default returns a running prebuild if there is any, otherwise creates a new workspace (using a prebuild if one is available)
Default = "default",
// ForceNew creates a new workspace irrespective of any running prebuilds. This mode is guaranteed to actually create a workspace - but may degrade user experience as currently runnig prebuilds are ignored.
ForceNew = "force-new",
// UsePrebuild polls the database waiting for a currently running prebuild to become available. This mode exists to handle the db-sync delay.
UsePrebuild = "use-prebuild",
// SelectIfRunning returns a list of currently running workspaces for the context URL if there are any, otherwise falls back to Default mode
SelectIfRunning = "select-if-running",
// UseLastSuccessfulPrebuild returns ...
UseLastSuccessfulPrebuild = "use-last-successful-prebuild",
}

export namespace WorkspaceCreationResult {
export function is(data: any): data is WorkspaceCreationResult {
return (
Expand Down
2 changes: 2 additions & 0 deletions components/gitpod-protocol/src/teams-projects-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface ProjectSettings {
useIncrementalPrebuilds?: boolean;
usePersistentVolumeClaim?: boolean;
keepOutdatedPrebuildsRunning?: boolean;
// whether new workspaces can start on older prebuilds and incrementally update
allowUsingPreviousPrebuilds?: boolean;
}

export interface Project {
Expand Down
1 change: 1 addition & 0 deletions components/server/ee/src/prebuilds/prebuild-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export class PrebuildManager {
const workspace = await this.workspaceFactory.createForContext(
{ span },
user,
project,
prebuildContext,
context.normalizedContextURL!,
);
Expand Down
109 changes: 56 additions & 53 deletions components/server/ee/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
WorkspaceTimeoutValues,
SetWorkspaceTimeoutResult,
WorkspaceContext,
CreateWorkspaceMode,
WorkspaceCreationResult,
PrebuiltWorkspaceContext,
CommitContext,
Expand Down Expand Up @@ -976,7 +975,8 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
parentCtx: TraceContext,
user: User,
context: WorkspaceContext,
mode: CreateWorkspaceMode,
ignoreRunningPrebuild?: boolean,
allowUsingPreviousPrebuilds?: boolean,
): Promise<WorkspaceCreationResult | PrebuiltWorkspaceContext | undefined> {
const ctx = TraceContext.childContext("findPrebuiltWorkspace", parentCtx);
try {
Expand All @@ -989,29 +989,38 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
const logCtx: LogContext = { userId: user.id };
const cloneUrl = context.repository.cloneUrl;
let prebuiltWorkspace: PrebuiltWorkspace | undefined;
const logPayload = {
allowUsingPreviousPrebuilds,
ignoreRunningPrebuild,
cloneUrl,
commit: commitSHAs,
prebuiltWorkspace,
};
if (OpenPrebuildContext.is(context)) {
prebuiltWorkspace = await this.workspaceDb.trace(ctx).findPrebuildByID(context.openPrebuildID);
if (prebuiltWorkspace?.cloneURL !== cloneUrl) {
if (
prebuiltWorkspace?.cloneURL !== cloneUrl &&
(ignoreRunningPrebuild || prebuiltWorkspace?.state === "available")
) {
// prevent users from opening arbitrary prebuilds this way - they must match the clone URL so that the resource guards are correct.
return;
}
} else {
prebuiltWorkspace = await this.workspaceDb
.trace(ctx)
.findPrebuiltWorkspaceByCommit(cloneUrl, commitSHAs);
}

const logPayload = { mode, cloneUrl, commit: commitSHAs, prebuiltWorkspace };
log.debug(logCtx, "Looking for prebuilt workspace: ", logPayload);
if (prebuiltWorkspace?.state !== "available" && mode === CreateWorkspaceMode.UseLastSuccessfulPrebuild) {
const { config } = await this.configProvider.fetchConfig({}, user, context);
const history = await this.incrementalPrebuildsService.getCommitHistoryForContext(context, user);
prebuiltWorkspace = await this.incrementalPrebuildsService.findGoodBaseForIncrementalBuild(
context,
config,
history,
user,
);
log.debug(logCtx, "Looking for prebuilt workspace: ", logPayload);
if (!allowUsingPreviousPrebuilds) {
prebuiltWorkspace = await this.workspaceDb
.trace(ctx)
.findPrebuiltWorkspaceByCommit(cloneUrl, commitSHAs);
} else {
const { config } = await this.configProvider.fetchConfig({}, user, context);
const history = await this.incrementalPrebuildsService.getCommitHistoryForContext(context, user);
prebuiltWorkspace = await this.incrementalPrebuildsService.findGoodBaseForIncrementalBuild(
context,
config,
history,
user,
);
}
}
if (!prebuiltWorkspace) {
return;
Expand All @@ -1026,13 +1035,9 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
};
return result;
} else if (prebuiltWorkspace.state === "queued" || prebuiltWorkspace.state === "building") {
if (mode === CreateWorkspaceMode.ForceNew) {
if (ignoreRunningPrebuild) {
// in force mode we ignore running prebuilds as we want to start a workspace as quickly as we can.
return;
// TODO(janx): Fall back to parent prebuild instead, if it's available:
// const buildWorkspace = await this.workspaceDb.trace({span}).findById(prebuiltWorkspace.buildWorkspaceId);
// const parentPrebuild = await this.workspaceDb.trace({span}).findPrebuildByID(buildWorkspace.basedOnPrebuildId);
// Also, make sure to initialize it by both printing the parent prebuild logs AND re-runnnig the before/init/prebuild tasks.
}

const workspaceID = prebuiltWorkspace.buildWorkspaceId;
Expand Down Expand Up @@ -1097,36 +1102,34 @@ export class GitpodServerEEImpl extends GitpodServerImpl {

const inSameCluster = wsi.region === this.config.installationShortname;
if (!inSameCluster) {
if (mode === CreateWorkspaceMode.UsePrebuild) {
/* We need to wait for this prebuild to finish before we return from here.
* This creation mode is meant to be used once we have gone through default mode, have confirmation from the
* message bus that the prebuild is done, and now only have to wait for dbsync to come through. Thus,
* in this mode we'll poll the database until the prebuild is ready (or we time out).
*
* Note: This polling mechanism only makes sense if the prebuild runs in cluster different from ours.
* Otherwise there's no dbsync inbetween that we might have to wait for.
*
* DB sync interval is 2 seconds at the moment, we wait ten "ticks" for the data to be synchronized.
*/
const finishedPrebuiltWorkspace = await this.pollDatabaseUntilPrebuildIsAvailable(
ctx,
prebuiltWorkspace.id,
20000,
/* We need to wait for this prebuild to finish before we return from here.
* This creation mode is meant to be used once we have gone through default mode, have confirmation from the
* message bus that the prebuild is done, and now only have to wait for dbsync to come through. Thus,
* in this mode we'll poll the database until the prebuild is ready (or we time out).
*
* Note: This polling mechanism only makes sense if the prebuild runs in cluster different from ours.
* Otherwise there's no dbsync inbetween that we might have to wait for.
*
* DB sync interval is 2 seconds at the moment, we wait ten "ticks" for the data to be synchronized.
*/
const finishedPrebuiltWorkspace = await this.pollDatabaseUntilPrebuildIsAvailable(
ctx,
prebuiltWorkspace.id,
20000,
);
if (!finishedPrebuiltWorkspace) {
log.warn(
logCtx,
"did not find a finished prebuild in the database despite waiting long enough after msgbus confirmed that the prebuild had finished",
logPayload,
);
if (!finishedPrebuiltWorkspace) {
log.warn(
logCtx,
"did not find a finished prebuild in the database despite waiting long enough after msgbus confirmed that the prebuild had finished",
logPayload,
);
return;
} else {
return {
title: context.title,
originalContext: context,
prebuiltWorkspace: finishedPrebuiltWorkspace,
} as PrebuiltWorkspaceContext;
}
return;
} else {
return {
title: context.title,
originalContext: context,
prebuiltWorkspace: finishedPrebuiltWorkspace,
} as PrebuiltWorkspaceContext;
}
}

Expand Down
Loading