/build/source/nativelink-worker/src/local_worker.rs
Line | Count | Source |
1 | | // Copyright 2024 The NativeLink Authors. 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 | | use core::hash::BuildHasher; |
16 | | use core::pin::Pin; |
17 | | use core::str; |
18 | | use core::sync::atomic::{AtomicU64, Ordering}; |
19 | | use core::time::Duration; |
20 | | use std::borrow::Cow; |
21 | | use std::collections::HashMap; |
22 | | use std::env; |
23 | | use std::process::Stdio; |
24 | | use std::sync::{Arc, Weak}; |
25 | | |
26 | | use futures::future::BoxFuture; |
27 | | use futures::stream::FuturesUnordered; |
28 | | use futures::{Future, FutureExt, StreamExt, TryFutureExt, select}; |
29 | | use nativelink_config::cas_server::{EnvironmentSource, LocalWorkerConfig}; |
30 | | use nativelink_error::{Code, Error, ResultExt, make_err, make_input_err}; |
31 | | use nativelink_metric::{MetricsComponent, RootMetricsComponent}; |
32 | | use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::update_for_worker::Update; |
33 | | use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::worker_api_client::WorkerApiClient; |
34 | | use nativelink_proto::com::github::trace_machina::nativelink::remote_execution::{ |
35 | | ActionResourceUsage, ExecuteComplete, ExecuteResult, GoingAwayRequest, KeepAliveRequest, |
36 | | UpdateForWorker, execute_result, |
37 | | }; |
38 | | use nativelink_store::fast_slow_store::FastSlowStore; |
39 | | use nativelink_util::action_messages::{ActionResult, ActionStage, OperationId}; |
40 | | use nativelink_util::common::fs; |
41 | | use nativelink_util::digest_hasher::DigestHasherFunc; |
42 | | use nativelink_util::metrics_utils::{AsyncCounterWrapper, CounterWithTime}; |
43 | | use nativelink_util::shutdown_guard::ShutdownGuard; |
44 | | use nativelink_util::store_trait::Store; |
45 | | use nativelink_util::{spawn, tls_utils}; |
46 | | use opentelemetry::context::Context; |
47 | | use tokio::sync::{broadcast, mpsc}; |
48 | | use tokio::{process, time}; |
49 | | use tokio_stream::wrappers::UnboundedReceiverStream; |
50 | | use tonic::Streaming; |
51 | | use tracing::{Level, debug, error, event, info, info_span, instrument, trace, warn}; |
52 | | |
53 | | use crate::running_actions_manager::{ |
54 | | ExecutionConfiguration, Metrics as RunningActionManagerMetrics, RunningAction, |
55 | | RunningActionsManager, RunningActionsManagerArgs, RunningActionsManagerImpl, |
56 | | }; |
57 | | use crate::worker_api_client_wrapper::{WorkerApiClientTrait, WorkerApiClientWrapper}; |
58 | | use crate::worker_utils::make_connect_worker_request; |
59 | | |
60 | | /// Amount of time to wait if we have actions in transit before we try to |
61 | | /// consider an error to have occurred. |
62 | | const ACTIONS_IN_TRANSIT_TIMEOUT_S: f32 = 10.; |
63 | | |
64 | | /// If we lose connection to the worker api server we will wait this many seconds |
65 | | /// before trying to connect. |
66 | | const CONNECTION_RETRY_DELAY_S: f32 = 0.5; |
67 | | |
68 | | /// Default endpoint timeout. If this value gets modified the documentation in |
69 | | /// `cas_server.rs` must also be updated. |
70 | | const DEFAULT_ENDPOINT_TIMEOUT_S: f32 = 5.; |
71 | | |
72 | | /// Default maximum amount of time a task is allowed to run for. |
73 | | /// If this value gets modified the documentation in `cas_server.rs` must also be updated. |
74 | | const DEFAULT_MAX_ACTION_TIMEOUT: Duration = Duration::from_mins(20); |
75 | | const DEFAULT_MAX_UPLOAD_TIMEOUT: Duration = Duration::from_mins(10); |
76 | | const DEFAULT_MAX_CLEANUP_WAIT: Duration = Duration::from_secs(30); |
77 | | const DEFAULT_MAX_CLEANUP_BACKOFF: Duration = Duration::from_millis(500); |
78 | | |
79 | | struct FinishedActionResult { |
80 | | action_result: ActionResult, |
81 | | resource_usage: Option<ActionResourceUsage>, |
82 | | } |
83 | | |
84 | | struct LocalWorkerImpl<'a, T: WorkerApiClientTrait + 'static, U: RunningActionsManager> { |
85 | | config: &'a LocalWorkerConfig, |
86 | | // According to the tonic documentation it is a cheap operation to clone this. |
87 | | grpc_client: T, |
88 | | worker_id: String, |
89 | | running_actions_manager: Arc<U>, |
90 | | // Number of actions that have been received in `Update::StartAction`, but |
91 | | // not yet processed by running_actions_manager's spawn. This number should |
92 | | // always be zero if there are no actions running and no actions being waited |
93 | | // on by the scheduler. |
94 | | actions_in_transit: Arc<AtomicU64>, |
95 | | metrics: Arc<Metrics>, |
96 | | } |
97 | | |
98 | 7 | pub async fn preconditions_met<H: BuildHasher + Sync>( |
99 | 7 | precondition_script: Option<String>, |
100 | 7 | extra_envs: &HashMap<String, String, H>, |
101 | 7 | ) -> Result<(), Error> { |
102 | 7 | let Some(precondition_script2 ) = &precondition_script else { |
103 | | // No script means we are always ok to proceed. |
104 | 5 | return Ok(()); |
105 | | }; |
106 | | // TODO: Might want to pass some information about the command to the |
107 | | // script, but at this point it's not even been downloaded yet, |
108 | | // so that's not currently possible. Perhaps we'll move this in |
109 | | // future to pass useful information through? Or perhaps we'll |
110 | | // have a pre-condition and a pre-execute script instead, although |
111 | | // arguably entrypoint already gives us that. |
112 | | |
113 | 2 | let maybe_split_cmd = shlex::split(precondition_script); |
114 | 2 | let (command, args) = match &maybe_split_cmd { |
115 | 2 | Some(split_cmd) => (&split_cmd[0], &split_cmd[1..]), |
116 | | None => { |
117 | 0 | return Err(make_input_err!( |
118 | 0 | "Could not parse the value of precondition_script: '{}'", |
119 | 0 | precondition_script, |
120 | 0 | )); |
121 | | } |
122 | | }; |
123 | | |
124 | 2 | let precondition_process = process::Command::new(command) |
125 | 2 | .args(args) |
126 | 2 | .kill_on_drop(true) |
127 | 2 | .stdin(Stdio::null()) |
128 | 2 | .stdout(Stdio::piped()) |
129 | 2 | .stderr(Stdio::null()) |
130 | 2 | .env_clear() |
131 | 2 | .envs(extra_envs) |
132 | 2 | .spawn() |
133 | 2 | .err_tip(|| format!0 ("Could not execute precondition command {precondition_script:?}"))?0 ; |
134 | 2 | let output = precondition_process.wait_with_output().await?0 ; |
135 | 2 | let stdout = str::from_utf8(&output.stdout).unwrap_or(""); |
136 | 2 | trace!(status = %output.status, %stdout, "Preconditions script returned"); |
137 | 2 | if output.status.code() == Some(0) { |
138 | 1 | Ok(()) |
139 | | } else { |
140 | 1 | Err(make_err!( |
141 | 1 | Code::ResourceExhausted, |
142 | 1 | "Preconditions script returned status {} - {}", |
143 | 1 | output.status, |
144 | 1 | stdout |
145 | 1 | )) |
146 | | } |
147 | 7 | } |
148 | | |
149 | | impl<'a, T: WorkerApiClientTrait + 'static, U: RunningActionsManager> LocalWorkerImpl<'a, T, U> { |
150 | 9 | fn new( |
151 | 9 | config: &'a LocalWorkerConfig, |
152 | 9 | grpc_client: T, |
153 | 9 | worker_id: String, |
154 | 9 | running_actions_manager: Arc<U>, |
155 | 9 | metrics: Arc<Metrics>, |
156 | 9 | ) -> Self { |
157 | 9 | Self { |
158 | 9 | config, |
159 | 9 | grpc_client, |
160 | 9 | worker_id, |
161 | 9 | running_actions_manager, |
162 | 9 | // Number of actions that have been received in `Update::StartAction`, but |
163 | 9 | // not yet processed by running_actions_manager's spawn. This number should |
164 | 9 | // always be zero if there are no actions running and no actions being waited |
165 | 9 | // on by the scheduler. |
166 | 9 | actions_in_transit: Arc::new(AtomicU64::new(0)), |
167 | 9 | metrics, |
168 | 9 | } |
169 | 9 | } |
170 | | |
171 | | /// Starts a background spawn/thread that will send a message to the server every `timeout / 2`. |
172 | 9 | async fn start_keep_alive(&self) -> Result<(), Error> {8 |
173 | | // According to tonic's documentation this call should be cheap and is the same stream. |
174 | 8 | let mut grpc_client = self.grpc_client.clone(); |
175 | 8 | let timeout = self |
176 | 8 | .config |
177 | 8 | .worker_api_endpoint |
178 | 8 | .timeout |
179 | 8 | .unwrap_or(DEFAULT_ENDPOINT_TIMEOUT_S); |
180 | | |
181 | 8 | info!(timeout, "Started KeepAlive"); |
182 | | |
183 | | // We always send 2 keep alive requests per timeout. Http2 should manage most of our |
184 | | // timeout issues, this is a secondary check to ensure we can still send data. |
185 | 8 | let mut interval = time::interval(Duration::from_secs_f32(timeout) / 2); |
186 | 8 | interval.set_missed_tick_behavior(time::MissedTickBehavior::Skip); |
187 | | |
188 | | // Skip the first interval as it happens immediately and we don't need a keep alive until timeout/2 has passed |
189 | 8 | interval.tick().await; |
190 | | |
191 | | // Explicitly spawn the keep alive loop so it goes onto a different thread from the execute commands |
192 | 1 | drop( |
193 | 7 | spawn!("keep alives", async move { |
194 | | loop { |
195 | 8 | interval.tick().await; |
196 | 2 | if let Err(e1 ) = grpc_client.keep_alive(KeepAliveRequest {}).await { |
197 | 1 | error!(?e, "Failed to send KeepAlive in LocalWorker"); |
198 | 1 | return; |
199 | 1 | } |
200 | 1 | debug!("Sent KeepAlive"); |
201 | | } |
202 | 1 | }) |
203 | 7 | .await, |
204 | | ); |
205 | 1 | Ok(()) |
206 | 1 | } |
207 | | |
208 | 9 | async fn run( |
209 | 9 | &self, |
210 | 9 | update_for_worker_stream: Streaming<UpdateForWorker>, |
211 | 9 | shutdown_rx: &mut broadcast::Receiver<ShutdownGuard>, |
212 | 9 | ) -> Result<(), Error> { |
213 | | // This big block of logic is designed to help simplify upstream components. Upstream |
214 | | // components can write standard futures that return a `Result<(), Error>` and this block |
215 | | // will forward the error up to the client and disconnect from the scheduler. |
216 | | // It is a common use case that an item sent through update_for_worker_stream will always |
217 | | // have a response but the response will be triggered through a callback to the scheduler. |
218 | | // This can be quite tricky to manage, so what we have done here is given access to a |
219 | | // `futures` variable which because this is in a single thread as well as a channel that you |
220 | | // send a future into that makes it into the `futures` variable. |
221 | | // This means that if you want to perform an action based on the result of the future |
222 | | // you use the `.map()` method and the new action will always come to live in this spawn, |
223 | | // giving mutable access to stuff in this struct. |
224 | | // NOTE: If you ever return from this function it will disconnect from the scheduler. |
225 | 9 | let mut futures = FuturesUnordered::new(); |
226 | 9 | futures.push(self.start_keep_alive().boxed()); |
227 | | |
228 | 9 | let (add_future_channel, add_future_rx) = mpsc::unbounded_channel(); |
229 | 9 | let mut add_future_rx = UnboundedReceiverStream::new(add_future_rx).fuse(); |
230 | | |
231 | 9 | let mut update_for_worker_stream = update_for_worker_stream.fuse(); |
232 | | // A notify which is triggered every time actions_in_flight is subtracted. |
233 | 9 | let actions_notify = Arc::new(tokio::sync::Notify::new()); |
234 | | // A counter of actions that are in-flight, this is similar to actions_in_transit but |
235 | | // includes the AC upload and notification to the scheduler. |
236 | 9 | let actions_in_flight = Arc::new(AtomicU64::new(0)); |
237 | | // Set to true when shutting down, this stops any new StartAction. |
238 | 9 | let mut shutting_down = false; |
239 | | |
240 | | loop { |
241 | 28 | select! { |
242 | 28 | maybe_update10 = update_for_worker_stream.next() => if !shutting_down10 || maybe_update0 .is_some0 () { |
243 | 10 | match maybe_update |
244 | 10 | .err_tip(|| "UpdateForWorker stream closed early")?2 |
245 | 8 | .err_tip(|| "Got error in UpdateForWorker stream")?0 |
246 | | .update |
247 | 8 | .err_tip(|| "Expected update to exist in UpdateForWorker")?0 |
248 | | { |
249 | | Update::ConnectionResult(_) => { |
250 | 0 | return Err(make_input_err!( |
251 | 0 | "Got ConnectionResult in LocalWorker::run which should never happen" |
252 | 0 | )); |
253 | | } |
254 | | // TODO(palfrey) We should possibly do something with this notification. |
255 | 0 | Update::Disconnect(()) => { |
256 | 0 | self.metrics.disconnects_received.inc(); |
257 | 0 | } |
258 | 0 | Update::KeepAlive(()) => { |
259 | 0 | self.metrics.keep_alives_received.inc(); |
260 | 0 | } |
261 | 1 | Update::KillOperationRequest(kill_operation_request) => { |
262 | 1 | let operation_id = OperationId::from(kill_operation_request.operation_id); |
263 | 1 | if let Err(err0 ) = self.running_actions_manager.kill_operation(&operation_id).await { |
264 | 0 | error!( |
265 | | %operation_id, |
266 | | ?err, |
267 | | "Failed to send kill request for operation" |
268 | | ); |
269 | 1 | } |
270 | | } |
271 | 7 | Update::StartAction(start_execute) => { |
272 | | // Don't accept any new requests if we're shutting down. |
273 | 7 | if shutting_down { |
274 | 0 | if let Some(instance_name) = start_execute.execute_request.map(|request| request.instance_name) { |
275 | 0 | self.grpc_client.clone().execution_response( |
276 | 0 | ExecuteResult{ |
277 | 0 | instance_name, |
278 | 0 | operation_id: start_execute.operation_id, |
279 | 0 | result: Some(execute_result::Result::InternalError(make_err!(Code::ResourceExhausted, "Worker shutting down").into())), |
280 | 0 | resource_usage: None, |
281 | 0 | } |
282 | 0 | ).await?; |
283 | 0 | } |
284 | 0 | continue; |
285 | 7 | } |
286 | | |
287 | 7 | self.metrics.start_actions_received.inc(); |
288 | | |
289 | 7 | let execute_request = start_execute.execute_request.as_ref(); |
290 | 7 | let operation_id = start_execute.operation_id.clone(); |
291 | 7 | let operation_id_to_log = operation_id.clone(); |
292 | 7 | let maybe_instance_name = execute_request.map(|v| v.instance_name.clone()); |
293 | 7 | let action_digest = execute_request.and_then(|v| v.action_digest.clone()); |
294 | 7 | let digest_hasher = execute_request |
295 | 7 | .ok_or_else(|| make_input_err!0 ("Expected execute_request to be set")) |
296 | 7 | .and_then(|v| DigestHasherFunc::try_from(v.digest_function)) |
297 | 7 | .err_tip(|| "In LocalWorkerImpl::new()")?0 ; |
298 | | |
299 | 7 | let start_action_fut = { |
300 | 7 | let precondition_script_cfg = self.config.experimental_precondition_script.clone(); |
301 | 7 | let mut extra_envs: HashMap<String, String> = HashMap::new(); |
302 | 7 | if let Some(ref additional_environment0 ) = self.config.additional_environment { |
303 | 0 | for (name, source) in additional_environment { |
304 | 0 | let value = match source { |
305 | 0 | EnvironmentSource::Property(property) => start_execute |
306 | 0 | .platform.as_ref().and_then(|p|p.properties.iter().find(|pr| &pr.name == property)) |
307 | 0 | .map_or_else(|| Cow::Borrowed(""), |v| Cow::Borrowed(v.value.as_str())), |
308 | 0 | EnvironmentSource::Value(value) => Cow::Borrowed(value.as_str()), |
309 | 0 | EnvironmentSource::FromEnvironment => Cow::Owned(env::var(name).unwrap_or_default()), |
310 | 0 | other => { |
311 | 0 | debug!(?other, "Worker doesn't support this type of additional environment"); |
312 | 0 | continue; |
313 | | } |
314 | | }; |
315 | 0 | extra_envs.insert(name.clone(), value.into_owned()); |
316 | | } |
317 | 7 | } |
318 | 7 | let actions_in_transit = self.actions_in_transit.clone(); |
319 | 7 | let worker_id = self.worker_id.clone(); |
320 | 7 | let running_actions_manager = self.running_actions_manager.clone(); |
321 | 7 | let mut grpc_client = self.grpc_client.clone(); |
322 | 7 | let complete = ExecuteComplete { |
323 | 7 | operation_id: operation_id.clone(), |
324 | 7 | }; |
325 | 7 | self.metrics.clone().wrap(move |metrics| async move {6 |
326 | 6 | metrics.preconditions.wrap(preconditions_met(precondition_script_cfg, &extra_envs)) |
327 | 6 | .and_then(|()| running_actions_manager5 .create_and_add_action5 (worker_id5 , start_execute5 )) |
328 | 6 | .map(move |r| { |
329 | | // Now that we either failed or registered our action, we can |
330 | | // consider the action to no longer be in transit. |
331 | 6 | actions_in_transit.fetch_sub(1, Ordering::Release); |
332 | 6 | r |
333 | 6 | }) |
334 | 6 | .and_then(|action| {5 |
335 | 5 | debug!( |
336 | 5 | operation_id = %action.get_operation_id(), |
337 | | "Received request to run action" |
338 | | ); |
339 | 5 | action |
340 | 5 | .clone() |
341 | 5 | .prepare_action() |
342 | 5 | .and_then(RunningAction::execute) |
343 | 5 | .and_then(|result| async move {2 |
344 | | // Notify that execution has completed so it can schedule a new action. |
345 | 2 | drop(grpc_client.execution_complete(complete).await); |
346 | 2 | Ok(result) |
347 | 4 | }) |
348 | 5 | .and_then(RunningAction::upload_results) |
349 | 5 | .and_then(|action| async move {2 |
350 | 2 | let resource_usage = action.resource_usage(); |
351 | 2 | let action_result = action.get_finished_result().await?0 ; |
352 | 2 | Ok(FinishedActionResult { |
353 | 2 | action_result, |
354 | 2 | resource_usage, |
355 | 2 | }) |
356 | 4 | }) |
357 | | // Note: We need ensure we run cleanup even if one of the other steps fail. |
358 | 5 | .then(|result| async move {4 |
359 | 4 | if let Err(e0 ) = action.cleanup().await { |
360 | 0 | return Result::<FinishedActionResult, Error>::Err(e).merge(result); |
361 | 4 | } |
362 | 4 | result |
363 | 8 | }) |
364 | 6 | }5 ).await |
365 | 11 | }) |
366 | | }; |
367 | | |
368 | 7 | let make_publish_future = { |
369 | 7 | let mut grpc_client = self.grpc_client.clone(); |
370 | | |
371 | 7 | let running_actions_manager = self.running_actions_manager.clone(); |
372 | 7 | let worker_id = self.worker_id.clone(); |
373 | 5 | move |res: Result<FinishedActionResult, Error>| async move { |
374 | 5 | let instance_name = maybe_instance_name |
375 | 5 | .err_tip(|| "`instance_name` could not be resolved; this is likely an internal error in local_worker.")?0 ; |
376 | 5 | match res { |
377 | 2 | Ok(FinishedActionResult { mut action_result, resource_usage }) => { |
378 | | // Save in the action cache before notifying the scheduler that we've completed. |
379 | 2 | if let Some(digest_info) = action_digest.clone().and_then(|action_digest| action_digest.try_into().ok()) && |
380 | 2 | let Err(err0 ) = running_actions_manager.cache_action_result(digest_info, &mut action_result, digest_hasher).await { |
381 | 0 | error!( |
382 | | ?err, |
383 | | ?action_digest, |
384 | | "Error saving action in store", |
385 | | ); |
386 | 2 | } |
387 | 2 | let action_stage = ActionStage::Completed(action_result); |
388 | 2 | let resource_usage = resource_usage.map(|mut resource_usage| {0 |
389 | 0 | resource_usage.operation_id.clone_from(&operation_id); |
390 | 0 | resource_usage.worker_id.clone_from(&worker_id); |
391 | 0 | resource_usage |
392 | 0 | }); |
393 | 2 | grpc_client.execution_response( |
394 | 2 | ExecuteResult{ |
395 | 2 | instance_name, |
396 | 2 | operation_id, |
397 | 2 | result: Some(execute_result::Result::ExecuteResponse(action_stage.into())), |
398 | 2 | resource_usage, |
399 | 2 | } |
400 | 2 | ) |
401 | 2 | .await |
402 | 0 | .err_tip(|| "Error while calling execution_response")?; |
403 | | }, |
404 | 3 | Err(e) => { |
405 | 3 | let is_cas_blob_missing = e.code == Code::NotFound |
406 | 2 | && e.message_string().contains("not found in either fast or slow store"); |
407 | 3 | if is_cas_blob_missing { |
408 | 1 | warn!( |
409 | | ?e, |
410 | | "Missing CAS inputs during prepare_action, returning FAILED_PRECONDITION" |
411 | | ); |
412 | 1 | let action_result = ActionResult { |
413 | 1 | error: Some(make_err!( |
414 | 1 | Code::FailedPrecondition, |
415 | 1 | "{}", |
416 | 1 | e.message_string() |
417 | 1 | )), |
418 | 1 | ..ActionResult::default() |
419 | 1 | }; |
420 | 1 | let action_stage = ActionStage::Completed(action_result); |
421 | 1 | grpc_client.execution_response(ExecuteResult{ |
422 | 1 | instance_name, |
423 | 1 | operation_id, |
424 | 1 | result: Some(execute_result::Result::ExecuteResponse(action_stage.into())), |
425 | 1 | resource_usage: None, |
426 | 1 | }).await.err_tip0 (|| "Error calling execution_response with missing inputs")?0 ; |
427 | | } else { |
428 | 2 | grpc_client.execution_response(ExecuteResult{ |
429 | 2 | instance_name, |
430 | 2 | operation_id, |
431 | 2 | result: Some(execute_result::Result::InternalError(e.into())), |
432 | 2 | resource_usage: None, |
433 | 2 | }).await.err_tip0 (|| "Error calling execution_response with error")?0 ; |
434 | | } |
435 | | }, |
436 | | } |
437 | 0 | Ok(()) |
438 | 5 | } |
439 | | }; |
440 | | |
441 | 7 | self.actions_in_transit.fetch_add(1, Ordering::Release); |
442 | | |
443 | 7 | let add_future_channel = add_future_channel.clone(); |
444 | | |
445 | 7 | info_span!( |
446 | | "worker_start_action_ctx", |
447 | | operation_id = operation_id_to_log, |
448 | 7 | digest_function = %digest_hasher.to_string(), |
449 | 7 | ).in_scope(|| { |
450 | 7 | let _guard = Context::current_with_value(digest_hasher) |
451 | 7 | .attach(); |
452 | | |
453 | 7 | let actions_in_flight = actions_in_flight.clone(); |
454 | 7 | let actions_notify = actions_notify.clone(); |
455 | 7 | let actions_in_flight_fail = actions_in_flight.clone(); |
456 | 7 | let actions_notify_fail = actions_notify.clone(); |
457 | 7 | actions_in_flight.fetch_add(1, Ordering::Release); |
458 | | |
459 | 7 | futures.push( |
460 | 7 | spawn!("worker_start_action", start_action_fut).map(move |res| {5 |
461 | 5 | let res = res.err_tip(|| "Failed to launch spawn")?0 ; |
462 | 5 | if let Err(err3 ) = &res { |
463 | 3 | error!(?err, "Error executing action"); |
464 | 2 | } |
465 | 5 | add_future_channel |
466 | 5 | .send(make_publish_future(res).then(move |res| {0 |
467 | 0 | actions_in_flight.fetch_sub(1, Ordering::Release); |
468 | 0 | actions_notify.notify_one(); |
469 | 0 | core::future::ready(res) |
470 | 5 | }0 ).boxed()) |
471 | 5 | .map_err(|err| |
472 | 0 | Error::from_std_err(Code::Internal, &err).append("LocalWorker could not send future") |
473 | 0 | )?; |
474 | 5 | Ok(()) |
475 | 5 | }) |
476 | 7 | .or_else(move |err| {0 |
477 | | // If the make_publish_future is not run we still need to notify. |
478 | 0 | actions_in_flight_fail.fetch_sub(1, Ordering::Release); |
479 | 0 | actions_notify_fail.notify_one(); |
480 | 0 | core::future::ready(Err(err)) |
481 | 0 | }) |
482 | 7 | .boxed() |
483 | | ); |
484 | 7 | }); |
485 | | } |
486 | | } |
487 | 0 | }, |
488 | 28 | res5 = add_future_rx.next() => { |
489 | 5 | let fut = res.err_tip(|| "New future stream receives should never be closed")?0 ; |
490 | 5 | futures.push(fut); |
491 | | }, |
492 | 28 | res7 = futures.next() => res7 .err_tip7 (|| "Keep-alive should always pending. Likely unable to send data to scheduler")?1 ?0 , |
493 | 28 | complete_msg0 = shutdown_rx.recv().fuse() => { |
494 | 0 | warn!("Worker loop received shutdown signal. Shutting down worker...",); |
495 | 0 | let mut grpc_client = self.grpc_client.clone(); |
496 | 0 | let shutdown_guard = complete_msg.map_err(|e| |
497 | 0 | Error::from_std_err(Code::Internal, &e).append("Failed to receive shutdown message"))?; |
498 | 0 | let actions_in_flight = actions_in_flight.clone(); |
499 | 0 | let actions_notify = actions_notify.clone(); |
500 | 0 | let shutdown_future = async move { |
501 | | // Wait for in-flight operations to be fully completed. |
502 | 0 | while actions_in_flight.load(Ordering::Acquire) > 0 { |
503 | 0 | actions_notify.notified().await; |
504 | | } |
505 | | // Sending this message immediately evicts all jobs from |
506 | | // this worker, of which there should be none. |
507 | 0 | if let Err(e) = grpc_client.going_away(GoingAwayRequest {}).await { |
508 | 0 | error!("Failed to send GoingAwayRequest: {e}",); |
509 | 0 | return Err(e); |
510 | 0 | } |
511 | | // Allow shutdown to occur now. |
512 | 0 | drop(shutdown_guard); |
513 | 0 | Ok::<(), Error>(()) |
514 | 0 | }; |
515 | 0 | futures.push(shutdown_future.boxed()); |
516 | 0 | shutting_down = true; |
517 | | }, |
518 | | }; |
519 | | } |
520 | | // Unreachable. |
521 | 3 | } |
522 | | } |
523 | | |
524 | | type ConnectionFactory<T> = Box<dyn Fn() -> BoxFuture<'static, Result<T, Error>> + Send + Sync>; |
525 | | |
526 | | pub struct LocalWorker<T: WorkerApiClientTrait + 'static, U: RunningActionsManager> { |
527 | | config: Arc<LocalWorkerConfig>, |
528 | | running_actions_manager: Arc<U>, |
529 | | connection_factory: ConnectionFactory<T>, |
530 | | sleep_fn: Option<Box<dyn Fn(Duration) -> BoxFuture<'static, ()> + Send + Sync>>, |
531 | | metrics: Arc<Metrics>, |
532 | | } |
533 | | |
534 | | impl< |
535 | | T: WorkerApiClientTrait + core::fmt::Debug + 'static, |
536 | | U: RunningActionsManager + core::fmt::Debug, |
537 | | > core::fmt::Debug for LocalWorker<T, U> |
538 | | { |
539 | 0 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
540 | 0 | f.debug_struct("LocalWorker") |
541 | 0 | .field("config", &self.config) |
542 | 0 | .field("running_actions_manager", &self.running_actions_manager) |
543 | 0 | .field("metrics", &self.metrics) |
544 | 0 | .finish_non_exhaustive() |
545 | 0 | } |
546 | | } |
547 | | |
548 | | /// Creates a new `LocalWorker`. The `cas_store` must be an instance of |
549 | | /// `FastSlowStore` and will be checked at runtime. |
550 | 2 | pub async fn new_local_worker( |
551 | 2 | config: Arc<LocalWorkerConfig>, |
552 | 2 | cas_store: Store, |
553 | 2 | ac_store: Option<Store>, |
554 | 2 | historical_store: Store, |
555 | 2 | ) -> Result<LocalWorker<WorkerApiClientWrapper, RunningActionsManagerImpl>, Error> { |
556 | 2 | let fast_slow_store = cas_store |
557 | 2 | .downcast_ref::<FastSlowStore>(None) |
558 | 2 | .err_tip(|| "Expected store for LocalWorker's store to be a FastSlowStore")?0 |
559 | 2 | .get_arc() |
560 | 2 | .err_tip(|| "FastSlowStore's Arc doesn't exist")?0 ; |
561 | | |
562 | | // Log warning about CAS configuration for multi-worker setups |
563 | 2 | event!( |
564 | 2 | Level::INFO, |
565 | 2 | worker_name = %config.name, |
566 | | "Starting worker '{}'. IMPORTANT: If running multiple workers, all workers \ |
567 | | must share the same CAS storage path to avoid 'Object not found' errors.", |
568 | 2 | config.name |
569 | | ); |
570 | | |
571 | 2 | if let Ok(path1 ) = fs::canonicalize(&config.work_directory).await { |
572 | 1 | fs::remove_dir_all(&path).await.err_tip(|| {0 |
573 | 0 | format!( |
574 | | "Could not remove work_directory '{}' in LocalWorker", |
575 | 0 | &path.as_path().to_str().unwrap_or("bad path") |
576 | | ) |
577 | 0 | })?; |
578 | 1 | } |
579 | | |
580 | 2 | fs::create_dir_all(&config.work_directory) |
581 | 2 | .await |
582 | 2 | .err_tip(|| format!0 ("Could not make work_directory : {}", config.work_directory0 ))?0 ; |
583 | 2 | let entrypoint = if config.entrypoint.is_empty() { |
584 | 2 | None |
585 | | } else { |
586 | 0 | Some(config.entrypoint.clone()) |
587 | | }; |
588 | 2 | let max_action_timeout = if config.max_action_timeout_s == 0 { |
589 | 2 | DEFAULT_MAX_ACTION_TIMEOUT |
590 | | } else { |
591 | 0 | Duration::from_secs(config.max_action_timeout_s as u64) |
592 | | }; |
593 | 2 | let max_upload_timeout = if config.max_upload_timeout_s == 0 { |
594 | 2 | DEFAULT_MAX_UPLOAD_TIMEOUT |
595 | | } else { |
596 | 0 | Duration::from_secs(config.max_upload_timeout_s as u64) |
597 | | }; |
598 | 2 | let max_cleanup_wait = if config.max_cleanup_wait_s == 0 { |
599 | 2 | DEFAULT_MAX_CLEANUP_WAIT |
600 | | } else { |
601 | 0 | Duration::from_secs(config.max_cleanup_wait_s as u64) |
602 | | }; |
603 | 2 | let max_cleanup_backoff = if config.max_cleanup_backoff_ms == 0 { |
604 | 2 | DEFAULT_MAX_CLEANUP_BACKOFF |
605 | | } else { |
606 | 0 | Duration::from_millis(config.max_cleanup_backoff_ms as u64) |
607 | | }; |
608 | | |
609 | | // Initialize directory cache if configured |
610 | 2 | let directory_cache = if let Some(cache_config0 ) = &config.directory_cache { |
611 | | use std::path::PathBuf; |
612 | | |
613 | | use crate::directory_cache::{ |
614 | | DirectoryCache, DirectoryCacheConfig as WorkerDirCacheConfig, |
615 | | }; |
616 | | |
617 | 0 | let cache_root = if cache_config.cache_root.is_empty() { |
618 | 0 | PathBuf::from(&config.work_directory).parent().map_or_else( |
619 | 0 | || PathBuf::from("/tmp/nativelink_directory_cache"), |
620 | 0 | |p| p.join("directory_cache"), |
621 | | ) |
622 | | } else { |
623 | 0 | PathBuf::from(&cache_config.cache_root) |
624 | | }; |
625 | | |
626 | 0 | let worker_cache_config = WorkerDirCacheConfig { |
627 | 0 | max_entries: cache_config.max_entries, |
628 | 0 | max_size_bytes: cache_config.max_size_bytes, |
629 | 0 | cache_root, |
630 | 0 | experimental_subtree_caching: cache_config.experimental_subtree_caching, |
631 | 0 | max_concurrent_fetches: cache_config.max_concurrent_fetches, |
632 | 0 | experimental_get_tree_prefetch: cache_config.experimental_get_tree_prefetch, |
633 | 0 | }; |
634 | | |
635 | 0 | match DirectoryCache::new(worker_cache_config, fast_slow_store.clone()).await { |
636 | 0 | Ok(cache) => { |
637 | 0 | tracing::info!("Directory cache initialized successfully"); |
638 | 0 | Some(Arc::new(cache)) |
639 | | } |
640 | 0 | Err(e) => { |
641 | 0 | tracing::warn!("Failed to initialize directory cache: {:?}", e); |
642 | 0 | None |
643 | | } |
644 | | } |
645 | | } else { |
646 | 2 | None |
647 | | }; |
648 | | |
649 | | #[cfg(target_os = "linux")] |
650 | 2 | let use_namespaces = if let Some(use_namespaces0 ) = &config.use_namespaces { |
651 | 0 | if *use_namespaces |
652 | 0 | && !crate::namespace_utils::namespaces_supported( |
653 | 0 | config.use_mount_namespace.unwrap_or_default(), |
654 | 0 | ) |
655 | | { |
656 | 0 | return Err(make_err!(Code::Unavailable, "Namespaces not supported")); |
657 | 0 | } |
658 | 0 | if !*use_namespaces { |
659 | 0 | crate::running_actions_manager::UseNamespaces::No |
660 | 0 | } else if config.use_mount_namespace.unwrap_or_default() { |
661 | 0 | crate::running_actions_manager::UseNamespaces::YesAndMount |
662 | | } else { |
663 | 0 | crate::running_actions_manager::UseNamespaces::Yes |
664 | | } |
665 | 2 | } else if config |
666 | 2 | .use_mount_namespace |
667 | 2 | .is_some_and(core::convert::identity) |
668 | | { |
669 | 0 | return Err(make_err!( |
670 | 0 | Code::Unavailable, |
671 | 0 | "Mount namespaces not supported" |
672 | 0 | )); |
673 | | } else { |
674 | 2 | crate::running_actions_manager::UseNamespaces::No |
675 | | }; |
676 | | |
677 | | #[cfg(not(target_os = "linux"))] |
678 | | if config.use_namespaces.is_some_and(core::convert::identity) { |
679 | | return Err(make_err!( |
680 | | Code::Unavailable, |
681 | | "Namespaces not supported on non-Linux OSes" |
682 | | )); |
683 | | } |
684 | | #[cfg(not(target_os = "linux"))] |
685 | | if config |
686 | | .use_mount_namespace |
687 | | .is_some_and(core::convert::identity) |
688 | | { |
689 | | return Err(make_err!( |
690 | | Code::Unavailable, |
691 | | "Mount namespaces not supported on non-Linux OSes" |
692 | | )); |
693 | | } |
694 | | |
695 | 2 | let running_actions_manager = |
696 | 2 | Arc::new(RunningActionsManagerImpl::new(RunningActionsManagerArgs { |
697 | 2 | root_action_directory: config.work_directory.clone(), |
698 | 2 | execution_configuration: ExecutionConfiguration { |
699 | 2 | entrypoint, |
700 | 2 | additional_environment: config.additional_environment.clone(), |
701 | 2 | }, |
702 | 2 | cas_store: fast_slow_store, |
703 | 2 | ac_store, |
704 | 2 | historical_store, |
705 | 2 | upload_action_result_config: &config.upload_action_result, |
706 | 2 | max_action_timeout, |
707 | 2 | max_upload_timeout, |
708 | 2 | max_cleanup_wait, |
709 | 2 | max_cleanup_backoff, |
710 | 2 | timeout_handled_externally: config.timeout_handled_externally, |
711 | 2 | directory_cache, |
712 | 2 | #[cfg(target_os = "linux")] |
713 | 2 | use_namespaces, |
714 | 2 | })?0 ); |
715 | 2 | let local_worker = LocalWorker::new_with_connection_factory_and_actions_manager( |
716 | 2 | config.clone(), |
717 | 2 | running_actions_manager, |
718 | 2 | Box::new(move || {0 |
719 | 0 | let config = config.clone(); |
720 | 0 | Box::pin(async move { |
721 | 0 | let timeout = config |
722 | 0 | .worker_api_endpoint |
723 | 0 | .timeout |
724 | 0 | .unwrap_or(DEFAULT_ENDPOINT_TIMEOUT_S); |
725 | 0 | let timeout_duration = Duration::from_secs_f32(timeout); |
726 | 0 | let tls_config = |
727 | 0 | tls_utils::load_client_config(&config.worker_api_endpoint.tls_config) |
728 | 0 | .err_tip(|| "Parsing local worker TLS configuration")?; |
729 | 0 | let endpoint = |
730 | 0 | tls_utils::endpoint_from(&config.worker_api_endpoint.uri, tls_config) |
731 | 0 | .map_err(|e| { |
732 | 0 | Error::from_std_err(Code::InvalidArgument, &e) |
733 | 0 | .append("Invalid URI for worker endpoint") |
734 | 0 | })? |
735 | 0 | .connect_timeout(timeout_duration) |
736 | 0 | .timeout(timeout_duration); |
737 | | |
738 | 0 | let transport = endpoint.connect().await.map_err(|e| { |
739 | 0 | Error::from_std_err(Code::Internal, &e).append(format!( |
740 | | "Could not connect to endpoint {}", |
741 | 0 | config.worker_api_endpoint.uri |
742 | | )) |
743 | 0 | })?; |
744 | 0 | Ok(WorkerApiClient::new(transport).into()) |
745 | 0 | }) |
746 | 0 | }), |
747 | 2 | Box::new(move |d| Box::pin0 (time::sleep0 (d0 ))), |
748 | | ); |
749 | 2 | Ok(local_worker) |
750 | 2 | } |
751 | | |
752 | | impl<T: WorkerApiClientTrait + 'static, U: RunningActionsManager> LocalWorker<T, U> { |
753 | 12 | pub fn new_with_connection_factory_and_actions_manager( |
754 | 12 | config: Arc<LocalWorkerConfig>, |
755 | 12 | running_actions_manager: Arc<U>, |
756 | 12 | connection_factory: ConnectionFactory<T>, |
757 | 12 | sleep_fn: Box<dyn Fn(Duration) -> BoxFuture<'static, ()> + Send + Sync>, |
758 | 12 | ) -> Self { |
759 | 12 | let metrics = Arc::new(Metrics::new(Arc::downgrade( |
760 | 12 | running_actions_manager.metrics(), |
761 | | ))); |
762 | 12 | Self { |
763 | 12 | config, |
764 | 12 | running_actions_manager, |
765 | 12 | connection_factory, |
766 | 12 | sleep_fn: Some(sleep_fn), |
767 | 12 | metrics, |
768 | 12 | } |
769 | 12 | } |
770 | | |
771 | | #[allow( |
772 | | clippy::missing_const_for_fn, |
773 | | reason = "False positive on stable, but not on nightly" |
774 | | )] |
775 | 0 | pub fn name(&self) -> &String { |
776 | 0 | &self.config.name |
777 | 0 | } |
778 | | |
779 | 14 | async fn register_worker( |
780 | 14 | &self, |
781 | 14 | client: &mut T, |
782 | 14 | ) -> Result<(String, Streaming<UpdateForWorker>), Error> { |
783 | 14 | let mut extra_envs: HashMap<String, String> = HashMap::new(); |
784 | 14 | if let Some(ref additional_environment0 ) = self.config.additional_environment { |
785 | 0 | for (name, source) in additional_environment { |
786 | 0 | let value = match source { |
787 | 0 | EnvironmentSource::Value(value) => Cow::Borrowed(value.as_str()), |
788 | | EnvironmentSource::FromEnvironment => { |
789 | 0 | Cow::Owned(env::var(name).unwrap_or_default()) |
790 | | } |
791 | 0 | other => { |
792 | 0 | debug!( |
793 | | ?other, |
794 | | "Worker registration doesn't support this type of additional environment" |
795 | | ); |
796 | 0 | continue; |
797 | | } |
798 | | }; |
799 | 0 | extra_envs.insert(name.clone(), value.into_owned()); |
800 | | } |
801 | 14 | } |
802 | | |
803 | 14 | let connect_worker_request = make_connect_worker_request( |
804 | 14 | self.config.name.clone(), |
805 | 14 | &self.config.platform_properties, |
806 | 14 | &extra_envs, |
807 | 14 | self.config.max_inflight_tasks, |
808 | 14 | ) |
809 | 14 | .await?0 ; |
810 | 14 | let mut update_for_worker_stream10 = client |
811 | 14 | .connect_worker(connect_worker_request) |
812 | 14 | .await |
813 | 10 | .err_tip(|| "Could not call connect_worker() in worker")?0 |
814 | 10 | .into_inner(); |
815 | | |
816 | 10 | let first_msg_update9 = update_for_worker_stream |
817 | 10 | .next() |
818 | 10 | .await |
819 | 10 | .err_tip(|| "Got EOF expected UpdateForWorker")?1 |
820 | 9 | .err_tip(|| "Got error when receiving UpdateForWorker")?0 |
821 | | .update; |
822 | | |
823 | 9 | let worker_id = match first_msg_update { |
824 | 9 | Some(Update::ConnectionResult(connection_result)) => connection_result.worker_id, |
825 | 0 | other => { |
826 | 0 | return Err(make_input_err!( |
827 | 0 | "Expected first response from scheduler to be a ConnectionResult got : {:?}", |
828 | 0 | other |
829 | 0 | )); |
830 | | } |
831 | | }; |
832 | 9 | Ok((worker_id, update_for_worker_stream)) |
833 | 10 | } |
834 | | |
835 | | #[instrument(skip(self), level = Level::INFO)] |
836 | 10 | pub async fn run( |
837 | 10 | mut self, |
838 | 10 | mut shutdown_rx: broadcast::Receiver<ShutdownGuard>, |
839 | 10 | ) -> Result<(), Error> { |
840 | | // Belt-and-suspenders QoS bump: the main binary already calls |
841 | | // this before runtime creation so the tokio worker threads |
842 | | // inherit P-core preference via pthread QoS inheritance, but |
843 | | // any thread that reaches this point should also be tagged in |
844 | | // case it was spawned by a path that bypassed `on_thread_start`. |
845 | | // No-op on non-macOS. |
846 | | let _ = crate::qos::set_user_initiated(); |
847 | | |
848 | | let sleep_fn = self |
849 | | .sleep_fn |
850 | | .take() |
851 | | .err_tip(|| "Could not unwrap sleep_fn in LocalWorker::run")?; |
852 | | let sleep_fn_pin = Pin::new(&sleep_fn); |
853 | 4 | let error_handler = Box::pin(move |err| async move { |
854 | 4 | error!(?err, "Error"); |
855 | 4 | (sleep_fn_pin)(Duration::from_secs_f32(CONNECTION_RETRY_DELAY_S)).await; |
856 | 8 | }); |
857 | | |
858 | | loop { |
859 | | // First connect to our endpoint. |
860 | | let mut client = match (self.connection_factory)().await { |
861 | | Ok(client) => client, |
862 | | Err(e) => { |
863 | | (error_handler)(e).await; |
864 | | continue; // Try to connect again. |
865 | | } |
866 | | }; |
867 | | |
868 | | debug!("Connected to endpoint"); |
869 | | |
870 | | // Next register our worker with the scheduler. |
871 | | let (inner, update_for_worker_stream) = match self.register_worker(&mut client).await { |
872 | | Err(e) => { |
873 | | (error_handler)(e).await; |
874 | | continue; // Try to connect again. |
875 | | } |
876 | | Ok((worker_id, update_for_worker_stream)) => ( |
877 | | LocalWorkerImpl::new( |
878 | | &self.config, |
879 | | client, |
880 | | worker_id, |
881 | | self.running_actions_manager.clone(), |
882 | | self.metrics.clone(), |
883 | | ), |
884 | | update_for_worker_stream, |
885 | | ), |
886 | | }; |
887 | | info!( |
888 | | worker_id = %inner.worker_id, |
889 | | "Worker registered with scheduler" |
890 | | ); |
891 | | |
892 | | // Now listen for connections and run all other services. |
893 | | if let Err(err) = inner.run(update_for_worker_stream, &mut shutdown_rx).await { |
894 | | // Give in-transit actions a chance to settle before we kill |
895 | | // them, so their results still reach the scheduler. |
896 | | const ITERATIONS: usize = 1_000; |
897 | | |
898 | | let sleep_duration = ACTIONS_IN_TRANSIT_TIMEOUT_S / ITERATIONS as f32; |
899 | | let mut drained = false; |
900 | | for _ in 0..ITERATIONS { |
901 | | if inner.actions_in_transit.load(Ordering::Acquire) == 0 { |
902 | | drained = true; |
903 | | break; |
904 | | } |
905 | | (sleep_fn_pin)(Duration::from_secs_f32(sleep_duration)).await; |
906 | | } |
907 | | if !drained { |
908 | | // Deliberately not fatal. Returning here propagates out of |
909 | | // the worker's main loop and aborts the process, so a |
910 | | // scheduler blip that happened to catch an action in |
911 | | // transit took the whole worker down — and every action it |
912 | | // held then had to run again elsewhere. At fleet scale that |
913 | | // is a restart storm. kill_all() below discards these |
914 | | // actions anyway, so the wait is a courtesy and overrunning |
915 | | // it costs nothing beyond the actions we were already |
916 | | // giving up on. |
917 | | error!( |
918 | | actions_in_transit = inner.actions_in_transit.load(Ordering::Acquire), |
919 | | "Actions in transit did not reach zero before we disconnected from the scheduler" |
920 | | ); |
921 | | } |
922 | | |
923 | | error!(?err, "Worker disconnected from scheduler, reconnecting"); |
924 | | // Kill off any existing actions because if we re-connect, we'll |
925 | | // get some more and it might resource lock us. |
926 | | self.running_actions_manager.kill_all().await; |
927 | | |
928 | | (error_handler)(err).await; // Try to connect again. |
929 | | } |
930 | | } |
931 | | // Unreachable. |
932 | 0 | } |
933 | | } |
934 | | |
935 | | #[derive(Debug, MetricsComponent)] |
936 | | pub struct Metrics { |
937 | | #[metric( |
938 | | help = "Total number of actions sent to this worker to process. This does not mean it started them, it just means it received a request to execute it." |
939 | | )] |
940 | | start_actions_received: CounterWithTime, |
941 | | #[metric(help = "Total number of disconnects received from the scheduler.")] |
942 | | disconnects_received: CounterWithTime, |
943 | | #[metric(help = "Total number of keep-alives received from the scheduler.")] |
944 | | keep_alives_received: CounterWithTime, |
945 | | #[metric( |
946 | | help = "Stats about the calls to check if an action satisfies the config supplied script." |
947 | | )] |
948 | | preconditions: AsyncCounterWrapper, |
949 | | #[metric] |
950 | | #[allow( |
951 | | clippy::struct_field_names, |
952 | | reason = "TODO Fix this. Triggers on nightly" |
953 | | )] |
954 | | running_actions_manager_metrics: Weak<RunningActionManagerMetrics>, |
955 | | } |
956 | | |
957 | | impl RootMetricsComponent for Metrics {} |
958 | | |
959 | | impl Metrics { |
960 | 12 | fn new(running_actions_manager_metrics: Weak<RunningActionManagerMetrics>) -> Self { |
961 | 12 | Self { |
962 | 12 | start_actions_received: CounterWithTime::default(), |
963 | 12 | disconnects_received: CounterWithTime::default(), |
964 | 12 | keep_alives_received: CounterWithTime::default(), |
965 | 12 | preconditions: AsyncCounterWrapper::default(), |
966 | 12 | running_actions_manager_metrics, |
967 | 12 | } |
968 | 12 | } |
969 | | } |
970 | | |
971 | | impl Metrics { |
972 | 7 | async fn wrap<U, T: Future<Output = U>, F: FnOnce(Arc<Self>) -> T>( |
973 | 7 | self: Arc<Self>, |
974 | 7 | fut: F, |
975 | 7 | ) -> U {6 |
976 | 6 | fut(self).await |
977 | 5 | } |
978 | | } |