@@ -16,13 +16,17 @@ import {
16
16
} from "@gitpod/gitpod-protocol" ;
17
17
import { log } from "@gitpod/gitpod-protocol/lib/util/logging" ;
18
18
import { PrebuiltWorkspaceState , WithCommitHistory } from "@gitpod/gitpod-protocol/lib/protocol" ;
19
- import { WorkspaceDB } from "@gitpod/gitpod-db/lib" ;
19
+ import { PrebuildWithWorkspace , WorkspaceDB } from "@gitpod/gitpod-db/lib" ;
20
20
import { Config } from "../config" ;
21
21
import { HostContextProvider } from "../auth/host-context-provider" ;
22
22
import { ImageSourceProvider } from "../workspace/image-source-provider" ;
23
+ import { ApplicationError , ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error" ;
24
+ import { TrustedValue } from "@gitpod/gitpod-protocol/lib/util/scrubbing" ;
23
25
24
26
const MAX_HISTORY_DEPTH = 100 ;
25
27
28
+ type IncrementalWorkspaceMatch = "none" | "incremental" | "exact" ;
29
+
26
30
@injectable ( )
27
31
export class IncrementalWorkspaceService {
28
32
@inject ( Config ) protected readonly config : Config ;
@@ -45,6 +49,7 @@ export class IncrementalWorkspaceService {
45
49
context . revision ,
46
50
maxDepth ,
47
51
) ;
52
+
48
53
history . commitHistory . unshift ( context . revision ) ;
49
54
if ( context . additionalRepositoryCheckoutInfo && context . additionalRepositoryCheckoutInfo . length > 0 ) {
50
55
const histories = context . additionalRepositoryCheckoutInfo . map ( async ( info ) => {
@@ -78,79 +83,128 @@ export class IncrementalWorkspaceService {
78
83
return ;
79
84
}
80
85
81
- const imageSourcePromise = this . imageSourceProvider . getImageSource ( { } , user , context , config ) ;
82
-
83
- // Note: This query returns only not-garbage-collected prebuilds in order to reduce cardinality
84
- // (e.g., at the time of writing, the Gitpod repository has 16K+ prebuilds, but only ~300 not-garbage-collected)
85
- const recentPrebuilds = await this . workspaceDB . findPrebuildsWithWorkspace ( projectId ) ;
86
- const imageSource = await imageSourcePromise ;
87
- for ( const recentPrebuild of recentPrebuilds ) {
88
- if (
89
- this . isGoodBaseforIncrementalBuild (
90
- history ,
91
- config ,
92
- imageSource ,
93
- recentPrebuild . prebuild ,
94
- recentPrebuild . workspace ,
95
- includeUnfinishedPrebuilds ,
96
- )
97
- ) {
98
- return recentPrebuild . prebuild ;
86
+ const [ recentPrebuilds , imageSource ] = await Promise . allSettled ( [
87
+ // Note: This query returns only not-garbage-collected prebuilds in order to reduce cardinality
88
+ // (e.g., at the time of writing, the Gitpod repository has 16K+ prebuilds, but only ~300 not-garbage-collected)
89
+ this . workspaceDB . findPrebuildsWithWorkspace ( projectId ) ,
90
+ this . imageSourceProvider . getImageSource ( { } , user , context , config ) ,
91
+ ] ) ;
92
+ if ( imageSource . status === "rejected" ) {
93
+ log . error ( "Image source promise was rejected" , { reason : imageSource . reason , userId : user . id } ) ;
94
+ throw new ApplicationError (
95
+ ErrorCodes . INTERNAL_SERVER_ERROR ,
96
+ "Something went wrong when looking up prebuilds" ,
97
+ ) ;
98
+ }
99
+ if ( recentPrebuilds . status === "rejected" ) {
100
+ log . error ( "Prebuild lookup promise was rejected" , { reason : recentPrebuilds . reason , userId : user . id } ) ;
101
+ throw new ApplicationError (
102
+ ErrorCodes . INTERNAL_SERVER_ERROR ,
103
+ "Something went wrong when looking up prebuilds" ,
104
+ ) ;
105
+ }
106
+
107
+ // traverse prebuilds by commit history instead of their creationTime, so that we don't match prebuilds created for older revisions but triggered later
108
+ const candidates : { candidate : PrebuildWithWorkspace ; index : number } [ ] = [ ] ;
109
+ for ( const recentPrebuild of recentPrebuilds . value ) {
110
+ const { prebuild, workspace } = recentPrebuild ;
111
+ const { match, index } = this . isMatchForIncrementalBuild (
112
+ history ,
113
+ config ,
114
+ imageSource . value ,
115
+ prebuild ,
116
+ workspace ,
117
+ includeUnfinishedPrebuilds ,
118
+ ) ;
119
+ if ( match === "exact" ) {
120
+ log . info ( "Found base for incremental build" , {
121
+ prebuildId : prebuild . id ,
122
+ match : new TrustedValue ( {
123
+ exact : true ,
124
+ distanceFromContext : 0 ,
125
+ } ) ,
126
+ } ) ;
127
+ return prebuild ;
128
+ }
129
+ if ( match === "incremental" ) {
130
+ candidates . push ( { candidate : recentPrebuild , index : index ! } ) ;
99
131
}
100
132
}
101
- return undefined ;
133
+
134
+ if ( candidates . length === 0 ) {
135
+ return undefined ;
136
+ }
137
+
138
+ // Sort by index ASC
139
+ candidates . sort ( ( a , b ) => a . index - b . index ) ;
140
+ const {
141
+ candidate : { prebuild } ,
142
+ index,
143
+ } = candidates [ 0 ] ;
144
+
145
+ log . info ( "Found base for incremental build" , {
146
+ prebuildId : prebuild . id ,
147
+ match : {
148
+ exact : true ,
149
+ distanceFromContext : index ,
150
+ } ,
151
+ } ) ;
152
+ return prebuild ;
102
153
}
103
154
104
- private isGoodBaseforIncrementalBuild (
155
+ private isMatchForIncrementalBuild (
105
156
history : WithCommitHistory ,
106
157
config : WorkspaceConfig ,
107
158
imageSource : WorkspaceImageSource ,
108
159
candidatePrebuild : PrebuiltWorkspace ,
109
160
candidateWorkspace : Workspace ,
110
161
includeUnfinishedPrebuilds ?: boolean ,
111
- ) : boolean {
112
- if ( ! history . commitHistory || history . commitHistory . length === 0 ) {
113
- return false ;
162
+ ) : { match : Omit < IncrementalWorkspaceMatch , "none" > ; index : number } | { match : "none" ; index ?: undefined } {
163
+ // make typescript happy, we know that history.commitHistory is defined
164
+ if ( ! history . commitHistory ) {
165
+ return { match : "none" } ;
114
166
}
115
167
if ( ! CommitContext . is ( candidateWorkspace . context ) ) {
116
- return false ;
168
+ return { match : "none" } ;
117
169
}
118
170
119
171
const acceptableStates : PrebuiltWorkspaceState [ ] = [ "available" ] ;
120
172
if ( includeUnfinishedPrebuilds ) {
121
173
acceptableStates . push ( "building" ) ;
122
174
acceptableStates . push ( "queued" ) ;
123
175
}
124
-
125
176
if ( ! acceptableStates . includes ( candidatePrebuild . state ) ) {
126
- return false ;
177
+ return { match : "none" } ;
127
178
}
128
179
129
- // we are only considering full prebuilds
130
- if ( ! ! candidateWorkspace . basedOnPrebuildId ) {
131
- return false ;
180
+ // we are only considering full prebuilds (we are not building on top of incremental prebuilds)
181
+ if ( candidateWorkspace . basedOnPrebuildId ) {
182
+ return { match : "none" } ;
132
183
}
133
184
185
+ // check if the amount of additional repositories matches the candidate
134
186
if (
135
187
candidateWorkspace . context . additionalRepositoryCheckoutInfo ?. length !==
136
188
history . additionalRepositoryCommitHistories ?. length
137
189
) {
138
- // different number of repos
139
- return false ;
190
+ return { match : "none" } ;
140
191
}
141
192
142
193
const candidateCtx = candidateWorkspace . context ;
143
- if ( ! history . commitHistory . some ( ( sha ) => sha === candidateCtx . revision ) ) {
144
- return false ;
194
+
195
+ // check for overlapping commit history
196
+ const commitIndexInHistory = history . commitHistory . indexOf ( candidateCtx . revision ) ;
197
+ if ( commitIndexInHistory === - 1 ) {
198
+ return { match : "none" } ;
145
199
}
146
200
147
- // check the commits are included in the commit history
148
- for ( const subRepo of candidateWorkspace . context . additionalRepositoryCheckoutInfo || [ ] ) {
149
- const matchIngRepo = history . additionalRepositoryCommitHistories ?. find (
201
+ // check for overlapping git history for each additional repo
202
+ for ( const subRepo of candidateWorkspace . context . additionalRepositoryCheckoutInfo ?? [ ] ) {
203
+ const matchingRepo = history . additionalRepositoryCommitHistories ?. find (
150
204
( repo ) => repo . cloneUrl === subRepo . repository . cloneUrl ,
151
205
) ;
152
- if ( ! matchIngRepo || ! matchIngRepo . commitHistory . some ( ( sha ) => sha === subRepo . revision ) ) {
153
- return false ;
206
+ if ( ! matchingRepo || ! matchingRepo . commitHistory . some ( ( sha ) => sha === subRepo . revision ) ) {
207
+ return { match : "none" } ;
154
208
}
155
209
}
156
210
@@ -160,29 +214,41 @@ export class IncrementalWorkspaceService {
160
214
imageSource,
161
215
parentImageSource : candidateWorkspace . imageSource ,
162
216
} ) ;
163
- return false ;
217
+ return { match : "none" } ;
164
218
}
165
219
166
220
// ensure the tasks haven't changed
167
- const filterPrebuildTasks = ( tasks : TaskConfig [ ] = [ ] ) =>
168
- tasks
169
- . map ( ( task ) =>
170
- Object . keys ( task )
171
- . filter ( ( key ) => [ "before" , "init" , "prebuild" ] . includes ( key ) )
172
- // @ts -ignore
173
- . reduce ( ( obj , key ) => ( { ...obj , [ key ] : task [ key ] } ) , { } ) ,
174
- )
175
- . filter ( ( task ) => Object . keys ( task ) . length > 0 ) ;
176
- const prebuildTasks = filterPrebuildTasks ( config . tasks ) ;
177
- const parentPrebuildTasks = filterPrebuildTasks ( candidateWorkspace . config . tasks ) ;
221
+ const prebuildTasks = this . filterPrebuildTasks ( config . tasks ) ;
222
+ const parentPrebuildTasks = this . filterPrebuildTasks ( candidateWorkspace . config . tasks ) ;
178
223
if ( JSON . stringify ( prebuildTasks ) !== JSON . stringify ( parentPrebuildTasks ) ) {
179
224
log . debug ( `Skipping parent prebuild: Outdated prebuild tasks` , {
180
225
prebuildTasks,
181
226
parentPrebuildTasks,
182
227
} ) ;
183
- return false ;
228
+ return { match : "none" } ;
184
229
}
185
230
186
- return true ;
231
+ if ( commitIndexInHistory === 0 ) {
232
+ return { match : "exact" , index : commitIndexInHistory } ;
233
+ }
234
+
235
+ return { match : "incremental" , index : commitIndexInHistory } ;
236
+ }
237
+
238
+ /**
239
+ * Given an array of tasks returns only the those which are to run during prebuilds, additionally stripping everything besides the prebuild-related configuration from them
240
+ */
241
+ private filterPrebuildTasks ( tasks : TaskConfig [ ] = [ ] ) : Record < string , string > [ ] {
242
+ return tasks
243
+ . map ( ( task ) => {
244
+ const filteredTask : Record < string , any > = { } ;
245
+ for ( const key of Object . keys ( task ) ) {
246
+ if ( [ "before" , "init" , "prebuild" ] . includes ( key ) ) {
247
+ filteredTask [ key ] = task [ key as keyof TaskConfig ] ;
248
+ }
249
+ }
250
+ return filteredTask ;
251
+ } )
252
+ . filter ( ( task ) => Object . keys ( task ) . length > 0 ) ;
187
253
}
188
254
}
0 commit comments