Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/gitpod-db/go/workspace_instance.go
2498 views
1
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package db
6
7
import (
8
"context"
9
"database/sql"
10
"fmt"
11
"strings"
12
"time"
13
14
"github.com/gitpod-io/gitpod/common-go/log"
15
16
"github.com/google/uuid"
17
"gorm.io/datatypes"
18
"gorm.io/gorm"
19
)
20
21
type WorkspaceInstance struct {
22
ID uuid.UUID `gorm:"primary_key;column:id;type:char;size:36;" json:"id"`
23
WorkspaceID string `gorm:"column:workspaceId;type:char;size:36;" json:"workspaceId"`
24
Configuration datatypes.JSON `gorm:"column:configuration;type:text;size:65535;" json:"configuration"`
25
Region string `gorm:"column:region;type:varchar;size:255;" json:"region"`
26
ImageBuildInfo sql.NullString `gorm:"column:imageBuildInfo;type:text;size:65535;" json:"imageBuildInfo"`
27
IdeURL string `gorm:"column:ideUrl;type:varchar;size:255;" json:"ideUrl"`
28
WorkspaceBaseImage string `gorm:"column:workspaceBaseImage;type:varchar;size:255;" json:"workspaceBaseImage"`
29
WorkspaceImage string `gorm:"column:workspaceImage;type:varchar;size:255;" json:"workspaceImage"`
30
UsageAttributionID AttributionID `gorm:"column:usageAttributionId;type:varchar;size:60;" json:"usageAttributionId"`
31
WorkspaceClass string `gorm:"column:workspaceClass;type:varchar;size:255;" json:"workspaceClass"`
32
33
CreationTime VarcharTime `gorm:"column:creationTime;type:varchar;size:255;" json:"creationTime"`
34
StartedTime VarcharTime `gorm:"column:startedTime;type:varchar;size:255;" json:"startedTime"`
35
DeployedTime VarcharTime `gorm:"column:deployedTime;type:varchar;size:255;" json:"deployedTime"`
36
StoppedTime VarcharTime `gorm:"column:stoppedTime;type:varchar;size:255;" json:"stoppedTime"`
37
LastModified time.Time `gorm:"column:_lastModified;type:timestamp;default:CURRENT_TIMESTAMP(6);" json:"_lastModified"`
38
StoppingTime VarcharTime `gorm:"column:stoppingTime;type:varchar;size:255;" json:"stoppingTime"`
39
40
LastHeartbeat string `gorm:"column:lastHeartbeat;type:varchar;size:255;" json:"lastHeartbeat"`
41
StatusOld sql.NullString `gorm:"column:status_old;type:varchar;size:255;" json:"status_old"`
42
Status datatypes.JSON `gorm:"column:status;type:json;" json:"status"`
43
// Phase is derived from Status by extracting JSON from it. Read-only (-> property).
44
Phase sql.NullString `gorm:"->:column:phase;type:char;size:32;" json:"phase"`
45
PhasePersisted string `gorm:"column:phasePersisted;type:char;size:32;" json:"phasePersisted"`
46
47
// deleted is restricted for use by periodic deleter
48
_ bool `gorm:"column:deleted;type:tinyint;default:0;" json:"deleted"`
49
}
50
51
// TableName sets the insert table name for this struct type
52
func (i *WorkspaceInstance) TableName() string {
53
return "d_b_workspace_instance"
54
}
55
56
// FindStoppedWorkspaceInstancesInRange finds WorkspaceInstanceForUsage that have been stopped between from (inclusive) and to (exclusive).
57
func FindStoppedWorkspaceInstancesInRange(ctx context.Context, conn *gorm.DB, from, to time.Time) ([]WorkspaceInstanceForUsage, error) {
58
var instances []WorkspaceInstanceForUsage
59
var instancesInBatch []WorkspaceInstanceForUsage
60
61
tx := queryWorkspaceInstanceForUsage(ctx, conn).
62
Where("wsi.stoppingTime >= ?", TimeToISO8601(from)).
63
Where("wsi.stoppingTime < ?", TimeToISO8601(to)).
64
Where("wsi.stoppingTime != ?", "").
65
Where("wsi.usageAttributionId != ?", "").
66
FindInBatches(&instancesInBatch, 1000, func(_ *gorm.DB, _ int) error {
67
instances = append(instances, instancesInBatch...)
68
return nil
69
})
70
if tx.Error != nil {
71
return nil, fmt.Errorf("failed to find workspace instances: %w", tx.Error)
72
}
73
74
return instances, nil
75
}
76
77
// FindRunningWorkspaceInstances finds WorkspaceInstanceForUsage that are running at the point in time the query is executed.
78
func FindRunningWorkspaceInstances(ctx context.Context, conn *gorm.DB) ([]WorkspaceInstanceForUsage, error) {
79
var instances []WorkspaceInstanceForUsage
80
var instancesInBatch []WorkspaceInstanceForUsage
81
82
tx := queryWorkspaceInstanceForUsage(ctx, conn).
83
Where("wsi.phasePersisted = ?", "running").
84
// We are only interested in instances that have been started within the last 10 days.
85
Where("wsi.startedTime > ?", TimeToISO8601(time.Now().Add(-10*24*time.Hour))).
86
// All other selectors are there to ensure data quality
87
Where("wsi.stoppingTime = ?", "").
88
Where("wsi.usageAttributionId != ?", "").
89
FindInBatches(&instancesInBatch, 1000, func(_ *gorm.DB, _ int) error {
90
instances = append(instances, instancesInBatch...)
91
return nil
92
})
93
if tx.Error != nil {
94
return nil, fmt.Errorf("failed to find running workspace instances: %w", tx.Error)
95
}
96
97
return instances, nil
98
}
99
100
// FindWorkspaceInstancesByIds finds WorkspaceInstanceForUsage by Id.
101
func FindWorkspaceInstancesByIds(ctx context.Context, conn *gorm.DB, workspaceInstanceIds []uuid.UUID) ([]WorkspaceInstanceForUsage, error) {
102
var instances []WorkspaceInstanceForUsage
103
var instancesInBatch []WorkspaceInstanceForUsage
104
var idChunks [][]uuid.UUID
105
chunkSize, totalSize := 1000, len(workspaceInstanceIds)
106
// explicit batching to reduce the lengths of the 'in'-part in the SELECT statement below
107
for i := 0; i < totalSize; i += chunkSize {
108
end := i + chunkSize
109
if end > totalSize {
110
end = totalSize
111
}
112
idChunks = append(idChunks, workspaceInstanceIds[i:end])
113
}
114
115
for _, idChunk := range idChunks {
116
err := queryWorkspaceInstanceForUsage(ctx, conn).
117
Where("wsi.id in ?", idChunk).
118
Where("wsi.usageAttributionId != ?", "").
119
Find(&instancesInBatch).Error
120
if err != nil {
121
return nil, fmt.Errorf("failed to find workspace instances by id: %w", err)
122
}
123
instances = append(instances, instancesInBatch...)
124
}
125
126
return instances, nil
127
}
128
129
func queryWorkspaceInstanceForUsage(ctx context.Context, conn *gorm.DB) *gorm.DB {
130
return conn.WithContext(ctx).
131
Table(fmt.Sprintf("%s as wsi", (&WorkspaceInstance{}).TableName())).
132
Select("wsi.id as id, "+
133
"ws.projectId as projectId, "+
134
"ws.contextUrl as contextUrl, "+
135
"ws.type as workspaceType, "+
136
"wsi.workspaceClass as workspaceClass, "+
137
"wsi.usageAttributionId as usageAttributionId, "+
138
"wsi.creationTime as creationTime, "+
139
"wsi.startedTime as startedTime, "+
140
"wsi.stoppingTime as stoppingTime, "+
141
"wsi.stoppedTime as stoppedTime, "+
142
"ws.ownerId as ownerId, "+
143
"wsi.workspaceId as workspaceId, "+
144
"ws.ownerId as userId, "+
145
"u.name as userName, "+
146
"u.avatarURL as userAvatarURL ",
147
).
148
Joins(fmt.Sprintf("LEFT JOIN %s AS ws ON wsi.workspaceId = ws.id", (&Workspace{}).TableName())).
149
Joins(fmt.Sprintf("LEFT JOIN %s AS u ON ws.ownerId = u.id", "d_b_user")).
150
// Instances without a StartedTime never actually started, we're not interested in these.
151
Where("wsi.startedTime != ?", "")
152
}
153
154
const (
155
attributionEntity_Team = "team"
156
)
157
158
func NewTeamAttributionID(teamID string) AttributionID {
159
return AttributionID(fmt.Sprintf("%s:%s", attributionEntity_Team, teamID))
160
}
161
162
// AttributionID consists of an entity, and an identifier in the form:
163
// <entity>:<identifier>, e.g. team:a7dcf253-f05e-4dcf-9a47-cf8fccc74717
164
type AttributionID string
165
166
func (a AttributionID) Values() (entity string, identifier string) {
167
tokens := strings.Split(string(a), ":")
168
if len(tokens) != 2 || tokens[0] != attributionEntity_Team || tokens[1] == "" {
169
return "", ""
170
}
171
172
return tokens[0], tokens[1]
173
}
174
175
func ParseAttributionID(s string) (AttributionID, error) {
176
tokens := strings.Split(s, ":")
177
if len(tokens) != 2 {
178
return "", fmt.Errorf("attribution ID (%s) does not have two parts", s)
179
}
180
_, err := uuid.Parse(tokens[1])
181
if err != nil {
182
return "", fmt.Errorf("The uuid part of attribution ID (%s) is not a valid UUID. %w", tokens[1], err)
183
}
184
185
switch tokens[0] {
186
case attributionEntity_Team:
187
return NewTeamAttributionID(tokens[1]), nil
188
default:
189
return "", fmt.Errorf("unknown attribution ID type: %s", s)
190
}
191
}
192
193
const (
194
WorkspaceClass_Default = "default"
195
)
196
197
type WorkspaceInstanceForUsage struct {
198
ID uuid.UUID `gorm:"column:id;type:char;size:36;" json:"id"`
199
WorkspaceID string `gorm:"column:workspaceId;type:char;size:36;" json:"workspaceId"`
200
OwnerID uuid.UUID `gorm:"column:ownerId;type:char;size:36;" json:"ownerId"`
201
ProjectID sql.NullString `gorm:"column:projectId;type:char;size:36;" json:"projectId"`
202
WorkspaceClass string `gorm:"column:workspaceClass;type:varchar;size:255;" json:"workspaceClass"`
203
Type WorkspaceType `gorm:"column:workspaceType;type:char;size:16;default:regular;" json:"workspaceType"`
204
UsageAttributionID AttributionID `gorm:"column:usageAttributionId;type:varchar;size:60;" json:"usageAttributionId"`
205
ContextURL string `gorm:"column:contextUrl;type:varchar;size:255;" json:"contextUrl"`
206
UserID uuid.UUID `gorm:"column:userId;type:varchar;size:255;" json:"userId"`
207
UserName string `gorm:"column:userName;type:varchar;size:255;" json:"userName"`
208
UserAvatarURL string `gorm:"column:userAvatarURL;type:varchar;size:255;" json:"userAvatarURL"`
209
210
CreationTime VarcharTime `gorm:"column:creationTime;type:varchar;size:255;" json:"creationTime"`
211
StartedTime VarcharTime `gorm:"column:startedTime;type:varchar;size:255;" json:"startedTime"`
212
StoppingTime VarcharTime `gorm:"column:stoppingTime;type:varchar;size:255;" json:"stoppingTime"`
213
StoppedTime VarcharTime `gorm:"column:stoppedTime;type:varchar;size:255;" json:"stoppedTime"`
214
}
215
216
// WorkspaceRuntimeSeconds computes how long this WorkspaceInstance has been running.
217
// If the instance is still running (no stopping time set), maxStopTime is used to to compute the duration - this is an upper bound on stop
218
func (i *WorkspaceInstanceForUsage) WorkspaceRuntimeSeconds(stopTimeIfInstanceIsStillRunning time.Time) int64 {
219
start := i.StartedTime.Time()
220
stop := stopTimeIfInstanceIsStillRunning
221
222
if i.StoppingTime.IsSet() {
223
stop = i.StoppingTime.Time()
224
} else if i.StoppedTime.IsSet() {
225
stop = i.StoppedTime.Time()
226
}
227
228
if stop.Before(start) {
229
log.
230
WithField("instance_id", i.ID).
231
WithField("workspace_id", i.WorkspaceID).
232
WithField("started_time", TimeToISO8601(i.StartedTime.Time())).
233
WithField("started_time_set", i.StartedTime.IsSet()).
234
WithField("stopping_time_set", i.StoppingTime.IsSet()).
235
WithField("stopping_time", TimeToISO8601(i.StoppingTime.Time())).
236
WithField("stopped_time_set", i.StoppedTime.IsSet()).
237
WithField("stopped_time", TimeToISO8601(i.StoppedTime.Time())).
238
WithField("stop_time_if_instance_still_running", stopTimeIfInstanceIsStillRunning).
239
Errorf("Instance %s had stop time before start time. Using startedTime as stop time.", i.ID)
240
241
stop = start
242
}
243
244
return int64(stop.Sub(start).Round(time.Second).Seconds())
245
}
246
247
func ListWorkspaceInstanceIDsWithPhaseStoppedButNoStoppingTime(ctx context.Context, conn *gorm.DB) ([]uuid.UUID, error) {
248
var ids []uuid.UUID
249
//var chunk []uuid.UUID
250
251
tx := conn.WithContext(ctx).
252
Table(fmt.Sprintf("%s as wsi", (&WorkspaceInstance{}).TableName())).
253
Joins(fmt.Sprintf("LEFT JOIN %s AS u ON wsi.id = u.id", (&Usage{}).TableName())).
254
Where("wsi.phasePersisted = ?", "stopped").
255
Where("wsi.stoppingTime = ''"). // empty
256
Pluck("wsi.id", &ids)
257
if tx.Error != nil {
258
return nil, fmt.Errorf("failed to list workspace instances with phase stopped but no stopping time: %w", tx.Error)
259
}
260
return ids, nil
261
}
262
263