Spaces:
Running on Zero
Running on Zero
File size: 9,156 Bytes
2031f77 6c60ea0 2031f77 6c60ea0 2031f77 6c60ea0 2031f77 6c60ea0 2031f77 6c60ea0 2031f77 6c60ea0 2031f77 623f6f8 6c60ea0 2031f77 6c60ea0 2031f77 6c60ea0 2031f77 6c60ea0 2031f77 6c60ea0 2031f77 6c60ea0 2031f77 1e1fd22 2031f77 1e1fd22 2031f77 1e1fd22 2031f77 6c60ea0 2031f77 1e1fd22 6c60ea0 2031f77 1e1fd22 2031f77 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | """Process-local result delivery. No disk cache, public URLs, or background service.
Run one ASGI worker. Restarts discard results. This does not promise physical
memory erasure or control provider logs, swap, snapshots, or response buffers.
"""
import asyncio
from contextlib import asynccontextmanager
import hashlib
import hmac
import secrets
import threading
import time
from fastapi import FastAPI, Header, HTTPException
from fastapi.responses import Response
from pydantic import BaseModel, Field
class Unavailable(Exception): pass
class CapacityExceeded(Exception): pass
class InvalidAnalysisOutput(Exception): pass
MAX_PRIMARY_BYTES = 4 * 1024 * 1024
MAX_ANALYSIS_BYTES = 32 * 1024 * 1024
def publish_worker_result(store, data, report, automatic=True, *, require_analysis=False):
"""Remove private worker artifacts before returning the public run report."""
if not isinstance(report, dict):
if require_analysis:
raise InvalidAnalysisOutput()
return store.publish(data, automatic), report
missing = object()
private_files = report.pop('_analysis_files', missing)
archive = None
if private_files is missing:
if require_analysis:
raise InvalidAnalysisOutput()
else:
try:
from portable_bundle import BundleError, make_analysis_archive
archive = make_analysis_archive(
private_files, primary=data, analysis_summary=report.get('analysis_summary'))
except BundleError:
raise InvalidAnalysisOutput() from None
return store.publish(data, automatic, analysis=archive), report
class ResultStore:
def __init__(self, clock=time.monotonic, wall_clock=time.time, max_bytes=64*1024*1024):
self.clock, self.wall_clock = clock, wall_clock
self.max_bytes = max_bytes
self.lock = threading.RLock()
self.jobs = {}
@property
def retained_bytes(self):
with self.lock:
return sum(len(j['data']) + (len(j['analysis']) if j['analysis'] is not None else 0)
for j in self.jobs.values())
def _delete(self, ident):
record = self.jobs.pop(ident)
for name in ('data', 'analysis'):
payload = record[name]
if payload is not None:
payload[:] = b'\0' * len(payload)
payload.clear()
def expire(self):
with self.lock:
for ident in list(self.jobs):
if self.clock() >= self.jobs[ident]['deadline']: self._delete(ident)
def clear(self):
with self.lock:
for ident in list(self.jobs): self._delete(ident)
def publish(self, data, automatic=True, analysis=None):
if (not isinstance(data, bytes) or not data or len(data) > MAX_PRIMARY_BYTES
or (analysis is not None and (not isinstance(analysis, bytes) or not analysis
or len(analysis) > MAX_ANALYSIS_BYTES))):
raise CapacityExceeded()
retained = len(data) + (len(analysis) if analysis is not None else 0)
with self.lock:
self.expire()
if len(self.jobs) >= 16 or self.retained_bytes + retained > self.max_bytes:
raise CapacityExceeded()
ident, key = secrets.token_urlsafe(24), secrets.token_urlsafe(32)
expires = self.wall_clock()+300
self.jobs[ident] = dict(key=key, data=bytearray(data),
analysis=bytearray(analysis) if analysis is not None else None,
automatic=automatic, deadline=self.clock()+300, expires_at=expires,
delivered=False, receipt=None, analysis_delivered=False,
analysis_receipt=None)
published = dict(id=ident,key=key,expires_at=expires,automatic=automatic,
sha256=hashlib.sha256(data).hexdigest(),size=len(data))
if analysis is not None:
published['analysis'] = {'size': len(analysis), 'sha256': hashlib.sha256(analysis).hexdigest()}
return published
def _get(self, ident, key):
self.expire()
record = self.jobs.get(ident)
if not record or not hmac.compare_digest(record['key'],key): raise Unavailable()
return record
def status(self, ident, key):
with self.lock:
r = self._get(ident,key)
return dict(expires_at=r['expires_at'],automatic=r['automatic'],delivered=r['delivered'])
def download(self, ident, key):
with self.lock:
r = self._get(ident,key)
# Paired results are retryable as a pair until ACK or expiry. Legacy
# primary-only automatic deliveries retain their one-attempt behavior.
if r['automatic'] and r['delivered'] and r['analysis'] is None: raise Unavailable()
r['delivered'] = True
if r['receipt'] is None:
r['receipt'] = secrets.token_urlsafe(32)
return bytes(r['data']), r['receipt']
def download_analysis(self, ident, key):
with self.lock:
r = self._get(ident,key)
if r['analysis'] is None or not r['delivered'] or r['receipt'] is None:
raise Unavailable()
r['analysis_delivered'] = True
if r['analysis_receipt'] is None:
r['analysis_receipt'] = secrets.token_urlsafe(32)
return bytes(r['analysis']), r['analysis_receipt']
def acknowledge(self, ident, key, receipt, analysis_receipt=None):
with self.lock:
r = self._get(ident,key)
if not r['receipt'] or not hmac.compare_digest(r['receipt'],receipt): raise Unavailable()
if r['analysis'] is not None:
if (not r['analysis_delivered'] or not r['analysis_receipt']
or not isinstance(analysis_receipt, str)
or not hmac.compare_digest(r['analysis_receipt'],analysis_receipt)):
raise Unavailable()
elif analysis_receipt is not None:
raise Unavailable()
if r['automatic']:
self._delete(ident)
return dict(deleted=True)
return dict(deleted=False,expires_at=r['expires_at'])
def delete(self, ident, key):
with self.lock:
self._get(ident,key)
self._delete(ident)
return dict(deleted=True)
class Acknowledgment(BaseModel):
receipt: str = Field(min_length=1,max_length=128)
analysis_receipt: str | None = Field(default=None,min_length=1,max_length=128)
def create_api(store=None):
store = store if store is not None else ResultStore()
@asynccontextmanager
async def lifespan(app):
async def expiry():
while True:
await asyncio.sleep(1)
store.expire()
task = asyncio.create_task(expiry())
try: yield
finally:
task.cancel()
try: await task
except asyncio.CancelledError: pass
store.clear()
app = FastAPI(lifespan=lifespan,docs_url=None,redoc_url=None,openapi_url=None)
app.state.results = store
@app.middleware('http')
async def headers(request,call_next):
response = await call_next(request)
response.headers['Cache-Control']='no-store'
response.headers['Referrer-Policy']='no-referrer'
response.headers['X-Content-Type-Options']='nosniff'
return response
def key(value):
if not value:
raise HTTPException(404,'Job unavailable')
if not value.isascii() or len(value)>128: raise HTTPException(404,'Job unavailable')
return value
def call(fn,*args):
try: return fn(*args)
except Unavailable: raise HTTPException(404,'Job unavailable') from None
@app.get('/api/jobs/{ident}')
def status(ident:str,x_job_key:str|None=Header(default=None)):
return call(store.status,ident,key(x_job_key))
@app.post('/api/jobs/{ident}/download')
def download(ident:str,x_job_key:str|None=Header(default=None)):
data,receipt = call(store.download,ident,key(x_job_key))
return Response(data,media_type='application/octet-stream',headers={
'Content-Disposition':'attachment; filename="candidate.evolution"',
'X-Delivery-Receipt':receipt})
@app.post('/api/jobs/{ident}/analysis/download')
def download_analysis(ident:str,x_job_key:str|None=Header(default=None)):
data,receipt = call(store.download_analysis,ident,key(x_job_key))
return Response(data,media_type='application/gzip',headers={
'Content-Disposition':'attachment; filename="analysis.json.gz"',
'X-Analysis-Delivery-Receipt':receipt})
@app.post('/api/jobs/{ident}/ack')
def ack(ident:str,body:Acknowledgment,x_job_key:str|None=Header(default=None)):
return call(store.acknowledge,ident,key(x_job_key),body.receipt,body.analysis_receipt)
@app.delete('/api/jobs/{ident}')
def delete(ident:str,x_job_key:str|None=Header(default=None)):
return call(store.delete,ident,key(x_job_key))
return app
|