Skip to content

Commit c9d32ea

Browse files
authored
fix(crates): Detect stale upload caches and retry (#98)
The publishing errors also happen locally. When publishing a crate with `--no-verify`, it seems that the crate caches can go out of date and `cargo` will fail to publish. We can detect this and retry publishing with exponential backoff. It is unclear what the root cause of this is. When cargo updates its index, it runs a "fast path" request against the GitHub API which seems to get cached. Waiting for 20-30 seconds seems sufficient to invalidate those caches.
1 parent 4dee772 commit c9d32ea

File tree

1 file changed

+51
-6
lines changed

1 file changed

+51
-6
lines changed

src/targets/crates.ts

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import { GithubGlobalConfig, TargetConfig } from '../schemas/project_config';
1111
import { forEachChained } from '../utils/async';
1212
import { ConfigurationError } from '../utils/errors';
1313
import { withTempDir } from '../utils/files';
14-
import { checkExecutableIsPresent, spawnProcess } from '../utils/system';
14+
import {
15+
checkExecutableIsPresent,
16+
sleepAsync,
17+
spawnProcess,
18+
} from '../utils/system';
1519
import { BaseTarget } from './base';
1620
import { BaseArtifactProvider } from '../artifact_providers/base';
1721

@@ -24,6 +28,30 @@ const DEFAULT_CARGO_BIN = 'cargo';
2428
*/
2529
const CARGO_BIN = process.env.CARGO_BIN || DEFAULT_CARGO_BIN;
2630

31+
/**
32+
* A message fragment emitted by cargo when publishing fails due to a missing
33+
* dependency. This sometimes indicates a false positive if the cache has not
34+
* been updated.
35+
*/
36+
const VERSION_ERROR = 'failed to select a version for the requirement';
37+
38+
/**
39+
* Maximum number of attempts including the initial one when publishing fails
40+
* due to a stale cache. After this number of retries, publishing fails.
41+
*/
42+
const MAX_ATTEMPTS = 5;
43+
44+
/**
45+
* Initial delay to wait between publish retries in seconds. Exponential backoff
46+
* is applied to this delay on retries.
47+
*/
48+
const RETRY_DELAY_SECS = 2;
49+
50+
/**
51+
* Exponential backoff that is applied to the initial retry delay.
52+
*/
53+
const RETRY_EXP_FACTOR = 2;
54+
2755
/** Options for "crates" target */
2856
export interface CratesTargetOptions extends TargetConfig {
2957
/** Crates API token */
@@ -203,9 +231,9 @@ export class CratesTarget extends BaseTarget {
203231

204232
const crates = this.getPublishOrder(unorderedCrates);
205233
logger.debug(
206-
`Publishing packages in the following order: [${crates
234+
`Publishing packages in the following order: ${crates
207235
.map(c => c.name)
208-
.toString()}]`
236+
.join(', ')}`
209237
);
210238
return forEachChained(crates, async crate => this.publishPackage(crate));
211239
}
@@ -227,9 +255,26 @@ export class CratesTarget extends BaseTarget {
227255
crate.manifest_path
228256
);
229257

230-
return spawnProcess(CARGO_BIN, args, {
231-
env: { ...process.env, CARGO_REGISTRY_TOKEN: this.cratesConfig.apiToken },
232-
});
258+
const env = {
259+
...process.env,
260+
CARGO_REGISTRY_TOKEN: this.cratesConfig.apiToken,
261+
};
262+
263+
let delay = RETRY_DELAY_SECS;
264+
logger.info(`Publishing ${crate.name}`);
265+
for (let i = 0; i <= MAX_ATTEMPTS; i++) {
266+
try {
267+
return await spawnProcess(CARGO_BIN, args, { env });
268+
} catch (e) {
269+
if (i < MAX_ATTEMPTS && e.message.includes(VERSION_ERROR)) {
270+
logger.warn(`Publish failed, trying again in ${delay}s...`);
271+
await sleepAsync(delay * 1000);
272+
delay *= RETRY_EXP_FACTOR;
273+
} else {
274+
throw e;
275+
}
276+
}
277+
}
233278
}
234279

235280
/**

0 commit comments

Comments
 (0)