@@ -16,13 +16,17 @@ import {
1616} from "@gitpod/gitpod-protocol" ;
1717import { log } from "@gitpod/gitpod-protocol/lib/util/logging" ;
1818import { 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" ;
2020import { Config } from "../config" ;
2121import { HostContextProvider } from "../auth/host-context-provider" ;
2222import { 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" ;
2325
2426const MAX_HISTORY_DEPTH = 100 ;
2527
28+ type IncrementalWorkspaceMatch = "none" | "incremental" | "exact" ;
29+
2630@injectable ( )
2731export class IncrementalWorkspaceService {
2832 @inject ( Config ) protected readonly config : Config ;
@@ -45,6 +49,7 @@ export class IncrementalWorkspaceService {
4549 context . revision ,
4650 maxDepth ,
4751 ) ;
52+
4853 history . commitHistory . unshift ( context . revision ) ;
4954 if ( context . additionalRepositoryCheckoutInfo && context . additionalRepositoryCheckoutInfo . length > 0 ) {
5055 const histories = context . additionalRepositoryCheckoutInfo . map ( async ( info ) => {
@@ -78,79 +83,128 @@ export class IncrementalWorkspaceService {
7883 return ;
7984 }
8085
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 ! } ) ;
99131 }
100132 }
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 ;
102153 }
103154
104- private isGoodBaseforIncrementalBuild (
155+ private isMatchForIncrementalBuild (
105156 history : WithCommitHistory ,
106157 config : WorkspaceConfig ,
107158 imageSource : WorkspaceImageSource ,
108159 candidatePrebuild : PrebuiltWorkspace ,
109160 candidateWorkspace : Workspace ,
110161 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" } ;
114166 }
115167 if ( ! CommitContext . is ( candidateWorkspace . context ) ) {
116- return false ;
168+ return { match : "none" } ;
117169 }
118170
119171 const acceptableStates : PrebuiltWorkspaceState [ ] = [ "available" ] ;
120172 if ( includeUnfinishedPrebuilds ) {
121173 acceptableStates . push ( "building" ) ;
122174 acceptableStates . push ( "queued" ) ;
123175 }
124-
125176 if ( ! acceptableStates . includes ( candidatePrebuild . state ) ) {
126- return false ;
177+ return { match : "none" } ;
127178 }
128179
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" } ;
132183 }
133184
185+ // check if the amount of additional repositories matches the candidate
134186 if (
135187 candidateWorkspace . context . additionalRepositoryCheckoutInfo ?. length !==
136188 history . additionalRepositoryCommitHistories ?. length
137189 ) {
138- // different number of repos
139- return false ;
190+ return { match : "none" } ;
140191 }
141192
142193 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" } ;
145199 }
146200
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 (
150204 ( repo ) => repo . cloneUrl === subRepo . repository . cloneUrl ,
151205 ) ;
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" } ;
154208 }
155209 }
156210
@@ -160,29 +214,41 @@ export class IncrementalWorkspaceService {
160214 imageSource,
161215 parentImageSource : candidateWorkspace . imageSource ,
162216 } ) ;
163- return false ;
217+ return { match : "none" } ;
164218 }
165219
166220 // 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 ) ;
178223 if ( JSON . stringify ( prebuildTasks ) !== JSON . stringify ( parentPrebuildTasks ) ) {
179224 log . debug ( `Skipping parent prebuild: Outdated prebuild tasks` , {
180225 prebuildTasks,
181226 parentPrebuildTasks,
182227 } ) ;
183- return false ;
228+ return { match : "none" } ;
184229 }
185230
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 ) ;
187253 }
188254}
0 commit comments