diff --git a/.bazelrc b/.bazelrc index 153fcc8a76..ce6952a3c3 100644 --- a/.bazelrc +++ b/.bazelrc @@ -18,7 +18,7 @@ common --repo_env=CARGO_NET_GIT_FETCH_WITH_CLI=true # host cargo would download another 418 MB toolchain on every CI job. common --repo_env=CARGO_UNSTABLE_GIT=shallow-deps common --repo_env=RUSTC_BOOTSTRAP=1 -# MODULE.bazel registers only nightly/2026-10-18. Without this, rules_rust +# MODULE.bazel registers only nightly/2026-10-19. Without this, rules_rust # resolves the stable channel and the build fails closed. # Hosts can extend this repository profile with their private fleet settings. build --config=tracedecay @@ -67,6 +67,8 @@ test --default_test_resources=cpu=HOST_CPUS build:clippy --aspects=@rules_rust//rust:defs.bzl%rust_clippy_aspect # Lint outputs need dependency metadata, not linked test executables. build:clippy --output_groups=clippy_checks +# Produce dependency metadata so linting does not require full Rust libraries. +build:clippy --@rules_rust//rust/settings:pipelined_compilation=true build:clippy --@rules_rust//rust/settings:clippy_flag=-Dwarnings build:clippy --@rules_rust//rust/settings:clippy.toml=//:clippy.toml diff --git a/MODULE.bazel b/MODULE.bazel index 4ea7098baf..a963acbd91 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -18,6 +18,8 @@ single_version_override( "//scripts:bazel/patches/rules-rust-windows-file-url-path-deps.patch", # Genrule tools must include Rustfmt's compiler runtime libraries. "//scripts:bazel/patches/rules-rust-rustfmt-runtime-inputs.patch", + # No-link lint actions can use Rust metadata for executable targets. + "//scripts:bazel/patches/rules-rust-clippy-metadata-inputs.patch", ], patch_strip = 1, ) @@ -48,14 +50,14 @@ rust.toolchain( "fastbuild": "1", "opt": "3", }, - versions = ["nightly/2026-10-18"], - rustfmt_version = "nightly/2026-10-18", - rust_analyzer_version = "nightly/2026-10-18", + versions = ["nightly/2026-10-19"], + rustfmt_version = "nightly/2026-10-19", + rust_analyzer_version = "nightly/2026-10-19", ) use_repo( rust, "rust_toolchains", - rust_analyzer_tools = "rust_analyzer_nightly-2026-10-18_tools", + rust_analyzer_tools = "rust_analyzer_nightly-2026-10-19_tools", ) register_toolchains("@rust_toolchains//:all") diff --git a/crates/tracedecay-private-fs/BUILD.bazel b/crates/tracedecay-private-fs/BUILD.bazel index b329dfdb6d..179ba3b0c6 100644 --- a/crates/tracedecay-private-fs/BUILD.bazel +++ b/crates/tracedecay-private-fs/BUILD.bazel @@ -53,7 +53,6 @@ rust_test( crate_features = ["test-helpers"], compile_data = glob(["assets/**", "data/**", "fixtures/**", "resources/**", "tests/**", "tests/fixtures/**", "vendor/**"], allow_empty = True), data = glob(["assets/**", "data/**", "fixtures/**", "resources/**", "tests/**", "tests/fixtures/**", "vendor/**"], allow_empty = True), - rustc_env = {"CARGO_TARGET_TMPDIR": "/tmp"}, ) rust_test( @@ -69,7 +68,6 @@ rust_test( crate_features = ["test-helpers"], compile_data = glob(["assets/**", "data/**", "fixtures/**", "resources/**", "tests/**", "tests/fixtures/**", "vendor/**"], allow_empty = True), data = glob(["assets/**", "data/**", "fixtures/**", "resources/**", "tests/**", "tests/fixtures/**", "vendor/**"], allow_empty = True), - rustc_env = {"CARGO_TARGET_TMPDIR": "/tmp"}, ) rust_test( diff --git a/crates/tracedecay-private-fs/tests/framed_log.rs b/crates/tracedecay-private-fs/tests/framed_log.rs index a662770690..7e89393fd5 100644 --- a/crates/tracedecay-private-fs/tests/framed_log.rs +++ b/crates/tracedecay-private-fs/tests/framed_log.rs @@ -118,54 +118,44 @@ fn prepared_publish_is_private_and_readable_through_the_adapter() { } } -/// A batch makes its own members durable and nothing else: syncing one small -/// member must not wait for another writer's dirty data on the same -/// filesystem. Asserted as a latency class against the cost of flushing that -/// unrelated data afterwards, on disk under the target directory (a tmpfs -/// would make both free). +/// Observe the barriers themselves: a filesystem can coalesce unrelated +/// writeback with a member's fsync, so their relative elapsed times cannot +/// establish which files the batch requested to flush. #[cfg(target_os = "linux")] #[test] -fn durable_batch_sync_is_sized_to_its_members_not_the_filesystem() { - // The Bazel test launcher passes its scratch dir at run time. - let target_tmp = std::env::var_os("CARGO_TARGET_TMPDIR") - .unwrap_or_else(|| env!("CARGO_TARGET_TMPDIR").into()); - let root = tempfile::tempdir_in(target_tmp).expect("batch fixture root"); - // Incompressible, so a compressing filesystem still has to write it. - let mut state = 0x9e37_79b9_7f4a_7c15_u64; - let chunk = (0..1 << 20) - .map(|_| { - state ^= state << 13; - state ^= state >> 7; - state ^= state << 17; - state.to_le_bytes()[0] - }) - .collect::>(); - let mut unrelated = fs::File::create(root.path().join("unrelated")).expect("unrelated file"); - for round in 0..128_u8 { - let mut block = chunk.clone(); - block[0] = round; - unrelated.write_all(&block).expect("unrelated dirty data"); - } - +fn durable_batch_sync_flushes_only_its_members() { + let root = tempfile::tempdir().expect("batch fixture root"); + let unrelated_path = root.path().join("unrelated"); + fs::write(&unrelated_path, b"unrelated dirty data").expect("unrelated file"); let member_path = root.path().join("member.tmp"); let mut member = fs::File::create(&member_path).expect("member file"); member.write_all(b"sealed segment").expect("member bytes"); let mut batch = DurableFileBatch::new(); batch.written(&member_path, &member).expect("record member"); - let started = std::time::Instant::now(); + let member_barriers = sync_latency::inject(&member_path, Duration::ZERO); + let unrelated_barriers = sync_latency::inject(&unrelated_path, Duration::ZERO); + let all_barriers = sync_latency::inject(root.path(), Duration::ZERO); batch.sync().expect("batch sync"); - let batch_sync = started.elapsed(); - let started = std::time::Instant::now(); - unrelated.sync_all().expect("unrelated sync"); - let unrelated_sync = started.elapsed(); + + assert_eq!(member_barriers.syncs(), 1, "the member must be flushed"); + assert_eq!( + unrelated_barriers.syncs(), + 0, + "unrelated data is not a member" + ); + assert_eq!(all_barriers.syncs(), 0, "no additional filesystem barriers"); assert_eq!( fs::read(&member_path).expect("member readback"), b"sealed segment" ); - assert!( - batch_sync * 4 < unrelated_sync, - "one-member batch sync took {batch_sync:?}; flushing the unrelated 128 MiB took {unrelated_sync:?}" + fs::remove_file(&member_path).expect("remove member before another sync"); + assert_eq!( + batch + .sync() + .expect_err("missing member must refuse durability") + .kind(), + ErrorKind::NotFound ); } diff --git a/crates/tracedecay/src/daemon/tests/bootstrap.rs b/crates/tracedecay/src/daemon/tests/bootstrap.rs index a08b73b9aa..e1ae5cf631 100644 --- a/crates/tracedecay/src/daemon/tests/bootstrap.rs +++ b/crates/tracedecay/src/daemon/tests/bootstrap.rs @@ -3715,8 +3715,9 @@ async fn direct_tool_cache_miss_returns_warming_while_project_opens_in_backgroun assert!(message.contains("retry"), "{message}"); // Retry the warming hint the way a client does. Each retry rides the - // detached open's own publication, and a retry that had to start a new - // open would show up as a second attempt. + // detached open's own publication. Its topology walk can still own the + // discovery slot, which is another typed retryable response. A retry that + // had to start a new open would show up as a second attempt. loop { match engine .project_server_for_request(&handshake, super::super::ProjectServerRequirement::Core) @@ -3724,7 +3725,11 @@ async fn direct_tool_cache_miss_returns_warming_while_project_opens_in_backgroun { Ok(_) => break, Err(error) => assert!( - super::super::error_is_project_warming(&error), + super::super::error_is_project_warming(&error) + || (super::super::error_is_repository_discovery_deferred(&error) + && error + .project_route_context() + .is_some_and(|(_, retryable, _)| retryable)), "the detached warm-up must publish, not fail: {error:?}" ), } diff --git a/crates/tracedecay/src/daemon/tests/ownership.rs b/crates/tracedecay/src/daemon/tests/ownership.rs index bb4b4c8641..92d0043209 100644 --- a/crates/tracedecay/src/daemon/tests/ownership.rs +++ b/crates/tracedecay/src/daemon/tests/ownership.rs @@ -366,30 +366,36 @@ async fn mounted_graph_listing_takes_no_server_lease_while_it_reads() { .expect("mount project server"); let swap = server.graph_slot().write_owned().await; + let mut registry_gate = Some(engine.store_administration.project_servers().lock().await); let listing = engine.store_administration.mounted_project_graphs(); let mut listing = std::pin::pin!(listing); let handles = Arc::strong_count(&server); - assert!( - std::future::Future::poll( - listing.as_mut(), - &mut std::task::Context::from_waker(std::task::Waker::noop()), - ) - .is_pending(), - "the listing parks on the held graph read" - ); + let registry_lock = engine.store_administration.project_servers().lock(); + let mut registry_lock = std::pin::pin!(registry_lock); + // Force the first listing poll to wait for another registry user. Tokio's + // cooperative budget can also defer it before the held graph slot. + // Drive both futures with real wakeups: the fair registry mutex admits + // the listing before this later lock request, then must release it while + // the graph write guard still prevents the listing from completing. + let registry = tokio::time::timeout( + std::time::Duration::from_secs(5), + std::future::poll_fn(|context| { + assert!( + std::future::Future::poll(listing.as_mut(), context).is_pending(), + "the listing parks on the held graph read" + ); + drop(registry_gate.take()); + std::future::Future::poll(registry_lock.as_mut(), context) + }), + ) + .await + .expect("a parked graph listing must release the registry lock for route binds"); assert_eq!( Arc::strong_count(&server), handles, "a parked graph listing must hold no server handle" ); - assert!( - engine - .store_administration - .project_servers() - .try_lock() - .is_ok(), - "a parked graph listing must not hold the registry lock, or every route bind waits on the swap" - ); + drop(registry); drop(swap); assert_eq!(listing.await.len(), 1); } diff --git a/renovate.json b/renovate.json index 40ca9c23fd..d18c91f19e 100644 --- a/renovate.json +++ b/renovate.json @@ -23,7 +23,10 @@ "description": "rules_rust's nightly pin, and the .bazelrc note that names it, must match rust-toolchain.toml", "customType": "regex", "managerFilePatterns": ["/^MODULE\\.bazel$/", "/^\\.bazelrc$/"], - "matchStrings": ["nightly/(?\\d{4}-\\d{2}-\\d{2})"], + "matchStrings": [ + "nightly/(?\\d{4}-\\d{2}-\\d{2})", + "rust_analyzer_nightly-(?\\d{4}-\\d{2}-\\d{2})_tools" + ], "depNameTemplate": "rust", "datasourceTemplate": "rust-version", "extractVersionTemplate": "^nightly-(?\\d{4}-\\d{2}-\\d{2})$", diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 5f396742b5..09be1af02d 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,6 +1,6 @@ [toolchain] # Dated nightly: asupersync's nightly-outcome-try default needs try_trait_v2. # 2026-09-27 includes rust-lang/rust#162275; 2026-08-25 does not. -channel = "nightly-2026-10-18" +channel = "nightly-2026-10-19" # LSP acceptance negotiates capabilities with the pinned upstream analyzer. components = ["clippy", "rustfmt", "rust-analyzer", "rust-src"] diff --git a/scripts/bazel/patches/rules-rust-clippy-metadata-inputs.patch b/scripts/bazel/patches/rules-rust-clippy-metadata-inputs.patch new file mode 100644 index 0000000000..98924c6cc1 --- /dev/null +++ b/scripts/bazel/patches/rules-rust-clippy-metadata-inputs.patch @@ -0,0 +1,114 @@ +diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl +--- a/rust/private/rustc.bzl ++++ b/rust/private/rustc.bzl +@@ -645,7 +645,7 @@ + visited_libs[name] = artifact + return ambiguous_libs + +-def _depend_on_metadata(crate_info, force_depend_on_objects): ++def _depend_on_metadata(crate_info, force_depend_on_objects, force_depend_on_metadata = False): + """Determines if we can depend on metadata for this crate. + + By default (when pipelining is disabled or when the crate type needs to link against +@@ -658,6 +658,8 @@ + Args: + crate_info (CrateInfo): The Crate to determine this for. + force_depend_on_objects (bool): if set we will not depend on metadata. ++ force_depend_on_metadata (bool): A no-link action, such as Clippy, can use ++ metadata even when checking an executable or procedural macro. + + Returns: + Whether we can depend on metadata for this crate. +@@ -665,7 +667,7 @@ + if force_depend_on_objects: + return False + +- return crate_info.type in ("rlib", "lib") ++ return force_depend_on_metadata or crate_info.type in ("rlib", "lib") + + def collect_inputs( + ctx, +@@ -683,7 +685,8 @@ + force_depend_on_objects = False, + experimental_use_cc_common_link = False, + include_linker_inputs = False, +- include_link_flags = True): ++ include_link_flags = True, ++ force_depend_on_metadata = False): + """Gather's the inputs and required input information for a rustc action + + Args: +@@ -707,6 +710,8 @@ + rust binaries. + include_linker_inputs (bool, optional): Whether to include linker inputs in transitive dependencies. + include_link_flags (bool, optional): Whether to include flags like `-l` that instruct the linker to search for a library. ++ force_depend_on_metadata (bool, optional): Select metadata dependencies and ++ omit transitive native linker artifacts for a no-link action. + + Returns: + tuple: A tuple: A tuple of the following items: +@@ -747,7 +752,7 @@ + # flattened on each transitive rust_library dependency. + libs_from_linker_inputs = [] + ambiguous_libs = {} +- if crate_info.type not in ("lib", "rlib") or include_linker_inputs: ++ if (not force_depend_on_metadata and crate_info.type not in ("lib", "rlib")) or include_linker_inputs: + linker_inputs = dep_info.transitive_noncrates.to_list() + ambiguous_libs = _disambiguate_libs(ctx.actions, toolchain, crate_info, dep_info, use_pic) + libs_from_linker_inputs = _collect_libs_from_linker_inputs(linker_inputs, use_pic) + [ +@@ -762,7 +767,7 @@ + linkstamp_outs = [] + + transitive_crate_outputs = dep_info.transitive_crate_outputs +- if _depend_on_metadata(crate_info, force_depend_on_objects): ++ if _depend_on_metadata(crate_info, force_depend_on_objects, force_depend_on_metadata): + transitive_crate_outputs = dep_info.transitive_metadata_outputs + + dependency_search_artifacts = None +@@ -1009,7 +1014,8 @@ + error_format = None, + allowed_unstable_rust_features = None, + link_std_dylib = False, +- dependency_search_artifacts = None): ++ dependency_search_artifacts = None, ++ force_depend_on_metadata = False): + """Builds an Args object containing common rustc flags + + Args: +@@ -1078,6 +1084,8 @@ + use_json_output (bool): Have rustc emit json and process_wrapper parse json messages to output rendered output. + build_metadata (bool): Generate CLI arguments for building *only* .rmeta files. This requires use_json_output. + force_depend_on_objects (bool): Force using `.rlib` object files instead of metadata (`.rmeta`) files even if they are available. ++ force_depend_on_metadata (bool): Use metadata dependencies for an action ++ that checks an executable without linking it. + skip_expanding_rustc_env (bool): Whether to skip expanding CrateInfo.rustc_env_attr + require_explicit_unstable_features (bool): Whether to require all unstable features to be explicitly opted in to using `-Zallow-features=...`. + error_format (str, optional): Error format to pass to the `--error-format` command line argument. If set to None, uses the "_error_format" entry in `attr`. +@@ -1421,7 +1429,7 @@ + if crate_info.type == "dylib" and toolchain.target_os in ["macos", "darwin"]: + rustc_flags.add("--codegen=link-arg=-Wl,-install_name,@rpath/" + crate_info.output.basename) + +- use_metadata = _depend_on_metadata(crate_info, force_depend_on_objects) ++ use_metadata = _depend_on_metadata(crate_info, force_depend_on_objects, force_depend_on_metadata) + + # These always need to be added, even if not linking this crate. + add_crate_link_flags(rustc_flags, dep_info, force_all_deps_direct, use_metadata, dependency_search_artifacts) +diff --git a/rust/private/clippy.bzl b/rust/private/clippy.bzl +--- a/rust/private/clippy.bzl ++++ b/rust/private/clippy.bzl +@@ -161,6 +161,7 @@ + dep_info, + build_info, + lint_files, ++ force_depend_on_metadata = True, + ) + + if clippy_diagnostics_file: +@@ -187,6 +188,7 @@ + build_env_files = build_env_files, + build_flags_files = build_flags_files, + emit = ["dep-info", "metadata"], ++ force_depend_on_metadata = True, + skip_expanding_rustc_env = True, + use_json_output = bool(clippy_diagnostics_file), + error_format = error_format,