Skip an account that already has two calls in flight so a crawl keeps moving.

An unlimited per-account cap lets one burst pile onto the few accounts already known to work.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
4cuckandCursor committed 2026-10-25 09:23:46 +00:00
1 parent d00136558a
commit 44b006821d
2 files changed
+57 -5

No files matched your search

+6 -4
View File
@@ -95,10 +95,12 @@ second search does not pay a new SOCKS + TLS handshake.
### Rate limits and cache
- `maxConcurrentReqs` is **per cookie session**, not global. A busy public
instance should set it to `1000000`. At that size the cap never queues a
call, which matters once the instance is popular enough that every account
would otherwise sit at a small limit.
- `maxConcurrentReqs` is **per cookie session**, not global. At `2`, an
account that already has two calls in flight is skipped and the next
account is used immediately. A crawl stays at two in flight per account
across the whole pool. Raising it into the millions lets one burst pile
onto the few accounts already known to work, which is what produces a
cliff of 429s.
- `clientBudget` caps how fast one visitor IP can spend uncached X calls.
Cache hits, media, and static files are free. SearchTimeline costs 2 tokens.
- First-page search is cached for 60 seconds, then served stale for 4 more
+51 -1
View File
@@ -356,7 +356,11 @@ impl SessionPool {
}
// Spend the local window under the write lock, then take a permit.
// A session that is only busy is remembered so we can wait on it.
// An account already at its concurrency cap is skipped. The call
// goes to the next account that still has a free slot, so a crawl
// keeps moving instead of queueing behind the account that filled up.
// Waiting is only for the case where every usable account is already
// at the cap.
let mut wait_on = None;
for index in eligible {
let api = api_for(self.sessions[index].credentials.kind);
@@ -1056,6 +1060,52 @@ mod tests {
);
}
#[tokio::test]
async fn a_full_account_hands_the_next_call_over_immediately() {
unsafe { env::set_var("TEAPOT_SESSION_STATE_FILE", "") };
let path = env::temp_dir().join(format!(
"teapot-session-pool-spread-{}.jsonl",
process::id()
));
fs::write(
&path,
concat!(
r#"{"id":1,"username":"one","kind":"cookie","auth_token":"a","ct0":"c"}"#,
"\n",
r#"{"id":2,"username":"two","kind":"cookie","auth_token":"b","ct0":"d"}"#,
"\n"
),
)
.await
.unwrap();
let pool = SessionPool::load(path.to_str().unwrap(), 2).await.unwrap();
let first = pool
.acquire("TweetDetail", Some(SessionKind::Cookie))
.await
.unwrap();
let other = if first.id == 1 { 2 } else { 1 };
let second = pool
.acquire_excluding("TweetDetail", Some(SessionKind::Cookie), Some(other))
.await
.unwrap();
assert_eq!(first.id, second.id);
let next = timeout(
Duration::from_millis(50),
pool.acquire("TweetDetail", Some(SessionKind::Cookie)),
)
.await
.expect("a full account should be skipped, not waited on")
.unwrap();
assert_eq!(next.id, other);
drop(first);
drop(second);
drop(next);
fs::remove_file(path).await.unwrap();
}
#[tokio::test]
async fn pool_filters_kinds_rotates_and_enforces_concurrency() {
let path = env::temp_dir().join(format!("teapot-session-pool-{}.jsonl", process::id()));