diff --git a/components/ws-daemon/pkg/content/initializer.go b/components/ws-daemon/pkg/content/initializer.go index 12a946af10f35c..e6a678df36b63d 100644 --- a/components/ws-daemon/pkg/content/initializer.go +++ b/components/ws-daemon/pkg/content/initializer.go @@ -68,15 +68,6 @@ var ( func CollectRemoteContent(ctx context.Context, rs storage.DirectAccess, ps storage.PresignedAccess, workspaceOwner string, initializer *csapi.WorkspaceInitializer) (rc map[string]storage.DownloadInfo, err error) { rc = make(map[string]storage.DownloadInfo) - backup, err := ps.SignDownload(ctx, rs.Bucket(workspaceOwner), rs.BackupObject(storage.DefaultBackup), &storage.SignedURLOptions{}) - if err == storage.ErrNotFound { - // no backup found - that's fine - } else if err != nil { - return nil, err - } else { - rc[storage.DefaultBackup] = *backup - } - si := initializer.GetSnapshot() pi := initializer.GetPrebuild() if ci := initializer.GetComposite(); ci != nil { @@ -89,33 +80,92 @@ func CollectRemoteContent(ctx context.Context, rs storage.DirectAccess, ps stora } } } + + // Parse snapshot/prebuild names before launching goroutines so we can fail fast + // on invalid input without wasting network calls. + var snapshotBkt, snapshotObj string if si != nil { - bkt, obj, err := storage.ParseSnapshotName(si.Snapshot) + snapshotBkt, snapshotObj, err = storage.ParseSnapshotName(si.Snapshot) if err != nil { return nil, err } - info, err := ps.SignDownload(ctx, bkt, obj, &storage.SignedURLOptions{}) - if err == storage.ErrNotFound { - return nil, errCannotFindSnapshot - } - if err != nil { - return nil, xerrors.Errorf("cannot find snapshot: %w", err) - } - - rc[si.Snapshot] = *info } + var prebuildBkt, prebuildObj string if pi != nil && pi.Prebuild != nil && pi.Prebuild.Snapshot != "" { - bkt, obj, err := storage.ParseSnapshotName(pi.Prebuild.Snapshot) + prebuildBkt, prebuildObj, err = storage.ParseSnapshotName(pi.Prebuild.Snapshot) if err != nil { return nil, err } - info, err := ps.SignDownload(ctx, bkt, obj, &storage.SignedURLOptions{}) + } + + // Fire all SignDownload calls in parallel since they are independent network + // operations (each signs a different storage object URL). + type result struct { + key string + info *storage.DownloadInfo + err error + } + + // Count how many downloads we need to perform. + numDownloads := 1 // always check backup + if si != nil { + numDownloads++ + } + if pi != nil && pi.Prebuild != nil && pi.Prebuild.Snapshot != "" { + numDownloads++ + } + + results := make(chan result, numDownloads) + + // 1. Backup download + go func() { + backup, err := ps.SignDownload(ctx, rs.Bucket(workspaceOwner), rs.BackupObject(storage.DefaultBackup), &storage.SignedURLOptions{}) if err == storage.ErrNotFound { - // no prebuild found - that's fine + results <- result{key: storage.DefaultBackup, info: nil, err: nil} } else if err != nil { - return nil, xerrors.Errorf("cannot find prebuild: %w", err) + results <- result{key: storage.DefaultBackup, err: err} } else { - rc[pi.Prebuild.Snapshot] = *info + results <- result{key: storage.DefaultBackup, info: backup} + } + }() + + // 2. Snapshot download (if needed) + if si != nil { + go func() { + info, err := ps.SignDownload(ctx, snapshotBkt, snapshotObj, &storage.SignedURLOptions{}) + if err == storage.ErrNotFound { + results <- result{key: si.Snapshot, err: errCannotFindSnapshot} + } else if err != nil { + results <- result{key: si.Snapshot, err: xerrors.Errorf("cannot find snapshot: %w", err)} + } else { + results <- result{key: si.Snapshot, info: info} + } + }() + } + + // 3. Prebuild download (if needed) + if pi != nil && pi.Prebuild != nil && pi.Prebuild.Snapshot != "" { + go func() { + info, err := ps.SignDownload(ctx, prebuildBkt, prebuildObj, &storage.SignedURLOptions{}) + if err == storage.ErrNotFound { + // no prebuild found - that's fine + results <- result{key: pi.Prebuild.Snapshot, info: nil, err: nil} + } else if err != nil { + results <- result{key: pi.Prebuild.Snapshot, err: xerrors.Errorf("cannot find prebuild: %w", err)} + } else { + results <- result{key: pi.Prebuild.Snapshot, info: info} + } + }() + } + + // Collect results + for i := 0; i < numDownloads; i++ { + r := <-results + if r.err != nil { + return nil, r.err + } + if r.info != nil { + rc[r.key] = *r.info } } diff --git a/components/ws-manager-mk2/controllers/workspace_controller.go b/components/ws-manager-mk2/controllers/workspace_controller.go index 3577974f65f12a..ece9794445bc3a 100644 --- a/components/ws-manager-mk2/controllers/workspace_controller.go +++ b/components/ws-manager-mk2/controllers/workspace_controller.go @@ -511,21 +511,33 @@ func (r *WorkspaceReconciler) deleteWorkspaceSecrets(ctx context.Context, ws *wo defer tracing.FinishSpan(span, &err) log := log.FromContext(ctx).WithValues("owi", ws.OWI()) - // if a secret cannot be deleted we do not return early because we want to attempt - // the deletion of the remaining secrets + // Delete env and token secrets in parallel since they are independent operations. + var envErr, tokenErr error + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + if e := r.deleteSecret(ctx, fmt.Sprintf("%s-%s", ws.Name, "env"), r.Config.Namespace); e != nil { + envErr = e + log.Error(e, "could not delete environment secret", "workspace", ws.Name) + } + }() + go func() { + defer wg.Done() + if e := r.deleteSecret(ctx, fmt.Sprintf("%s-%s", ws.Name, "tokens"), r.Config.SecretsNamespace); e != nil { + tokenErr = e + log.Error(e, "could not delete token secret", "workspace", ws.Name) + } + }() + wg.Wait() + var errs []string - err = r.deleteSecret(ctx, fmt.Sprintf("%s-%s", ws.Name, "env"), r.Config.Namespace) - if err != nil { - errs = append(errs, err.Error()) - log.Error(err, "could not delete environment secret", "workspace", ws.Name) + if envErr != nil { + errs = append(errs, envErr.Error()) } - - err = r.deleteSecret(ctx, fmt.Sprintf("%s-%s", ws.Name, "tokens"), r.Config.SecretsNamespace) - if err != nil { - errs = append(errs, err.Error()) - log.Error(err, "could not delete token secret", "workspace", ws.Name) + if tokenErr != nil { + errs = append(errs, tokenErr.Error()) } - if len(errs) != 0 { return fmt.Errorf("%s", strings.Join(errs, ":")) } diff --git a/components/ws-manager-mk2/service/manager.go b/components/ws-manager-mk2/service/manager.go index fe4e4868012dd3..683697ed7e2812 100644 --- a/components/ws-manager-mk2/service/manager.go +++ b/components/ws-manager-mk2/service/manager.go @@ -298,14 +298,24 @@ func (wsm *WorkspaceManagerServer) StartWorkspace(ctx context.Context, req *wsma return nil, status.Errorf(codes.AlreadyExists, "workspace %s already exists", req.Metadata.MetaId) } - err = wsm.createWorkspaceSecret(ctx, &ws, envSecretName, wsm.Config.Namespace, envData) - if err != nil { - return nil, fmt.Errorf("cannot create env secret for workspace %s: %w", req.Id, err) + // Create env and token secrets in parallel since they are independent operations. + var envSecretErr, tokenSecretErr error + var secretWg sync.WaitGroup + secretWg.Add(2) + go func() { + defer secretWg.Done() + envSecretErr = wsm.createWorkspaceSecret(ctx, &ws, envSecretName, wsm.Config.Namespace, envData) + }() + go func() { + defer secretWg.Done() + tokenSecretErr = wsm.createWorkspaceSecret(ctx, &ws, fmt.Sprintf("%s-%s", req.Id, "tokens"), wsm.Config.SecretsNamespace, tokenData) + }() + secretWg.Wait() + if envSecretErr != nil { + return nil, fmt.Errorf("cannot create env secret for workspace %s: %w", req.Id, envSecretErr) } - - err = wsm.createWorkspaceSecret(ctx, &ws, fmt.Sprintf("%s-%s", req.Id, "tokens"), wsm.Config.SecretsNamespace, tokenData) - if err != nil { - return nil, fmt.Errorf("cannot create token secret for workspace %s: %w", req.Id, err) + if tokenSecretErr != nil { + return nil, fmt.Errorf("cannot create token secret for workspace %s: %w", req.Id, tokenSecretErr) } wsm.metrics.recordWorkspaceStart(&ws)