Skip to content

Commit

Permalink
# This is a combination of 2 commits.
Browse files Browse the repository at this point in the history
# This is the 1st commit message:

[usage] Refactor controller package into scheduler

# This is the commit message #2:

[usage] Refactor controller package into scheduler
  • Loading branch information
easyCZ authored and iQQBot committed Sep 14, 2022
1 parent deb1c5a commit c99cf66
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 183 deletions.
2 changes: 1 addition & 1 deletion components/ide-metrics/pkg/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func Test_allowListCollector_Reconcile(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := c.Reconcile(tt.args.labels); !reflect.DeepEqual(got, tt.want) {
t.Errorf("allowListCollector.Reconcile() = %v, want %v", got, tt.want)
t.Errorf("allowListCollector.Run() = %v, want %v", got, tt.want)
}
})
}
Expand Down
84 changes: 0 additions & 84 deletions components/usage/pkg/controller/controller.go

This file was deleted.

55 changes: 0 additions & 55 deletions components/usage/pkg/controller/controller_test.go

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,72 @@
// Licensed under the GNU Affero General Public License (AGPL).
// See License-AGPL.txt in the project root for license information.

package controller
package scheduler

import (
"context"
"fmt"
"github.com/gitpod-io/gitpod/common-go/log"
v1 "github.com/gitpod-io/gitpod/usage-api/v1"
"github.com/robfig/cron"
"google.golang.org/protobuf/types/known/timestamppb"
"time"
)

type Reconciler interface {
Reconcile() error
type Job interface {
Run() error
}

type ReconcilerFunc func() error
func NewLedgerTriggerJobSpec(schedule time.Duration, job Job) (JobSpec, error) {
parsed, err := cron.Parse(fmt.Sprintf("@every %s", schedule.String()))
if err != nil {
return JobSpec{}, fmt.Errorf("failed to parse ledger job schedule: %w", err)
}

func (f ReconcilerFunc) Reconcile() error {
return f()
return JobSpec{
Job: job,
ID: "ledger",
Schedule: parsed,
}, nil
}

func NewLedgerReconciler(usageClient v1.UsageServiceClient, billingClient v1.BillingServiceClient) *LedgerReconciler {
return &LedgerReconciler{
func NewLedgerTrigger(usageClient v1.UsageServiceClient, billingClient v1.BillingServiceClient) *LedgerJob {
return &LedgerJob{
usageClient: usageClient,
billingClient: billingClient,

running: make(chan struct{}, 1),
}
}

type LedgerReconciler struct {
type LedgerJob struct {
usageClient v1.UsageServiceClient
billingClient v1.BillingServiceClient

running chan struct{}
}

func (r *LedgerReconciler) Reconcile() (err error) {
func (r *LedgerJob) Run() (err error) {
ctx := context.Background()

select {
// attempt a write to signal we want to run
case r.running <- struct{}{}:
// we managed to write, there's no other job executing. Cases are not fall through so we continue executing our main logic.
default:
// we could not write, so another instance is already running. Skip current run.
log.Infof("Skipping ledger run, another run is already in progress.")
return nil
}

now := time.Now().UTC()
hourAgo := now.Add(-1 * time.Hour)

reportUsageReconcileStarted()
defer func() {
reportUsageReconcileFinished(time.Since(now), err)
}()

logger := log.
WithField("from", hourAgo).
WithField("to", now)

logger.Info("Starting ledger reconciliation.")
logger.Info("Running ledger job. Reconciling usage records.")
_, err = r.usageClient.ReconcileUsageWithLedger(ctx, &v1.ReconcileUsageWithLedgerRequest{
From: timestamppb.New(hourAgo),
To: timestamppb.New(now),
Expand Down
63 changes: 63 additions & 0 deletions components/usage/pkg/scheduler/job_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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 scheduler

import (
"context"
v1 "github.com/gitpod-io/gitpod/usage-api/v1"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"sync"
"sync/atomic"
"testing"
"time"
)

func TestLedgerJob_PreventsConcurrentInvocations(t *testing.T) {
client := &fakeUsageClient{}
job := NewLedgerTrigger(client, nil)

invocations := 3
wg := sync.WaitGroup{}
wg.Add(invocations)
for i := 0; i < invocations; i++ {
go func() {
_ = job.Run()
wg.Done()
}()
}
wg.Wait()

require.Equal(t, 1, int(client.ReconcileUsageWithLedgerCallCount))
}

type fakeUsageClient struct {
ReconcileUsageWithLedgerCallCount int32
}

// GetCostCenter retrieves the active cost center for the given attributionID
func (c *fakeUsageClient) GetCostCenter(ctx context.Context, in *v1.GetCostCenterRequest, opts ...grpc.CallOption) (*v1.GetCostCenterResponse, error) {
return nil, status.Error(codes.Unauthenticated, "not implemented")
}

// SetCostCenter stores the given cost center
func (c *fakeUsageClient) SetCostCenter(ctx context.Context, in *v1.SetCostCenterRequest, opts ...grpc.CallOption) (*v1.SetCostCenterResponse, error) {
return nil, status.Error(codes.Unauthenticated, "not implemented")
}

// Triggers reconciliation of usage with ledger implementation.
func (c *fakeUsageClient) ReconcileUsageWithLedger(ctx context.Context, in *v1.ReconcileUsageWithLedgerRequest, opts ...grpc.CallOption) (*v1.ReconcileUsageWithLedgerResponse, error) {
atomic.AddInt32(&c.ReconcileUsageWithLedgerCallCount, 1)
time.Sleep(1 * time.Second)

return nil, status.Error(codes.Unauthenticated, "not implemented")
}

// ListUsage retrieves all usage for the specified attributionId and theb given time range
func (c *fakeUsageClient) ListUsage(ctx context.Context, in *v1.ListUsageRequest, opts ...grpc.CallOption) (*v1.ListUsageResponse, error) {
return nil, status.Error(codes.Unauthenticated, "not implemented")
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the GNU Affero General Public License (AGPL).
// See License-AGPL.txt in the project root for license information.

package controller
package scheduler

import (
"fmt"
Expand All @@ -16,26 +16,26 @@ const (
)

var (
reconcileStartedTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
jobStartedSeconds = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "reconcile_started_total",
Help: "Number of usage reconciliation runs started",
}, []string{})
Name: "scheduler_job_started",
Help: "Number of jobs started",
}, []string{"job"})

reconcileStartedDurationSeconds = prometheus.NewHistogramVec(prometheus.HistogramOpts{
jobCompletedSeconds = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "reconcile_completed_duration_seconds",
Help: "Histogram of reconcile duration",
Name: "scheduler_job_completed_seconds",
Help: "Histogram of job duration",
Buckets: prometheus.LinearBuckets(30, 30, 10), // every 30 secs, starting at 30secs
}, []string{"outcome"})
}, []string{"job", "outcome"})
)

func RegisterMetrics(reg *prometheus.Registry) error {
metrics := []prometheus.Collector{
reconcileStartedTotal,
reconcileStartedDurationSeconds,
jobStartedSeconds,
jobCompletedSeconds,
}
for _, metric := range metrics {
err := reg.Register(metric)
Expand All @@ -47,14 +47,14 @@ func RegisterMetrics(reg *prometheus.Registry) error {
return nil
}

func reportUsageReconcileStarted() {
reconcileStartedTotal.WithLabelValues().Inc()
func reportJobStarted(id string) {
jobStartedSeconds.WithLabelValues(id).Inc()
}

func reportUsageReconcileFinished(duration time.Duration, err error) {
func reportJobCompleted(id string, duration time.Duration, err error) {
outcome := "success"
if err != nil {
outcome = "error"
}
reconcileStartedDurationSeconds.WithLabelValues(outcome).Observe(duration.Seconds())
jobCompletedSeconds.WithLabelValues(id, outcome).Observe(duration.Seconds())
}
Loading

0 comments on commit c99cf66

Please sign in to comment.