Queued builds

This commit is contained in:
Kasra Bigdeli
2019-08-03 14:48:04 -04:00
parent 00101d10bf
commit d9c313b47e
8 changed files with 138 additions and 26 deletions

View File

@@ -68,21 +68,9 @@ router.use(function(req, res, next) {
}
if (threadLockNamespace[namespace]) {
let response = new BaseApi(
ApiStatusCodes.STATUS_ERROR_GENERIC,
'Another operation still in progress... please wait...'
)
res.send(response)
return
}
let activeBuildAppName = serviceManager.isAnyBuildRunning()
if (activeBuildAppName) {
let response = new BaseApi(
ApiStatusCodes.STATUS_ERROR_GENERIC,
`An active build (${activeBuildAppName}) is in progress... please wait...`
)
res.send(response)
// Changed to HTTP status code so that the webhook and 3rd party services can understand this.
res.status(429)
res.send('Another operation still in progress... please wait...')
return
}

View File

@@ -98,7 +98,7 @@ router.post('/:appName/', upload.single('sourceFile'), function(
}
Promise.resolve().then(function() {
const promiseToDeployNewVer = serviceManager.deployNewVersion(appName, {
const promiseToDeployNewVer = serviceManager.scheduleDeployNewVersion(appName, {
uploadedTarPathSource: !!tarballSourceFilePath
? {
uploadedTarPath: tarballSourceFilePath,

View File

@@ -251,7 +251,7 @@ router.post('/register/', function(req, res, next) {
appCreated = true
})
.then(function() {
return serviceManager.deployNewVersion(appName, {
return serviceManager.scheduleDeployNewVersion(appName, {
captainDefinitionContentSource: {
captainDefinitionContent: DEFAULT_APP_CAPTAIN_DEFINITION,
gitHash: '',

View File

@@ -88,7 +88,7 @@ router.post('/triggerbuild', urlencodedParser, function(req, res, next) {
}
}
return serviceManager.deployNewVersion(appName, {
return serviceManager.scheduleDeployNewVersion(appName, {
repoInfoSource: repoInfo,
})
})

View File

@@ -57,8 +57,7 @@ export default class ImageMaker {
private dockerRegistryHelper: DockerRegistryHelper,
private dockerApi: DockerApi,
private namespace: string,
private buildLogs: IHashMapGeneric<BuildLog>,
private activeBuilds: IHashMapGeneric<boolean>
private buildLogs: IHashMapGeneric<BuildLog>
) {
//
}
@@ -84,7 +83,6 @@ export default class ImageMaker {
): Promise<IBuiltImage> {
const self = this
this.activeBuilds[appName] = true
this.buildLogs[appName] =
this.buildLogs[appName] ||
new BuildLog(CaptainConstants.configs.buildLogSize)
@@ -207,7 +205,6 @@ export default class ImageMaker {
return Promise.reject(err)
})
.then(function() {
self.activeBuilds[appName] = false
self.buildLogs[appName].log(`Build has finished successfully!`)
return {
imageName: fullImageName,
@@ -215,7 +212,6 @@ export default class ImageMaker {
}
})
.catch(function(error) {
self.activeBuilds[appName] = false
self.buildLogs[appName].log(`Build has failed!`)
return Promise.reject(error)
})

View File

@@ -14,6 +14,18 @@ import Authenticator = require('./Authenticator')
const serviceMangerCache = {} as IHashMapGeneric<ServiceManager>
interface QueuedPromise {
resolve: undefined | ((reason?: unknown) => void)
reject: undefined | ((reason?: any) => void)
promise: undefined | (Promise<unknown>)
}
interface QueuedBuild {
appName: string
source: IImageSource
promiseToSave: QueuedPromise
}
class ServiceManager {
static get(
namespace: string,
@@ -37,6 +49,7 @@ class ServiceManager {
private activeBuilds: IHashMapGeneric<boolean>
private buildLogs: IHashMapGeneric<BuildLog>
private queuedBuilds: QueuedBuild[]
private isReady: boolean
private imageMaker: ImageMaker
private dockerRegistryHelper: DockerRegistryHelper
@@ -49,6 +62,7 @@ class ServiceManager {
private domainResolveChecker: DomainResolveChecker
) {
this.activeBuilds = {}
this.queuedBuilds = []
this.buildLogs = {}
this.isReady = true
this.dockerRegistryHelper = new DockerRegistryHelper(
@@ -59,8 +73,7 @@ class ServiceManager {
this.dockerRegistryHelper,
this.dockerApi,
this.dataStore.getNameSpace(),
this.buildLogs,
this.activeBuilds
this.buildLogs
)
}
@@ -72,10 +85,72 @@ class ServiceManager {
return this.isReady
}
deployNewVersion(appName: string, source: IImageSource) {
scheduleDeployNewVersion(appName: string, source: IImageSource) {
const self = this
let activeBuildAppName = self.isAnyBuildRunning()
if (activeBuildAppName) {
self.buildLogs[appName] =
self.buildLogs[appName] ||
new BuildLog(CaptainConstants.configs.buildLogSize)
this.activeBuilds[appName] = true
const existingBuildForTheSameApp = self.queuedBuilds.find(
v => v.appName === appName
)
if (existingBuildForTheSameApp) {
self.buildLogs[appName].log(
`A build for ${appName} was queued, it's now being replaced with a new build...`
)
// replacing the new source!
existingBuildForTheSameApp.source = source
const existingPromise =
existingBuildForTheSameApp.promiseToSave.promise
if (!existingPromise)
throw new Error(
'Existing promise for the queued app is NULL!!'
)
return existingPromise
}
self.buildLogs[appName].log(
`An active build (${activeBuildAppName}) is in progress. This build is queued...`
)
let promiseToSave: QueuedPromise = {
resolve: undefined,
reject: undefined,
promise: undefined,
}
let promise = new Promise(function(resolve, reject) {
promiseToSave.resolve = resolve
promiseToSave.reject = reject
})
promiseToSave.promise = promise
self.queuedBuilds.push({ appName, source, promiseToSave })
// This should only return when the build is finished,
// somehow we need save the promise in queue - for "attached builds"
return promise
}
return this.startDeployingNewVersion(appName, source)
}
startDeployingNewVersion(appName: string, source: IImageSource) {
const self = this
const dataStore = this.dataStore
let deployedVersion: number
return Promise.resolve() //
.then(function() {
return dataStore.getAppsDataStore().createNewVersion(appName)
@@ -104,9 +179,11 @@ class ServiceManager {
)
})
.then(function() {
self.onBuildFinished(appName)
return self.ensureServiceInitedAndUpdated(appName)
})
.catch(function(error) {
self.onBuildFinished(appName)
return new Promise<void>(function(resolve, reject) {
self.logBuildFailed(appName, error)
reject(error)
@@ -114,6 +191,17 @@ class ServiceManager {
})
}
onBuildFinished(appName: string) {
const self = this
self.activeBuilds[appName] = false
Promise.resolve().then(function() {
let newBuild = self.queuedBuilds.shift()
if (newBuild)
self.startDeployingNewVersion(newBuild.appName, newBuild.source)
})
}
enableCustomDomainSsl(appName: string, customDomain: string) {
const self = this

View File

@@ -40,6 +40,12 @@ export default class Utils {
})
}
static filterInPlace<T>(arr: T[], condition: (value: T) => boolean) {
let newArray = arr.filter(condition)
arr.splice(0, arr.length)
newArray.forEach(value => arr.push(value))
}
static dropFirstElements(arr: any[], maxLength: number) {
arr = arr || []
maxLength = Number(maxLength)

View File

@@ -51,3 +51,37 @@ test('Testing dropFirstElements - smaller (0)', () => {
expect(Utils.dropFirstElements(originalArray, 3).join(',')) //
.toBe('')
})
interface TestArray {
val1: string
val2: string
}
function createTestArray() {
const originalArray: TestArray[] = []
for (let index = 0; index < 2; index++) {
originalArray.push({
val1: 'e-1-' + (index + 1),
val2: 'e-2-' + (index + 1),
})
}
return originalArray
}
test('Testing filter in place - remove 1st', () => {
const originalArray = createTestArray()
Utils.filterInPlace(originalArray, v => v.val1 !== 'e-1-1')
expect(originalArray.length).toBe(1)
expect(originalArray[0].val1).toBe('e-1-2')
expect(originalArray[0].val2).toBe('e-2-2')
})
test('Testing filter in place - remove 2nd', () => {
const originalArray = createTestArray()
Utils.filterInPlace(originalArray, v => v.val1 !== 'e-1-2')
expect(originalArray.length).toBe(1)
expect(originalArray[0].val1).toBe('e-1-1')
expect(originalArray[0].val2).toBe('e-2-1')
})