Auto-merge for PR #483 via VersionBot

Resumable deltas
This commit is contained in:
resin-io-versionbot[bot] 2017-08-16 09:31:26 +00:00 committed by GitHub
commit 05ab8ebf94
5 changed files with 63 additions and 27 deletions

View File

@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY! automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
This project adheres to [Semantic Versioning](http://semver.org/). This project adheres to [Semantic Versioning](http://semver.org/).
## v6.2.0 - 2017-08-16
* Try to resume the download of a delta if it fails due to flaky network #483 [Akis Kesoglou]
## v6.1.4 - 2017-08-07 ## v6.1.4 - 2017-08-07
* Fix references in deploy-to-resin.js and use github credentials when pushing in pr-to-meta-resin.sh #481 [Pablo Carranza Velez] * Fix references in deploy-to-resin.js and use github credentials when pushing in pr-to-meta-resin.sh #481 [Pablo Carranza Velez]

View File

@ -1,7 +1,7 @@
{ {
"name": "resin-supervisor", "name": "resin-supervisor",
"description": "This is resin.io's Supervisor, a program that runs on IoT devices and has the task of running user Apps (which are Docker containers), and updating them as Resin's API informs it to.", "description": "This is resin.io's Supervisor, a program that runs on IoT devices and has the task of running user Apps (which are Docker containers), and updating them as Resin's API informs it to.",
"version": "6.1.4", "version": "6.2.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",
@ -29,7 +29,7 @@
"buffer-equal-constant-time": "^1.0.1", "buffer-equal-constant-time": "^1.0.1",
"coffee-loader": "^0.7.3", "coffee-loader": "^0.7.3",
"coffee-script": "~1.11.0", "coffee-script": "~1.11.0",
"docker-delta": "1.1.1", "docker-delta": "^2.0.1",
"docker-progress": "^2.6.0", "docker-progress": "^2.6.0",
"docker-toolbelt": "^3.0.1", "docker-toolbelt": "^3.0.1",
"event-stream": "^3.0.20", "event-stream": "^3.0.20",
@ -46,7 +46,7 @@
"pinejs-client": "^2.4.0", "pinejs-client": "^2.4.0",
"pubnub": "^3.7.13", "pubnub": "^3.7.13",
"request": "^2.51.0", "request": "^2.51.0",
"request-progress": "^2.0.1", "resumable-request": "^1.0.0",
"resin-lint": "^1.3.1", "resin-lint": "^1.3.1",
"resin-register-device": "^3.0.0", "resin-register-device": "^3.0.0",
"rimraf": "^2.5.4", "rimraf": "^2.5.4",

View File

@ -19,6 +19,8 @@ proxyvisor = require './proxyvisor'
osRelease = require './lib/os-release' osRelease = require './lib/os-release'
deviceConfig = require './device-config' deviceConfig = require './device-config'
DEFAULT_DELTA_APPLY_TIMEOUT = 300 * 1000 # 6 minutes
class UpdatesLockedError extends TypedError class UpdatesLockedError extends TypedError
ImageNotFoundError = (err) -> ImageNotFoundError = (err) ->
return "#{err.statusCode}" is '404' return "#{err.statusCode}" is '404'
@ -218,9 +220,15 @@ fetch = (app, setDeviceUpdateState = true) ->
Promise.join utils.getConfig('apiKey'), utils.getConfig('uuid'), (apiKey, uuid) -> Promise.join utils.getConfig('apiKey'), utils.getConfig('uuid'), (apiKey, uuid) ->
if conf['RESIN_SUPERVISOR_DELTA'] == '1' if conf['RESIN_SUPERVISOR_DELTA'] == '1'
logSystemEvent(logTypes.downloadAppDelta, app) logSystemEvent(logTypes.downloadAppDelta, app)
requestTimeout = checkInt(conf['RESIN_SUPERVISOR_DELTA_REQUEST_TIMEOUT'], positive: true) ? 30 * 60 * 1000 deltaOpts = {
totalTimeout = checkInt(conf['RESIN_SUPERVISOR_DELTA_TOTAL_TIMEOUT'], positive: true) ? 24 * 60 * 60 * 1000 uuid, apiKey
dockerUtils.rsyncImageWithProgress(app.imageId, { requestTimeout, totalTimeout, uuid, apiKey }, onProgress) # use user-defined timeouts, but fallback to defaults if none is provided.
requestTimeout: checkInt(conf['RESIN_SUPERVISOR_DELTA_REQUEST_TIMEOUT'], positive: true)
applyTimeout: checkInt(conf['RESIN_SUPERVISOR_DELTA_APPLY_TIMEOUT'], positive: true) ? DEFAULT_DELTA_APPLY_TIMEOUT
retryCount: checkInt(conf['RESIN_SUPERVISOR_DELTA_RETRY_COUNT'], positive: true)
retryInterval: checkInt(conf['RESIN_SUPERVISOR_DELTA_RETRY_INTERVAL'], positive: true)
}
dockerUtils.rsyncImageWithProgress(app.imageId, deltaOpts, onProgress)
else else
logSystemEvent(logTypes.downloadApp, app) logSystemEvent(logTypes.downloadApp, app)
dockerUtils.fetchImageWithProgress(app.imageId, onProgress, { uuid, apiKey }) dockerUtils.fetchImageWithProgress(app.imageId, onProgress, { uuid, apiKey })

View File

@ -4,12 +4,11 @@ process.env.DOCKER_HOST ?= "unix://#{config.dockerSocket}"
Docker = require 'docker-toolbelt' Docker = require 'docker-toolbelt'
{ DockerProgress } = require 'docker-progress' { DockerProgress } = require 'docker-progress'
Promise = require 'bluebird' Promise = require 'bluebird'
progress = require 'request-progress'
dockerDelta = require 'docker-delta' dockerDelta = require 'docker-delta'
_ = require 'lodash' _ = require 'lodash'
knex = require './db' knex = require './db'
{ request } = require './request' { request, resumable } = require './request'
Lock = require 'rwlock' Lock = require 'rwlock'
utils = require './utils' utils = require './utils'
rimraf = Promise.promisify(require('rimraf')) rimraf = Promise.promisify(require('rimraf'))
@ -53,6 +52,23 @@ getRepoAndTag = (image) ->
registry = '' registry = ''
return { repo: "#{registry}#{imageName}", tag: tagName } return { repo: "#{registry}#{imageName}", tag: tagName }
applyDelta = (imgSrc, deltaUrl, { requestTimeout, applyTimeout, retryCount, retryInterval }, onProgress) ->
new Promise (resolve, reject) ->
resumable(request, { url: deltaUrl, timeout: requestTimeout })
.on('progress', onProgress)
.on('retry', onProgress)
.on('error', reject)
.on 'response', (res) ->
if res.statusCode isnt 200
reject(new Error("Got #{res.statusCode} when requesting delta from storage."))
else if parseInt(res.headers['content-length']) is 0
reject(new Error('Invalid delta URL.'))
else
deltaStream = dockerDelta.applyDelta(imgSrc, timeout: applyTimeout)
res.pipe(deltaStream)
.on('id', resolve)
.on('error', reject)
do -> do ->
_lock = new Lock() _lock = new Lock()
_writeLock = Promise.promisify(_lock.async.writeLock) _writeLock = Promise.promisify(_lock.async.writeLock)
@ -66,7 +82,7 @@ do ->
.disposer (release) -> .disposer (release) ->
release() release()
exports.rsyncImageWithProgress = (imgDest, { requestTimeout, totalTimeout, uuid, apiKey, startFromEmpty = false }, onProgress) -> exports.rsyncImageWithProgress = (imgDest, { requestTimeout, applyTimeout, retryCount, retryInterval, uuid, apiKey, startFromEmpty = false }, onProgress) ->
Promise.using readLockImages(), -> Promise.using readLockImages(), ->
Promise.try -> Promise.try ->
if startFromEmpty if startFromEmpty
@ -87,36 +103,30 @@ do ->
.get(1) .get(1)
.then (b) -> .then (b) ->
opts = opts =
followRedirect: false
timeout: requestTimeout timeout: requestTimeout
if b?.token? if b?.token?
deltaAuthOpts = opts.auth =
auth: bearer: b.token
bearer: b?.token sendImmediately: true
sendImmediately: true
opts = _.merge(opts, deltaAuthOpts)
new Promise (resolve, reject) -> new Promise (resolve, reject) ->
progress request.get("#{config.deltaHost}/api/v2/delta?src=#{imgSrc}&dest=#{imgDest}", opts) request.get("#{config.deltaHost}/api/v2/delta?src=#{imgSrc}&dest=#{imgDest}", opts)
.on 'progress', (progress) ->
# In request-progress ^2.0.1, "percentage" is a ratio from 0 to 1
onProgress(percentage: progress.percentage * 100)
.on 'end', ->
onProgress(percentage: 100)
.on 'response', (res) -> .on 'response', (res) ->
if res.statusCode is 504 res.resume() # discard response body -- we only care about response headers
if res.statusCode in [ 502, 504 ]
reject(new Error('Delta server is still processing the delta, will retry')) reject(new Error('Delta server is still processing the delta, will retry'))
else if res.statusCode isnt 200 else if not (300 <= res.statusCode < 400 and res.headers['location']?)
reject(new Error("Got #{res.statusCode} when requesting image from delta server.")) reject(new Error("Got #{res.statusCode} when requesting image from delta server."))
else else
deltaUrl = res.headers['location']
if imgSrc is 'resin/scratch' if imgSrc is 'resin/scratch'
deltaSrc = null deltaSrc = null
else else
deltaSrc = imgSrc deltaSrc = imgSrc
res.pipe(dockerDelta.applyDelta(deltaSrc, imgDest)) deltaOpts = { requestTimeout, applyTimeout, retryCount, retryInterval }
.on('id', resolve) resolve(applyDelta(deltaSrc, deltaUrl, deltaOpts, onProgress))
.on('error', reject)
.on 'error', reject .on 'error', reject
.timeout(totalTimeout)
.then (id) -> .then (id) ->
getRepoAndTag(imgDest) getRepoAndTag(imgDest)
.then ({ repo, tag }) -> .then ({ repo, tag }) ->

View File

@ -2,6 +2,7 @@ config = require './config'
PlatformAPI = require 'pinejs-client' PlatformAPI = require 'pinejs-client'
Promise = require 'bluebird' Promise = require 'bluebird'
request = require 'request' request = require 'request'
resumable = require 'resumable-request'
url = require 'url' url = require 'url'
osRelease = require './lib/os-release' osRelease = require './lib/os-release'
@ -16,12 +17,23 @@ if osVersion?
else else
userAgent += " (Linux; #{osVersion})" userAgent += " (Linux; #{osVersion})"
# With these settings, the device must be unable to receive a single byte
# from the network for a continuous period of 20 minutes before we give up.
# (reqTimeout + retryInterval) * retryCount / 1000ms / 60sec ~> minutes
DEFAULT_REQUEST_TIMEOUT = 30000 # ms
DEFAULT_REQUEST_RETRY_INTERVAL = 10000 # ms
DEFAULT_REQUEST_RETRY_COUNT = 30
requestOpts = requestOpts =
gzip: true gzip: true
timeout: 30000 timeout: DEFAULT_REQUEST_TIMEOUT
headers: headers:
'User-Agent': userAgent 'User-Agent': userAgent
resumableOpts =
maxRetries: DEFAULT_REQUEST_RETRY_COUNT
retryInterval: DEFAULT_REQUEST_RETRY_INTERVAL
try try
PLATFORM_ENDPOINT = url.resolve(config.apiEndpoint, '/v2/') PLATFORM_ENDPOINT = url.resolve(config.apiEndpoint, '/v2/')
exports.resinApi = resinApi = new PlatformAPI exports.resinApi = resinApi = new PlatformAPI
@ -35,3 +47,5 @@ catch
request = request.defaults(requestOpts) request = request.defaults(requestOpts)
exports.request = Promise.promisifyAll(request, multiArgs: true) exports.request = Promise.promisifyAll(request, multiArgs: true)
exports.resumable = resumable.defaults(resumableOpts)