Repository: kalbasit/ncps
Pull request #193: feat(cache): support custom priority
Author: login: numinit
Created: 2025-03-07T01:41:17Z | Merged: 2025-03-19T20:29:02Z

Loading the priority from the nix-cache-info at service start results in a service failure if we are unable to load the priority. Allow a custom priority in the URL, similar to how Nix works.


Diff:
diff --git a/pkg/cache/upstream/cache.go b/pkg/cache/upstream/cache.go
index c94269f3d..bbef5bc34 100644
--- a/pkg/cache/upstream/cache.go
+++ b/pkg/cache/upstream/cache.go
@@ -7,6 +7,7 @@ import (
 	"io"
 	"net/http"
 	"net/url"
+	"strconv"
 	"time"
 
 	"github.com/nix-community/go-nix/pkg/narinfo"
@@ -87,9 +88,24 @@ func New(ctx context.Context, u *url.URL, pubKeys []string) (Cache, error) {
 		c.publicKeys = append(c.publicKeys, pk)
 	}
 
-	priority, err := c.parsePriority(ctx)
-	if err != nil {
-		return c, fmt.Errorf("error parsing the priority for %q: %w", u, err)
+	var priority uint64
+
+	if u.Query().Has("priority") {
+		parsedPriority, err := strconv.ParseUint(u.Query().Get("priority"), 10, 16)
+		if err != nil {
+			return c, fmt.Errorf("error parsing the priority from the URL %q: %w", u, err)
+		}
+
+		priority = parsedPriority
+	}
+
+	if priority < 1 {
+		parsedPriority, err := c.parsePriority(ctx)
+		if err != nil {
+			return c, fmt.Errorf("error parsing the priority for %q: %w", u, err)
+		}
+
+		priority = parsedPriority
 	}
 
 	c.priority = priority
diff --git a/pkg/cache/upstream/cache_test.go b/pkg/cache/upstream/cache_test.go
index 6942fc4fd..dc5498c9a 100644
--- a/pkg/cache/upstream/cache_test.go
+++ b/pkg/cache/upstream/cache_test.go
@@ -84,6 +84,40 @@ func TestNew(t *testing.T) {
 
 		assert.EqualValues(t, 40, c.GetPriority())
 	})
+
+	//nolint:paralleltest
+	t.Run("priority parsed from URL", func(t *testing.T) {
+		c, err := upstream.New(
+			newContext(),
+			testhelper.MustParseURL(t, ts.URL+"?priority=42"),
+			testdata.PublicKeys(),
+		)
+		require.NoError(t, err)
+
+		assert.EqualValues(t, 42, c.GetPriority())
+	})
+
+	//nolint:paralleltest
+	t.Run("priority in URL is zero", func(t *testing.T) {
+		c, err := upstream.New(
+			newContext(),
+			testhelper.MustParseURL(t, ts.URL+"?priority=0"),
+			testdata.PublicKeys(),
+		)
+		require.NoError(t, err)
+
+		assert.EqualValues(t, 40, c.GetPriority())
+	})
+
+	//nolint:paralleltest
+	t.Run("priority in URL is invalid", func(t *testing.T) {
+		_, err := upstream.New(
+			newContext(),
+			testhelper.MustParseURL(t, ts.URL+"?priority=-1"),
+			testdata.PublicKeys(),
+		)
+		assert.ErrorContains(t, err, "error parsing the priority from the URL")
+	})
 }
 
 func TestGetNarInfo(t *testing.T) {


Reviews:
user: login: kalbasit
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-03-09T17:51:36Z
user: login: numinit
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T07:06:36Z
user: login: kalbasit
state: APPROVED
author_association: OWNER
submitted_at: 2025-03-19T20:23:51Z

Review comments:
diff_hunk: @@ -185,6 +186,14 @@ func getUpstreamCaches(ctx context.Context, cmd *cli.Command) ([]upstream.Cache,
 			return nil, fmt.Errorf("error parsing --upstream-cache=%q: %w", us, err)
 		}
 
+		var priority int64 = -1
path: cmd/serve.go
user: login: kalbasit
body: Is zero a valid priority? I feel like 0 is invalid and thus you should just start with zero and send uint64 everywhere. If not then fair enough. 
created_at: 2025-03-09T17:47:37Z
author_association: OWNER
diff_hunk: @@ -52,7 +52,7 @@ type Cache struct {
 	publicKeys []signature.PublicKey
 }
 
-func New(ctx context.Context, u *url.URL, pubKeys []string) (Cache, error) {
+func New(ctx context.Context, u *url.URL, pubKeys []string, priority int64) (Cache, error) {
path: pkg/cache/upstream/cache.go
user: login: kalbasit
body: Why not have the parsePriority passes it from the url instead of passing it from main? 
created_at: 2025-03-09T17:51:26Z
author_association: OWNER
diff_hunk: @@ -52,7 +52,7 @@ type Cache struct {
 	publicKeys []signature.PublicKey
 }
 
-func New(ctx context.Context, u *url.URL, pubKeys []string) (Cache, error) {
+func New(ctx context.Context, u *url.URL, pubKeys []string, priority int64) (Cache, error) {
path: pkg/cache/upstream/cache.go
user: login: numinit
body: Ah, very good point - much cleaner!
created_at: 2025-03-15T07:06:36Z
author_association: CONTRIBUTOR

PR comments:
user: login: numinit
created_at: 2025-03-19T22:26:37Z
body: Thanks for the merge!

Out of curiosity, what happens if two caches have the same priority? Are they currently tried in parallel and the first one to the finish wins, or is it in serial? (This may determine what I contribute next...)

BTW, you'll enjoy the Planet Nix talk where I successfully used ncps to fetch derivations over a mesh network :-) 
author_association: CONTRIBUTOR
user: login: kalbasit
created_at: 2025-03-20T00:09:53Z
body: > Thanks for the merge!
> 
> Out of curiosity, what happens if two caches have the same priority? Are they currently tried in parallel and the first one to the finish wins, or is it in serial? (This may determine what I contribute next...)

The behavior for selecting an upstream cache is in [this function](https://github.com/kalbasit/ncps/blob/1a8b6a9de0be32dd49bd200a754245e1406b43d2/pkg/cache/cache.go#L1469). My first iteration was to respect the priority and try them one by one sequentially, but this proved to be quite slow. So now all upstreams are tried each in a goroutine and whichever returns 200 first wins; Obviously this isn't great because an upstream with lower priority could be selected and this algorithm could be improved to honor priority but still reaches out to them in parallel.

> BTW, you'll enjoy the Planet Nix talk where I successfully used ncps to fetch derivations over a mesh network :-)

Oh awesome, do you have a link to the talk; I'm interested in watching it!
author_association: OWNER
user: login: numinit
created_at: 2025-03-20T03:19:20Z
body: I guess I've got my next contribution in mind :-) 

They're still cutting them together - I see them going up on https://www.youtube.com/channel/UCN2nbMPLJWv3Y__4JuF_hMQ but Adventures in Mesh Networking with NixOS isn't up yet.
author_association: CONTRIBUTOR