mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-03-13 07:54:01 +00:00
Merge pull request #2396 from balena-os/switch-to-image-pull-if-delta-failure
Switch to image pull if delta failure
This commit is contained in:
commit
dd0253ff1f
9
package-lock.json
generated
9
package-lock.json
generated
@ -58,7 +58,7 @@
|
|||||||
"copy-webpack-plugin": "^12.0.0",
|
"copy-webpack-plugin": "^12.0.0",
|
||||||
"deep-object-diff": "1.1.0",
|
"deep-object-diff": "1.1.0",
|
||||||
"docker-delta": "^4.1.0",
|
"docker-delta": "^4.1.0",
|
||||||
"docker-progress": "^5.2.3",
|
"docker-progress": "^5.2.4",
|
||||||
"dockerode": "^4.0.2",
|
"dockerode": "^4.0.2",
|
||||||
"duration-js": "^4.0.0",
|
"duration-js": "^4.0.0",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
@ -4794,10 +4794,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/docker-progress": {
|
"node_modules/docker-progress": {
|
||||||
"version": "5.2.3",
|
"version": "5.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/docker-progress/-/docker-progress-5.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/docker-progress/-/docker-progress-5.2.4.tgz",
|
||||||
"integrity": "sha512-tsiqpC61pzaDOkKhbvr7ABQB2bL3bx+sVa7r4IZFf3tzwcMIhcU/sr5fqsXOKzIspxiCL+UHNS9gNO5ly9JxWg==",
|
"integrity": "sha512-sgEXTJh78YOj8pIBIzZHLo3KpamJ5N0/3pU7DkpZBBvxZ9PmO0d9ND6x7TExQZf4hgvlFRBS41aN+GHx6vu5KQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/dockerode": "^3.3.23",
|
"@types/dockerode": "^3.3.23",
|
||||||
"JSONStream": "^1.3.5",
|
"JSONStream": "^1.3.5",
|
||||||
|
@ -84,7 +84,7 @@
|
|||||||
"copy-webpack-plugin": "^12.0.0",
|
"copy-webpack-plugin": "^12.0.0",
|
||||||
"deep-object-diff": "1.1.0",
|
"deep-object-diff": "1.1.0",
|
||||||
"docker-delta": "^4.1.0",
|
"docker-delta": "^4.1.0",
|
||||||
"docker-progress": "^5.2.3",
|
"docker-progress": "^5.2.4",
|
||||||
"dockerode": "^4.0.2",
|
"dockerode": "^4.0.2",
|
||||||
"duration-js": "^4.0.0",
|
"duration-js": "^4.0.0",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
import type { ProgressCallback } from 'docker-progress';
|
|
||||||
import { DockerProgress } from 'docker-progress';
|
import { DockerProgress } from 'docker-progress';
|
||||||
|
import type { ProgressCallback } from 'docker-progress';
|
||||||
import Dockerode from 'dockerode';
|
import Dockerode from 'dockerode';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import memoizee from 'memoizee';
|
import memoizee from 'memoizee';
|
||||||
|
|
||||||
import { applyDelta, OutOfSyncError } from 'docker-delta';
|
import { applyDelta, OutOfSyncError } from 'docker-delta';
|
||||||
|
|
||||||
import type { SchemaReturn } from '../config/schema-type';
|
import log from './supervisor-console';
|
||||||
import { envArrayToObject } from './conversions';
|
import { envArrayToObject } from './conversions';
|
||||||
|
import * as request from './request';
|
||||||
import {
|
import {
|
||||||
DeltaStillProcessingError,
|
DeltaStillProcessingError,
|
||||||
ImageAuthenticationError,
|
ImageAuthenticationError,
|
||||||
InvalidNetGatewayError,
|
InvalidNetGatewayError,
|
||||||
|
DeltaServerError,
|
||||||
|
DeltaApplyError,
|
||||||
|
isStatusError,
|
||||||
} from './errors';
|
} from './errors';
|
||||||
import * as request from './request';
|
|
||||||
import type { EnvVarObject } from '../types';
|
import type { EnvVarObject } from '../types';
|
||||||
|
import type { SchemaReturn } from '../config/schema-type';
|
||||||
import log from './supervisor-console';
|
|
||||||
|
|
||||||
export type FetchOptions = SchemaReturn<'fetchOptions'>;
|
export type FetchOptions = SchemaReturn<'fetchOptions'>;
|
||||||
export type DeltaFetchOptions = FetchOptions & {
|
export type DeltaFetchOptions = FetchOptions & {
|
||||||
@ -41,6 +42,18 @@ type ImageNameParts = {
|
|||||||
// (10 mins)
|
// (10 mins)
|
||||||
const DELTA_TOKEN_TIMEOUT = 10 * 60 * 1000;
|
const DELTA_TOKEN_TIMEOUT = 10 * 60 * 1000;
|
||||||
|
|
||||||
|
// How many times to retry a v3 delta apply before falling back to a regular pull.
|
||||||
|
// A delta is applied to the base image when pulling, so a failure could be due to
|
||||||
|
// "layers from manifest don't match image configuration", which can occur before
|
||||||
|
// or after downloading delta image layers.
|
||||||
|
//
|
||||||
|
// Other causes of failure have not been documented as clearly as "layers from manifest"
|
||||||
|
// but could manifest as well, though unclear if they occur before, after, or during
|
||||||
|
// downloading delta image layers.
|
||||||
|
//
|
||||||
|
// See: https://github.com/balena-os/balena-engine/blob/master/distribution/pull_v2.go#L43
|
||||||
|
const DELTA_APPLY_RETRY_COUNT = 3;
|
||||||
|
|
||||||
export const docker = new Dockerode();
|
export const docker = new Dockerode();
|
||||||
export const dockerProgress = new DockerProgress({
|
export const dockerProgress = new DockerProgress({
|
||||||
docker,
|
docker,
|
||||||
@ -113,11 +126,7 @@ export async function fetchDeltaWithProgress(
|
|||||||
onProgress: ProgressCallback,
|
onProgress: ProgressCallback,
|
||||||
serviceName: string,
|
serviceName: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const deltaSourceId =
|
const deltaSourceId = deltaOpts.deltaSourceId ?? deltaOpts.deltaSource;
|
||||||
deltaOpts.deltaSourceId != null
|
|
||||||
? deltaOpts.deltaSourceId
|
|
||||||
: deltaOpts.deltaSource;
|
|
||||||
|
|
||||||
const timeout = deltaOpts.deltaApplyTimeout;
|
const timeout = deltaOpts.deltaApplyTimeout;
|
||||||
|
|
||||||
const logFn = (str: string) =>
|
const logFn = (str: string) =>
|
||||||
@ -143,7 +152,7 @@ export async function fetchDeltaWithProgress(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Since the supevisor never calls this function with a source anymore,
|
// Since the supevisor never calls this function with a source anymore,
|
||||||
// this should never happen, but w ehandle it anyway
|
// this should never happen, but we handle it anyway
|
||||||
if (deltaOpts.deltaSource == null) {
|
if (deltaOpts.deltaSource == null) {
|
||||||
logFn('Falling back to regular pull due to lack of a delta source');
|
logFn('Falling back to regular pull due to lack of a delta source');
|
||||||
return fetchImageWithProgress(imgDest, deltaOpts, onProgress);
|
return fetchImageWithProgress(imgDest, deltaOpts, onProgress);
|
||||||
@ -210,6 +219,10 @@ export async function fetchDeltaWithProgress(
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
|
// If 400s status code, throw a more specific error & revert immediately to a regular pull
|
||||||
|
if (res.statusCode >= 400 && res.statusCode < 500) {
|
||||||
|
throw new DeltaServerError(res.statusCode, res.statusMessage);
|
||||||
|
}
|
||||||
if (res.statusCode !== 200) {
|
if (res.statusCode !== 200) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Got ${res.statusCode} when requesting v3 delta from delta server.`,
|
`Got ${res.statusCode} when requesting v3 delta from delta server.`,
|
||||||
@ -225,24 +238,62 @@ export async function fetchDeltaWithProgress(
|
|||||||
`Got an error when parsing delta server response for v3 delta: ${e}`,
|
`Got an error when parsing delta server response for v3 delta: ${e}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
id = await applyBalenaDelta(name, token, onProgress, logFn);
|
// Try to apply delta DELTA_APPLY_RETRY_COUNT times, then throw DeltaApplyError
|
||||||
|
let lastError: Error | undefined = undefined;
|
||||||
|
for (
|
||||||
|
let tryCount = 0;
|
||||||
|
tryCount < DELTA_APPLY_RETRY_COUNT;
|
||||||
|
tryCount++
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
id = await applyBalenaDelta(name, token, onProgress, logFn);
|
||||||
|
break;
|
||||||
|
} catch (e) {
|
||||||
|
if (isStatusError(e)) {
|
||||||
|
// A status error during delta pull indicates network issues,
|
||||||
|
// so we should throw an error to the handler that indicates that
|
||||||
|
// the delta pull should be retried until network issues are resolved,
|
||||||
|
// rather than falling back to a regular pull.
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
lastError = e as Error;
|
||||||
|
logFn(
|
||||||
|
`Delta apply failed, retrying (${tryCount + 1}/${DELTA_APPLY_RETRY_COUNT})...`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastError) {
|
||||||
|
throw new DeltaApplyError(lastError.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported delta version: ${deltaOpts.deltaVersion}`);
|
throw new Error(`Unsupported delta version: ${deltaOpts.deltaVersion}`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// Log appropriate message based on error type
|
||||||
if (e instanceof OutOfSyncError) {
|
if (e instanceof OutOfSyncError) {
|
||||||
logFn('Falling back to regular pull due to delta out of sync error');
|
logFn('Falling back to regular pull due to delta out of sync error');
|
||||||
return await fetchImageWithProgress(imgDest, deltaOpts, onProgress);
|
} else if (e instanceof DeltaServerError) {
|
||||||
|
logFn(
|
||||||
|
`Falling back to regular pull due to delta server error (${e.statusCode})${e.statusMessage ? `: ${e.statusMessage}` : ''}`,
|
||||||
|
);
|
||||||
|
} else if (e instanceof DeltaApplyError) {
|
||||||
|
// A delta apply error is raised from the Engine and doesn't have a status code
|
||||||
|
logFn(
|
||||||
|
`Falling back to regular pull due to delta apply error ${e.message ? `: ${e.message}` : ''}`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
logFn(`Delta failed with ${e}`);
|
logFn(`Delta failed with ${e}`);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For handled errors, fall back to regular pull
|
||||||
|
return fetchImageWithProgress(imgDest, deltaOpts, onProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
logFn(`Delta applied successfully`);
|
logFn(`Delta applied successfully`);
|
||||||
return id;
|
return id!;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchImageWithProgress(
|
export async function fetchImageWithProgress(
|
||||||
|
@ -70,6 +70,13 @@ export class InvalidNetGatewayError extends TypedError {}
|
|||||||
|
|
||||||
export class DeltaStillProcessingError extends TypedError {}
|
export class DeltaStillProcessingError extends TypedError {}
|
||||||
|
|
||||||
|
export class DeltaServerError extends StatusError {}
|
||||||
|
export class DeltaApplyError extends Error {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class UpdatesLockedError extends TypedError {}
|
export class UpdatesLockedError extends TypedError {}
|
||||||
|
|
||||||
export function isHttpConflictError(err: { statusCode: number }): boolean {
|
export function isHttpConflictError(err: { statusCode: number }): boolean {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user