From 437e19ce291e9ece62787a77480637c60ce4e9fd Mon Sep 17 00:00:00 2001 From: Milan Pavlik Date: Tue, 24 May 2022 19:50:46 +0000 Subject: [PATCH] [usage] Setup controller and reconciler --- components/usage/cmd/run.go | 15 ++++- components/usage/go.mod | 1 + components/usage/go.sum | 2 + components/usage/pkg/controller/controller.go | 66 +++++++++++++++++++ .../usage/pkg/controller/controller_test.go | 37 +++++++++++ components/usage/pkg/controller/reconciler.go | 22 +++++++ 6 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 components/usage/pkg/controller/controller.go create mode 100644 components/usage/pkg/controller/controller_test.go create mode 100644 components/usage/pkg/controller/reconciler.go diff --git a/components/usage/cmd/run.go b/components/usage/cmd/run.go index 543d927472a7cb..274adb00c92b0c 100644 --- a/components/usage/cmd/run.go +++ b/components/usage/cmd/run.go @@ -7,10 +7,12 @@ package cmd import ( "github.com/gitpod-io/gitpod/common-go/baseserver" "github.com/gitpod-io/gitpod/common-go/log" + "github.com/gitpod-io/gitpod/usage/pkg/controller" "github.com/gitpod-io/gitpod/usage/pkg/db" "github.com/spf13/cobra" "net" "os" + "time" ) func init() { @@ -29,8 +31,6 @@ func run() *cobra.Command { Run: func(cmd *cobra.Command, args []string) { log.Init(ServiceName, Version, true, verbose) - log.Info("Hello world usage server") - _, err := db.Connect(db.ConnectionParams{ User: os.Getenv("DB_USERNAME"), Password: os.Getenv("DB_PASSWORD"), @@ -41,6 +41,17 @@ func run() *cobra.Command { log.WithError(err).Fatal("Failed to establish database connection.") } + ctrl, err := controller.New(1*time.Minute, controller.ReconcilerFunc(controller.HelloWorldReconciler)) + if err != nil { + log.WithError(err).Fatal("Failed to initialize usage controller.") + } + + err = ctrl.Start() + if err != nil { + log.WithError(err).Fatal("Failed to start usage controller.") + } + defer ctrl.Stop() + srv, err := baseserver.New("usage") if err != nil { log.WithError(err).Fatal("Failed to initialize server.") diff --git a/components/usage/go.mod b/components/usage/go.mod index ab59b0781aa252..3b23b2aecb52f5 100644 --- a/components/usage/go.mod +++ b/components/usage/go.mod @@ -58,6 +58,7 @@ require ( github.com/gitpod-io/gitpod/common-go v0.0.0-00010101000000-000000000000 github.com/go-sql-driver/mysql v1.6.0 github.com/relvacode/iso8601 v1.1.0 + github.com/robfig/cron v1.2.0 github.com/spf13/cobra v1.4.0 github.com/stretchr/testify v1.7.0 gorm.io/datatypes v1.0.6 diff --git a/components/usage/go.sum b/components/usage/go.sum index 79748a3221cd35..87bcd0bd169a77 100644 --- a/components/usage/go.sum +++ b/components/usage/go.sum @@ -299,6 +299,8 @@ github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0 github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/relvacode/iso8601 v1.1.0 h1:2nV8sp0eOjpoKQ2vD3xSDygsjAx37NHG2UlZiCkDH4I= github.com/relvacode/iso8601 v1.1.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= diff --git a/components/usage/pkg/controller/controller.go b/components/usage/pkg/controller/controller.go new file mode 100644 index 00000000000000..8a6ae3189e0213 --- /dev/null +++ b/components/usage/pkg/controller/controller.go @@ -0,0 +1,66 @@ +// Copyright (c) 2022 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 controller + +import ( + "fmt" + "github.com/gitpod-io/gitpod/common-go/log" + "github.com/robfig/cron" + "sync" + "time" +) + +func New(schedule time.Duration, reconciler Reconciler) (*Controller, error) { + return &Controller{ + schedule: schedule, + reconciler: reconciler, + scheduler: cron.NewWithLocation(time.UTC), + }, nil +} + +type Controller struct { + schedule time.Duration + reconciler Reconciler + + scheduler *cron.Cron + + runningJobs sync.WaitGroup +} + +func (c *Controller) Start() error { + log.Info("Starting usage controller.") + + err := c.scheduler.AddFunc(fmt.Sprintf("@every %s", c.schedule.String()), cron.FuncJob(func() { + log.Info("Starting usage reconciliation.") + + c.runningJobs.Add(1) + defer c.runningJobs.Done() + + err := c.reconciler.Reconcile() + if err != nil { + log.WithError(err).Errorf("Reconciliation run failed.") + } else { + log.Info("Completed usage reconciliation run without errors.") + } + })) + if err != nil { + return fmt.Errorf("failed to add function to scheduler: %w", err) + } + + c.scheduler.Start() + + return nil +} + +// Stop terminates the Controller and awaits for all running jobs to complete. +func (c *Controller) Stop() { + log.Info("Stopping usage controller.") + // Stop any new jobs from running + c.scheduler.Stop() + + log.Info("Awaiting existing reconciliation runs to complete..") + // Wait for existing jobs to finish + c.runningJobs.Wait() +} diff --git a/components/usage/pkg/controller/controller_test.go b/components/usage/pkg/controller/controller_test.go new file mode 100644 index 00000000000000..b639dfa5abdcdd --- /dev/null +++ b/components/usage/pkg/controller/controller_test.go @@ -0,0 +1,37 @@ +// Copyright (c) 2022 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 controller + +import ( + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestController(t *testing.T) { + schedule := time.Second + triggered := false + + ctrl, err := New(schedule, ReconcilerFunc(func() error { + triggered = true + return nil + })) + require.NoError(t, err) + + require.NoError(t, ctrl.Start()) + time.Sleep(schedule + 20*time.Millisecond) + require.True(t, triggered, "must trigger reconciler function") + ctrl.Stop() +} + +func TestController_GracefullyHandlesPanic(t *testing.T) { + ctrl, err := New(20*time.Millisecond, ReconcilerFunc(func() error { + panic("pls help") + })) + require.NoError(t, err) + + require.NoError(t, ctrl.Start()) + ctrl.Stop() +} diff --git a/components/usage/pkg/controller/reconciler.go b/components/usage/pkg/controller/reconciler.go new file mode 100644 index 00000000000000..fa844330fd3418 --- /dev/null +++ b/components/usage/pkg/controller/reconciler.go @@ -0,0 +1,22 @@ +// Copyright (c) 2022 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 controller + +import "github.com/gitpod-io/gitpod/common-go/log" + +type Reconciler interface { + Reconcile() error +} + +type ReconcilerFunc func() error + +func (f ReconcilerFunc) Reconcile() error { + return f() +} + +func HelloWorldReconciler() error { + log.Info("Hello world reconciler!") + return nil +}