Skip to content

Commit

Permalink
Build - make backend-plugin readiness a condition of JetBrains IDEs r…
Browse files Browse the repository at this point in the history
…eadiness (#19710)

* Make backend-plugin readiness a condition of IDE readiness

* Add compatibility to `2022.3.3`

* fixup

* Observe ide readiness as Inf after 11 seconds

* shutdown after 10 minutes

* fix hot-deploy.sh

* Add shutdown reason

* address feedback

* Add Feature Flags

* fixup

* fix

* Add one line log

* address feedback
  • Loading branch information
mustard-mh committed May 14, 2024
1 parent 2a397c3 commit 92fccc8
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ class GitpodCLIService : RestService() {
/**
* prod: curl http://localhost:63342/api/gitpod/cli?op=metrics
* dev: curl http://localhost:63343/api/gitpod/cli?op=metrics
*
* We will use this endpoint in JetBrains launcher to check if backend-plugin is ready.
* Please make sure this operation:metrics to respond 200
*/
if (operation == "metrics") {
val out = BufferExposingByteArrayOutputStream()
Expand Down
2 changes: 2 additions & 0 deletions components/ide/jetbrains/launcher/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ require (

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/configcat/go-sdk/v7 v7.6.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gitpod-io/gitpod/components/scrubber v0.0.0-00010101000000-000000000000 // indirect
github.com/golang/mock v1.6.0 // indirect
Expand Down
10 changes: 10 additions & 0 deletions components/ide/jetbrains/launcher/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 55 additions & 1 deletion components/ide/jetbrains/launcher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"google.golang.org/grpc/credentials/insecure"
yaml "gopkg.in/yaml.v2"

"github.com/gitpod-io/gitpod/common-go/experiments"
"github.com/gitpod-io/gitpod/common-go/log"
"github.com/gitpod-io/gitpod/common-go/util"
gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"
Expand Down Expand Up @@ -64,6 +65,8 @@ type LaunchContext struct {
info *ProductInfo
backendVersion *version.Version
wsInfo *supervisor.WorkspaceInfoResponse
exps experiments.Client
gitpodHostname string

vmOptionsFile string
platformPropertiesFile string
Expand Down Expand Up @@ -91,7 +94,8 @@ func main() {
return
}

log.Init(ServiceName, Version, true, false)
debugEnabled := os.Getenv("SUPERVISOR_DEBUG_ENABLE") == "true"
log.Init(ServiceName, Version, true, debugEnabled)
log.Info(ServiceName + ": " + Version)
startTime := time.Now()

Expand Down Expand Up @@ -149,6 +153,16 @@ func main() {
return
}

var exps experiments.Client
var gitpodHostname string
if gitpodUrl, err := url.Parse(wsInfo.GitpodHost); err == nil {
gitpodHost := gitpodUrl.Hostname()
gitpodHostname = gitpodHost
exps = experiments.NewClient(experiments.WithGitpodProxy(gitpodHost))
} else {
log.WithField("gitpodHost", wsInfo.GitpodHost).WithError(err).Error("failed to parse url")
}

launchCtx := &LaunchContext{
startTime: startTime,

Expand All @@ -163,6 +177,8 @@ func main() {
info: info,
backendVersion: backendVersion,
wsInfo: wsInfo,
exps: exps,
gitpodHostname: gitpodHostname,
}

if launchCtx.warmup {
Expand Down Expand Up @@ -272,6 +288,10 @@ func serve(launchCtx *LaunchContext) {
if backendPort == "" {
backendPort = defaultBackendPort
}
if err := isBackendPluginReady(r.Context(), backendPort, launchCtx.exps, launchCtx.gitpodHostname); err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
gatewayLink, err := resolveGatewayLink(backendPort, launchCtx.wsInfo)
if err != nil {
log.WithError(err).Error("cannot resolve gateway link")
Expand All @@ -293,6 +313,37 @@ func serve(launchCtx *LaunchContext) {
}
}

// isBackendPluginReady checks if the backend plugin is ready via backend plugin CLI GitpodCLIService.kt
func isBackendPluginReady(ctx context.Context, backendPort string, exps experiments.Client, gitpodHostname string) error {
if exps == nil {
log.Error("no experiments.Client setup")
return nil
}
if !exps.GetBoolValue(ctx, "jb_wait_backend_plugin_readiness", false, experiments.Attributes{GitpodHost: gitpodHostname}) {
log.Debug("will not wait plugin ready")
return nil
}
log.WithField("backendPort", backendPort).Debug("wait backend plugin to be ready")
// Use op=metrics so that we don't need to rebuild old backend-plugin
url, err := url.Parse("http://localhost:" + backendPort + "/api/gitpod/cli?op=metrics")
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("backend plugin is not ready: %d", resp.StatusCode)
}
return nil
}

func restart(r *http.Request) {
backendPort := r.URL.Query().Get("backendPort")
if backendPort == "" {
Expand Down Expand Up @@ -1068,6 +1119,9 @@ func getProductConfig(config *gitpod.GitpodConfig, alias string) *gitpod.Jetbrai

func linkRemotePlugin(launchCtx *LaunchContext) error {
remotePluginsFolder := launchCtx.configDir + "/plugins"
if launchCtx.info.Version == "2022.3.3" {
remotePluginsFolder = launchCtx.backendDir + "/plugins"
}
remotePluginDir := remotePluginsFolder + "/gitpod-remote"
_, err := os.Stat(remotePluginDir)
if err == nil || !errors.Is(err, os.ErrNotExist) {
Expand Down
7 changes: 6 additions & 1 deletion components/supervisor/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

"github.com/gitpod-io/gitpod/common-go/log"
"github.com/gitpod-io/gitpod/common-go/process"
"github.com/gitpod-io/gitpod/supervisor/pkg/shared"
"github.com/gitpod-io/gitpod/supervisor/pkg/supervisor"
"github.com/prometheus/procfs"
reaper "github.com/ramr/go-reaper"
Expand Down Expand Up @@ -83,7 +84,11 @@ var initCmd = &cobra.Command{
if err != nil && !(strings.Contains(err.Error(), "signal: ") || strings.Contains(err.Error(), "no child processes")) {
if eerr, ok := err.(*exec.ExitError); ok && eerr.ExitCode() != 0 {
logs := extractFailureFromRun()
log.WithError(fmt.Errorf(logs)).Fatal("supervisor run error with unexpected exit code")
if shared.IsExpectedShutdown(eerr.ExitCode()) {
log.Fatal(logs)
} else {
log.WithError(fmt.Errorf(logs)).Fatal("supervisor run error with unexpected exit code")
}
}
log.WithError(err).Error("supervisor run error")
return
Expand Down
2 changes: 1 addition & 1 deletion components/supervisor/hot-deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ echo "Image Version: $version"
bldfn="/tmp/build-$version.tar.gz"

docker ps &> /dev/null || (echo "You need a working Docker daemon. Maybe set DOCKER_HOST?"; exit 1)
leeway build -Dversion="$version" -DimageRepoBase=eu.gcr.io/gitpod-core-dev/build .:docker --save "$bldfn"
leeway build -Dversion="$version" -DimageRepoBase=eu.gcr.io/gitpod-dev-artifact/build .:docker --save "$bldfn"
dev_image="$(tar xfO "$bldfn" ./imgnames.txt | head -n1)"
echo "Dev Image: $dev_image"

Expand Down
3 changes: 2 additions & 1 deletion components/supervisor/pkg/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package metrics
import (
"fmt"

"github.com/gitpod-io/gitpod/supervisor/pkg/shared"
"github.com/prometheus/client_golang/prometheus"
)

Expand All @@ -22,7 +23,7 @@ func NewMetrics() *SupervisorMetrics {
IDEReadyDurationTotal: prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "supervisor_ide_ready_duration_total",
Help: "the IDE startup time",
Buckets: []float64{0.1, 0.5, 1, 1.5, 2, 2.5, 5, 10},
Buckets: []float64{0.1, 0.5, 1, 1.5, 2, 2.5, 5, shared.IDEReadyDurationTotalMaxBucketSecond},
}, []string{"kind"}),
InitializerHistogram: prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "supervisor_initializer_bytes_second",
Expand Down
14 changes: 14 additions & 0 deletions components/supervisor/pkg/shared/constant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) 2024 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

package shared

const (
IDEReadyDurationTotalMaxBucketSecond = 10
ExitCodeReasonIDEReadinessTimedOut = 2
)

func IsExpectedShutdown(exitCode int) bool {
return exitCode == ExitCodeReasonIDEReadinessTimedOut
}
11 changes: 8 additions & 3 deletions components/supervisor/pkg/supervisor/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,14 @@ type DesktopIDEStatus struct {
}

type ideReadyState struct {
ready bool
info *DesktopIDEStatus
cond *sync.Cond
ready bool
ideConfig *IDEConfig
info *DesktopIDEStatus
cond *sync.Cond
}

func newIDEReadyState(ideConfig *IDEConfig) *ideReadyState {
return &ideReadyState{cond: sync.NewCond(&sync.Mutex{}), ideConfig: ideConfig}
}

// Wait returns a channel that emits when IDE is ready.
Expand Down
Loading

0 comments on commit 92fccc8

Please sign in to comment.