about summary refs log tree commit diff
path: root/tvix
diff options
context:
space:
mode:
authorFlorian Klink <flokli@flokli.de>2024-09-25T20·05+0200
committerclbot <clbot@tvl.fyi>2024-09-30T10·04+0000
commit2e4a373a040b5e8355d05b8030341494d1ff386b (patch)
tree3ff8d03b83f7d37d1895e2d7efb0aab28da478d1 /tvix
parent16a3b9012501ba9ad87e7e4dc6ff5e0792ea165d (diff)
feat(tvix/nar-bridge): implement range request for NARs r/8737
With an implementation of AsyncRead + AsyncSeek, axum-range can answer
range requests.

We only use it if a range has been requested, as it uses more memory
than the linear variant.

Change-Id: I0072b0a09b328f3e932f14567a2caa3a49abcbf7
Reviewed-on: https://cl.tvl.fyi/c/depot/+/12509
Autosubmit: flokli <flokli@flokli.de>
Tested-by: BuildkiteCI
Reviewed-by: raitobezarius <tvl@lahfa.xyz>
Reviewed-by: yuka <yuka@yuka.dev>
Diffstat (limited to 'tvix')
-rw-r--r--tvix/Cargo.lock64
-rw-r--r--tvix/Cargo.nix234
-rw-r--r--tvix/Cargo.toml2
-rw-r--r--tvix/nar-bridge/Cargo.toml2
-rw-r--r--tvix/nar-bridge/src/nar.rs71
5 files changed, 352 insertions, 21 deletions
diff --git a/tvix/Cargo.lock b/tvix/Cargo.lock
index 22f4d194d43a..32674fbdfb91 100644
--- a/tvix/Cargo.lock
+++ b/tvix/Cargo.lock
@@ -362,6 +362,44 @@ dependencies = [
 ]
 
 [[package]]
+name = "axum-extra"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733"
+dependencies = [
+ "axum",
+ "axum-core",
+ "bytes",
+ "futures-util",
+ "headers",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "serde",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-range"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1c30398a7f716ebdd7f3c8a4f7a7a6df48a30e002007fd57b2a7a00fac864bd"
+dependencies = [
+ "axum",
+ "axum-extra",
+ "bytes",
+ "futures",
+ "http-body",
+ "pin-project",
+ "tokio",
+]
+
+[[package]]
 name = "backtrace"
 version = "0.3.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1501,6 +1539,30 @@ dependencies = [
 ]
 
 [[package]]
+name = "headers"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9"
+dependencies = [
+ "base64 0.21.7",
+ "bytes",
+ "headers-core",
+ "http",
+ "httpdate",
+ "mime",
+ "sha1",
+]
+
+[[package]]
+name = "headers-core"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
+dependencies = [
+ "http",
+]
+
+[[package]]
 name = "heck"
 version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2133,6 +2195,8 @@ name = "nar-bridge"
 version = "0.1.0"
 dependencies = [
  "axum",
+ "axum-extra",
+ "axum-range",
  "bytes",
  "clap",
  "data-encoding",
diff --git a/tvix/Cargo.nix b/tvix/Cargo.nix
index 027aaba4845b..a1cb903b70a1 100644
--- a/tvix/Cargo.nix
+++ b/tvix/Cargo.nix
@@ -1338,6 +1338,173 @@ rec {
         };
         resolvedDefaultFeatures = [ "tracing" ];
       };
+      "axum-extra" = rec {
+        crateName = "axum-extra";
+        version = "0.9.3";
+        edition = "2021";
+        sha256 = "0cyp22wy0lykpmkikkr7z0c0rg6j7cw2xpphd83vav5rr44ymrhb";
+        libName = "axum_extra";
+        dependencies = [
+          {
+            name = "axum";
+            packageId = "axum";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "axum-core";
+            packageId = "axum-core";
+          }
+          {
+            name = "bytes";
+            packageId = "bytes";
+          }
+          {
+            name = "futures-util";
+            packageId = "futures-util";
+            usesDefaultFeatures = false;
+            features = [ "alloc" ];
+          }
+          {
+            name = "headers";
+            packageId = "headers";
+            optional = true;
+          }
+          {
+            name = "http";
+            packageId = "http";
+          }
+          {
+            name = "http-body";
+            packageId = "http-body";
+          }
+          {
+            name = "http-body-util";
+            packageId = "http-body-util";
+          }
+          {
+            name = "mime";
+            packageId = "mime";
+          }
+          {
+            name = "pin-project-lite";
+            packageId = "pin-project-lite";
+          }
+          {
+            name = "serde";
+            packageId = "serde";
+          }
+          {
+            name = "tower";
+            packageId = "tower";
+            usesDefaultFeatures = false;
+            features = [ "util" ];
+          }
+          {
+            name = "tower-layer";
+            packageId = "tower-layer";
+          }
+          {
+            name = "tower-service";
+            packageId = "tower-service";
+          }
+          {
+            name = "tracing";
+            packageId = "tracing";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+        ];
+        devDependencies = [
+          {
+            name = "axum";
+            packageId = "axum";
+          }
+          {
+            name = "serde";
+            packageId = "serde";
+            features = [ "derive" ];
+          }
+          {
+            name = "tower";
+            packageId = "tower";
+            features = [ "util" ];
+          }
+        ];
+        features = {
+          "async-read-body" = [ "dep:tokio-util" "tokio-util?/io" "dep:tokio" ];
+          "cookie" = [ "dep:cookie" ];
+          "cookie-key-expansion" = [ "cookie" "cookie?/key-expansion" ];
+          "cookie-private" = [ "cookie" "cookie?/private" ];
+          "cookie-signed" = [ "cookie" "cookie?/signed" ];
+          "default" = [ "tracing" ];
+          "erased-json" = [ "dep:serde_json" ];
+          "form" = [ "dep:serde_html_form" ];
+          "json-deserializer" = [ "dep:serde_json" "dep:serde_path_to_error" ];
+          "json-lines" = [ "dep:serde_json" "dep:tokio-util" "dep:tokio-stream" "tokio-util?/io" "tokio-stream?/io-util" "dep:tokio" ];
+          "multipart" = [ "dep:multer" ];
+          "protobuf" = [ "dep:prost" ];
+          "query" = [ "dep:serde_html_form" ];
+          "tracing" = [ "dep:tracing" "axum-core/tracing" ];
+          "typed-header" = [ "dep:headers" ];
+          "typed-routing" = [ "dep:axum-macros" "dep:percent-encoding" "dep:serde_html_form" "dep:form_urlencoded" ];
+        };
+        resolvedDefaultFeatures = [ "default" "tracing" "typed-header" ];
+      };
+      "axum-range" = rec {
+        crateName = "axum-range";
+        version = "0.4.0";
+        edition = "2021";
+        sha256 = "1gb4r3x00yiaggapy002w0q8mx3dg9x4z2iwgzfyn5pplyc07hxi";
+        libName = "axum_range";
+        dependencies = [
+          {
+            name = "axum";
+            packageId = "axum";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "axum-extra";
+            packageId = "axum-extra";
+            features = [ "typed-header" ];
+          }
+          {
+            name = "bytes";
+            packageId = "bytes";
+          }
+          {
+            name = "futures";
+            packageId = "futures";
+            usesDefaultFeatures = false;
+            features = [ "std" ];
+          }
+          {
+            name = "http-body";
+            packageId = "http-body";
+          }
+          {
+            name = "pin-project";
+            packageId = "pin-project";
+          }
+          {
+            name = "tokio";
+            packageId = "tokio";
+            features = [ "fs" "io-util" ];
+          }
+        ];
+        devDependencies = [
+          {
+            name = "axum";
+            packageId = "axum";
+            features = [ "macros" ];
+          }
+          {
+            name = "tokio";
+            packageId = "tokio";
+            features = [ "rt" "rt-multi-thread" "macros" ];
+          }
+        ];
+        features = { };
+      };
       "backtrace" = rec {
         crateName = "backtrace";
         version = "0.3.69";
@@ -1412,7 +1579,7 @@ rec {
           "default" = [ "std" ];
           "std" = [ "alloc" ];
         };
-        resolvedDefaultFeatures = [ "alloc" "std" ];
+        resolvedDefaultFeatures = [ "alloc" "default" "std" ];
       };
       "base64 0.22.1" = rec {
         crateName = "base64";
@@ -4734,6 +4901,63 @@ rec {
         };
         resolvedDefaultFeatures = [ "ahash" "allocator-api2" "default" "inline-more" "raw" ];
       };
+      "headers" = rec {
+        crateName = "headers";
+        version = "0.4.0";
+        edition = "2015";
+        sha256 = "1abari69kjl2yv2dg06g2x17qgd1a20xp7aqmmg2vfhcppk0c89j";
+        authors = [
+          "Sean McArthur <sean@seanmonstar.com>"
+        ];
+        dependencies = [
+          {
+            name = "base64";
+            packageId = "base64 0.21.7";
+          }
+          {
+            name = "bytes";
+            packageId = "bytes";
+          }
+          {
+            name = "headers-core";
+            packageId = "headers-core";
+          }
+          {
+            name = "http";
+            packageId = "http";
+          }
+          {
+            name = "httpdate";
+            packageId = "httpdate";
+          }
+          {
+            name = "mime";
+            packageId = "mime";
+          }
+          {
+            name = "sha1";
+            packageId = "sha1";
+          }
+        ];
+        features = { };
+      };
+      "headers-core" = rec {
+        crateName = "headers-core";
+        version = "0.3.0";
+        edition = "2015";
+        sha256 = "1r1w80i2bhmyh8s5mjr2dz6baqlrm6cak6yvzm4jq96lacjs5d2l";
+        libName = "headers_core";
+        authors = [
+          "Sean McArthur <sean@seanmonstar.com>"
+        ];
+        dependencies = [
+          {
+            name = "http";
+            packageId = "http";
+          }
+        ];
+
+      };
       "heck 0.4.1" = rec {
         crateName = "heck";
         version = "0.4.1";
@@ -6616,6 +6840,14 @@ rec {
             features = [ "http2" ];
           }
           {
+            name = "axum-extra";
+            packageId = "axum-extra";
+          }
+          {
+            name = "axum-range";
+            packageId = "axum-range";
+          }
+          {
             name = "bytes";
             packageId = "bytes";
           }
diff --git a/tvix/Cargo.toml b/tvix/Cargo.toml
index 16bf9c2dd99c..bcb8e6f7e78e 100644
--- a/tvix/Cargo.toml
+++ b/tvix/Cargo.toml
@@ -46,6 +46,8 @@ async-process = "2.2.4"
 async-stream = "0.3.5"
 async-tempfile = "0.4.0"
 axum = "0.7.5"
+axum-extra = "0.9.3"
+axum-range = "0.4.0"
 # https://github.com/liufuyang/bigtable_rs/pull/86
 bigtable_rs = { git = "https://github.com/liufuyang/bigtable_rs", rev = "1818355a5373a5bc2c84287e3a4e3807154ac8ef" }
 bitflags = "2.6.0"
diff --git a/tvix/nar-bridge/Cargo.toml b/tvix/nar-bridge/Cargo.toml
index 6ca0479a9a81..ac23b597311f 100644
--- a/tvix/nar-bridge/Cargo.toml
+++ b/tvix/nar-bridge/Cargo.toml
@@ -5,6 +5,8 @@ edition = "2021"
 
 [dependencies]
 axum = { workspace = true, features = ["http2"] }
+axum-extra = { workspace = true }
+axum-range = { workspace = true }
 tower = { workspace = true }
 tower-http = { workspace = true, features = ["trace"] }
 bytes = { workspace = true }
diff --git a/tvix/nar-bridge/src/nar.rs b/tvix/nar-bridge/src/nar.rs
index 2f6c0cc5a0ef..707c01e4bcda 100644
--- a/tvix/nar-bridge/src/nar.rs
+++ b/tvix/nar-bridge/src/nar.rs
@@ -1,7 +1,9 @@
-use axum::body::Body;
 use axum::extract::Query;
 use axum::http::StatusCode;
 use axum::response::Response;
+use axum::{body::Body, response::IntoResponse};
+use axum_extra::{headers::Range, TypedHeader};
+use axum_range::{KnownSize, Ranged};
 use bytes::Bytes;
 use data_encoding::BASE64URL_NOPAD;
 use futures::TryStreamExt;
@@ -22,6 +24,7 @@ pub(crate) struct GetNARParams {
 
 #[instrument(skip(blob_service, directory_service))]
 pub async fn get(
+    ranges: Option<TypedHeader<Range>>,
     axum::extract::Path(root_node_enc): axum::extract::Path<String>,
     axum::extract::Query(GetNARParams { nar_size }): Query<GetNARParams>,
     axum::extract::State(AppState {
@@ -29,7 +32,7 @@ pub async fn get(
         directory_service,
         ..
     }): axum::extract::State<AppState>,
-) -> Result<Response, StatusCode> {
+) -> Result<impl axum::response::IntoResponse, StatusCode> {
     use prost::Message;
     // b64decode the root node passed *by the user*
     let root_node_proto = BASE64URL_NOPAD
@@ -62,24 +65,52 @@ pub async fn get(
         return Err(StatusCode::BAD_REQUEST);
     }
 
-    let (w, r) = tokio::io::duplex(1024 * 8);
-
-    // spawn a task rendering the NAR to the client
-    tokio::spawn(async move {
-        if let Err(e) =
-            tvix_store::nar::write_nar(w, &root_node, blob_service, directory_service).await
-        {
-            warn!(err=%e, "failed to write out NAR");
-        }
-    });
-
-    Ok(Response::builder()
-        .status(StatusCode::OK)
-        .header("cache-control", "max-age=31536000, immutable")
-        .header("content-length", nar_size)
-        .header("content-type", nix_http::MIME_TYPE_NAR)
-        .body(Body::from_stream(ReaderStream::new(r)))
-        .unwrap())
+    Ok((
+        // headers
+        [
+            ("cache-control", "max-age=31536000, immutable"),
+            ("content-type", nix_http::MIME_TYPE_NAR),
+        ],
+        // If this is a range request, construct a seekable NAR reader
+        if let Some(TypedHeader(ranges)) = ranges {
+            let r =
+                tvix_store::nar::seekable::Reader::new(root_node, blob_service, directory_service)
+                    .await
+                    .map_err(|e| {
+                        warn!(err=%e, "failed to construct seekable nar reader");
+                        StatusCode::INTERNAL_SERVER_ERROR
+                    })?;
+
+            // ensure the user-supplied nar size was correct, no point returning data otherwise.
+            if r.stream_len() != nar_size {
+                warn!(
+                    actual_nar_size = r.stream_len(),
+                    supplied_nar_size = nar_size,
+                    "wrong nar size supplied"
+                );
+                return Err(StatusCode::BAD_REQUEST);
+            }
+            Ranged::new(Some(ranges), KnownSize::sized(r, nar_size)).into_response()
+        } else {
+            // use the non-seekable codepath if there's no range(s) requested,
+            // as it uses less memory.
+            let (w, r) = tokio::io::duplex(1024 * 8);
+
+            // spawn a task rendering the NAR to the client.
+            tokio::spawn(async move {
+                if let Err(e) =
+                    tvix_store::nar::write_nar(w, &root_node, blob_service, directory_service).await
+                {
+                    warn!(err=%e, "failed to write out NAR");
+                }
+            });
+
+            Response::builder()
+                .header("content-length", nar_size)
+                .body(Body::from_stream(ReaderStream::new(r)))
+                .unwrap()
+        },
+    ))
 }
 
 #[instrument(skip(blob_service, directory_service, request))]