Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-scheduler/src/worker_capability_index.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
//! Worker capability index for fast worker matching.
16
//!
17
//! This module provides an index that accelerates worker matching by property.
18
//! Instead of iterating all workers for each action, we maintain an inverted index
19
//! that maps property values to sets of workers that have those values.
20
//!
21
//! ## Complexity Analysis
22
//!
23
//! Without index: O(W × P) where W = workers, P = properties per action
24
//! With index: O(P × log(W)) for exact properties + O(W' × P') for minimum properties
25
//!   where W' = filtered workers, P' = minimum property count (typically small)
26
//!
27
//! For typical workloads (few minimum properties), this reduces matching from
28
//! O(n × m) to approximately O(log n).
29
30
use std::collections::{HashMap, HashSet};
31
32
use nativelink_util::action_messages::WorkerId;
33
use nativelink_util::platform_properties::{PlatformProperties, PlatformPropertyValue};
34
use tracing::info;
35
36
/// A property key-value pair used for indexing.
37
#[derive(Clone, Hash, Eq, PartialEq, Debug)]
38
struct PropertyKey {
39
    name: String,
40
    value: PlatformPropertyValue,
41
}
42
43
/// Index structure for fast worker capability lookup.
44
///
45
/// Maintains an inverted index from property values to worker IDs.
46
/// Only indexes `Exact` and `Priority` properties since `Minimum` properties
47
/// are dynamic and require runtime comparison.
48
#[derive(Debug, Default)]
49
pub struct WorkerCapabilityIndex {
50
    /// Maps `(property_name, property_value)` -> Set of worker IDs with that property.
51
    /// Only contains `Exact` and `Priority` properties.
52
    exact_index: HashMap<PropertyKey, HashSet<WorkerId>>,
53
54
    /// Maps `property_name` -> Set of worker IDs that have this property (any value).
55
    /// Used for fast "has property" checks for `Priority` and `Minimum` properties.
56
    property_presence: HashMap<String, HashSet<WorkerId>>,
57
58
    /// Set of all indexed worker IDs.
59
    all_workers: HashSet<WorkerId>,
60
}
61
62
impl WorkerCapabilityIndex {
63
    /// Creates a new empty capability index.
64
380
    pub fn new() -> Self {
65
380
        Self::default()
66
380
    }
67
68
    /// Adds a worker to the index with their platform properties.
69
2.27k
    pub fn add_worker(&mut self, worker_id: &WorkerId, properties: &PlatformProperties) {
70
2.27k
        self.all_workers.insert(worker_id.clone());
71
72
2.27k
        for (
name333
,
value333
) in &properties.properties {
73
            // Track property presence
74
333
            self.property_presence
75
333
                .entry(name.clone())
76
333
                .or_default()
77
333
                .insert(worker_id.clone());
78
79
333
            match value {
80
                PlatformPropertyValue::Exact(_)
81
                | PlatformPropertyValue::Priority(_)
82
98
                | PlatformPropertyValue::Unknown(_) => {
83
98
                    // Index exact-match properties
84
98
                    let key = PropertyKey {
85
98
                        name: name.clone(),
86
98
                        value: value.clone(),
87
98
                    };
88
98
                    self.exact_index
89
98
                        .entry(key)
90
98
                        .or_default()
91
98
                        .insert(worker_id.clone());
92
98
                }
93
235
                PlatformPropertyValue::Minimum(_) | PlatformPropertyValue::Ignore(_) => {
94
235
                    // Minimum properties are tracked via `property_presence` only.
95
235
                    // Their actual values are checked at runtime since they're dynamic.
96
235
97
235
                    // Ignore properties we just drop
98
235
                }
99
            }
100
        }
101
2.27k
    }
102
103
    /// Removes a worker from the index.
104
1.21k
    pub fn remove_worker(&mut self, worker_id: &WorkerId) {
105
1.21k
        self.all_workers.remove(worker_id);
106
107
        // Remove from exact index
108
1.21k
        self.exact_index.retain(|_, workers| 
{8
109
8
            workers.remove(worker_id);
110
8
            !workers.is_empty()
111
8
        });
112
113
        // Remove from presence index
114
1.21k
        self.property_presence.retain(|_, workers| 
{60
115
60
            workers.remove(worker_id);
116
60
            !workers.is_empty()
117
60
        });
118
1.21k
    }
119
120
    /// Finds workers that can satisfy the given action properties.
121
    ///
122
    /// Returns a set of worker IDs that match all required properties.
123
    /// The caller should apply additional filtering (e.g., worker availability).
124
    ///
125
    /// IMPORTANT: This method returns candidates based on STATIC properties only.
126
    /// - Exact and Unknown properties are fully matched; Unknown properties
127
    ///   additionally match workers that do not declare the key
128
    /// - Priority properties just require the key to exist
129
    /// - Minimum properties return workers that HAVE the property (presence check only)
130
    ///
131
    /// The caller MUST still verify Minimum property values at runtime because
132
    /// worker resources change dynamically as jobs are assigned/completed.
133
11.6k
    pub fn find_matching_workers(
134
11.6k
        &self,
135
11.6k
        action_properties: &PlatformProperties,
136
11.6k
        full_worker_logging: bool,
137
11.6k
    ) -> HashSet<WorkerId> {
138
11.6k
        if self.all_workers.is_empty() {
139
2
            if full_worker_logging {
140
2
                info!("No workers available to match!");
141
0
            }
142
2
            return HashSet::new();
143
11.6k
        }
144
145
11.6k
        if action_properties.properties.is_empty() {
146
            // No properties required, all workers match
147
11.1k
            return self.all_workers.clone();
148
542
        }
149
150
542
        let mut candidates: Option<HashSet<WorkerId>> = None;
151
152
1.05k
        for (name, value) in 
&action_properties.properties542
{
153
1.05k
            match value {
154
                PlatformPropertyValue::Exact(_) | PlatformPropertyValue::Unknown(_) => {
155
                    // Look up workers with exact match
156
155
                    let key = PropertyKey {
157
155
                        name: name.clone(),
158
155
                        value: value.clone(),
159
155
                    };
160
161
155
                    let mut matching = self.exact_index.get(&key).cloned().unwrap_or_default();
162
163
                    // Unknown properties only restrict workers that declare the
164
                    // key, so workers without the key remain candidates.
165
155
                    if 
matches!152
(value, PlatformPropertyValue::Unknown(_)) {
166
3
                        match self.property_presence.get(name) {
167
1
                            Some(with_key) => {
168
1
                                matching.extend(self.all_workers.difference(with_key).cloned());
169
1
                            }
170
2
                            None => matching.extend(self.all_workers.iter().cloned()),
171
                        }
172
152
                    }
173
174
155
                    let internal_candidates = match candidates {
175
89
                        Some(existing) => existing.intersection(&matching).cloned().collect(),
176
66
                        None => matching,
177
                    };
178
179
                    // Early exit if no candidates
180
155
                    if internal_candidates.is_empty() {
181
14
                        if full_worker_logging {
182
3
                            let values: Vec<_> = self
183
3
                                .exact_index
184
3
                                .iter()
185
5
                                .
filter3
(|pk| &pk.0.name == name)
186
3
                                .map(|pk| pk.0.value.clone())
187
3
                                .collect();
188
3
                            info!(
189
                                "No candidate workers due to a lack of matching '{name}' = {value:?}. Workers have: {values:?}"
190
                            );
191
11
                        }
192
14
                        return HashSet::new();
193
141
                    }
194
141
                    candidates = Some(internal_candidates);
195
                }
196
                PlatformPropertyValue::Priority(_) | PlatformPropertyValue::Minimum(_) => {
197
                    // Priority: just requires the key to exist
198
                    // Minimum: worker must have the property (value checked at runtime by caller)
199
                    // We only check presence here because Minimum values are DYNAMIC -
200
                    // they change as jobs are assigned to workers.
201
901
                    let workers_with_property = self
202
901
                        .property_presence
203
901
                        .get(name)
204
901
                        .cloned()
205
901
                        .unwrap_or_default();
206
207
901
                    let internal_candidates = match candidates {
208
426
                        Some(existing) => existing
209
426
                            .intersection(&workers_with_property)
210
426
                            .cloned()
211
426
                            .collect(),
212
475
                        None => workers_with_property,
213
                    };
214
215
901
                    if internal_candidates.is_empty() {
216
1
                        if full_worker_logging {
217
1
                            info!(
218
                                "No candidate workers due to a lack of key '{name}'. Job asked for {value:?}"
219
                            );
220
0
                        }
221
1
                        return HashSet::new();
222
900
                    }
223
900
                    candidates = Some(internal_candidates);
224
                }
225
1
                PlatformPropertyValue::Ignore(_) => {}
226
            }
227
        }
228
229
527
        candidates.unwrap_or_else(|| 
self.all_workers1
.
clone1
())
230
11.6k
    }
231
232
    /// Returns the number of indexed workers.
233
2
    pub fn worker_count(&self) -> usize {
234
2
        self.all_workers.len()
235
2
    }
236
237
    /// Returns true if the index is empty.
238
0
    pub fn is_empty(&self) -> bool {
239
0
        self.all_workers.is_empty()
240
0
    }
241
}