code wiki / (root) / nx_mcp_pending_http_t174.nx

nx_mcp_pending_http_t174.nx source

↩ module page · 393 lines · 19566 B

1// MCP-specific identity ownership over the generic process pool. 2import "nx_mcp_control.nx" 3import "nx_request_pool.nx" 4 5struct NxMcpPending { 6 pid: i64, 7 kind: i64, 8 method: i64, 9 data: *u8, 10 length: i64, 11 allocated: i64, 12} 13// mp_spawn returns this (not a bare -1) when the id is ALREADY in flight, so the 14// caller can answer that ONE request with a correlated error and keep serving the 15// rest instead of tearing the session down. Still < 0, so mp_spawn's existing 16// "< 0 means not spawned" contract (and its gate) are unchanged. 17const MP_SPAWN_DUP: i64 = 0 - 2 18// Parent-side recovery error codes (JSON-RPC server range / internal). A worker 19// that died WITHOUT self-reporting (a crash, or the SIGALRM deadline killing it) 20// is turned into one of these correlated errors by mp_recover_pump. 21const MP_CODE_DEADLINE: i64 = 0 - 32012 22const MP_CODE_WORKER: i64 = 0 - 32603 23const MP_SIG_ALARM: i64 = 14 24// Capacity is an admitted execution budget, not an instruction to block the 25// control channel. Only requests can receive a correlated refusal response. 26func mp_admit(p: *NxRequestPool, body: *u8, control: *NxMcpControl, output: i64) -> i64 { 27 if p.active < p.capacity { return 1 } 28 if control.id_kind == 0 { return -1 } 29 let prefix: *u8="{\"jsonrpc\":\"2.0\",\"id\":" as *u8 30 let suffix: *u8=",\"error\":{\"code\":-32000,\"message\":\"Connector execution capacity is occupied\",\"data\":{\"owner\":\"nx_mcp_stdio\",\"stage\":\"admission\",\"cause\":\"admitted_workers_in_use\",\"dispatched\":false,\"next\":\"Wait for an outstanding request to finish or cancel it before retrying\"}}}\n" as *u8 31 var off: i64=control.id_off 32 var count: i64=control.id_len 33 if control.id_kind == NX_JSON_STRING { off=off-1; count=count+2 } 34 if rp_write(output,prefix,mr_len(prefix)) != 0 { return -1 } 35 if rp_write(output,body+off,count) != 0 { return -1 } 36 if rp_write(output,suffix,mr_len(suffix)) != 0 { return -1 } 37 return 0 38} 39func mp_at(rows: *u8, slot: i64) -> *NxMcpPending { 40 return (rows+slot*__size_of(NxMcpPending)) as *NxMcpPending 41} 42func mp_clear(row: *NxMcpPending) -> i64 { 43 if row.allocated > 0 { sys_munmap(row.data,row.allocated) } 44 row.pid=0; row.kind=0; row.method=0; row.data=0 as *u8; row.length=0; row.allocated=0 45 return 0 46} 47func mp_sweep(p: *NxRequestPool, rows: *u8) -> i64 { 48 var i: i64=0 49 while i<p.capacity { 50 let row: *NxMcpPending=mp_at(rows,i) 51 if row.pid != 0 { if row.pid != rp_worker(p,i).pid { mp_clear(row) } } 52 i=i+1 53 } 54 return 0 55} 56func mp_normalize(kind: i64, src: *u8, n: i64, dst: *u8) -> i64 { 57 if kind == NX_JSON_STRING { return jx_decode_span(src,n,dst,n+1) } 58 if kind != NX_JSON_NUMBER { return -1 } 59 var i: i64=0 60 // JSON -0 and 0 denote the same integer identity. 61 if n == 2 { if src[0] == (45 as u8) { if src[1] == (48 as u8) { dst[0]=48 as u8; return 1 } } } 62 while i<n { dst[i]=src[i]; i=i+1 } 63 return n 64} 65func mp_find(p: *NxRequestPool, rows: *u8, kind: i64, data: *u8, n: i64) -> i64 { 66 mp_sweep(p,rows) 67 var slot: i64=0 68 while slot<p.capacity { 69 let row: *NxMcpPending=mp_at(rows,slot) 70 if row.pid != 0 { if row.kind == kind { if row.length == n { 71 var equal: i64=1 72 var i: i64=0 73 while i<n { if row.data[i] != data[i] { equal=0 }; i=i+1 } 74 if equal != 0 { return slot } 75 } } } 76 slot=slot+1 77 } 78 return -1 79} 80func mp_spawn(p: *NxRequestPool, rows: *u8, body: *u8, control: *NxMcpControl) -> i64 { 81 if control.id_kind == 0 { return rp_fork(p) } 82 let allocated: i64=control.id_len+1 83 let data: *u8=sys_mmap(allocated) 84 let n: i64=mp_normalize(control.id_kind,body+control.id_off,control.id_len,data) 85 if n<0 { sys_munmap(data,allocated); return -1 } 86 if mp_find(p,rows,control.id_kind,data,n)>=0 { sys_munmap(data,allocated); return MP_SPAWN_DUP } 87 let pid: i64=rp_fork(p) 88 if pid==0 { return 0 } 89 if pid<0 { sys_munmap(data,allocated); return pid } 90 var slot: i64=0 91 while slot<p.capacity { 92 if rp_worker(p,slot).pid==pid { 93 let row: *NxMcpPending=mp_at(rows,slot) 94 mp_clear(row) 95 row.pid=pid; row.kind=control.id_kind; row.method=control.method 96 row.data=data; row.length=n; row.allocated=allocated 97 return pid 98 } 99 slot=slot+1 100 } 101 sys_munmap(data,allocated) 102 return -1 103} 104// Returns 1 for confirmed local worker cancellation, 0 for no active target, 105// -1 for failed/prohibited cancellation. This does not cancel a remote job. 106func mp_cancel(p: *NxRequestPool, rows: *u8, body: *u8, control: *NxMcpControl) -> i64 { 107 let allocated: i64=control.cancel_len+1 108 let data: *u8=sys_mmap(allocated) 109 let n: i64=mp_normalize(control.cancel_kind,body+control.cancel_off,control.cancel_len,data) 110 if n<0 { sys_munmap(data,allocated); return -1 } 111 let slot: i64=mp_find(p,rows,control.cancel_kind,data,n) 112 sys_munmap(data,allocated) 113 if slot<0 { return 0 } 114 let row: *NxMcpPending=mp_at(rows,slot) 115 if row.method==MCT_INITIALIZE { return -1 } 116 if rp_cancel_owned(p,slot,row.pid)!=0 { return -1 } 117 mp_clear(row) 118 return 1 119} 120 121// Extract a top-level "id" and its COMPLETE value from a (possibly truncated) request prefix -- the 122// bytes we DID retain before an oversized or malformed line. Returns 1 and fills kind/off/len (string 123// content EXCLUDES the quotes; number is its digit span with an optional leading '-'); returns 0 when 124// no whole top-level id is present (nested only, absent, or the value is cut short by the truncation). 125// Lives here (not in the connector's main-bearing file) so the gate can test it directly. 126func mo_prefix_id(src: *u8, n: i64, kind: *i64, off: *i64, len: *i64) -> i64 { 127 var depth: i64 = 0 128 var quoted: i64 = 0 129 var escaped: i64 = 0 130 var start: i64 = 0 131 var i: i64 = 0 132 while i < n { 133 let c: i64 = src[i] as i64 134 if quoted == 1 { 135 if escaped == 1 { escaped = 0 } 136 else { if c == 92 { escaped = 1 } 137 else { if c == 34 { 138 quoted = 0 139 if depth == 1 { if i - start == 2 { 140 if src[start] == (105 as u8) { if src[start + 1] == (100 as u8) { 141 var j: i64 = i + 1 142 while j < n { if src[j] == (32 as u8) { j = j + 1 } else { break } } 143 if j < n { if src[j] == (58 as u8) { 144 j = j + 1 145 while j < n { if src[j] == (32 as u8) { j = j + 1 } else { break } } 146 if j >= n { return 0 } 147 let vc: i64 = src[j] as i64 148 if vc == 34 { 149 let vstart: i64 = j + 1 150 var k: i64 = vstart 151 var esc: i64 = 0 152 var closed: i64 = 0 153 while k < n { 154 let cc: i64 = src[k] as i64 155 if esc == 1 { esc = 0 } 156 else { if cc == 92 { esc = 1 } else { if cc == 34 { closed = 1; break } } } 157 k = k + 1 158 } 159 if closed == 0 { return 0 } 160 *kind = NX_JSON_STRING 161 *off = vstart 162 *len = k - vstart 163 return 1 164 } 165 var k2: i64 = j 166 if src[k2] == (45 as u8) { k2 = k2 + 1 } 167 let dstart: i64 = k2 168 while k2 < n { if nx_ascii_is_digit(src[k2]) == 1 { k2 = k2 + 1 } else { break } } 169 if k2 == dstart { return 0 } 170 if k2 >= n { return 0 } 171 *kind = NX_JSON_NUMBER 172 *off = j 173 *len = k2 - j 174 return 1 175 } } 176 } } 177 } } 178 } } } 179 } else { 180 if c == 34 { quoted = 1; start = i + 1 } 181 else { if c == 123 { depth = depth + 1 } 182 else { if c == 125 { depth = depth - 1 } } } 183 } 184 i = i + 1 185 } 186 return 0 187} 188 189// ---- CORRELATED JSON-RPC ERROR EMISSION ------------------------------------- 190// One place builds the JSON-RPC 2.0 error envelope so the request CHILD (rich 191// soft-failure errors, original id span) and the PARENT recovery (a crashed 192// worker, decoded id from its pending row) produce the SAME shape. Self-contained 193// cat helpers: nx_mcp_pending sees neither the transport's mc_* nor the tools API. 194func mpe_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 195func mpe_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; var p: i64 = o; while s[i] != (0 as u8) { d[p] = s[i]; p = p + 1; i = i + 1 } return p } 196func mpe_catb(d: *u8, o: i64, s: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { d[o+i] = s[i]; i = i + 1 } return o + i } 197func mpe_catn(d: *u8, o: i64, v: i64) -> i64 { 198 var w: i64 = o 199 var x: i64 = v 200 if x < 0 { d[w] = 45 as u8; w = w + 1; x = 0 - x } 201 let t: *u8 = sys_mmap(24) 202 var k: i64 = 0 203 if x == 0 { t[0] = 48 as u8; k = 1 } 204 while x > 0 { t[k] = (48 + (x % 10)) as u8; x = x / 10; k = k + 1 } 205 var i: i64 = 0 206 while i < k { d[w] = t[k-1-i]; w = w + 1; i = i + 1 } 207 sys_munmap(t, 24) 208 return w 209} 210// JSON-escape a DECODED id string span (parent path): a raw quote, backslash, or 211// control byte in a decoded id would break the envelope, so escape them. The 212// child path passes the ORIGINAL span verbatim (already valid JSON) and skips this. 213func mpe_cat_esc(d: *u8, o: i64, s: *u8, n: i64) -> i64 { 214 var w: i64 = o 215 var i: i64 = 0 216 while i < n { 217 let c: i64 = s[i] as i64 218 if c == 34 { d[w] = 92 as u8; d[w+1] = 34 as u8; w = w + 2 } 219 else { if c == 92 { d[w] = 92 as u8; d[w+1] = 92 as u8; w = w + 2 } 220 else { if c < 32 { 221 d[w] = 92 as u8; d[w+1] = 117 as u8; d[w+2] = 48 as u8; d[w+3] = 48 as u8 222 let hi: i64 = (c >> 4) & 15 223 let lo: i64 = c & 15 224 var hc: i64 = 48 + hi 225 if hi > 9 { hc = 87 + hi } 226 var lc: i64 = 48 + lo 227 if lo > 9 { lc = 87 + lo } 228 d[w+4] = hc as u8; d[w+5] = lc as u8; w = w + 6 229 } else { d[w] = c as u8; w = w + 1 } } } 230 i = i + 1 231 } 232 return w 233} 234// Write a correlated JSON-RPC 2.0 error to fd for request id (id_kind/id_ptr/id_len). 235// escape=0: id_ptr is the ORIGINAL request id span (string content or number 236// digits) -- emitted verbatim. escape=1: id_ptr is a DECODED id (pending row) -- 237// string content is re-escaped. id_kind==0 (a notification) writes NOTHING and 238// returns 0: a notification never gets a response. Returns 0 on success (or on 239// nothing-to-write), non-zero when the protocol fd write failed (broken output). 240func mpe_write_error_detail(fd: i64, id_kind: i64, id_ptr: *u8, id_len: i64, escape: i64, code: i64, stage: *u8, cause: *u8, measured: i64, next: *u8, detail:*u8, detail_n:i64) -> i64 { 241 if id_kind == 0 { return 0 } 242 let cap: i64 = id_len*6 + mpe_slen(stage) + mpe_slen(cause)*2 + mpe_slen(next) + 512 + detail_n 243 let d: *u8 = sys_mmap(cap) 244 var o: i64 = mpe_cat(d, 0, "{\"jsonrpc\":\"2.0\",\"id\":" as *u8) 245 if id_kind == NX_JSON_STRING { 246 d[o] = 34 as u8; o = o + 1 247 if escape == 0 { o = mpe_catb(d, o, id_ptr, id_len) } else { o = mpe_cat_esc(d, o, id_ptr, id_len) } 248 d[o] = 34 as u8; o = o + 1 249 } else { 250 o = mpe_catb(d, o, id_ptr, id_len) 251 } 252 o = mpe_cat(d, o, ",\"error\":{\"code\":" as *u8) 253 o = mpe_catn(d, o, code) 254 o = mpe_cat(d, o, ",\"message\":\"" as *u8) 255 o = mpe_cat(d, o, cause) 256 o = mpe_cat(d, o, "\",\"data\":{\"owner\":\"nx_mcp_stdio\",\"stage\":\"" as *u8) 257 o = mpe_cat(d, o, stage) 258 o = mpe_cat(d, o, "\",\"cause\":\"" as *u8) 259 o = mpe_cat(d, o, cause) 260 o = mpe_cat(d, o, "\",\"measured\":" as *u8) 261 o = mpe_catn(d, o, measured) 262 o = mpe_cat(d, o, ",\"next\":\"" as *u8) 263 o = mpe_cat(d, o, next) 264 o = mpe_cat(d, o, "\"" as *u8) 265 if (detail as i64)>0&&detail_n>0{o=mpe_cat(d,o,",\"http\":");o=mpe_catb(d,o,detail,detail_n)} 266 o = mpe_cat(d, o, "}}}\n" as *u8) 267 let rc: i64 = rp_write(fd, d, o) 268 sys_munmap(d, cap) 269 return rc 270} 271 272// ---- mp_recover_pump: A PER-REQUEST WORKER FAILURE IS ONE ERROR, NOT AN OUTAGE 273// Wrap rp_pump so a child that failed WITHOUT self-reporting (a crash, or the 274// SIGALRM deadline that killed it) becomes a CORRELATED error for that one request 275// id and the connector keeps serving every other request. Only a genuine fatal -- 276// poll failure, a broken protocol fd (rp_pump last_error -3), or a reap fault -- 277// returns -1. Success/progress (1 stdin-ready, 0 progress) is passed through; 278// a recovered per-request failure returns 0 (progress, keep going). 279func mp_recover_pump(p: *NxRequestPool, rows: *u8, accept_input: i64, protocol_fd: i64, timeout_secs: i64) -> i64 { 280 let r: i64 = rp_pump(p, accept_input, protocol_fd) 281 if r >= 0 { mp_sweep(p, rows); return r } 282 // r == -1. A known failed slot AND not a broken-output-fd fatal => recoverable. 283 let slot: i64 = p.failed_slot 284 if slot < 0 { return 0 - 1 } 285 if p.last_error == 0 - 3 { return 0 - 1 } 286 // Over-budget leaves the child alive; reap it before reusing the slot. A 287 // non-zero exit already released it (rp_worker(p,slot).pid == 0), so skip. 288 if rp_worker(p, slot).pid != 0 { 289 if rp_cancel_owned(p, slot, rp_worker(p, slot).pid) != 0 { return 0 - 1 } 290 } 291 let row: *NxMcpPending = mp_at(rows, slot) 292 var wrc: i64 = 0 293 if row.pid != 0 { 294 var code: i64 = MP_CODE_WORKER 295 var stage: *u8 = "worker" as *u8 296 var cause: *u8 = "the request worker terminated before delivering a response" as *u8 297 var measured: i64 = p.last_error 298 if wait_term_signal(p.last_error) == MP_SIG_ALARM { 299 code = MP_CODE_DEADLINE 300 stage = "deadline" as *u8 301 cause = "the request exceeded its per-request time budget" as *u8 302 measured = timeout_secs 303 } 304 wrc = mpe_write_error(protocol_fd, row.kind, row.data, row.length, 1, code, stage, cause, measured, "check service health and byte budgets; verify any write outcome before retrying" as *u8) 305 mp_clear(row) 306 } 307 p.failed_slot = 0 - 1 308 mp_sweep(p, rows) 309 if wrc != 0 { return 0 - 1 } 310 return 0 311} 312 313// Selective HTTP evidence for per-request failures. Raw response bodies, cookies, 314// authorization headers and free-form upstream detail never enter protocol errors. 315import "nx_http_response_parse.nx" 316import "nx_mcp_route.nx" 317import "nx_sha256.nx" 318func mpe_http_header(src:*u8,n:i64,p:*i64,key:*u8,offset:*i64,length:*i64)->i64 { 319 var pos:i64=p[4];var count:i64=0 320 var no:i64=0;var nl:i64=0;var vo:i64=0;var vl:i64=0;var next:i64=0 321 while pos<p[6]-2{ 322 if nx_http_resp_parse_header_line(src,n,pos,&no,&nl,&vo,&vl,&next)!=NX_HTTP_RESP_OK{return -1} 323 if next<=pos{return -1} 324 if nx_http_resp_cieq(src+no,nl,key,mpe_slen(key))==1{ 325 var i:i64=0;while i<vl{let c:i64=src[vo+i] as i64;if c<32||c>126{return -1};i=i+1} 326 count=count+1;*offset=vo;*length=vl 327 };pos=next 328 } 329 return count 330} 331func mpe_http_field(d:*u8,o0:i64,src:*u8,n:i64,p:*i64,name:*u8,label:*u8)->i64 { 332 var off:i64=0;var len:i64=0;let count:i64=mpe_http_header(src,n,p,name,&off,&len) 333 var o:i64=mpe_cat(d,o0,",\"");o=mpe_cat(d,o,label);o=mpe_cat(d,o,"\":") 334 if count==1{o=mpe_cat(d,o,"\"");o=mpe_cat_esc(d,o,src+off,len);o=mpe_cat(d,o,"\"")}else{o=mpe_cat(d,o,"null")} 335 o=mpe_cat(d,o,",\"");o=mpe_cat(d,o,label);o=mpe_cat(d,o,"_observation\":\"") 336 var state:*u8="absent";if count==1{state="present"};if count>1{state="ambiguous-duplicate"};if count<0{state="malformed"} 337 o=mpe_cat(d,o,state);return mpe_cat(d,o,"\"") 338} 339// Returned object is validated/escaped here and borrowed by the error emitter. 340// Allocations live only in the existing one-request worker; no persistent collector. 341func mpe_http_evidence(src:*u8,n:i64,p:*i64,out_len:*i64)->*u8 { 342 *out_len=0 343 if n<=0||n>(NX_RA_SIZE_MAX-4096)/6||p[6]<0||p[6]>n{return 0 as *u8} 344 let cap:i64=n*6+4096;let d:*u8=sys_mmap(cap);if (d as i64)<=0{return 0 as *u8} 345 let bn:i64=n-p[6];var o:i64=mpe_cat(d,0,"{\"schema\":\"nishi.mcp-http-observation.v1\",\"status\":") 346 o=mpe_catn(d,o,p[1]);o=mpe_cat(d,o,",\"observed_at_unix\":");o=mpe_catn(d,o,sys_now_realtime_sec()) 347 o=mpe_http_field(d,o,src,n,p,"Retry-After","retry_after") 348 o=mpe_http_field(d,o,src,n,p,"X-Request-Id","request_id") 349 o=mpe_http_field(d,o,src,n,p,"X-Nishi-Request-Id","nishi_request_id") 350 o=mpe_http_field(d,o,src,n,p,"Traceparent","traceparent") 351 o=mpe_http_field(d,o,src,n,p,"Content-Type","content_type") 352 o=mpe_cat(d,o,",\"observed_body_bytes\":");o=mpe_catn(d,o,bn) 353 o=mpe_cat(d,o,",\"declared_body_bytes\":") 354 if p[8]==NX_HTTP_BODY_CONTENT_LENGTH{o=mpe_catn(d,o,p[7])}else{o=mpe_cat(d,o,"null")} 355 var complete:i64=0 356 if p[8]==NX_HTTP_BODY_CONTENT_LENGTH&&p[7]==bn{complete=1} 357 o=mpe_cat(d,o,",\"body_framing\":\"") 358 var framing:*u8="unverified-transfer";if p[8]==NX_HTTP_BODY_CONTENT_LENGTH{framing="length-mismatch";if complete==1{framing="content-length-complete"}} 359 o=mpe_cat(d,o,framing);o=mpe_cat(d,o,"\",\"observed_body_sha256\":") 360 let hash:*u8=sys_mmap(SHA256_DIGEST_BYTES) 361 if (hash as i64)>0&&sha256_digest_checked_native(src+p[6],bn,hash)==0{ 362 let hex:*u8="0123456789abcdef";o=mpe_cat(d,o,"\"");var i:i64=0 363 while i<SHA256_DIGEST_BYTES{let b:i64=hash[i] as i64;d[o]=hex[b>>4];d[o+1]=hex[b&15];o=o+2;i=i+1} 364 o=mpe_cat(d,o,"\"") 365 }else{o=mpe_cat(d,o,"null")} 366 if (hash as i64)>0{sys_munmap(hash,SHA256_DIGEST_BYTES)} 367 var stage:*u8=0 as *u8;var retry:*u8=0 as *u8 368 if complete==1{ 369 var parsed:i64=0;let root:*NxValue=nx_value_parse_json(src+p[6],bn,&parsed) 370 if parsed==NX_VAL_PARSE_OK{ 371 var bad:i64=0;let sv:*NxValue=mr_member(root,"stage",&bad);let rv:*NxValue=mr_member(root,"retry",&bad) 372 if bad==0{ 373 if sv!=(0 as *NxValue){if sv.kind==NX_VAL_STRING{ 374 if mr_equal(sv.str_ptr,sv.str_len,"connect")==1{stage="connect"} 375 if mr_equal(sv.str_ptr,sv.str_len,"read")==1{stage="read"} 376 if mr_equal(sv.str_ptr,sv.str_len,"deadline")==1{stage="deadline"} 377 if mr_equal(sv.str_ptr,sv.str_len,"read-after-accept")==1{stage="read-after-accept"} 378 }} 379 if rv!=(0 as *NxValue){if rv.kind==NX_VAL_STRING{if mr_equal(rv.str_ptr,rv.str_len,"unsafe")==1{retry="unsafe"}}} 380 } 381 } 382 } 383 o=mpe_cat(d,o,",\"upstream_stage\":") 384 if (stage as i64)>0{o=mpe_cat(d,o,"\"");o=mpe_cat(d,o,stage);o=mpe_cat(d,o,"\"")}else{o=mpe_cat(d,o,"null")} 385 o=mpe_cat(d,o,",\"upstream_retry\":") 386 if (retry as i64)>0{o=mpe_cat(d,o,"\"");o=mpe_cat(d,o,retry);o=mpe_cat(d,o,"\"")}else{o=mpe_cat(d,o,"null")} 387 o=mpe_cat(d,o,",\"request_outcome\":\"unknown\",\"automatic_retry\":false,\"body_retention\":\"digest-only; original body not retained\",\"correlation\":\"outer JSON-RPC id preserved; optional upstream headers are declarations\"}") 388 *out_len=o;return d 389} 390 391func mpe_write_error(fd:i64,id_kind:i64,id_ptr:*u8,id_len:i64,escape:i64,code:i64,stage:*u8,cause:*u8,measured:i64,next:*u8)->i64 { 392 return mpe_write_error_detail(fd,id_kind,id_ptr,id_len,escape,code,stage,cause,measured,next,0 as *u8,0) 393}