How to extend NativeLink
Complete file-by-file recipes for adding a store, a config field, a metric, or a gRPC service.
Who this is for: anyone adding a new component rather than changing an existing one. What you'll have at the end: four recipes, each listing every file a working change touches, including the ones that compile fine when you forget them and fail in CI. Time: use as a checklist.
Each recipe below is derived from the smallest existing example in the tree. Copy that example rather than starting from a blank file: it already has the right license header, the right derives, and the right last line.
Read codebase internals first if you have not; the trait split and the spawning rules are assumed here.
Adding a store
Model: nativelink-store/src/noop_store.rs, the smallest complete
StoreDriver in the tree.
Add the spec to
nativelink-config/src/stores.rs. A new variant onStoreSpec(the JSON5 key is the snake_case form of the variant name; box the payload if it is large) plus a spec struct modelled onMemorySpec:#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(deny_unknown_fields)] #[cfg_attr(feature = "dev-schema", derive(JsonSchema))] pub struct MyStoreSpec { /// What this field is for. /// Default: 0 pub some_field: usize, }The
dev-schemacfg_attris not optional; without it your store never appears in the generated configuration reference.Write the store at
nativelink-store/src/my_store.rs: the license header copied from a neighbouring file, the struct, aMetricsComponentimpl,pub fn new(...) -> Arc<Self>,#[async_trait] impl StoreDriver, anddefault_health_status_indicator!(MyStore);as the last line.Declare the module:
pub mod my_store;innativelink-store/src/lib.rs.Wire the factory: an import and a match arm in
nativelink-store/src/default_store_factory.rs. The match is exhaustive, so this one is a compile error if you forget it. It is the only step that is.Add the file to
nativelink-store/BUILD.bazel, in thesrcslist, alphabetically. Cargo does not care. Bazel does, and CI runs Bazel; this is the single most common way a store PR goes red after a green local run.Add a test at
nativelink-store/tests/my_store_test.rsand register it in the crate'srust_test_suite. See the testing guide.
The store examples file is generated from your doc comments
nativelink-config/examples/stores-config.json5 is produced by the
generate-stores-config tool from the ```json fenced blocks in
the doc comments in stores.rs, via a pre-commit hook. And
tests/json5_test.rs parses every example, so a malformed JSON fence in a
doc comment is not a cosmetic problem; it breaks the build.
Two semantics to get right, both covered in the store model:
inner_store must return self if your store changes the bytes on the
way through, and forward to the inner store if it only routes. Returning the
wrong one is a runtime error, not a compile error.
update must drain its reader even when it is discarding the data.
NoopStore does this; a store that returns early without draining will hang
its writer.
Adding a config field
Config fields are documentation. The doc comment becomes the description in
the generated reference, and the ```json fences in stores.rs are
also harvested into the examples file, which is why they have to be valid
JSON rather than illustrative fragments.
Add the field with a doc comment ending in a default line:
/// How long to wait before giving up on a connection. /// Default: 5 (seconds) #[serde(default, deserialize_with = "convert_duration_with_shellexpand")] pub connection_timeout_s: u64,The
Default:line is parsed out of the description by the docs generator. Without it the reference shows no default.Pick the right
deserialize_withhelper from serde_utils.rs. They all expand shell variables, which is how a config file references the environment.Regenerate the reference if you want to see the result:
bun --filter @nativelink/docs gen:config-referencefromweb/.
The helpers that exist:
| Helper | For |
|---|---|
convert_numeric_with_shellexpand | Any integer type |
convert_optional_numeric_with_shellexpand | Option<N> |
convert_string_with_shellexpand | String |
convert_optional_string_with_shellexpand | Option<String> |
convert_vec_string_with_shellexpand | Vec<String> |
convert_boolean_with_shellexpand | bool |
convert_data_size_with_shellexpand | Byte sizes; accepts 1GB, 512MiB |
convert_optional_data_size_with_shellexpand | Option<size> |
convert_duration_with_shellexpand | Durations |
convert_duration_with_shellexpand_and_negative | Durations that may be negative |
There is no `default_as_true` helper
A bool that should default to true needs its own
fn default_true() -> bool { true } and #[serde(default = "default_true")].
Several fields do this; there is no shared helper.
To rename a field without breaking existing configs, add a shim in
backcompat.rs; both existing shims are #[serde(untagged)] enums that
accept the old and new shapes and emit a tracing::warn! on the deprecated
one.
Adding a metric
Decide which system first; see two metric systems.
The derive path touches one file. Add the field, annotate it:
#[metric(help = "Number of blobs currently resident")]
resident_blobs: AtomicU64,The struct already derives MetricsComponent, and the value appears in the
introspection tree automatically.
The OpenTelemetry path touches three:
Declare the instrument in the appropriate
LazyLockbundle innativelink-util/src/metrics.rs(CACHE_METRICS,EXECUTION_METRICS,WORKER_METRICS,RPC_METRICS,SCHEDULER_METRICS,STORE_TIER_METRICS,HEALTH_METRICSorCONNECTION_METRICS). Use a dotted name, a.with_description, and a.with_unitthat is a UCUM unit ("By","s","ms") or a braced count such as"{entry}". Then regenerate the metrics reference withbun --filter @nativelink/docs gen:metrics-referencefromweb/, which reads this file.Record it at the call site, using the pre-computed attribute struct where one exists (
CacheMetricAttrs/ExecutionMetricAttrs) rather than building attributes inline.Extend
nativelink-util/tests/metrics_test.rs.
Two things to not do here
nativelink-util/src/metrics.rs is Business Source License; keep its
header, do not paste the FSL one in. And never add an action digest or a
worker ID as an attribute on a hot instrument: the cardinality will kill
the metrics backend long before it affects NativeLink.
Adding a gRPC service
Model: nativelink-service/src/fetch_server.rs: 165 lines, and it has
every convention in it.
Write the server at
nativelink-service/src/my_server.rs. Import the generated trait and server type with the standard alias:use nativelink_proto::…::my_server::{My, MyServer as Server};Hold per-instance state in a
HashMap<String, _>keyed by instance name. Constructor signature:pub fn new( configs: &[WithInstanceName<MyConfig>], store_manager: &StoreManager, ) -> Result<Self, Error>Provide
into_service, exactly this name, because theservice_setup!macro calls it:pub fn into_service(self) -> Server<Self> { Server::new(self) }Split each RPC in two. A private
inner_<rpc>returningnativelink_error::Errorholds the logic; the public#[tonic::async_trait]method wraps it, converts toStatus, and carries the instrumentation:#[instrument( err(level = Level::WARN), ret(level = Level::INFO), skip_all, fields(request = ?grpc_request.get_ref()) )]Register it in the
Routes::builder().routes()chain insrc/bin/nativelink.rs, with.add_optional_service(...)andservice_setup!. The macro applies the message-size limit (4 MiB by default) and compression settings, so registering without it silently opts out of both.
If the service needs new protos: add the package to PROTO_NAMES and to
the gen_rs_protos srcs list in nativelink-proto/BUILD.bazel, then
regenerate and commit the output:
bazel run nativelink-proto:update_protos//nativelink-proto:update_protos_test is the CI gate that catches a
forgotten regeneration.
Worker-facing services belong on their own port
The CAS server carries an explicit note about this: the worker_api
service must not share a listener with client-facing services, because it
has no authentication of its own. Anything you add with worker-level
privileges inherits that requirement; see
security hardening.
Common questions
How the suites are organised, what #[nativelink_test] does for you, and
which local environment problems produce which confusing failure.
Codebase internals
The traits, the error model, the async rules and the two metric systems: the conventions a reviewer will assume you already know.
Testing guide
How NativeLink's tests are organised, what `#[nativelink_test]` gives you, and which local environment problems cause which confusing failure.