nx_apistack_idempotency_reconcile_candidate_t219.nx source
↩ module page · 212 lines · 9007 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.
3import "nx_syscalls.nx"
4import "nx_fio.nx"
5const ID_NOT_FOUND:i64=0-1
6const ID_EVIDENCE_ERROR:i64=0-2
7const ID_MISSING:i64=0-3
8const ID_CAPACITY:i64=0-4
9const ID_RESOURCE:i64=0-5
10const ID_SIGNED_MAX:i64=9223372036854775807
11const ID_O_RDONLY:i64=0
12const ID_O_RDWR_APPEND_CREATE:i64=1090
13const ID_O_NONBLOCK:i64=2048
14const ID_O_NOFOLLOW:i64=131072
15const ID_O_CLOEXEC:i64=524288
16struct NxIdEvidence{
17 buffer:*u8,bytes:i64,allocated:i64,result:*u8,result_bytes:i64,state:i64,system_code:i64
18}
19func id_evidence_init(r:*NxIdEvidence)->i64{
20 r.buffer=0 as *u8;r.bytes=0;r.allocated=0;r.result=0 as *u8
21 r.result_bytes=0;r.state=ID_EVIDENCE_ERROR;r.system_code=0;return 0
22}
23func id_evidence_close(r:*NxIdEvidence)->i64{
24 if (r.buffer as i64)>0{sys_munmap_direct(r.buffer,r.allocated)}
25 r.buffer=0 as *u8;r.result=0 as *u8;r.allocated=0;return 0
26}
27func id_field_valid(p:*u8,n:i64,key:i64)->i64{
28 if n<0 || (p as i64)<=0{return 0}
29 if key==1&&n==0{return 0}
30 var i:i64=0;while i<n{
31 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
32 };return 1
33}
34func id_read_evidence(path:*u8,key:*u8,key_n:i64,r:*NxIdEvidence)->i64{
35 id_evidence_init(r)
36 if (path as i64)<=0 || id_field_valid(key,key_n,1)==0{return r.state}
37 let stat:*u8=sys_mmap_try(NX_STAT_X64_BYTES)
38 if (stat as i64)<=0{r.state=ID_RESOURCE;return r.state}
39 let fd:i64=__syscall(257,AT_FDCWD,path,ID_O_RDONLY|ID_O_NONBLOCK|ID_O_NOFOLLOW|ID_O_CLOEXEC,0,0,0)
40 if fd<0{
41 sys_munmap_direct(stat,NX_STAT_X64_BYTES);r.system_code=fd
42 if fd==0-2{r.state=ID_MISSING};return r.state
43 }
44 var rc:i64=sys_flock(fd,SYS_LOCK_EX|SYS_LOCK_NB)
45 if rc==0{rc=sys_fstat_fd(fd,stat)}
46 var n:i64=0
47 if rc==0{
48 let mode:i64=*((stat+NX_STAT_X64_MODE_OFFSET) as *i64)
49 n=*((stat+NX_STAT_X64_SIZE_OFFSET) as *i64)
50 if (mode&NX_STAT_TYPE_MASK)!=NX_STAT_REGULAR_FILE || n<0 || n==ID_SIGNED_MAX{rc=FIO_EINVAL}
51 }
52 if rc==0{
53 r.allocated=n+1;r.buffer=sys_mmap_try(r.allocated)
54 if (r.buffer as i64)<=0{r.buffer=0 as *u8;r.allocated=0;r.state=ID_RESOURCE;rc=0-12}
55 }
56 var got:i64=0
57 while rc==0&&got<n{
58 let z:i64=sys_read(fd,r.buffer+got,n-got)
59 if z==FIO_EINTR{continue}
60 if z<=0{rc=z;if z==0{rc=FIO_EIO}}else{got=got+z}
61 }
62 let unlock:i64=sys_flock(fd,SYS_LOCK_UN);let closed:i64=sys_close(fd)
63 sys_munmap_direct(stat,NX_STAT_X64_BYTES)
64 if rc==0&&unlock!=0{rc=unlock};if rc==0&&closed!=0{rc=closed}
65 if rc!=0{r.system_code=rc;id_evidence_close(r);return r.state}
66 r.bytes=n;r.buffer[n]=0 as u8
67 if n>0{if r.buffer[n-1]!=10 as u8{r.system_code=FIO_EBADMSG;return r.state}}
68 var start:i64=0;var found:i64=0;var chosen:i64=0;var chosen_n:i64=0
69 while start<n{
70 var end:i64=start;while end<n&&r.buffer[end]!=10 as u8{end=end+1}
71 if end>start&&r.buffer[start]!=35 as u8{
72 var tab:i64=0-1;var i:i64=start
73 while i<end{
74 let c:u8=r.buffer[i]
75 if c==0 as u8||c==13 as u8{r.system_code=FIO_EBADMSG;return r.state}
76 if c==9 as u8{if tab>=0{r.system_code=FIO_EBADMSG;return r.state};tab=i}
77 i=i+1
78 }
79 if tab<=start{r.system_code=FIO_EBADMSG;return r.state}
80 if found==0&&tab-start==key_n{
81 var same:i64=1;i=0;while i<key_n{if r.buffer[start+i]!=key[i]{same=0};i=i+1}
82 if same==1{found=1;chosen=tab+1;chosen_n=end-tab-1}
83 }
84 };start=end+1
85 }
86 if found==0{r.state=ID_NOT_FOUND;return r.state}
87 r.result=r.buffer+chosen;r.result_bytes=chosen_n;r.result[chosen_n]=0 as u8
88 r.state=chosen_n;return r.state
89}
90// Legacy caller-buffer API: success returns complete result length; -1 alone means unseen.
91// Other negative values require reconciliation; no truncated success or out-of-bounds terminator.
92func id_lookup(path:*u8,key:*u8,key_n:i64,out:*u8,cap:i64)->i64{
93 if cap<=0||(out as i64)<=0{return ID_CAPACITY};out[0]=0 as u8
94 let r:*NxIdEvidence=sys_mmap_try(__size_of(NxIdEvidence)) as *NxIdEvidence
95 if (r as i64)<=0{return ID_RESOURCE}
96 var rc:i64=id_read_evidence(path,key,key_n,r)
97 if rc>=0{
98 if rc>=cap{rc=ID_CAPACITY}else{
99 var i:i64=0;while i<rc{out[i]=r.result[i];i=i+1};out[rc]=0 as u8
100 }
101 }
102 id_evidence_close(r);sys_munmap_direct(r as *u8,__size_of(NxIdEvidence));return rc
103}
104func id_seen(path:*u8,key:*u8,key_n:i64)->i64{
105 let r:*NxIdEvidence=sys_mmap_try(__size_of(NxIdEvidence)) as *NxIdEvidence
106 if (r as i64)<=0{return ID_RESOURCE}
107 let rc:i64=id_read_evidence(path,key,key_n,r)
108 id_evidence_close(r);sys_munmap_direct(r as *u8,__size_of(NxIdEvidence))
109 if rc>=0{return 1};if rc==ID_NOT_FOUND{return 0};return rc
110}
111// A lock protects the complete append against cooperating recorders. It does not cover
112// a caller's prior lookup or mutation, nor make a crashed mutation exactly-once.
113func id_record(path:*u8,key:*u8,key_n:i64,result:*u8,result_n:i64)->i64{
114 if (path as i64)<=0||id_field_valid(key,key_n,1)==0||id_field_valid(result,result_n,0)==0{return FIO_EINVAL}
115 if key_n>ID_SIGNED_MAX-result_n-2{return ID_CAPACITY}
116 let bytes:i64=key_n+result_n+2
117 let row:*u8=sys_mmap_try(bytes)
118 let stat:*u8=sys_mmap_try(NX_STAT_X64_BYTES)
119 if (row as i64)<=0||(stat as i64)<=0{
120 if (row as i64)>0{sys_munmap_direct(row,bytes)}
121 if (stat as i64)>0{sys_munmap_direct(stat,NX_STAT_X64_BYTES)}
122 return ID_RESOURCE
123 }
124 var i:i64=0;while i<key_n{row[i]=key[i];i=i+1};row[key_n]=9 as u8
125 i=0;while i<result_n{row[key_n+1+i]=result[i];i=i+1};row[bytes-1]=10 as u8
126 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)
127 var rc:i64=fd
128 if fd>=0{
129 rc=sys_flock(fd,SYS_LOCK_EX)
130 if rc==0{rc=sys_fstat_fd(fd,stat)}
131 if rc==0{
132 let mode:i64=*((stat+NX_STAT_X64_MODE_OFFSET) as *i64)
133 let n:i64=*((stat+NX_STAT_X64_SIZE_OFFSET) as *i64)
134 if (mode&NX_STAT_TYPE_MASK)!=NX_STAT_REGULAR_FILE||n<0{rc=FIO_EINVAL}
135 if rc==0&&n>0{
136 if sys_lseek(fd,n-1,0)!=n-1{rc=FIO_EIO}else{
137 let got:i64=sys_read(fd,stat,1);if got!=1||stat[0]!=10 as u8{rc=FIO_EBADMSG}
138 }
139 }
140 }
141 var written:i64=0
142 while rc==0&&written<bytes{
143 let z:i64=sys_write(fd,row+written,bytes-written)
144 if z==FIO_EINTR{continue}
145 if z<=0{rc=z;if z==0{rc=FIO_EIO}}else{written=written+z}
146 }
147 if rc==0{rc=sys_fsync(fd)}
148 let unlock:i64=sys_flock(fd,SYS_LOCK_UN);let closed:i64=sys_close(fd)
149 if rc==0&&unlock!=0{rc=unlock};if rc==0&&closed!=0{rc=closed}
150 }
151 sys_munmap_direct(row,bytes);sys_munmap_direct(stat,NX_STAT_X64_BYTES)
152 if rc==0{return 1};return rc
153}
154
155
156// Lossless legacy row inspection only. Shape cannot prove a request boundary:
157// four unlocked legacy writes and an embedded TAB can produce identical bytes.
158// Offsets refer to the unchanged source snapshot; no row is repaired or skipped.
159// Flags describe syntax, not permission to execute or replay an unknown request.
160const ID_ROW_EMPTY:i64=1
161const ID_ROW_COMMENT:i64=2
162const ID_ROW_NO_TAB:i64=4
163const ID_ROW_EXTRA_TAB:i64=8
164const ID_ROW_EMPTY_KEY:i64=16
165const ID_ROW_CONTROL:i64=32
166const ID_ROW_UNTERMINATED:i64=64
167struct NxIdLegacyRow{
168 start:i64,end:i64,next:i64,flags:i64,tabs:i64,first_tab:i64,second_tab:i64,
169 legacy_key_start:i64,legacy_key_bytes:i64,legacy_result_start:i64,legacy_result_bytes:i64
170}
171// Returns 1 for a row, 0 at exact EOF, negative for invalid memory/extent.
172// Caller owns disjoint row storage; source may have no sentinel and is never written.
173func id_legacy_row_next(source:*u8,bytes:i64,start:i64,row:*NxIdLegacyRow)->i64{
174 let base:i64=source as i64;let rp:i64=row as i64
175 let rn:i64=__size_of(NxIdLegacyRow)
176 if bytes<0||start<0||start>bytes||rp<=0{return FIO_EINVAL}
177 if rp>ID_SIGNED_MAX-rn{return FIO_EINVAL}
178 if bytes>0{
179 if base<=0{return FIO_EINVAL}
180 if base>ID_SIGNED_MAX-bytes{return FIO_EINVAL}
181 if rp<base+bytes&&base<rp+rn{return FIO_EINVAL}
182 }
183 row.start=start;row.end=start;row.next=start;row.flags=0;row.tabs=0
184 row.first_tab=0-1;row.second_tab=0-1
185 row.legacy_key_start=start;row.legacy_key_bytes=0
186 row.legacy_result_start=start;row.legacy_result_bytes=0
187 if start==bytes{return 0}
188 var end:i64=start
189 while end<bytes{
190 let ch:u8=source[end]
191 if ch==10 as u8{break}
192 if ch==9 as u8{
193 row.tabs=row.tabs+1
194 if row.first_tab<0{row.first_tab=end}else{if row.second_tab<0{row.second_tab=end}}
195 }
196 if ch==0 as u8||ch==13 as u8{row.flags=row.flags|ID_ROW_CONTROL}
197 end=end+1
198 }
199 row.end=end;row.next=end
200 if end<bytes{row.next=end+1}else{row.flags=row.flags|ID_ROW_UNTERMINATED}
201 if end==start{row.flags=row.flags|ID_ROW_EMPTY;return 1}
202 if source[start]==35 as u8{row.flags=row.flags|ID_ROW_COMMENT;return 1}
203 row.legacy_key_bytes=end-start;row.legacy_result_start=end
204 if row.first_tab<0{row.flags=row.flags|ID_ROW_NO_TAB;return 1}
205 row.legacy_key_bytes=row.first_tab-start
206 row.legacy_result_start=row.first_tab+1
207 var result_end:i64=end
208 if row.second_tab>=0{result_end=row.second_tab;row.flags=row.flags|ID_ROW_EXTRA_TAB}
209 row.legacy_result_bytes=result_end-row.legacy_result_start
210 if row.legacy_key_bytes==0{row.flags=row.flags|ID_ROW_EMPTY_KEY}
211 return 1
212}