Skip to content
This repository has been archived by the owner on Oct 10, 2022. It is now read-only.

Commit

Permalink
Merge pull request #22 from netlify/large-deploy-handling
Browse files Browse the repository at this point in the history
Support async deploys
  • Loading branch information
bcomnes authored Sep 24, 2018
2 parents af73856 + 877667c commit 6eb479f
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 21 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ Optional `opts` include:
deployTimeout: 1.2e6, // 20 mins
parallelHash: 100, // number of parallel hashing calls
parallelUpload: 4, // number of files to upload in parallel
maxRetry: 5, // number of times to try on failed file uploads
filter: filename => { /* return false to filter a file from the deploy */ },
tmpDir: tempy.directory(), // a temporary directory to zip loose files into
statusCb: statusObj => {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"dependencies": {
"@netlify/open-api": "^0.4.1",
"archiver": "^3.0.0",
"backoff": "^2.5.0",
"clean-deep": "^3.0.2",
"flush-write-stream": "^1.0.3",
"folder-walker": "^3.2.0",
Expand Down
8 changes: 7 additions & 1 deletion src/deploy/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const hashFiles = require('./hash-files')
const hashFns = require('./hash-fns')
const cleanDeep = require('clean-deep')
const tempy = require('tempy')
const { waitForDiff } = require('./util')

const { waitForDeploy, getUploadList, defaultFilter } = require('./util')

Expand All @@ -18,6 +19,8 @@ module.exports = async (api, siteId, dir, opts) => {
concurrentHash: 100, // concurrent file hash calls
concurrentUpload: 15, // Number of concurrent uploads
filter: defaultFilter,
syncFileLimit: 7000, // number of files
maxRetry: 5, // number of times to retry an upload
statusCb: statusObj => {
/* default to noop */
/* statusObj: {
Expand Down Expand Up @@ -64,11 +67,14 @@ module.exports = async (api, siteId, dir, opts) => {
body: {
files,
functions,
async: Object.keys(files).length > opts.syncFileLimit,
draft: opts.draft
}
})

let deploy = await api.createSiteDeploy(deployParams)
if (deployParams.body.async) deploy = await waitForDiff(api, deploy.id, siteId, opts.deployTimeout)

const { id: deployId, required: requiredFiles, required_functions: requiredFns } = deploy

statusCb({
Expand All @@ -88,7 +94,7 @@ module.exports = async (api, siteId, dir, opts) => {
msg: 'Waiting for deploy to go live...',
phase: 'start'
})
deploy = await waitForDeploy(api, deployId, opts.deployTimeout)
deploy = await waitForDeploy(api, deployId, siteId, opts.deployTimeout)

statusCb({
type: 'wait-for-deploy',
Expand Down
74 changes: 61 additions & 13 deletions src/deploy/upload-files.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
const pMap = require('p-map')
const fs = require('fs')
const backoff = require('backoff')

module.exports = uploadFiles
async function uploadFiles(api, deployId, uploadList, { concurrentUpload, statusCb }) {
if (!concurrentUpload || !statusCb) throw new Error('Missing required option concurrentUpload')
async function uploadFiles(api, deployId, uploadList, { concurrentUpload, statusCb, maxRetry }) {
if (!concurrentUpload || !statusCb || !maxRetry) throw new Error('Missing required option concurrentUpload')
statusCb({
type: 'upload',
msg: `Uploading ${uploadList.length} files`,
Expand All @@ -22,20 +23,28 @@ async function uploadFiles(api, deployId, uploadList, { concurrentUpload, status
let response
switch (assetType) {
case 'file': {
response = await api.uploadDeployFile({
body: readStream,
deployId,
path: encodeURI(normalizedPath)
})
response = await retryUpload(
() =>
api.uploadDeployFile({
body: readStream,
deployId,
path: encodeURI(normalizedPath)
}),
maxRetry
)
break
}
case 'function': {
response = await api.uploadDeployFunction({
body: readStream,
deployId,
name: encodeURI(normalizedPath),
runtime
})
response = await await retryUpload(
() =>
api.uploadDeployFunction({
body: readStream,
deployId,
name: encodeURI(normalizedPath),
runtime
}),
maxRetry
)
break
}
default: {
Expand All @@ -56,3 +65,42 @@ async function uploadFiles(api, deployId, uploadList, { concurrentUpload, status
})
return results
}

function retryUpload(uploadFn, maxRetry) {
return new Promise((resolve, reject) => {
let lastError
const fibonacciBackoff = backoff.fibonacci({
randomisationFactor: 0.5,
initialDelay: 100,
maxDelay: 10000
})
fibonacciBackoff.failAfter(maxRetry)

fibonacciBackoff.on('backoff', (number, delay) => {
// Do something when backoff starts, e.g. show to the
// user the delay before next reconnection attempt.
})

fibonacciBackoff.on('ready', tryUpload)

fibonacciBackoff.on('fail', () => {
reject(lastError)
})

function tryUpload(number, delay) {
uploadFn()
.then(results => resolve(results))
.catch(e => {
lastError = e
if (e.name === 'FetchError') {
console.log(e)
fibonacciBackoff.backoff()
} else {
return reject(e)
}
})
}

tryUpload(0, 0)
})
}
67 changes: 60 additions & 7 deletions src/deploy/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,47 @@ exports.normalizePath = relname => {
)
}

exports.waitForDiff = waitForDiff
// poll an async deployId until its done diffing
async function waitForDiff(api, deployId, siteId, timeout) {
let deploy // capture ready deploy during poll

await pWaitFor(loadDeploy, {
interval: 1000,
timeout,
message: 'Timeout while waiting for deploy'
})

return deploy

async function loadDeploy() {
const d = await api.getSiteDeploy({ siteId, deployId })

switch (d.state) {
// https://github.com/netlify/bitballoon/blob/master/app/models/deploy.rb#L21-L33
case 'error': {
const deployError = new Error(`Deploy ${deployId} had an error`)
deployError.deploy = d
throw deployError
}
case 'prepared':
case 'uploading':
case 'uploaded':
case 'ready': {
deploy = d
return true
}
case 'preparing':
default: {
return false
}
}
}
}

// Poll a deployId until its ready
exports.waitForDeploy = waitForDeploy
async function waitForDeploy(api, deployId, timeout) {
async function waitForDeploy(api, deployId, siteId, timeout) {
let deploy // capture ready deploy during poll

await pWaitFor(loadDeploy, {
Expand All @@ -47,12 +85,25 @@ async function waitForDeploy(api, deployId, timeout) {
return deploy

async function loadDeploy() {
const d = await api.getDeploy({ deployId })
if (d.state === 'ready') {
deploy = d
return true
} else {
return false
const d = await api.getSiteDeploy({ siteId, deployId })
switch (d.state) {
// https://github.com/netlify/bitballoon/blob/master/app/models/deploy.rb#L21-L33
case 'error': {
const deployError = new Error(`Deploy ${deployId} had an error`)
deployError.deploy = d
throw deployError
}
case 'ready': {
deploy = d
return true
}
case 'preparing':
case 'prepared':
case 'uploaded':
case 'uploading':
default: {
return false
}
}
}
}
Expand All @@ -77,3 +128,5 @@ exports.isExe = stat => {

return Boolean(mode & 0o0001 || (mode & 0o0010 && isGroup) || (mode & 0o0100 && isUser))
}

exports.retry = async (fn, errHandler, opts) => {}

0 comments on commit 6eb479f

Please sign in to comment.