Coverage Report

Created: 2026-09-18 20:40

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-service/src/fetch_server.rs
Line
Count
Source
1
// Copyright 2025 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::convert::Into;
16
use std::collections::HashMap;
17
18
use nativelink_config::cas_server::{FetchConfig, WithInstanceName};
19
use nativelink_error::{Error, ResultExt, make_err, make_input_err};
20
use nativelink_proto::build::bazel::remote::asset::v1::fetch_server::{
21
    Fetch, FetchServer as Server,
22
};
23
use nativelink_proto::build::bazel::remote::asset::v1::{
24
    FetchBlobRequest, FetchBlobResponse, FetchDirectoryRequest, FetchDirectoryResponse,
25
};
26
use nativelink_proto::google::rpc::Status as GoogleStatus;
27
use nativelink_store::store_manager::StoreManager;
28
use nativelink_util::digest_hasher::{default_digest_hasher_func, make_ctx_for_hash_func};
29
use nativelink_util::store_trait::{Store, StoreLike};
30
use opentelemetry::context::FutureExt;
31
use prost::Message;
32
use tonic::{Code, Request, Response, Status};
33
use tracing::{Instrument, Level, error_span, info, instrument};
34
35
use crate::remote_asset_proto::{RemoteAssetArtifact, RemoteAssetQuery};
36
37
#[derive(Debug, Clone)]
38
pub struct FetchStoreInfo {
39
    store: Store,
40
}
41
42
#[derive(Debug, Clone)]
43
pub struct FetchServer {
44
    stores: HashMap<String, FetchStoreInfo>,
45
}
46
47
impl FetchServer {
48
2
    pub fn new(
49
2
        configs: &[WithInstanceName<FetchConfig>],
50
2
        store_manager: &StoreManager,
51
2
    ) -> Result<Self, Error> {
52
2
        let mut stores = HashMap::with_capacity(configs.len());
53
2
        for config in configs {
54
2
            let store = store_manager
55
2
                .get_store(&config.fetch_store)
56
2
                .ok_or_else(|| 
{0
57
0
                    make_input_err!("'fetch_store': '{}' does not exist", config.fetch_store)
58
0
                })?;
59
2
            stores.insert(config.instance_name.clone(), FetchStoreInfo { store });
60
        }
61
2
        Ok(Self {
62
2
            stores: stores.clone(),
63
2
        })
64
2
    }
65
66
0
    pub fn into_service(self) -> Server<Self> {
67
0
        Server::new(self)
68
0
    }
69
70
2
    async fn inner_fetch_blob(
71
2
        &self,
72
2
        request: FetchBlobRequest,
73
2
    ) -> Result<Response<FetchBlobResponse>, Error> {
74
2
        let instance_name = &request.instance_name;
75
2
        let store_info = self
76
2
            .stores
77
2
            .get(instance_name)
78
2
            .err_tip(|| 
format!0
("'instance_name' not configured for '{instance_name}'"))
?0
;
79
80
2
        if request.uris.is_empty() {
81
0
            return Err(Error::new(
82
0
                Code::InvalidArgument,
83
0
                "No uris in fetch request".to_owned(),
84
0
            ));
85
2
        }
86
2
        for uri in &request.uris {
87
2
            let asset_request = RemoteAssetQuery::new(uri.clone(), request.qualifiers.clone());
88
2
            let asset_digest = asset_request.digest();
89
2
            let asset_response_possible = store_info
90
2
                .store
91
2
                .get_part_unchunked(asset_digest, 0, None)
92
2
                .await;
93
94
2
            info!(
95
                uri = uri,
96
2
                digest = format!("{}", asset_digest),
97
                "Looked up fetch asset"
98
            );
99
100
2
            if let Ok(asset_response_raw) = asset_response_possible {
101
2
                let 
asset_response1
= RemoteAssetArtifact::decode(asset_response_raw)
102
2
                    .err_tip(|| "Failed to decode stored RemoteAssetArtifact")
?1
;
103
1
                return Ok(Response::new(FetchBlobResponse {
104
1
                    status: Some(GoogleStatus {
105
1
                        code: Code::Ok.into(),
106
1
                        message: "Fetch object found".to_owned(),
107
1
                        details: vec![],
108
1
                    }),
109
1
                    uri: asset_response.uri,
110
1
                    qualifiers: asset_response.qualifiers,
111
1
                    expires_at: asset_response.expire_at,
112
1
                    blob_digest: asset_response.blob_digest,
113
1
                    digest_function: asset_response.digest_function,
114
1
                }));
115
0
            }
116
        }
117
0
        Ok(Response::new(FetchBlobResponse {
118
0
            status: Some(make_err!(Code::NotFound, "No item found").into()),
119
0
            uri: request.uris.first().cloned().unwrap_or(String::new()),
120
0
            qualifiers: vec![],
121
0
            expires_at: None,
122
0
            blob_digest: None,
123
0
            digest_function: default_digest_hasher_func().proto_digest_func().into(),
124
0
        }))
125
2
    }
126
}
127
128
#[tonic::async_trait]
129
impl Fetch for FetchServer {
130
    #[allow(clippy::blocks_in_conditions)]
131
    #[instrument(
132
        err(level = Level::WARN),
133
        ret(level = Level::INFO),
134
        skip_all,
135
        fields(request = ?grpc_request.get_ref())
136
    )]
137
    async fn fetch_blob(
138
        &self,
139
        grpc_request: Request<FetchBlobRequest>,
140
    ) -> Result<Response<FetchBlobResponse>, Status> {
141
        let request = grpc_request.into_inner();
142
        let digest_function = request.digest_function;
143
        self.inner_fetch_blob(request)
144
            .instrument(error_span!("fetch_server_fetch_blob"))
145
            .with_context(
146
                make_ctx_for_hash_func(digest_function).err_tip(|| "In FetchServer::fetch_blob")?,
147
            )
148
            .await
149
            .err_tip(|| "Failed on fetch_blob() command")
150
            .map_err(Into::into)
151
    }
152
153
    #[allow(clippy::blocks_in_conditions)]
154
    #[instrument(
155
        err(level = Level::WARN),
156
        ret(level = Level::INFO),
157
        skip_all,
158
        fields(request = ?_grpc_request.get_ref())
159
    )]
160
    async fn fetch_directory(
161
        &self,
162
        _grpc_request: Request<FetchDirectoryRequest>,
163
    ) -> Result<Response<FetchDirectoryResponse>, Status> {
164
        Err(Status::unimplemented("FetchDirectory not implemented"))
165
    }
166
}