Skip to content

Commit 04d836f

Browse files
committed
feat(publish): Support 'publish.timeout' config behind '-Zpublish-timeout'
Originally, crates.io would block on publish requests until the publish was complete, giving `cargo publish` this behavior by extension. When crates.io switched to asynchronous publishing, this intermittently broke people's workflows when publishing multiple crates. I say interittent because it usually works until it doesn't and it is unclear why to the end user because it will be published by the time they check. In the end, callers tend to either put in timeouts (and pray), poll the server's API, or use `crates-index` crate to poll the index. This isn't sufficient because - For any new interested party, this is a pit of failure they'll fall into - crates-index has re-implemented index support incorrectly in the past, currently doesn't handle auth, doesn't support `git-cli`, etc. - None of these previous options work if we were to implement workspace-publish support (#1169) - The new sparse registry might increase the publish times, making the delay easier to hit manually - The new sparse registry goes through CDNs so checking the server's API might not be sufficient - Once the sparse registry is available, crates-index users will find out when the package is ready in git but it might not be ready through the sparse registry because of CDNs This introduces unstable support for blocking by setting `publish.timeout` to non-zero value. A step towards #9507
1 parent 66b62d2 commit 04d836f

File tree

5 files changed

+170
-42
lines changed

5 files changed

+170
-42
lines changed

crates/cargo-test-support/src/compare.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ fn substitute_macros(input: &str) -> String {
197197
("[MIGRATING]", " Migrating"),
198198
("[EXECUTABLE]", " Executable"),
199199
("[SKIPPING]", " Skipping"),
200+
("[WAITING]", " Waiting"),
200201
];
201202
let mut result = input.to_owned();
202203
for &(pat, subst) in &macros {

src/cargo/core/features.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,7 @@ unstable_cli_options!(
686686
rustdoc_map: bool = ("Allow passing external documentation mappings to rustdoc"),
687687
separate_nightlies: bool = (HIDDEN),
688688
terminal_width: Option<Option<usize>> = ("Provide a terminal width to rustc for error truncation"),
689+
publish_timeout: bool = ("Enable the `publish.timeout` key in .cargo/config.toml file"),
689690
unstable_options: bool = ("Allow the usage of unstable options"),
690691
// TODO(wcrichto): move scrape example configuration into Cargo.toml before stabilization
691692
// See: https://github.com/rust-lang/cargo/pull/9525#discussion_r728470927
@@ -930,6 +931,7 @@ impl CliUnstable {
930931
"jobserver-per-rustc" => self.jobserver_per_rustc = parse_empty(k, v)?,
931932
"host-config" => self.host_config = parse_empty(k, v)?,
932933
"target-applies-to-host" => self.target_applies_to_host = parse_empty(k, v)?,
934+
"publish-timeout" => self.publish_timeout = parse_empty(k, v)?,
933935
"features" => {
934936
// `-Z features` has been stabilized since 1.51,
935937
// but `-Z features=compare` is still allowed for convenience

src/cargo/ops/registry.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ use termcolor::Color::Green;
1818
use termcolor::ColorSpec;
1919

2020
use crate::core::dependency::DepKind;
21+
use crate::core::dependency::Dependency;
2122
use crate::core::manifest::ManifestMetadata;
2223
use crate::core::resolver::CliFeatures;
2324
use crate::core::source::Source;
25+
use crate::core::QueryKind;
2426
use crate::core::{Package, SourceId, Workspace};
2527
use crate::ops;
2628
use crate::ops::Packages;
@@ -183,6 +185,19 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
183185
reg_ids.original,
184186
opts.dry_run,
185187
)?;
188+
if !opts.dry_run {
189+
const DEFAULT_TIMEOUT: u64 = 0;
190+
let timeout = if opts.config.cli_unstable().publish_timeout {
191+
let timeout: Option<u64> = opts.config.get("publish.timeout")?;
192+
timeout.unwrap_or(DEFAULT_TIMEOUT)
193+
} else {
194+
DEFAULT_TIMEOUT
195+
};
196+
if 0 < timeout {
197+
let timeout = std::time::Duration::from_secs(timeout);
198+
wait_for_publish(opts.config, reg_ids.original, pkg, timeout)?;
199+
}
200+
}
186201

187202
Ok(())
188203
}
@@ -374,6 +389,72 @@ fn transmit(
374389
Ok(())
375390
}
376391

392+
fn wait_for_publish(
393+
config: &Config,
394+
registry_src: SourceId,
395+
pkg: &Package,
396+
timeout: std::time::Duration,
397+
) -> CargoResult<()> {
398+
let version_req = format!("={}", pkg.version());
399+
let mut source = SourceConfigMap::empty(config)?.load(registry_src, &HashSet::new())?;
400+
let source_description = source.describe();
401+
let query = Dependency::parse(pkg.name(), Some(&version_req), registry_src)?;
402+
403+
let now = std::time::Instant::now();
404+
let sleep_time = std::time::Duration::from_secs(1);
405+
let mut logged = false;
406+
loop {
407+
{
408+
let _lock = config.acquire_package_cache_lock()?;
409+
// Force re-fetching the source
410+
//
411+
// As pulling from a git source is expensive, we track when we've done it within the
412+
// process to only do it once, but we are one of the rare cases that needs to do it
413+
// multiple times
414+
config
415+
.updated_sources()
416+
.remove(&source.replaced_source_id());
417+
source.invalidate_cache();
418+
let summaries = loop {
419+
// Exact to avoid returning all for path/git
420+
match source.query_vec(&query, QueryKind::Exact) {
421+
std::task::Poll::Ready(res) => {
422+
break res?;
423+
}
424+
std::task::Poll::Pending => source.block_until_ready()?,
425+
}
426+
};
427+
if !summaries.is_empty() {
428+
break;
429+
}
430+
}
431+
432+
if timeout < now.elapsed() {
433+
config.shell().warn(format!(
434+
"timed out waiting for `{}` to be in {}",
435+
pkg.name(),
436+
source_description
437+
))?;
438+
break;
439+
}
440+
441+
if !logged {
442+
config.shell().status(
443+
"Waiting",
444+
format!(
445+
"on `{}` to propagate to {} (ctrl-c to wait asynchronously)",
446+
pkg.name(),
447+
source_description
448+
),
449+
)?;
450+
logged = true;
451+
}
452+
std::thread::sleep(sleep_time);
453+
}
454+
455+
Ok(())
456+
}
457+
377458
/// Returns the index and token from the config file for the given registry.
378459
///
379460
/// `registry` is typically the registry specified on the command-line. If

src/doc/src/reference/unstable.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Each new feature described below should explain how to use it.
9999
* [credential-process](#credential-process) — Adds support for fetching registry tokens from an external authentication program.
100100
* [`cargo logout`](#cargo-logout) — Adds the `logout` command to remove the currently saved registry token.
101101
* [sparse-registry](#sparse-registry) — Adds support for fetching from static-file HTTP registries (`sparse+`)
102+
* [publish-timeout](#publish-timeout) — Controls the timeout between uploading the crate and being available in the index
102103

103104
### allow-features
104105

@@ -841,6 +842,23 @@ crates, which can save significant time and bandwidth.
841842

842843
The format of the sparse index is identical to a checkout of a git-based index.
843844

845+
### publish-timeout
846+
* Tracking Issue: [11222](https://github.com/rust-lang/cargo/issues/11222)
847+
848+
The `publish.timeout` key in a config file can be used to control how long
849+
`cargo publish` waits between posting a package to the registry and it being
850+
available in the local index.
851+
852+
A timeout of `0` prevents any checks from occurring.
853+
854+
It requires the `-Zpublish-timeout` command-line options to be set.
855+
856+
```toml
857+
# config.toml
858+
[publish]
859+
timeout = 300 # in seconds
860+
```
861+
844862
### credential-process
845863
* Tracking Issue: [#8933](https://github.com/rust-lang/cargo/issues/8933)
846864
* RFC: [#2730](https://github.com/rust-lang/rfcs/pull/2730)

tests/testsuite/publish.rs

Lines changed: 68 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2263,7 +2263,7 @@ fn http_api_not_noop() {
22632263
}
22642264

22652265
#[cargo_test]
2266-
fn delayed_publish_errors() {
2266+
fn wait_for_publish() {
22672267
// Counter for number of tries before the package is "published"
22682268
let arc: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
22692269
let arc2 = arc.clone();
@@ -2304,10 +2304,17 @@ fn delayed_publish_errors() {
23042304
"#,
23052305
)
23062306
.file("src/lib.rs", "")
2307+
.file(
2308+
".cargo/config",
2309+
"
2310+
[publish]
2311+
timeout = 60
2312+
",
2313+
)
23072314
.build();
23082315

2309-
p.cargo("publish --no-verify -Z sparse-registry")
2310-
.masquerade_as_nightly_cargo(&["sparse-registry"])
2316+
p.cargo("publish --no-verify -Z sparse-registry -Z publish-timeout")
2317+
.masquerade_as_nightly_cargo(&["sparse-registry", "publish-timeout"])
23112318
.replace_crates_io(registry.index_url())
23122319
.with_status(0)
23132320
.with_stderr(
@@ -2317,13 +2324,15 @@ fn delayed_publish_errors() {
23172324
See [..]
23182325
[PACKAGING] delay v0.0.1 ([CWD])
23192326
[UPLOADING] delay v0.0.1 ([CWD])
2327+
[UPDATING] `crates-io` index
2328+
[WAITING] on `delay` to propagate to `crates-io` index (which is replacing registry `crates-io`) (ctrl-c to wait asynchronously)
23202329
",
23212330
)
23222331
.run();
23232332

2324-
// Check nothing has touched the responder
2333+
// Verify the responder has been pinged
23252334
let lock = arc2.lock().unwrap();
2326-
assert_eq!(*lock, 0);
2335+
assert_eq!(*lock, 2);
23272336
drop(lock);
23282337

23292338
let p = project()
@@ -2341,23 +2350,6 @@ See [..]
23412350
.file("src/main.rs", "fn main() {}")
23422351
.build();
23432352

2344-
p.cargo("build -Z sparse-registry")
2345-
.masquerade_as_nightly_cargo(&["sparse-registry"])
2346-
.with_status(101)
2347-
.with_stderr(
2348-
"\
2349-
[UPDATING] [..]
2350-
[ERROR] no matching package named `delay` found
2351-
location searched: registry `crates-io`
2352-
required by package `foo v0.0.1 ([..]/foo)`
2353-
",
2354-
)
2355-
.run();
2356-
2357-
let lock = arc2.lock().unwrap();
2358-
assert_eq!(*lock, 1);
2359-
drop(lock);
2360-
23612353
p.cargo("build -Z sparse-registry")
23622354
.masquerade_as_nightly_cargo(&["sparse-registry"])
23632355
.with_status(0)
@@ -2368,7 +2360,7 @@ required by package `foo v0.0.1 ([..]/foo)`
23682360
/// the responder twice per cargo invocation. If that ever gets changed
23692361
/// this test will need to be changed accordingly.
23702362
#[cargo_test]
2371-
fn delayed_publish_errors_underscore() {
2363+
fn wait_for_publish_underscore() {
23722364
// Counter for number of tries before the package is "published"
23732365
let arc: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
23742366
let arc2 = arc.clone();
@@ -2409,10 +2401,17 @@ fn delayed_publish_errors_underscore() {
24092401
"#,
24102402
)
24112403
.file("src/lib.rs", "")
2404+
.file(
2405+
".cargo/config",
2406+
"
2407+
[publish]
2408+
timeout = 60
2409+
",
2410+
)
24122411
.build();
24132412

2414-
p.cargo("publish --no-verify -Z sparse-registry")
2415-
.masquerade_as_nightly_cargo(&["sparse-registry"])
2413+
p.cargo("publish --no-verify -Z sparse-registry -Z publish-timeout")
2414+
.masquerade_as_nightly_cargo(&["sparse-registry", "publish-timeout"])
24162415
.replace_crates_io(registry.index_url())
24172416
.with_status(0)
24182417
.with_stderr(
@@ -2422,13 +2421,16 @@ fn delayed_publish_errors_underscore() {
24222421
See [..]
24232422
[PACKAGING] delay_with_underscore v0.0.1 ([CWD])
24242423
[UPLOADING] delay_with_underscore v0.0.1 ([CWD])
2424+
[UPDATING] `crates-io` index
2425+
[WAITING] on `delay_with_underscore` to propagate to `crates-io` index (which is replacing registry `crates-io`) (ctrl-c to wait asynchronously)
24252426
",
24262427
)
24272428
.run();
24282429

2429-
// Check nothing has touched the responder
2430+
// Verify the repsponder has been pinged
24302431
let lock = arc2.lock().unwrap();
2431-
assert_eq!(*lock, 0);
2432+
// NOTE: package names with - or _ hit the responder twice per cargo invocation
2433+
assert_eq!(*lock, 3);
24322434
drop(lock);
24332435

24342436
let p = project()
@@ -2448,24 +2450,48 @@ See [..]
24482450

24492451
p.cargo("build -Z sparse-registry")
24502452
.masquerade_as_nightly_cargo(&["sparse-registry"])
2451-
.with_status(101)
2453+
.with_status(0)
2454+
.run();
2455+
}
2456+
2457+
#[cargo_test]
2458+
fn skip_wait_for_publish() {
2459+
// Intentionally using local registry so the crate never makes it to the index
2460+
let registry = registry::init();
2461+
2462+
let p = project()
2463+
.file(
2464+
"Cargo.toml",
2465+
r#"
2466+
[package]
2467+
name = "foo"
2468+
version = "0.0.1"
2469+
authors = []
2470+
license = "MIT"
2471+
description = "foo"
2472+
"#,
2473+
)
2474+
.file("src/main.rs", "fn main() {}")
2475+
.file(
2476+
".cargo/config",
2477+
"
2478+
[publish]
2479+
timeout = 0
2480+
",
2481+
)
2482+
.build();
2483+
2484+
p.cargo("publish --no-verify -Zpublish-timeout")
2485+
.replace_crates_io(registry.index_url())
2486+
.masquerade_as_nightly_cargo(&["publish-timeout"])
24522487
.with_stderr(
24532488
"\
2454-
[UPDATING] [..]
2455-
[ERROR] no matching package named `delay_with_underscore` found
2456-
location searched: registry `crates-io`
2457-
required by package `foo v0.0.1 ([..]/foo)`
2489+
[UPDATING] crates.io index
2490+
[WARNING] manifest has no documentation, [..]
2491+
See [..]
2492+
[PACKAGING] foo v0.0.1 ([CWD])
2493+
[UPLOADING] foo v0.0.1 ([CWD])
24582494
",
24592495
)
24602496
.run();
2461-
2462-
let lock = arc2.lock().unwrap();
2463-
// package names with - or _ hit the responder twice per cargo invocation
2464-
assert_eq!(*lock, 2);
2465-
drop(lock);
2466-
2467-
p.cargo("build -Z sparse-registry")
2468-
.masquerade_as_nightly_cargo(&["sparse-registry"])
2469-
.with_status(0)
2470-
.run();
24712497
}

0 commit comments

Comments
 (0)