9
9
"fmt"
10
10
"github.com/gitpod-io/gitpod/common-go/log"
11
11
"github.com/gitpod-io/gitpod/usage/pkg/db"
12
+ "github.com/google/uuid"
12
13
"gorm.io/gorm"
13
14
"time"
14
15
)
@@ -23,12 +24,20 @@ func (f ReconcilerFunc) Reconcile() error {
23
24
return f ()
24
25
}
25
26
27
+ type UsageReconciler struct {
28
+ conn * gorm.DB
29
+ }
30
+
26
31
func NewUsageReconciler (conn * gorm.DB ) * UsageReconciler {
27
32
return & UsageReconciler {conn : conn }
28
33
}
29
34
30
- type UsageReconciler struct {
31
- conn * gorm.DB
35
+ type UsageReconcileStatus struct {
36
+ StartTime time.Time
37
+ EndTime time.Time
38
+
39
+ WorkspaceInstances int
40
+ InvalidWorkspaceInstances int
32
41
}
33
42
34
43
func (u * UsageReconciler ) Reconcile () error {
@@ -38,16 +47,100 @@ func (u *UsageReconciler) Reconcile() error {
38
47
startOfCurrentMonth := time .Date (now .Year (), now .Month (), 1 , 0 , 0 , 0 , 0 , time .UTC )
39
48
startOfNextMonth := startOfCurrentMonth .AddDate (0 , 1 , 0 )
40
49
41
- return u .reconcile (ctx , startOfCurrentMonth , startOfNextMonth )
50
+ status , err := u .ReconcileTimeRange (ctx , startOfCurrentMonth , startOfNextMonth )
51
+ if err != nil {
52
+ return err
53
+ }
54
+ log .WithField ("usage_reconcile_status" , status ).Info ("Reconcile completed." )
55
+ return nil
42
56
}
43
57
44
- func (u * UsageReconciler ) reconcile (ctx context.Context , from , to time.Time ) error {
58
+ func (u * UsageReconciler ) ReconcileTimeRange (ctx context.Context , from , to time.Time ) ( * UsageReconcileStatus , error ) {
45
59
log .Infof ("Gathering usage data from %s to %s" , from , to )
46
- instances , err := db .ListWorkspaceInstancesInRange (ctx , u .conn , from , to )
60
+ status := & UsageReconcileStatus {
61
+ StartTime : from ,
62
+ EndTime : to ,
63
+ }
64
+ instances , invalidInstances , err := u .loadWorkspaceInstances (ctx , from , to )
47
65
if err != nil {
48
- return fmt .Errorf ("failed to list instances: %w" , err )
66
+ return nil , fmt .Errorf ("failed to load workspace instances: %w" , err )
49
67
}
68
+ status .WorkspaceInstances = len (instances )
69
+ status .InvalidWorkspaceInstances = len (invalidInstances )
50
70
71
+ if len (invalidInstances ) > 0 {
72
+ log .WithField ("invalid_workspace_instances" , invalidInstances ).Errorf ("Detected %d invalid instances. These will be skipped in the current run." , len (invalidInstances ))
73
+ }
74
+
75
+ log .WithField ("workspace_instances" , instances ).Debug ("Successfully loaded workspace instances." )
76
+ return status , nil
77
+ }
78
+
79
+ func (u * UsageReconciler ) loadWorkspaceInstances (ctx context.Context , from , to time.Time ) ([]db.WorkspaceInstance , []invalidWorkspaceInstance , error ) {
80
+ log .Infof ("Gathering usage data from %s to %s" , from , to )
81
+ instances , err := db .ListWorkspaceInstancesInRange (ctx , u .conn , from , to )
82
+ if err != nil {
83
+ return nil , nil , fmt .Errorf ("failed to list instances from db: %w" , err )
84
+ }
51
85
log .Infof ("Identified %d instances between %s and %s" , len (instances ), from , to )
52
- return nil
86
+
87
+ valid , invalid := validateInstances (instances )
88
+ trimmed := trimStartStopTime (valid , from , to )
89
+ return trimmed , invalid , nil
90
+ }
91
+
92
+ type invalidWorkspaceInstance struct {
93
+ reason string
94
+ workspaceInstanceID uuid.UUID
95
+ }
96
+
97
+ func validateInstances (instances []db.WorkspaceInstance ) (valid []db.WorkspaceInstance , invalid []invalidWorkspaceInstance ) {
98
+ for _ , i := range instances {
99
+ // i is a pointer to the current element, we need to assign it to ensure we're copying the value, not the current pointer.
100
+ instance := i
101
+
102
+ // Each instance must have a start time, without it, we do not have a baseline for usage computation.
103
+ if ! instance .CreationTime .IsSet () {
104
+ invalid = append (invalid , invalidWorkspaceInstance {
105
+ reason : "missing creation time" ,
106
+ workspaceInstanceID : instance .ID ,
107
+ })
108
+ continue
109
+ }
110
+
111
+ start := instance .CreationTime .Time ()
112
+
113
+ // Currently running instances do not have a stopped time set, so we ignore these.
114
+ if instance .StoppedTime .IsSet () {
115
+ stop := instance .StoppedTime .Time ()
116
+ if stop .Before (start ) {
117
+ invalid = append (invalid , invalidWorkspaceInstance {
118
+ reason : "stop time is before start time" ,
119
+ workspaceInstanceID : instance .ID ,
120
+ })
121
+ continue
122
+ }
123
+ }
124
+
125
+ valid = append (valid , instance )
126
+ }
127
+ return valid , invalid
128
+ }
129
+
130
+ // trimStartStopTime ensures that start time or stop time of an instance is never outside of specified start or stop time range.
131
+ func trimStartStopTime (instances []db.WorkspaceInstance , maximumStart , minimumStop time.Time ) []db.WorkspaceInstance {
132
+ var updated []db.WorkspaceInstance
133
+
134
+ for _ , instance := range instances {
135
+ if instance .StartedTime .Time ().Before (maximumStart ) {
136
+ instance .StartedTime = db .NewVarcharTime (maximumStart )
137
+ }
138
+
139
+ if instance .StoppedTime .Time ().After (minimumStop ) {
140
+ instance .StoppedTime = db .NewVarcharTime (minimumStop )
141
+ }
142
+
143
+ updated = append (updated , instance )
144
+ }
145
+ return updated
53
146
}
0 commit comments