gitpod/dev/sweeper/main.go

127 lines
3.5 KiB
Go

// Copyright (c) 2020 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 main
import (
"database/sql"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"strings"
"time"
_ "github.com/go-sql-driver/mysql"
flag "github.com/spf13/pflag"
)
var (
dbUser string
dbPass string
dbHost string
dbName string
command string
period time.Duration
timeout time.Duration
readyEndpointAddr string
)
func init() {
flag.StringVar(&dbHost, "db-host", "db:3306", "database hostname")
flag.StringVar(&dbName, "db-name", "gitpod", "database name")
flag.StringVar(&dbUser, "db-user", "root", "database username")
flag.StringVar(&dbPass, "db-pass", "test", "database password")
flag.DurationVar(&timeout, "timeout", 4*time.Hour, "time until the dev-staging installation is removed - must be a valid duration")
flag.DurationVar(&period, "period", 1*time.Minute, "time between activity checks - must be a valid duration")
flag.StringVarP(&command, "command", "c", "echo time is up", "command to execute once we've timed out")
flag.StringVar(&readyEndpointAddr, "ready-endpoint-addr", ":8080", "address where to serve the Kubernetes ready endpoint")
}
func main() {
flag.Parse()
log.Printf("sweeper started")
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s", dbUser, dbPass, dbHost, dbName))
if err != nil {
log.Fatalf("cannot connect to DB: %+v", err)
return
}
go func() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "ready")
})
log.Printf("serving ready endpoint on %s", readyEndpointAddr)
log.Fatal(http.ListenAndServe(readyEndpointAddr, nil))
}()
tick := time.NewTicker(period)
for {
t0 := getLastActivity(db)
if t0 == nil {
log.Fatalf("cannot determine last activity")
return
}
dt := time.Since(*t0)
log.Printf("last activity: %v (%s ago, %s until timeout)", t0.Format(time.RFC3339), dt.String(), (timeout - dt).String())
if dt > timeout {
log.Printf("timeout after %s, executing command: %s", dt.String(), command)
segs := strings.Split(command, " ")
cmd := exec.Command(segs[0], segs[1:]...)
cmd.Env = os.Environ()
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
log.Printf("cannot run command \"%s\": %+v", command, err)
}
}
<-tick.C
}
}
func getLastActivity(db *sql.DB) (lastActivity *time.Time) {
log.Printf("attempting to determine last time of activity")
srcs := []struct {
Name string
Query string
Format string
}{
{"latest instance", "SELECT creationTime FROM d_b_workspace_instance ORDER BY creationTime DESC LIMIT 1", time.RFC3339},
{"latest user", "SELECT creationDate FROM d_b_user ORDER BY creationDate DESC LIMIT 1", time.RFC3339},
{"heartbeat", "SELECT lastSeen FROM d_b_workspace_instance_user ORDER BY lastSeen DESC LIMIT 1", "2006-01-02 15:04:05.999999"},
}
var errors []error
for _, src := range srcs {
var rt string
err := db.QueryRow(src.Query).Scan(&rt)
if err != nil {
log.Printf("cannot query %s: %+v", src.Name, err)
errors = append(errors, err)
continue
}
var t time.Time
t, err = time.Parse(src.Format, rt)
if err != nil {
log.Printf("cannot parse %s: %+v", src.Name, err)
errors = append(errors, err)
continue
}
if lastActivity == nil || t.After(*lastActivity) {
lastActivity = &t
}
}
return lastActivity
}