Skip to main content

max / makenotwork

mt: spawn the read-position bump off the thread GET The thread view awaited update_read_position inline, so every tracked viewer of an unauthenticated, unthrottled GET blocked the page render on a write to the request pool (fuzz-2026-07-06 write-on-GET). Spawn it best-effort — the page renders immediately and a dropped bump just re-bumps on the next view. The read-position test polls for the async write. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-07 17:34 UTC
Commit: 049230ae3407b40730d7ce81c9985c90f9dee2c6
Parent: 8f36dfa
2 files changed, +30 insertions, -16 deletions
@@ -198,15 +198,21 @@ pub(in crate::routes) async fn thread(
198 198 })
199 199 .collect();
200 200
201 - // If tracked, update read position to the last post on the current page
201 + // If tracked, update read position to the last post on the current page.
202 + // Spawned off the response path: this is a DB write on an unauthenticated,
203 + // unthrottled GET, so awaiting it inline made every tracked viewer block the
204 + // page render on a write to the request pool (fuzz-2026-07-06 write-on-GET).
205 + // Best-effort — a dropped bump just re-bumps on the next view.
202 206 if is_tracked
203 207 && let Some(ref user) = session_user
204 208 && let Some(last_post) = posts.last()
205 209 && let Ok(last_post_id) = uuid::Uuid::parse_str(&last_post.id)
206 210 {
207 - let _ = mt_db::mutations::update_read_position(
208 - &state.db, user.user_id, thread_uuid, last_post_id,
209 - ).await;
211 + let db = state.db.clone();
212 + let user_id = user.user_id;
213 + tokio::spawn(async move {
214 + let _ = mt_db::mutations::update_read_position(&db, user_id, thread_uuid, last_post_id).await;
215 + });
210 216 }
211 217
212 218 let session_user = session_user.as_ref().map(|u| template_user(u, state.config.platform_admin_id));
@@ -123,21 +123,29 @@ async fn read_position_updates_on_view() {
123 123 // Track the thread
124 124 mt_db::mutations::track_thread(&h.db, user_id, thread_id).await.unwrap();
125 125
126 - // View thread — should update read position
126 + // View thread — should update read position. The bump is now spawned off the
127 + // response path, so poll briefly for it rather than reading immediately.
127 128 let thread_url = format!("/p/test/general/{}", thread_id);
128 129 h.client.get(&thread_url).await;
129 130
130 - // Check last_read_post_id is set
131 - let row: Option<(Option<uuid::Uuid>,)> = sqlx::query_as(
132 - "SELECT last_read_post_id FROM tracked_threads WHERE user_id = $1 AND thread_id = $2",
133 - )
134 - .bind(user_id)
135 - .bind(thread_id)
136 - .fetch_optional(&h.db)
137 - .await
138 - .unwrap();
139 - assert!(row.is_some(), "Tracking row should exist");
140 - assert!(row.unwrap().0.is_some(), "last_read_post_id should be set after viewing");
131 + let mut last_read: Option<uuid::Uuid> = None;
132 + for _ in 0..50 {
133 + let row: Option<(Option<uuid::Uuid>,)> = sqlx::query_as(
134 + "SELECT last_read_post_id FROM tracked_threads WHERE user_id = $1 AND thread_id = $2",
135 + )
136 + .bind(user_id)
137 + .bind(thread_id)
138 + .fetch_optional(&h.db)
139 + .await
140 + .unwrap();
141 + assert!(row.is_some(), "Tracking row should exist");
142 + last_read = row.unwrap().0;
143 + if last_read.is_some() {
144 + break;
145 + }
146 + tokio::time::sleep(std::time::Duration::from_millis(20)).await;
147 + }
148 + assert!(last_read.is_some(), "last_read_post_id should be set after viewing");
141 149 }
142 150
143 151 #[tokio::test]