code wiki / _hdl_build / nx_apistack_idempotency.nx

nx_apistack_idempotency.nx source

↩ module page · 245 lines · 11245 B

1// Retained idempotency evidence. Sequential replay is not an exactly-once transaction: 2// mutation and journal commit remain separate. Missing or malformed evidence is not NEW. 3// 4// THE READ LOCK IS SHARED (2026-09-16, debt 1789503583). A reader used to take LOCK_EX|LOCK_NB, so a concurrent 5// reader, or the recorder inside its one append+fsync, made every keyed tools/call fail closed as EVIDENCE_UNAVAILABLE 6// and the tools API could not be re-shipped from this tree. Readers now take LOCK_SH and wait: a recorder holds LOCK_EX 7// for exactly one append and one fsync, and flock releases on death, so the wait is bounded by one write and is never 8// held by another reader. Blocking is chosen over a retry budget on purpose: a budget is a magic number that fails 9// closed under exactly the load that makes the wait long, and the old immediate refusal WAS the defect. 10// MALFORMED ROWS ARE QUARANTINED, NOT POISONOUS (same day). One row with a CR, a NUL, two TABs, no TAB, or a torn tail 11// used to refuse the whole ledger for every key. A malformed row is now skipped and COUNTED (r.quarantined, with the 12// first bad line in r.first_bad_line, both handed out by id_lookup_q) -- EXCEPT when its key field is the key being 13// looked up: evidence about the sought key that cannot be read is not NEW, so that one lookup still fails closed 14// (EBADMSG) exactly as before. A torn last row (no trailing newline) is a torn append and is judged by the same rule. 15import "nx_syscalls.nx" 16import "nx_fio.nx" 17const ID_NOT_FOUND:i64=0-1 18const ID_EVIDENCE_ERROR:i64=0-2 19const ID_MISSING:i64=0-3 20const ID_CAPACITY:i64=0-4 21const ID_RESOURCE:i64=0-5 22const ID_SIGNED_MAX:i64=9223372036854775807 23const ID_O_RDONLY:i64=0 24const ID_O_RDWR_APPEND_CREATE:i64=1090 25const ID_O_NONBLOCK:i64=2048 26const ID_O_NOFOLLOW:i64=131072 27const ID_O_CLOEXEC:i64=524288 28struct NxIdEvidence{ 29 buffer:*u8,bytes:i64,allocated:i64,result:*u8,result_bytes:i64,state:i64,system_code:i64,quarantined:i64,first_bad_line:i64 30} 31func id_evidence_init(r:*NxIdEvidence)->i64{ 32 r.buffer=0 as *u8;r.bytes=0;r.allocated=0;r.result=0 as *u8 33 r.result_bytes=0;r.state=ID_EVIDENCE_ERROR;r.system_code=0;r.quarantined=0;r.first_bad_line=0;return 0 34} 35func id_evidence_close(r:*NxIdEvidence)->i64{ 36 if (r.buffer as i64)>0{sys_munmap_direct(r.buffer,r.allocated)} 37 r.buffer=0 as *u8;r.result=0 as *u8;r.allocated=0;return 0 38} 39func id_field_valid(p:*u8,n:i64,key:i64)->i64{ 40 if n<0 || (p as i64)<=0{return 0} 41 if key==1&&n==0{return 0} 42 var i:i64=0;while i<n{ 43 if p[i]==0 as u8 || p[i]==9 as u8 || p[i]==10 as u8 || p[i]==13 as u8{return 0};i=i+1 44 };return 1 45} 46func id_read_evidence(path:*u8,key:*u8,key_n:i64,r:*NxIdEvidence)->i64{ 47 id_evidence_init(r) 48 if (path as i64)<=0 || id_field_valid(key,key_n,1)==0{return r.state} 49 let stat:*u8=sys_mmap_try(NX_STAT_X64_BYTES) 50 if (stat as i64)<=0{r.state=ID_RESOURCE;return r.state} 51 let fd:i64=__syscall(257,AT_FDCWD,path,ID_O_RDONLY|ID_O_NONBLOCK|ID_O_NOFOLLOW|ID_O_CLOEXEC,0,0,0) 52 if fd<0{ 53 sys_munmap_direct(stat,NX_STAT_X64_BYTES);r.system_code=fd 54 if fd==0-2{r.state=ID_MISSING};return r.state 55 } 56 var rc:i64=sys_flock(fd,SYS_LOCK_SH) 57 if rc==0{rc=sys_fstat_fd(fd,stat)} 58 var n:i64=0 59 if rc==0{ 60 let mode:i64=*((stat+NX_STAT_X64_MODE_OFFSET) as *i64) 61 n=*((stat+NX_STAT_X64_SIZE_OFFSET) as *i64) 62 if (mode&NX_STAT_TYPE_MASK)!=NX_STAT_REGULAR_FILE || n<0 || n==ID_SIGNED_MAX{rc=FIO_EINVAL} 63 } 64 if rc==0{ 65 r.allocated=n+1;r.buffer=sys_mmap_try(r.allocated) 66 if (r.buffer as i64)<=0{r.buffer=0 as *u8;r.allocated=0;r.state=ID_RESOURCE;rc=0-12} 67 } 68 var got:i64=0 69 while rc==0&&got<n{ 70 let z:i64=sys_read(fd,r.buffer+got,n-got) 71 if z==FIO_EINTR{continue} 72 if z<=0{rc=z;if z==0{rc=FIO_EIO}}else{got=got+z} 73 } 74 let unlock:i64=sys_flock(fd,SYS_LOCK_UN);let closed:i64=sys_close(fd) 75 sys_munmap_direct(stat,NX_STAT_X64_BYTES) 76 if rc==0&&unlock!=0{rc=unlock};if rc==0&&closed!=0{rc=closed} 77 if rc!=0{r.system_code=rc;id_evidence_close(r);return r.state} 78 r.bytes=n;r.buffer[n]=0 as u8 79 var tail_torn:i64=0 80 if n>0{if r.buffer[n-1]!=10 as u8{tail_torn=1}} 81 var start:i64=0;var found:i64=0;var chosen:i64=0;var chosen_n:i64=0 82 var line:i64=0;var bad:i64=0;var first_bad:i64=0;var bad_hit:i64=0 83 while start<n{ 84 line=line+1 85 var end:i64=start;while end<n&&r.buffer[end]!=10 as u8{end=end+1} 86 var malformed:i64=0 87 if end>=n{if tail_torn==1{malformed=1}} 88 if end>start&&r.buffer[start]!=35 as u8{ 89 var tab:i64=0-1;var i:i64=start 90 while i<end{ 91 let c:u8=r.buffer[i] 92 if c==0 as u8||c==13 as u8{malformed=1} 93 if c==9 as u8{if tab>=0{malformed=1}else{tab=i}} 94 i=i+1 95 } 96 if tab<=start{malformed=1} 97 if malformed==1{ 98 bad=bad+1;if first_bad==0{first_bad=line} 99 var kend:i64=end;if tab>start{kend=tab} 100 if kend-start==key_n{var same:i64=1;i=0;while i<key_n{if r.buffer[start+i]!=key[i]{same=0};i=i+1};if same==1{bad_hit=1}} 101 }else{ 102 if found==0&&tab-start==key_n{ 103 var same:i64=1;i=0;while i<key_n{if r.buffer[start+i]!=key[i]{same=0};i=i+1} 104 if same==1{found=1;chosen=tab+1;chosen_n=end-tab-1} 105 } 106 } 107 }else{if malformed==1{bad=bad+1;if first_bad==0{first_bad=line}}} 108 start=end+1 109 } 110 r.quarantined=bad;r.first_bad_line=first_bad 111 if bad_hit==1{r.system_code=FIO_EBADMSG;return r.state} 112 if found==0{r.state=ID_NOT_FOUND;return r.state} 113 r.result=r.buffer+chosen;r.result_bytes=chosen_n;r.result[chosen_n]=0 as u8 114 r.state=chosen_n;return r.state 115} 116// Legacy caller-buffer API: success returns complete result length; -1 alone means unseen. 117// Other negative values require reconciliation; no truncated success or out-of-bounds terminator. 118// id_lookup_q additionally hands out the quarantine count and the first quarantined line (q[0], q[1]) so a caller can 119// announce what it skipped; q may be 0 when the caller does not want them. 120func id_lookup_q(path:*u8,key:*u8,key_n:i64,out:*u8,cap:i64,q:*i64)->i64{ 121 if cap<=0||(out as i64)<=0{return ID_CAPACITY};out[0]=0 as u8 122 if (q as i64)>0{q[0]=0;q[1]=0} 123 let r:*NxIdEvidence=sys_mmap_try(__size_of(NxIdEvidence)) as *NxIdEvidence 124 if (r as i64)<=0{return ID_RESOURCE} 125 var rc:i64=id_read_evidence(path,key,key_n,r) 126 if (q as i64)>0{q[0]=r.quarantined;q[1]=r.first_bad_line} 127 if rc>=0{ 128 if rc>=cap{rc=ID_CAPACITY}else{ 129 var i:i64=0;while i<rc{out[i]=r.result[i];i=i+1};out[rc]=0 as u8 130 } 131 } 132 id_evidence_close(r);sys_munmap_direct(r as *u8,__size_of(NxIdEvidence));return rc 133} 134func id_lookup(path:*u8,key:*u8,key_n:i64,out:*u8,cap:i64)->i64{ 135 return id_lookup_q(path,key,key_n,out,cap,0 as *i64) 136} 137func id_seen(path:*u8,key:*u8,key_n:i64)->i64{ 138 let r:*NxIdEvidence=sys_mmap_try(__size_of(NxIdEvidence)) as *NxIdEvidence 139 if (r as i64)<=0{return ID_RESOURCE} 140 let rc:i64=id_read_evidence(path,key,key_n,r) 141 id_evidence_close(r);sys_munmap_direct(r as *u8,__size_of(NxIdEvidence)) 142 if rc>=0{return 1};if rc==ID_NOT_FOUND{return 0};return rc 143} 144// A lock protects the complete append against cooperating recorders. It does not cover 145// a caller's prior lookup or mutation, nor make a crashed mutation exactly-once. 146func id_record(path:*u8,key:*u8,key_n:i64,result:*u8,result_n:i64)->i64{ 147 if (path as i64)<=0||id_field_valid(key,key_n,1)==0||id_field_valid(result,result_n,0)==0{return FIO_EINVAL} 148 if key_n>ID_SIGNED_MAX-result_n-2{return ID_CAPACITY} 149 let bytes:i64=key_n+result_n+2 150 let row:*u8=sys_mmap_try(bytes) 151 let stat:*u8=sys_mmap_try(NX_STAT_X64_BYTES) 152 if (row as i64)<=0||(stat as i64)<=0{ 153 if (row as i64)>0{sys_munmap_direct(row,bytes)} 154 if (stat as i64)>0{sys_munmap_direct(stat,NX_STAT_X64_BYTES)} 155 return ID_RESOURCE 156 } 157 var i:i64=0;while i<key_n{row[i]=key[i];i=i+1};row[key_n]=9 as u8 158 i=0;while i<result_n{row[key_n+1+i]=result[i];i=i+1};row[bytes-1]=10 as u8 159 let fd:i64=__syscall(257,AT_FDCWD,path,ID_O_RDWR_APPEND_CREATE|ID_O_NONBLOCK|ID_O_NOFOLLOW|ID_O_CLOEXEC,MODE_0644,0,0) 160 var rc:i64=fd 161 if fd>=0{ 162 rc=sys_flock(fd,SYS_LOCK_EX) 163 if rc==0{rc=sys_fstat_fd(fd,stat)} 164 if rc==0{ 165 let mode:i64=*((stat+NX_STAT_X64_MODE_OFFSET) as *i64) 166 let n:i64=*((stat+NX_STAT_X64_SIZE_OFFSET) as *i64) 167 if (mode&NX_STAT_TYPE_MASK)!=NX_STAT_REGULAR_FILE||n<0{rc=FIO_EINVAL} 168 if rc==0&&n>0{ 169 if sys_lseek(fd,n-1,0)!=n-1{rc=FIO_EIO}else{ 170 let got:i64=sys_read(fd,stat,1);if got!=1||stat[0]!=10 as u8{rc=FIO_EBADMSG} 171 } 172 } 173 } 174 var written:i64=0 175 while rc==0&&written<bytes{ 176 let z:i64=sys_write(fd,row+written,bytes-written) 177 if z==FIO_EINTR{continue} 178 if z<=0{rc=z;if z==0{rc=FIO_EIO}}else{written=written+z} 179 } 180 if rc==0{rc=sys_fsync(fd)} 181 let unlock:i64=sys_flock(fd,SYS_LOCK_UN);let closed:i64=sys_close(fd) 182 if rc==0&&unlock!=0{rc=unlock};if rc==0&&closed!=0{rc=closed} 183 } 184 sys_munmap_direct(row,bytes);sys_munmap_direct(stat,NX_STAT_X64_BYTES) 185 if rc==0{return 1};return rc 186} 187 188 189// Lossless legacy row inspection only. Shape cannot prove a request boundary: 190// four unlocked legacy writes and an embedded TAB can produce identical bytes. 191// Offsets refer to the unchanged source snapshot; no row is repaired or skipped. 192// Flags describe syntax, not permission to execute or replay an unknown request. 193const ID_ROW_EMPTY:i64=1 194const ID_ROW_COMMENT:i64=2 195const ID_ROW_NO_TAB:i64=4 196const ID_ROW_EXTRA_TAB:i64=8 197const ID_ROW_EMPTY_KEY:i64=16 198const ID_ROW_CONTROL:i64=32 199const ID_ROW_UNTERMINATED:i64=64 200struct NxIdLegacyRow{ 201 start:i64,end:i64,next:i64,flags:i64,tabs:i64,first_tab:i64,second_tab:i64, 202 legacy_key_start:i64,legacy_key_bytes:i64,legacy_result_start:i64,legacy_result_bytes:i64 203} 204// Returns 1 for a row, 0 at exact EOF, negative for invalid memory/extent. 205// Caller owns disjoint row storage; source may have no sentinel and is never written. 206func id_legacy_row_next(source:*u8,bytes:i64,start:i64,row:*NxIdLegacyRow)->i64{ 207 let base:i64=source as i64;let rp:i64=row as i64 208 let rn:i64=__size_of(NxIdLegacyRow) 209 if bytes<0||start<0||start>bytes||rp<=0{return FIO_EINVAL} 210 if rp>ID_SIGNED_MAX-rn{return FIO_EINVAL} 211 if bytes>0{ 212 if base<=0{return FIO_EINVAL} 213 if base>ID_SIGNED_MAX-bytes{return FIO_EINVAL} 214 if rp<base+bytes&&base<rp+rn{return FIO_EINVAL} 215 } 216 row.start=start;row.end=start;row.next=start;row.flags=0;row.tabs=0 217 row.first_tab=0-1;row.second_tab=0-1 218 row.legacy_key_start=start;row.legacy_key_bytes=0 219 row.legacy_result_start=start;row.legacy_result_bytes=0 220 if start==bytes{return 0} 221 var end:i64=start 222 while end<bytes{ 223 let ch:u8=source[end] 224 if ch==10 as u8{break} 225 if ch==9 as u8{ 226 row.tabs=row.tabs+1 227 if row.first_tab<0{row.first_tab=end}else{if row.second_tab<0{row.second_tab=end}} 228 } 229 if ch==0 as u8||ch==13 as u8{row.flags=row.flags|ID_ROW_CONTROL} 230 end=end+1 231 } 232 row.end=end;row.next=end 233 if end<bytes{row.next=end+1}else{row.flags=row.flags|ID_ROW_UNTERMINATED} 234 if end==start{row.flags=row.flags|ID_ROW_EMPTY;return 1} 235 if source[start]==35 as u8{row.flags=row.flags|ID_ROW_COMMENT;return 1} 236 row.legacy_key_bytes=end-start;row.legacy_result_start=end 237 if row.first_tab<0{row.flags=row.flags|ID_ROW_NO_TAB;return 1} 238 row.legacy_key_bytes=row.first_tab-start 239 row.legacy_result_start=row.first_tab+1 240 var result_end:i64=end 241 if row.second_tab>=0{result_end=row.second_tab;row.flags=row.flags|ID_ROW_EXTRA_TAB} 242 row.legacy_result_bytes=result_end-row.legacy_result_start 243 if row.legacy_key_bytes==0{row.flags=row.flags|ID_ROW_EMPTY_KEY} 244 return 1 245}