From 4858bac305516669d32dd6183d978ba2cb02aca7 Mon Sep 17 00:00:00 2001 From: Niels Mokkenstorm Date: Wed, 29 Jul 2026 00:12:48 +0200 Subject: [PATCH] perf(server): run first-login backfill async after oauth callback --- .../src/at_record_server/handlers/oauth.gleam | 38 +++++++++++-------- server/test/oauth_test.gleam | 24 +++++++++++- 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/server/src/at_record_server/handlers/oauth.gleam b/server/src/at_record_server/handlers/oauth.gleam index ecee195..458fbe8 100644 --- a/server/src/at_record_server/handlers/oauth.gleam +++ b/server/src/at_record_server/handlers/oauth.gleam @@ -10,6 +10,7 @@ import at_record_server/oauth/sessions import at_record_server/oauth/store import at_record_server/oauth/tokens import at_record_server/user_backfill +import gleam/erlang/process import gleam/json import gleam/list import gleam/option.{None, Some} @@ -179,15 +180,17 @@ fn exchange_and_start_session( } } -/// Seeds this one user's existing releases, adoptions, edits, and shelf -/// events into `catalog_index`/`shelf_index` on login via -/// `user_backfill.backfill_user`: the one-time -/// boot backfill (`backfill_catalog_index` in `at_record_server`) only ever -/// covers users already known at boot, so a first-time login is the only -/// chance to backfill a user's pre-existing repo contents (Jetstream only -/// surfaces commits from the point it started tailing, not history). -/// `backfill_user` is already best-effort (paging failures degrade to a -/// warning log), so this can never fail the login. +/// Fire-and-forget: spawns an unlinked process that seeds this one user's +/// existing releases, adoptions, edits, and shelf events into +/// `catalog_index`/`shelf_index` via `user_backfill.backfill_user`, off the +/// callback's response path. The one-time boot backfill +/// (`backfill_catalog_index` in `at_record_server`) only ever covers users +/// already known at boot, so a first-time login is the only chance to +/// backfill a user's pre-existing repo contents (Jetstream only surfaces +/// commits from the point it started tailing, not history). `backfill_user` +/// is already best-effort (paging failures degrade to a warning log), and +/// running it unlinked means even an unexpected crash in the spawned process +/// can never fail or delay the login. fn backfill_user_catalog(ctx: Context, session: sessions.OauthSession) -> Nil { let user = known_users.KnownUser( @@ -195,12 +198,15 @@ fn backfill_user_catalog(ctx: Context, session: sessions.OauthSession) -> Nil { handle: session.handle, pds: session.pds, ) - let _ = - user_backfill.backfill_user( - ctx.atproto.client, - user, - ctx.catalog_index, - ctx.shelf_index, - ) + process.spawn_unlinked(fn() { + let _ = + user_backfill.backfill_user( + ctx.atproto.client, + user, + ctx.catalog_index, + ctx.shelf_index, + ) + Nil + }) Nil } diff --git a/server/test/oauth_test.gleam b/server/test/oauth_test.gleam index eab5b4c..125bbea 100644 --- a/server/test/oauth_test.gleam +++ b/server/test/oauth_test.gleam @@ -16,6 +16,7 @@ import at_record_server/oauth/tokens import atproto/xrpc import gleam/bit_array import gleam/crypto +import gleam/erlang/process import gleam/http import gleam/http/request import gleam/http/response @@ -522,10 +523,30 @@ pub fn callback_token_exchange_failure_redirects_test() { const list_records_response = "{\"records\":[{\"uri\":\"at://did:plc:abc/dev.mokkenstorm.crate.catalog.release/r1\",\"cid\":\"bafycid\",\"value\":{\"createdAt\":\"2024-01-01T00:00:00Z\",\"title\":\"Loveless\"}}]}" +/// Polls `check` until it holds or 500ms elapse (in 10ms steps): the +/// first-login backfill now runs off `callback`'s response path on a +/// spawned, unlinked process, so its effects land some indeterminate short +/// while after `callback` itself returns. +fn wait_until(check: fn() -> Bool) -> Nil { + wait_until_loop(check, 50) +} + +fn wait_until_loop(check: fn() -> Bool, retries_left: Int) -> Nil { + case check() || retries_left <= 0 { + True -> Nil + False -> { + process.sleep(10) + wait_until_loop(check, retries_left - 1) + } + } +} + /// A successful login must backfill the just-logged-in user's own existing /// releases into `catalog_index`: that repo's contents predate this login and /// Jetstream only tails commits from here forward, so this is the only chance -/// to seed them. +/// to seed them. The backfill itself runs asynchronously (spawned off the +/// callback's response path), so this test polls for it rather than +/// asserting immediately. pub fn callback_success_backfills_new_users_releases_into_catalog_index_test() { let stub = xrpc.Client(send: fn(req) { @@ -567,6 +588,7 @@ pub fn callback_success_backfills_new_users_releases_into_catalog_index_test() { let resp = oauth_handler.callback(req, ctx) assert resp.status == 303 + wait_until(fn() { index.releases.list() != [] }) let assert [row] = index.releases.list() assert row.title == "Loveless" assert row.publisher_did == "did:plc:abc" -- 2.51.2