Path: blob/main/components/ws-daemon/pkg/controller/housekeeping.go
2500 views
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package controller56import (7"context"8"errors"9"fmt"10"io/fs"11"os"12"path/filepath"13"strings"14"time"1516"github.com/gitpod-io/gitpod/common-go/log"17"github.com/gitpod-io/gitpod/common-go/tracing"18"github.com/opentracing/opentracing-go"19)2021const minContentGCAge = 1 * time.Hour2223type Housekeeping struct {24Location string25Interval time.Duration26}2728func NewHousekeeping(location string, interval time.Duration) *Housekeeping {29return &Housekeeping{30Location: location,31Interval: interval,32}33}3435func (h *Housekeeping) Start(ctx context.Context) {36span, ctx := opentracing.StartSpanFromContext(ctx, "Housekeeping.Start")37defer tracing.FinishSpan(span, nil)38log.WithField("interval", h.Interval.String()).Debug("started workspace housekeeping")3940ticker := time.NewTicker(h.Interval)41defer ticker.Stop()4243run := true44for run {45var errs []error46select {47case <-ticker.C:48errs = h.doHousekeeping(ctx)49case <-ctx.Done():50run = false51}5253for _, err := range errs {54log.WithError(err).Error("error during housekeeping")55}56}5758span.Finish()59log.Debug("stopping workspace housekeeping")60}6162func (h *Housekeeping) doHousekeeping(ctx context.Context) (errs []error) {63span, _ := opentracing.StartSpanFromContext(ctx, "doHousekeeping")64defer func() {65msgs := make([]string, len(errs))66for i, err := range errs {67msgs[i] = err.Error()68}6970var err error71if len(msgs) > 0 {72err = fmt.Errorf("%s", strings.Join(msgs, ". "))73}74tracing.FinishSpan(span, &err)75}()7677errs = make([]error, 0)7879// Find workspace directories which are left over.80files, err := os.ReadDir(h.Location)81if err != nil {82return []error{fmt.Errorf("cannot list existing workspaces content directory: %w", err)}83}8485for _, f := range files {86if !f.IsDir() {87continue88}8990// If this is the -daemon directory, make sure we assume the correct state file name91name := f.Name()92name = strings.TrimSuffix(name, string(filepath.Separator))93name = strings.TrimSuffix(name, "-daemon")9495if _, err := os.Stat(filepath.Join(h.Location, fmt.Sprintf("%s.workspace.json", name))); !errors.Is(err, fs.ErrNotExist) {96continue97}9899// We have found a workspace content directory without a workspace state file, which means we don't manage this folder.100// Within the working area/location of a session store we must be the only one who creates directories, because we want to101// make sure we don't leak files over time.102103// For good measure we wait a while before deleting that directory.104nfo, err := f.Info()105if err != nil {106log.WithError(err).Warn("Found workspace content directory without a corresponding state file, but could not retrieve its info")107errs = append(errs, err)108continue109}110if time.Since(nfo.ModTime()) < minContentGCAge {111continue112}113114err = os.RemoveAll(filepath.Join(h.Location, f.Name()))115if err != nil {116log.WithError(err).Warn("Found workspace content directory without a corresponding state file, but could not delete the content directory")117errs = append(errs, err)118continue119}120121log.WithField("directory", f.Name()).Info("deleted workspace content directory without corresponding state file")122}123124return errs125}126127128