~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

An author: filter reads kept handles rather than asking each time

Merged
Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:serve/author-filter

What changes

An author: filter on a repository's pulls or issues now finds the account in the handles the index keeps (Index::did_of_handle, by a new index on handle (handle)). A handle not kept there is looked up at its host once, through the same cache avatars and claims use (authors.json). What was found, an account or none, is kept for ten minutes. Pages asking at once share one lookup and wait five seconds at most.

Why

did() in web/items.rs called resolve_handle on every request, with no cache. On prod, a crawler loads /chadtmiller.com/knotless/issues every minute or two. Each load made a request to a handle's /.well-known/atproto-did that took 1.5–2.7s, out of a 1.7–3s page.

How it was checked

forge.rs's filter test covers both ways: by the author's handle, which the index keeps once the page has listed authors, and by author:nobody.invalid, which is looked up and found to name no one. Left to CI.

Shortcomings

A handle kept in the index can be out of date for a day, as the handles the pages show are. A filter by a handle that moved to another account can match the old one until the handle is looked up again.

🤖 Generated with Claude Code

Asked for changes

This makes the author: filter read the handle from the index, and otherwise look it up once through a 10-minute Kept cache (authors.json). The direction is right, and the docs page is updated. Two problems in the cache itself.

  1. The cache is keyed by text anyone can send, and it is never pruned. author: comes straight from the query string. Every distinct value, even a nonsense one, adds an entry to AUTHORS (Some(None) is stored). Every put then rewrites the whole authors.json with store::write. A crawler or attacker varying the filter grows the map and the file without bound, and each new key costs a full file write. AVATARS and CLAIMS are keyed by real DIDs, so they don't have this problem. Keep only values that look like a handle (and drop anything else before it reaches the cache), and/or bound the size.
  2. A failed lookup is kept as "no such account" for ten minutes. resolve_handle(...).ok() turns a timeout or a 5xx at the handle's host into Some(None), and put stores it. The host is already known to take seconds to answer, so a slow or failing answer would make a real author's filter match no one until the entry expires. Cache a negative only when the host answered that the handle names no one. On other errors, return None from look so nothing is kept.
crates/knotless-serve/src/web/items.rsline 113
let (ctx, handle) = (app.ctx.clone(), author.to_string());
let look = async move {
let found = knotless::identity::resolve_handle(&ctx, &handle).await;
Some(found.ok())
review.knotless.dev

found.ok() makes any error, including a timeout or 5xx, into Some(None), which through stores for ten minutes. Only a real "names no one" answer should be kept. Return None for the other errors.

crates/knotless-serve/src/web/items.rsline 115
let found = knotless::identity::resolve_handle(&ctx, &handle).await;
Some(found.ok())
};
let found = AUTHORS.through(&app.ctx.env.cache_dir, author, look).await;
review.knotless.dev

author is user input used as the cache key. Each distinct value adds an entry and rewrites all of authors.json, with no bound. Check that it looks like a handle before it reaches the cache, and/or cap the size.

Merged as 56e27dc