/build/source/src/bin/nativelink.rs
Line | Count | Source |
1 | | // Copyright 2024 Trace Machina, Inc. All rights reserved. |
2 | | // |
3 | | // Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License"); |
4 | | // you may not use this file except in compliance with the License. |
5 | | // You may obtain a copy of the License at |
6 | | // |
7 | | // See LICENSE file for details |
8 | | // |
9 | | // Unless required by applicable law or agreed to in writing, software |
10 | | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | | // See the License for the specific language governing permissions and |
13 | | // limitations under the License. |
14 | | |
15 | | // The worker's action future is a long chain of combinators; proving it |
16 | | // Send for the spawn walks the whole chain, and the default limit of 128 is |
17 | | // a few steps short of it on the sanitizer toolchain. |
18 | | #![recursion_limit = "256"] |
19 | | |
20 | | use clap::Parser; |
21 | | use futures::future::try_join_all; |
22 | | use mimalloc::MiMalloc; |
23 | | use nativelink_config::cas_server::{CasConfig, GlobalConfig}; |
24 | | use nativelink_config::stores::ConfigDigestHashFunction; |
25 | | use nativelink_error::{Code, Error, ResultExt, make_err}; |
26 | | use nativelink_util::common::fs::set_open_file_limit; |
27 | | use nativelink_util::digest_hasher::{DigestHasherFunc, set_default_digest_hasher_func}; |
28 | | use nativelink_util::fs; |
29 | | #[cfg(target_family = "unix")] |
30 | | use nativelink_util::shutdown_guard::Priority; |
31 | | use nativelink_util::shutdown_guard::ShutdownGuard; |
32 | | use nativelink_util::store_trait::{ |
33 | | DEFAULT_DIGEST_SIZE_HEALTH_CHECK_CFG, set_default_digest_size_health_check, |
34 | | }; |
35 | | use nativelink_util::telemetry::init_tracing; |
36 | | use tokio::select; |
37 | | #[cfg(target_family = "unix")] |
38 | | use tokio::signal::unix::{SignalKind, signal}; |
39 | | use tokio::sync::oneshot::Sender; |
40 | | use tokio::sync::{broadcast, oneshot}; |
41 | | use tracing::trace_span; |
42 | | #[cfg(target_family = "unix")] |
43 | | use tracing::warn; |
44 | | |
45 | | #[global_allocator] |
46 | | static GLOBAL: MiMalloc = MiMalloc; |
47 | | |
48 | | /// Broadcast Channel Capacity |
49 | | /// Note: The actual capacity may be greater than the provided capacity. |
50 | | const BROADCAST_CAPACITY: usize = 1; |
51 | | |
52 | 0 | fn install_default_rustls_crypto_provider() { |
53 | 0 | drop(tokio_rustls::rustls::crypto::ring::default_provider().install_default()); |
54 | 0 | } |
55 | | |
56 | | /// Backend for bazel remote execution / cache API. |
57 | | #[derive(Parser, Debug)] |
58 | | #[clap( |
59 | | author = "Trace Machina, Inc. <nativelink@tracemachina.com>", |
60 | | version, |
61 | | about, |
62 | | long_about = None |
63 | | )] |
64 | | struct Args { |
65 | | /// Config file to use. |
66 | | #[clap(value_parser)] |
67 | | config_file: String, |
68 | | } |
69 | | |
70 | 0 | async fn inner_main( |
71 | 0 | cfg: CasConfig, |
72 | 0 | shutdown_tx: broadcast::Sender<ShutdownGuard>, |
73 | 0 | scheduler_shutdown_tx: Sender<()>, |
74 | 0 | ) -> Result<(), Error> { |
75 | 0 | let server = nativelink::build_server(cfg, shutdown_tx, scheduler_shutdown_tx).await?; |
76 | | |
77 | 0 | let result = select! { |
78 | 0 | result = try_join_all(server.root_futures) => result.map(|_| ()), |
79 | 0 | result = server.single_use_complete_rx, if server.single_use_enabled => { |
80 | 0 | result.map_err(|err| make_err!(Code::Internal, "Single-use worker completion channel closed: {err}")) |
81 | | }, |
82 | | }; |
83 | 0 | if let Err(e) = result { |
84 | 0 | panic!("{e:?}"); |
85 | 0 | } |
86 | | |
87 | 0 | Ok(()) |
88 | 0 | } |
89 | | |
90 | 0 | fn get_config() -> Result<CasConfig, Error> { |
91 | 0 | let args = Args::parse(); |
92 | 0 | CasConfig::try_from_json5_file(&args.config_file) |
93 | 0 | } |
94 | | |
95 | 0 | fn main() -> Result<(), Box<dyn core::error::Error>> { |
96 | 0 | install_default_rustls_crypto_provider(); |
97 | | |
98 | | // Set QoS to USER_INITIATED on the main thread *before* the tokio |
99 | | // runtime is built so the spawned worker threads inherit P-core |
100 | | // scheduling preference via pthread QoS inheritance on Apple |
101 | | // Silicon. `on_thread_start` below is a belt-and-suspenders hook |
102 | | // for any thread that misses the inherited class (e.g. tokio |
103 | | // blocking pool threads created lazily). No-op on non-macOS. |
104 | 0 | let _ = nativelink_worker::qos::set_user_initiated(); |
105 | | |
106 | | #[expect(clippy::disallowed_methods, reason = "starting main runtime")] |
107 | 0 | let runtime = tokio::runtime::Builder::new_multi_thread() |
108 | 0 | .on_thread_start(|| { |
109 | 0 | let _ = nativelink_worker::qos::set_user_initiated(); |
110 | 0 | }) |
111 | 0 | .enable_all() |
112 | 0 | .build()?; |
113 | | |
114 | | // The OTLP exporters need to run in a Tokio context |
115 | | // Do this first so all the other logging works |
116 | | #[expect(clippy::disallowed_methods, reason = "tracing init on main runtime")] |
117 | 0 | runtime.block_on(async { tokio::spawn(async { init_tracing().await }).await? })?; |
118 | | |
119 | 0 | let mut cfg = get_config()?; |
120 | | |
121 | 0 | let global_cfg = if let Some(global_cfg) = &mut cfg.global { |
122 | 0 | if global_cfg.max_open_files == 0 { |
123 | 0 | global_cfg.max_open_files = fs::DEFAULT_OPEN_FILE_LIMIT; |
124 | 0 | } |
125 | 0 | if global_cfg.default_digest_size_health_check == 0 { |
126 | 0 | global_cfg.default_digest_size_health_check = DEFAULT_DIGEST_SIZE_HEALTH_CHECK_CFG; |
127 | 0 | } |
128 | | |
129 | 0 | *global_cfg |
130 | | } else { |
131 | 0 | GlobalConfig { |
132 | 0 | max_open_files: fs::DEFAULT_OPEN_FILE_LIMIT, |
133 | 0 | default_digest_hash_function: None, |
134 | 0 | default_digest_size_health_check: DEFAULT_DIGEST_SIZE_HEALTH_CHECK_CFG, |
135 | 0 | } |
136 | | }; |
137 | 0 | set_open_file_limit(global_cfg.max_open_files); |
138 | 0 | set_default_digest_hasher_func(DigestHasherFunc::from( |
139 | 0 | global_cfg |
140 | 0 | .default_digest_hash_function |
141 | 0 | .unwrap_or(ConfigDigestHashFunction::Sha256), |
142 | 0 | ))?; |
143 | 0 | set_default_digest_size_health_check(global_cfg.default_digest_size_health_check)?; |
144 | | |
145 | | // Initiates the shutdown process by broadcasting the shutdown signal via the `oneshot::Sender` to all listeners. |
146 | | // Each listener will perform its cleanup and then drop its `oneshot::Sender`, signaling completion. |
147 | | // Once all `oneshot::Sender` instances are dropped, the worker knows it can safely terminate. |
148 | 0 | let (shutdown_tx, _) = broadcast::channel::<ShutdownGuard>(BROADCAST_CAPACITY); |
149 | | #[cfg(target_family = "unix")] |
150 | 0 | let shutdown_tx_clone = shutdown_tx.clone(); |
151 | | #[cfg(target_family = "unix")] |
152 | 0 | let mut shutdown_guard = ShutdownGuard::default(); |
153 | | |
154 | 0 | let (scheduler_shutdown_tx, scheduler_shutdown_rx) = oneshot::channel(); |
155 | | |
156 | | // Not used in non-unix targets |
157 | | #[cfg(not(target_family = "unix"))] |
158 | | drop(scheduler_shutdown_rx); |
159 | | |
160 | | // SIGINT takes the SIGTERM path: a worker stopped from a terminal |
161 | | // drains like one stopped by its supervisor. A second SIGINT during |
162 | | // the drain exits at once, for the operator who meant it. |
163 | | #[cfg(target_family = "unix")] |
164 | | #[expect(clippy::disallowed_methods, reason = "signal handler on main runtime")] |
165 | 0 | runtime.spawn(async move { |
166 | 0 | let mut sigterm = signal(SignalKind::terminate()).expect("Failed to listen to SIGTERM"); |
167 | 0 | let mut sigint = signal(SignalKind::interrupt()).expect("Failed to listen to SIGINT"); |
168 | 0 | let exit_code = tokio::select! { |
169 | 0 | _ = sigterm.recv() => { warn!("Process terminated via SIGTERM"); 143 } |
170 | 0 | _ = sigint.recv() => { warn!("Process terminated via SIGINT, draining; send it again to exit at once"); 130 } |
171 | | }; |
172 | 0 | tokio::spawn(async move { |
173 | 0 | sigint.recv().await; |
174 | 0 | eprintln!("User terminated process via second SIGINT"); |
175 | 0 | std::process::exit(130); |
176 | | }); |
177 | 0 | drop(shutdown_tx_clone.send(shutdown_guard.clone())); |
178 | 0 | scheduler_shutdown_rx |
179 | 0 | .await |
180 | 0 | .expect("Failed to receive scheduler shutdown"); |
181 | 0 | let () = shutdown_guard.wait_for(Priority::P0).await; |
182 | 0 | warn!("Successfully shut down nativelink."); |
183 | 0 | std::process::exit(exit_code); |
184 | | }); |
185 | | |
186 | | #[expect(clippy::disallowed_methods, reason = "waiting on everything to finish")] |
187 | 0 | runtime |
188 | 0 | .block_on(async { |
189 | 0 | trace_span!("main") |
190 | 0 | .in_scope(|| async { inner_main(cfg, shutdown_tx, scheduler_shutdown_tx).await }) |
191 | 0 | .await |
192 | 0 | }) |
193 | 0 | .err_tip(|| "main() function failed")?; |
194 | 0 | Ok(()) |
195 | 0 | } |