Skip to content

Workspace Timeout improvements (JetBrains) #16267

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 6 commits into from
Feb 10, 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
10 changes: 6 additions & 4 deletions components/gitpod-cli/cmd/timeout-set.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ var setTimeoutCmd = &cobra.Command{
Short: "Set timeout of current workspace",
Long: `Set timeout of current workspace.

Duration must be in the format of <n>m (minutes), <n>h (hours), or <n>d (days).
For example, 30m, 1h, 2d, etc.`,
Duration must be in the format of <n>m (minutes), <n>h (hours) and cannot be longer than 24 hours.
For example: 30m or 1h`,
Example: `gitpod timeout set 1h`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second)
Expand All @@ -45,13 +45,15 @@ For example, 30m, 1h, 2d, etc.`,
if err != nil {
return GpError{Err: err, OutCome: utils.Outcome_UserErr, ErrorCode: utils.UserErrorCode_InvalidArguments}
}
if _, err = client.SetWorkspaceTimeout(ctx, wsInfo.WorkspaceId, duration); err != nil {

res, err := client.SetWorkspaceTimeout(ctx, wsInfo.WorkspaceId, duration)
if err != nil {
if err, ok := err.(*jsonrpc2.Error); ok && err.Code == serverapi.PLAN_PROFESSIONAL_REQUIRED {
return GpError{OutCome: utils.Outcome_UserErr, Message: "Cannot extend workspace timeout for current plan, please upgrade your plan", ErrorCode: utils.UserErrorCode_NeedUpgradePlan}
}
return err
}
fmt.Printf("Workspace timeout has been set to %d minutes.\n", int(duration.Minutes()))
fmt.Printf("Workspace timeout has been set to %s.\n", res.HumanReadableDuration)
return nil
},
}
Expand Down
6 changes: 1 addition & 5 deletions components/gitpod-cli/cmd/timeout-show.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,7 @@ var showTimeoutCommand = &cobra.Command{
return err
}

duration, err := time.ParseDuration(res.Duration)
if err != nil {
return err
}
fmt.Printf("Workspace timeout is set to %d minutes.\n", int(duration.Minutes()))
fmt.Printf("Workspace timeout is set to %s.\n", res.HumanReadableDuration)
return nil
},
}
Expand Down
6 changes: 4 additions & 2 deletions components/gitpod-protocol/go/gitpod-service.go
Original file line number Diff line number Diff line change
Expand Up @@ -1898,8 +1898,9 @@ type StartWorkspaceOptions struct {

// GetWorkspaceTimeoutResult is the GetWorkspaceTimeoutResult message type
type GetWorkspaceTimeoutResult struct {
CanChange bool `json:"canChange,omitempty"`
Duration string `json:"duration,omitempty"`
CanChange bool `json:"canChange,omitempty"`
Duration string `json:"duration,omitempty"`
HumanReadableDuration string `json:"humanReadableDuration,omitempty"`
}

// WorkspaceInstancePort is the WorkspaceInstancePort message type
Expand Down Expand Up @@ -2250,6 +2251,7 @@ type GetTokenSearchOptions struct {
// SetWorkspaceTimeoutResult is the SetWorkspaceTimeoutResult message type
type SetWorkspaceTimeoutResult struct {
ResetTimeoutOnWorkspaces []string `json:"resetTimeoutOnWorkspaces,omitempty"`
HumanReadableDuration string `json:"humanReadableDuration,omitempty"`
}

// UserMessage is the UserMessage message type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@

public class SetWorkspaceTimeoutResult {
private String[] resetTimeoutOnWorkspaces;
private String humanReadableDuration;

public SetWorkspaceTimeoutResult(String[] resetTimeoutOnWorkspaces) {
public SetWorkspaceTimeoutResult(String[] resetTimeoutOnWorkspaces, String humanReadableDuration) {
this.resetTimeoutOnWorkspaces = resetTimeoutOnWorkspaces;
this.humanReadableDuration = humanReadableDuration;
}

public String[] getResetTimeoutOnWorkspaces() {
return resetTimeoutOnWorkspaces;
}

public String getHumanReadableDuration() {
return humanReadableDuration;
}
}
15 changes: 13 additions & 2 deletions components/gitpod-protocol/src/gitpod-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,17 +358,26 @@ export interface ClientHeaderFields {
clientRegion?: string;
}

const WORKSPACE_MAXIMUM_TIMEOUT_HOURS = 24;

export type WorkspaceTimeoutDuration = string;
export namespace WorkspaceTimeoutDuration {
export function validate(duration: string): WorkspaceTimeoutDuration {
duration = duration.toLowerCase();
const unit = duration.slice(-1);
if (!["m", "h", "d"].includes(unit)) {
if (!["m", "h"].includes(unit)) {
throw new Error(`Invalid timeout unit: ${unit}`);
}
const value = parseInt(duration.slice(0, -1));
const value = parseInt(duration.slice(0, -1), 10);
if (isNaN(value) || value <= 0) {
throw new Error(`Invalid timeout value: ${duration}`);
}
if (
(unit === "h" && value > WORKSPACE_MAXIMUM_TIMEOUT_HOURS) ||
(unit === "m" && value > WORKSPACE_MAXIMUM_TIMEOUT_HOURS * 60)
) {
throw new Error("Workspace inactivity timeout cannot exceed 24h");
}
return duration;
}
}
Expand Down Expand Up @@ -402,11 +411,13 @@ export const createServerMock = function <C extends GitpodClient, S extends Gitp

export interface SetWorkspaceTimeoutResult {
resetTimeoutOnWorkspaces: string[];
humanReadableDuration: string;
}

export interface GetWorkspaceTimeoutResult {
duration: WorkspaceTimeoutDuration;
canChange: boolean;
humanReadableDuration: string;
}

export interface StartWorkspaceResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,64 @@ import com.intellij.openapi.diagnostic.thisLogger
import io.gitpod.gitpodprotocol.api.entities.WorkspaceTimeoutDuration
import io.gitpod.jetbrains.remote.GitpodManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import javax.swing.JComponent
import javax.swing.JTextField
import javax.swing.JPanel
import javax.swing.JComboBox
import javax.swing.BoxLayout

// validation from https://github.com/gitpod-io/gitpod/blob/74ccaea38db8df2d1666161a073015485ebb90ca/components/gitpod-protocol/src/gitpod-service.ts#L361-L383
const val WORKSPACE_MAXIMUM_TIMEOUT_HOURS = 24
fun validate(duration: Int, unit: Char): String {
if (duration <= 0) {
throw IllegalArgumentException("Invalid timeout value: ${duration}${unit}")
}
if (
(unit == 'h' && duration > WORKSPACE_MAXIMUM_TIMEOUT_HOURS) ||
(unit == 'm' && duration > WORKSPACE_MAXIMUM_TIMEOUT_HOURS * 60)
) {
throw IllegalArgumentException("Workspace inactivity timeout cannot exceed 24h")
}
return "valid"
}

class InputDurationDialog : DialogWrapper(null, true) {
private val textField = JTextField(10)
private val unitComboBox = JComboBox(arrayOf("minutes", "hours"))

init {
init()
title = "Set timeout duration"
}

override fun createCenterPanel(): JComponent {
val customComponent = JPanel()
customComponent.layout = BoxLayout(customComponent, BoxLayout.X_AXIS)
customComponent.add(textField)
customComponent.add(unitComboBox)

textField.text = "180"

return customComponent
}

override fun doValidate(): ValidationInfo? {
try {
val selectedUnit = unitComboBox.selectedItem.toString()
validate(textField.text.toInt(), selectedUnit[0])
return null
} catch (e: IllegalArgumentException) {
return ValidationInfo(e.message ?: "An unknown error has occurred", textField)
}
}

fun getDuration(): String {
val selectedUnit = unitComboBox.selectedItem.toString()
return "${textField.text}${selectedUnit[0]}"
}
}

class ExtendWorkspaceTimeoutAction : AnAction() {
private val manager = service<GitpodManager>()
Expand All @@ -21,26 +79,25 @@ class ExtendWorkspaceTimeoutAction : AnAction() {
"action" to "extend-timeout"
))

manager.client.server.setWorkspaceTimeout(workspaceInfo.workspaceId, WorkspaceTimeoutDuration.DURATION_180M.toString()).whenComplete { result, e ->
var message: String
var notificationType: NotificationType

if (e != null) {
message = "Cannot extend workspace timeout: ${e.message}"
notificationType = NotificationType.ERROR
thisLogger().error("gitpod: failed to extend workspace timeout", e)
} else {
if (result.resetTimeoutOnWorkspaces.isNotEmpty()) {
message = "Workspace timeout has been extended to three hours. This reset the workspace timeout for other workspaces."
notificationType = NotificationType.WARNING
val dialog = InputDurationDialog()
if (dialog.showAndGet()) {
val duration = dialog.getDuration()
manager.client.server.setWorkspaceTimeout(workspaceInfo.workspaceId, duration.toString()).whenComplete { result, e ->
var message: String
var notificationType: NotificationType

if (e != null) {
message = "Cannot extend workspace timeout: ${e.message}"
notificationType = NotificationType.ERROR
thisLogger().error("gitpod: failed to extend workspace timeout", e)
} else {
message = "Workspace timeout has been extended to three hours."
message = "Workspace timeout has been extended to ${result.humanReadableDuration}."
notificationType = NotificationType.INFORMATION
}
}

val notification = manager.notificationGroup.createNotification(message, notificationType)
notification.notify(null)
val notification = manager.notificationGroup.createNotification(message, notificationType)
notification.notify(null)
}
}
}
}
Expand Down
37 changes: 35 additions & 2 deletions components/server/ee/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,38 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
return { valid: true };
}

goDurationToHumanReadable(goDuration: string): string {
const [, value, unit] = goDuration.match(/^(\d+)([mh])$/)!;
let duration = parseInt(value);

switch (unit) {
case "m":
duration *= 60;
break;
case "h":
duration *= 60 * 60;
break;
}

const hours = Math.floor(duration / 3600);
duration %= 3600;
const minutes = Math.floor(duration / 60);
duration %= 60;

let result = "";
if (hours) {
result += `${hours} hour${hours === 1 ? "" : "s"}`;
if (minutes) {
result += " and ";
}
}
if (minutes) {
result += `${minutes} minute${minutes === 1 ? "" : "s"}`;
}

return result;
}

public async setWorkspaceTimeout(
ctx: TraceContext,
workspaceId: string,
Expand Down Expand Up @@ -404,6 +436,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {

return {
resetTimeoutOnWorkspaces: [workspace.id],
humanReadableDuration: this.goDurationToHumanReadable(validatedDuration),
};
}

Expand All @@ -423,7 +456,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
if (!runningInstance) {
log.warn({ userId: user.id, workspaceId }, "Can only get keep-alive for running workspaces");
const duration = WORKSPACE_TIMEOUT_DEFAULT_SHORT;
return { duration, canChange };
return { duration, canChange, humanReadableDuration: this.goDurationToHumanReadable(duration) };
}
await this.guardAccess({ kind: "workspaceInstance", subject: runningInstance, workspace: workspace }, "get");

Expand All @@ -437,7 +470,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
const desc = await client.describeWorkspace(ctx, req);
const duration = desc.getStatus()!.getSpec()!.getTimeout();

return { duration, canChange };
return { duration, canChange, humanReadableDuration: this.goDurationToHumanReadable(duration) };
}

public async isPrebuildDone(ctx: TraceContext, pwsId: string): Promise<boolean> {
Expand Down