nx_avail_map.nx source
↩ module page · 154 lines · 7129 B
1// nx_avail_map.nx -- shared cross-peer piece-AVAILABILITY map for rarest-first selection.
2//
3// module: nishi-core.torrent.avail_map
4// depends: nx_peerwire.nx (nx_pw_bitfield_has -- the peer-has filter),
5// nx_piece_manager.nx (nx_pm_rarest -- the CANONICAL rarest-first tie-break;
6// wiring it here un-orphans the smart layer, DRY: one picker),
7// nx_syscalls.nx (transitive: mmap/openat/lseek/read/write/close/FLOCK).
8// capability: SWARM_INTELLIGENCE
9//
10// WHY this organ exists:
11// The download worker forks one child PER PEER, so each child only knows ITS OWN peer's
12// bitfield -- there is no in-process view of how rare each piece is across the swarm, which
13// is exactly the input rarest-first needs. This is that missing cross-PROCESS view: a small
14// on-disk array of per-piece peer counts that every child contributes to and reads back to
15// pick the rarest piece IT can actually serve. Replacing the worker's linear stride scan with
16// this is the Phase-2 "smart layer" rung (operator's rebuild order: reliability -> smart).
17//
18// CORRECTNESS vs HEURISTIC (deliberate split):
19// The counts are a SELECTION heuristic, never a data-integrity invariant -- every received
20// piece is still SHA1-verified against the metadata before it touches disk. So:
21// * increments (read-modify-write) ARE locked (flock LOCK_EX) -- the proven framed-append
22// floor idiom; concurrent unlocked RMW silently LOSES counts (gate proves this), which
23// would bias selection, so we pay the lock on the (rare, once-per-peer) contribution path.
24// * reads (am_load / am_pick_rarest) are LOCK-FREE -- a momentarily-stale or slightly-torn
25// count only nudges the ordering, never corrupts data, and the pick is on the hot path.
26//
27// FILE FORMAT: `npc` bytes; byte p = saturating count (0..255) of pool peers advertising piece p.
28// One byte/piece keeps even a 10,000-piece torrent's map at 10 KB.
29
30import "nx_peerwire.nx"
31import "nx_piece_manager.nx"
32
33const AM_SAT: i64 = 255 // per-piece count ceiling (1 byte storage)
34
35// open the avail file O_RDWR|O_CREAT, NO truncate (mirrors the worker's tg_open_rw idiom exactly).
36func am_open(path: *u8) -> i64 { return __syscall(SYS_OPENAT, AT_FDCWD, path as i64, 0x42, 0x1a4, 0, 0) }
37
38// zero the first `npc` bytes -- called ONCE by the parent on metadata-resolve so availability is
39// rebuilt from the LIVE swarm each session (stale counts from a prior run are meaningless). mmap
40// is kernel-zero-filled, so we just write npc zero bytes over any prior content.
41func am_init(path: *u8, npc: i64) -> i64 {
42 let fd: i64 = am_open(path)
43 if fd < 0 { return 0 - 1 }
44 let z: *u8 = sys_mmap(npc + 16)
45 sys_lseek(fd, 0, 0)
46 sys_write(fd, z, npc)
47 sys_close(fd)
48 return 0
49}
50
51// contribute one peer's whole bitfield to the shared counts (LOCKED read-modify-write). For each
52// piece the peer HAS, increment its saturating count. ONE lock acquisition per call -- a child
53// calls this once, when it learns the peer's BITFIELD. Returns how many pieces this peer added.
54func am_add_bitfield(path: *u8, peerbits: *u8, byte_len: i64, npc: i64) -> i64 {
55 let fd: i64 = am_open(path)
56 if fd < 0 { return 0 }
57 sys_flock(fd, SYS_LOCK_EX)
58 let buf: *u8 = sys_mmap(npc + 16)
59 sys_lseek(fd, 0, 0); sys_read(fd, buf, npc)
60 var added: i64 = 0; var p: i64 = 0
61 while p < npc {
62 if nx_pw_bitfield_has(peerbits, byte_len, p) == 1 {
63 let c: i64 = buf[p] as i64
64 if c < AM_SAT { buf[p] = (c + 1) as u8 }
65 added = added + 1
66 }
67 p = p + 1
68 }
69 sys_lseek(fd, 0, 0); sys_write(fd, buf, npc)
70 sys_flock(fd, SYS_LOCK_UN)
71 sys_close(fd)
72 return added
73}
74
75// UNLOCKED twin of am_add_bitfield -- EXISTS ONLY as the gate's negative control (it demonstrates
76// that the lock is load-bearing: concurrent RMW without it loses increments). NEVER call this from
77// the live worker. Kept beside its locked sibling so the two stay byte-identical except the lock.
78func am_add_bitfield_unlocked(path: *u8, peerbits: *u8, byte_len: i64, npc: i64) -> i64 {
79 let fd: i64 = am_open(path)
80 if fd < 0 { return 0 }
81 let buf: *u8 = sys_mmap(npc + 16)
82 sys_lseek(fd, 0, 0); sys_read(fd, buf, npc)
83 var added: i64 = 0; var p: i64 = 0
84 while p < npc {
85 if nx_pw_bitfield_has(peerbits, byte_len, p) == 1 {
86 let c: i64 = buf[p] as i64
87 if c < AM_SAT { buf[p] = (c + 1) as u8 }
88 added = added + 1
89 }
90 p = p + 1
91 }
92 sys_lseek(fd, 0, 0); sys_write(fd, buf, npc)
93 sys_close(fd)
94 return added
95}
96
97// incremental single-piece bump for a peer's HAVE message after its initial bitfield (LOCKED).
98func am_add_have(path: *u8, piece_idx: i64) -> i64 {
99 let fd: i64 = am_open(path)
100 if fd < 0 { return 0 - 1 }
101 sys_flock(fd, SYS_LOCK_EX)
102 let b: *u8 = sys_mmap(16)
103 sys_lseek(fd, piece_idx, 0); let r: i64 = sys_read(fd, b, 1)
104 var c: i64 = 0
105 if r == 1 { c = b[0] as i64 }
106 if c < AM_SAT { b[0] = (c + 1) as u8 }
107 sys_lseek(fd, piece_idx, 0); sys_write(fd, b, 1)
108 sys_flock(fd, SYS_LOCK_UN)
109 sys_close(fd)
110 return 0
111}
112
113// snapshot the byte counts into a caller-owned i64 array for the canonical picker (LOCK-FREE read;
114// a heuristic ordering, never a data path). Bytes past EOF / missing file read as 0. Returns npc.
115func am_load(path: *u8, out_counts: *i64, npc: i64) -> i64 {
116 let fd: i64 = am_open(path)
117 if fd < 0 {
118 var z: i64 = 0; while z < npc { out_counts[z] = 0; z = z + 1 }
119 return 0
120 }
121 let buf: *u8 = sys_mmap(npc + 16)
122 sys_lseek(fd, 0, 0); let r: i64 = sys_read(fd, buf, npc); sys_close(fd)
123 var p: i64 = 0
124 while p < npc {
125 if p < r { out_counts[p] = buf[p] as i64 } else { out_counts[p] = 0 }
126 p = p + 1
127 }
128 return npc
129}
130
131// RAREST-FIRST pick restricted to pieces THIS peer can actually serve.
132// Builds an "effective-have" array -- a piece is effectively-had (i.e. SKIP it) if we already
133// have it OR this peer lacks it -- and hands av + eff to the canonical nx_pm_rarest, so the
134// swarm-wide rarest tie-break lives in exactly ONE place (DRY; this is the call that un-orphans
135// the smart layer). `havebits`==0 means the peer sent no bitfield (fast-ext have-all seeder):
136// treat it as has-everything. Caller passes scratch buffers `av`/`eff` (each >= npc i64) to
137// avoid per-pick mmap churn. Returns the rarest needed+serveable piece index, or -1 if none
138// (caller then falls back to its linear scan -- so this can only improve, never regress).
139func am_pick_rarest(path: *u8, donebits: *u8, peerbits: *u8, byte_len: i64, havebits: i64, npc: i64, av: *i64, eff: *i64) -> i64 {
140 am_load(path, av, npc)
141 var p: i64 = 0
142 while p < npc {
143 var skip: i64 = 0
144 if (donebits[p] as i64) == 1 { skip = 1 }
145 else {
146 if havebits == 1 {
147 if nx_pw_bitfield_has(peerbits, byte_len, p) == 0 { skip = 1 }
148 }
149 }
150 eff[p] = skip
151 p = p + 1
152 }
153 return nx_pm_rarest(av, eff, npc)
154}