code wiki / (root) / nx_endgame.nx

nx_endgame.nx source

↩ module page · 106 lines · 5977 B

1// nx_endgame.nx -- endgame last-piece racing for the fork-per-peer download model. 2// 3// module: nishi-core.torrent.endgame 4// depends: nx_avail_map.nx (am_open/am_load + the locked-RMW flock idiom; transitively nx_pm_rarest, 5// nx_pw_bitfield_has, nx_syscalls), nx_stream_picker.nx (nx_sp_in_endgame -- the canonical 6// endgame-entry threshold; importing it un-orphans the streaming smart layer). 7// capability: SWARM_INTELLIGENCE 8// 9// WHY: the dominant source of completion-time variance is the ONE slow peer holding the last piece. 10// Our forked children normally AVOID duplicating work (they skip pieces already done via the donefile), 11// which is correct for the bulk of a download. But in the TAIL -- when only a few obtainable pieces 12// remain and they are all already in-flight on some (possibly slow) peer -- a child with nothing fresh 13// should RACE one of those in-flight pieces from a second peer and let the faster connection win. This 14// organ is that decision plus the shared IN-FLIGHT refcount map the children coordinate through. 15// 16// HONEST BOUNDARY: a piece that NO connected peer has (availability 0) can never be raced -- you cannot 17// download data the swarm does not hold. eg_pick never returns such a piece; eg_obtainable_remaining 18// excludes them. That is exactly the "stuck at 99.x%, no seeder has the last piece" reality -- the fix 19// for THAT is webseed (BEP-19) / PEX (BEP-11), a later rung, not endgame. 20// 21// FILE FORMAT (download.inflight): npc bytes; byte p = saturating count (0..255) of children currently 22// downloading piece p. Locked read-modify-write (same flock floor as the availability map). 23 24import "nx_avail_map.nx" 25import "nx_stream_picker.nx" 26 27// open O_RDWR|O_CREAT, NO truncate (the shared worker idiom). 28func eg_open(path: *u8) -> i64 { return __syscall(SYS_OPENAT, AT_FDCWD, path as i64, 0x42, 0x1a4, 0, 0) } 29 30// zero the first npc bytes -- parent calls once/session before forking (in-flight is per-session). 31func eg_init(path: *u8, npc: i64) -> i64 { 32 let fd: i64 = eg_open(path); if fd < 0 { return 0 - 1 } 33 let z: *u8 = sys_mmap(npc + 16); sys_lseek(fd, 0, 0); sys_write(fd, z, npc); sys_close(fd); return 0 34} 35 36// child claims it is about to download piece p (LOCKED +1, saturating). 37func eg_mark(path: *u8, p: i64) -> i64 { 38 let fd: i64 = eg_open(path); if fd < 0 { return 0 - 1 } 39 sys_flock(fd, SYS_LOCK_EX) 40 let b: *u8 = sys_mmap(16); sys_lseek(fd, p, 0); let r: i64 = sys_read(fd, b, 1) 41 var c: i64 = 0; if r == 1 { c = b[0] as i64 } 42 if c < 255 { b[0] = (c + 1) as u8 } 43 sys_lseek(fd, p, 0); sys_write(fd, b, 1) 44 sys_flock(fd, SYS_LOCK_UN); sys_close(fd); return 0 45} 46 47// child finished or abandoned piece p (LOCKED -1, floor 0). 48func eg_clear(path: *u8, p: i64) -> i64 { 49 let fd: i64 = eg_open(path); if fd < 0 { return 0 - 1 } 50 sys_flock(fd, SYS_LOCK_EX) 51 let b: *u8 = sys_mmap(16); sys_lseek(fd, p, 0); let r: i64 = sys_read(fd, b, 1) 52 var c: i64 = 0; if r == 1 { c = b[0] as i64 } 53 if c > 0 { c = c - 1 } 54 b[0] = c as u8 55 sys_lseek(fd, p, 0); sys_write(fd, b, 1) 56 sys_flock(fd, SYS_LOCK_UN); sys_close(fd); return 0 57} 58 59// count NEEDED + OBTAINABLE pieces (done==0 AND avail>0) -- the outstanding count that drives endgame 60// entry. Unobtainable pieces (avail==0) can never complete, so they are NOT raceable outstanding work. 61func eg_obtainable_remaining(donebits: *u8, av: *i64, npc: i64) -> i64 { 62 var c: i64 = 0; var p: i64 = 0 63 while p < npc { if (donebits[p] as i64) == 0 { if av[p] > 0 { c = c + 1 } } p = p + 1 } 64 return c 65} 66 67// THE PICK. Two phases: 68// (1) NORMAL: rarest NEEDED piece this peer HAS that is NOT already in-flight (no duplicate work). 69// (2) ENDGAME: if (1) finds nothing fresh AND obtainable-remaining <= threshold, RACE -- the rarest 70// NEEDED piece this peer HAS even if in-flight (a 2nd peer races the slow holder; caller cancels 71// the loser on first PIECE). Reuses the canonical nx_pm_rarest (av>0 + done filter via eff) and 72// nx_sp_in_endgame (the threshold). Returns the piece index, or -1 (peer has nothing needed, or 73// not yet endgame and all its needed pieces are in-flight -> caller waits / frees the slot). 74// Caller passes scratch av/eff/inf (>= npc i64 each) to avoid per-pick churn. 75func eg_pick(availpath: *u8, inflightpath: *u8, donebits: *u8, peerbits: *u8, byte_len: i64, havebits: i64, npc: i64, threshold: i64, av: *i64, eff: *i64, inf: *i64) -> i64 { 76 am_load(availpath, av, npc) 77 am_load(inflightpath, inf, npc) 78 // A fast-ext have-all seeder sends NO bitfield (havebits==0) so it never contributed to the avail 79 // map -> its pieces could read avail 0 and be wrongly skipped (nx_pm_rarest needs avail>0). The 80 // seeder HAS every piece, so floor availability at 1 for the pick (local scratch only, not the file). 81 if havebits == 0 { var s: i64 = 0; while s < npc { if av[s] == 0 { av[s] = 1 } s = s + 1 } } 82 // phase 1: skip if done OR peer-lacks OR in-flight 83 var p: i64 = 0 84 while p < npc { 85 var skip: i64 = 0 86 if (donebits[p] as i64) == 1 { skip = 1 } 87 else { if havebits == 1 { if nx_pw_bitfield_has(peerbits, byte_len, p) == 0 { skip = 1 } } } 88 if skip == 0 { if inf[p] > 0 { skip = 1 } } 89 eff[p] = skip 90 p = p + 1 91 } 92 let fresh: i64 = nx_pm_rarest(av, eff, npc) 93 if fresh >= 0 { return fresh } 94 // phase 2: endgame race -- only when few obtainable pieces remain 95 let outstanding: i64 = eg_obtainable_remaining(donebits, av, npc) 96 if nx_sp_in_endgame(outstanding, threshold) == 0 { return 0 - 1 } 97 p = 0 98 while p < npc { 99 var skip2: i64 = 0 100 if (donebits[p] as i64) == 1 { skip2 = 1 } 101 else { if havebits == 1 { if nx_pw_bitfield_has(peerbits, byte_len, p) == 0 { skip2 = 1 } } } 102 eff[p] = skip2 103 p = p + 1 104 } 105 return nx_pm_rarest(av, eff, npc) // in-flight allowed (race); still avail>0 + peer-has + needed 106}