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}