code wiki / _hdl_build / nx_crawl_pace.nx

nx_crawl_pace.nx source

↩ module page · 176 lines · 8568 B

1// nx_crawl_pace.nx -- SOTA polite crawler pacing (operator 2026-07-05: "pacing is a great callout on all the 2// systems... get to state of the art"). Per-host adaptive rate limiting: a min interval between hits to the 3// same host, HONOR Retry-After on 429/503, EXPONENTIAL backoff on repeated throttles (reset on success), and 4// robots crawl-delay override. State persists across invocations (each nx_url_index run is a fresh process) in 5// a fixed hash-table file, so serial crawls stay polite and don't get IP-banned (a ban = LOST reach, not just 6// rudeness -- reddit 429'd us on rapid feed hits). A shared organ any fetcher can call: pace_before(host) 7// before a request, pace_after(host,status,retry_after) after. license_tier: ORIGINAL 8import "nx_syscalls.nx" 9const PACE_MAGIC_5381: i64 = 5381 10const PACE_MAGIC_1024: i64 = 1024 11const PACE_MAGIC_1000000: i64 = 1000000 12 13// --- policy config (v1 surface; a later rung reads svc-config per Cardinal 11) --- 14const PACE_BASE_MS: i64 = 1000 // polite default: >= 1s between requests to the same host 15const PACE_MAX_BACKOFF_MS: i64 = 300000 // cap any single backoff at 5 min 16const PACE_WAIT_CAP_MS: i64 = 60000 // never block ONE fetch > 60s (caller may defer beyond) 17const PACE_SLOTS: i64 = 4096 18const PACE_REC: i64 = 32 // host_hash(8) | next_allowed_ms(8) | consec_throttle(8) | crawl_delay_ms(8) 19const PACE_TBL_BYTES: i64 = 131072 // PACE_SLOTS * PACE_REC 20const PACE_TBL_MODE: i64 = 0x1A4 // 0o644 file MODE for sys_openat_wr (flags O_CREAT|O_WRONLY|O_TRUNC 21 // are hardcoded inside it). Passing flags here made the file 0o1101 22 // (no owner-write) -> 2nd open EACCES; a real mode fixes re-writes. 23 24// --- little-endian i64 load/store into the table buffer --- 25func pace_ld(tbl: *u8, off: i64) -> i64 { var v: i64=0; var i: i64=0; while i<8 { v = v | ((tbl[off+i] as i64) << (i*8)); i=i+1 } return v } 26func pace_st(tbl: *u8, off: i64, val: i64) -> i64 { var i: i64=0; while i<8 { tbl[off+i] = ((val >> (i*8)) & 0xff) as u8; i=i+1 } return 0 } 27 28// stable positive host hash (djb2, masked) 29func pace_hash(h: *u8, n: i64) -> i64 { 30 var x: i64 = PACE_MAGIC_5381 31 var i: i64 = 0 32 while i < n { x = (((x << 5) + x) + (h[i] as i64)) & 0x7fffffffffffffff; i = i + 1 } 33 if x == 0 { x = 1 } // 0 reserved for "empty slot" 34 return x 35} 36 37// --- persistence: read-modify-write a fixed table file (serial-crawler safe) --- 38func pace_load_tbl() -> *u8 { 39 // ALWAYS return a PRIVATE anon buffer (never the file's own mapping): sys_read_file hands back a shared 40 // file mapping, and pace_save_tbl's O_TRUNC would then wipe the buffer mid-write. Copy in, decouple. 41 let dst: *u8 = sys_mmap(PACE_TBL_BYTES) // anon, zero-filled 42 let lp: *i64 = sys_mmap(16) as *i64 43 let src: *u8 = sys_read_file("knowledge/status/pace_crawl.tbl" as *u8, lp) 44 if (src as i64) != 0 { if lp[0] >= PACE_TBL_BYTES { 45 var i: i64 = 0 46 while i < PACE_TBL_BYTES { dst[i] = src[i]; i = i + 1 } 47 } } 48 return dst 49} 50func pace_save_tbl(tbl: *u8) -> i64 { 51 let fd: i64 = sys_openat_wr("knowledge/status/pace_crawl.tbl" as *u8, PACE_TBL_MODE) 52 if fd < 0 { return 0 - 1 } 53 var w: i64 = 0 // loop: a single write() need not flush 128KB in one go 54 while w < PACE_TBL_BYTES { 55 let k: i64 = sys_write(fd, (tbl as i64 + w) as *u8, PACE_TBL_BYTES - w) 56 if k <= 0 { sys_close(fd); return 0 - 1 } 57 w = w + k 58 } 59 sys_close(fd) 60 return 0 61} 62// find the slot for hh (existing match or first empty), linear probe. -1 if full. 63func pace_slot(tbl: *u8, hh: i64) -> i64 { 64 var s: i64 = hh % PACE_SLOTS 65 var tries: i64 = 0 66 while tries < PACE_SLOTS { 67 let stored: i64 = pace_ld(tbl, s * PACE_REC) 68 if stored == 0 { return s } 69 if stored == hh { return s } 70 s = (s + 1) % PACE_SLOTS 71 tries = tries + 1 72 } 73 return 0 - 1 74} 75 76// --- PURE POLICY (deterministic, gate-tested): post-fetch delay before the host may be hit again --- 77// status 429/503 -> honor Retry-After (s), else exponential base*2^consec (capped); success -> reset to base. 78func pace_delay_after(base_ms: i64, crawl_delay_ms: i64, consec_in: i64, 79 status: i64, retry_after_s: i64, out_consec: *i64) -> i64 { 80 var eff: i64 = base_ms 81 if crawl_delay_ms > eff { eff = crawl_delay_ms } 82 var throttle: i64 = 0 83 if status == 429 { throttle = 1 } 84 if status == 503 { throttle = 1 } 85 if throttle == 1 { 86 out_consec[0] = consec_in + 1 87 var delay: i64 = 0 88 if retry_after_s > 0 { delay = retry_after_s * 1000 } 89 else { 90 var mult: i64 = 1; var k: i64 = 0 91 while k < consec_in { mult = mult * 2; if mult >= PACE_MAGIC_1024 { mult = PACE_MAGIC_1024; k = consec_in } else { k = k + 1 } } 92 delay = eff * mult 93 } 94 if delay < eff { delay = eff } 95 if delay > PACE_MAX_BACKOFF_MS { delay = PACE_MAX_BACKOFF_MS } 96 return delay 97 } 98 out_consec[0] = 0 99 return eff 100} 101// wait (ms) required before hitting the host now, given its next-allowed time. capped. 102func pace_wait_ms(now_ms: i64, next_allowed_ms: i64) -> i64 { 103 var w: i64 = next_allowed_ms - now_ms 104 if w <= 0 { return 0 } 105 if w > PACE_WAIT_CAP_MS { return PACE_WAIT_CAP_MS } 106 return w 107} 108 109// --- top-level: call BEFORE a request to a host (loads state, sleeps the polite interval) --- 110func pace_before(host: *u8, host_len: i64) -> i64 { 111 let tbl: *u8 = pace_load_tbl() 112 let hh: i64 = pace_hash(host, host_len) 113 let s: i64 = pace_slot(tbl, hh) 114 if s < 0 { return 0 } 115 let off: i64 = s * PACE_REC 116 if pace_ld(tbl, off) != hh { return 0 } // no prior state -> no wait 117 let w: i64 = pace_wait_ms(sys_now_realtime_ms(), pace_ld(tbl, off + 8)) 118 if w > 0 { sys_sleep_ms(w) } 119 return w 120} 121// call AFTER the response: update next-allowed + backoff. returns the delay applied (ms). 122func pace_after(host: *u8, host_len: i64, status: i64, retry_after_s: i64) -> i64 { 123 let tbl: *u8 = pace_load_tbl() 124 let hh: i64 = pace_hash(host, host_len) 125 let s: i64 = pace_slot(tbl, hh) 126 if s < 0 { return 0 - 1 } 127 let off: i64 = s * PACE_REC 128 var consec_in: i64 = 0 129 var crawl_delay: i64 = 0 130 if pace_ld(tbl, off) == hh { consec_in = pace_ld(tbl, off + 16); crawl_delay = pace_ld(tbl, off + 24) } 131 let oc: *i64 = sys_mmap(16) as *i64 132 let delay: i64 = pace_delay_after(PACE_BASE_MS, crawl_delay, consec_in, status, retry_after_s, oc) 133 pace_st(tbl, off, hh) 134 pace_st(tbl, off + 8, sys_now_realtime_ms() + delay) 135 pace_st(tbl, off + 16, oc[0]) 136 pace_st(tbl, off + 24, crawl_delay) 137 pace_save_tbl(tbl) 138 return delay 139} 140// robots.txt crawl-delay for a host (ms): recorded so pace_delay_after uses max(base, crawl_delay). 141func pace_set_crawl_delay(host: *u8, host_len: i64, cd_ms: i64) -> i64 { 142 let tbl: *u8 = pace_load_tbl() 143 let hh: i64 = pace_hash(host, host_len) 144 let s: i64 = pace_slot(tbl, hh) 145 if s < 0 { return 0 - 1 } 146 let off: i64 = s * PACE_REC 147 pace_st(tbl, off, hh) 148 if pace_ld(tbl, off + 8) == 0 { pace_st(tbl, off + 8, 0) } 149 pace_st(tbl, off + 24, cd_ms) 150 pace_save_tbl(tbl) 151 return 0 152} 153// parse "Retry-After: <seconds>" from response headers (integer-seconds form). 0 if absent/date-form. 154func pace_retry_after(resp: *u8, n: i64) -> i64 { 155 let key: *u8 = "retry-after:" as *u8 156 var i: i64 = 0 157 while i + 12 < n { 158 var m: i64 = 1; var j: i64 = 0 159 while j < 12 { let c: i64 = resp[i+j] as i64; var lc: i64 = c; if c >= 65 { if c <= 90 { lc = c + 32 } } if lc != (key[j] as i64) { m = 0; j = 12 } else { j = j + 1 } } 160 if m == 1 { 161 var p: i64 = i + 12 162 while p < n { let c: i64 = resp[p] as i64; if c == 32 { p = p + 1 } else { if c == 9 { p = p + 1 } else { p = n + PACE_MAGIC_1000000 } } } 163 var q: i64 = i + 12 164 var st: i64 = 0 165 while st == 0 { if q >= n { st = 1 } else { let c: i64 = resp[q] as i64; if c == 32 { q = q + 1 } else { if c == 9 { q = q + 1 } else { st = 1 } } } } 166 var v: i64 = 0; var got: i64 = 0 167 while q < n { let d: i64 = resp[q] as i64; if d >= 48 { if d <= 57 { v = v*10 + (d - 48); got = 1; q = q + 1 } else { q = n } } else { q = n } } 168 if got == 1 { return v } 169 return 0 170 } 171 i = i + 1 172 } 173 return 0 174} 175 176func main() -> i64 { return 0 }