HTTP 服务与远程 SDK 契约
首版可选服务通过 Python 库执行查询和验证,拥有一个来源管理器。协议为 /v1,普通结果 JSON schema_version=1,完整导出使用交换 schema_version=2;不提供生产 RTR 服务器或历史数据库。服务使用 asyncio,核心库与远程 SDK 的 Trio 支持独立验收。公开入口见源码参考,实际路由及边界模型生成 /openapi.json。
生命周期和边界
rpkiparrot.service.create_app(service_config, *, client_config, transports=None, readers=None, persistence=None) 创建 ASGI 应用但不启动网络或数据库;ASGI lifespan 进入时打开 Client,在受管理任务组中启动 run,退出时关闭。配置 workers 必须为 1;禁止复制多个拥有者连接同一来源和数据库。
默认监听 127.0.0.1:8323。非本机监听要求显式认证和 TLS,TLS 可由配置的受信任反向代理终止。服务入口校验配置,不能通过信任任意转发头绕过;应用工厂不自动部署代理或修改宿主日志。
ServiceConfig 位于 config,并在 service 重导出。host/port 必须与宿主实际监听配置一致;本机豁免仅接受 loopback IP,不能用待解析的 DNS 名证明本机绑定;auth_mode=none 时每个请求的实际 TCP peer 也必须是回环地址,缺失 peer 信息同样拒绝。auth_mode 为 none 或 bearer;bearer 必须提供 token_provider,由 lifespan 在受管理线程中调用一次。秘密只供内存中的常量时间比较,不进入状态、快照或错误文本;轮换凭据需要新生命周期。direct TLS 模式指定 tls_cert_file/tls_key_file,宿主为 Uvicorn 设置对应证书,应用同时拒绝实际 ASGI scheme 不是 https 的请求。proxy TLS 模式必须使用 bearer 认证,并指定 trusted_proxy_ips 的 IP/CIDR 白名单,禁止通配符与全网网段;原始 TCP peer 必须匹配且恰有一个 X-Forwarded-Proto: https。宿主必须关闭 Uvicorn proxy_headers 改写,不能通过 X-Forwarded-For 冒充受信 peer。重复 Authorization 或 X-Forwarded-Proto 均拒绝。认证适用于所有 HTTP 路由,包括状态、健康与交互文档。
请求体默认最多 4 MiB,超限为 413。验证批量数和解释限制复用库配置;HTTP 层不能另建一套验证算法。客户端断开取消相应请求/长轮询,不取消常驻同步;请求不得用阻塞工作占住同步任务。
请求资源与截止时间
| ServiceConfig 字段 | 默认值 | 行为 |
|---|---|---|
| max_concurrent_requests / max_pending_requests | 4 / 16 | 普通请求执行位与 FIFO 等待名额;满额立即 429 |
| request_timeout | 120 秒 | 从 ASGI 请求进入起的总截止,覆盖准入、收体、计算、序列化和发送 |
| max_body_bytes / max_response_bytes | 4 MiB / 16 MiB | 请求体/普通完整响应上限;导出另用 Limits.max_export_bytes |
| max_concurrent_polls | 32 | 独立长轮询执行位,无等待队列 |
| long_poll_timeout | 25 秒 | 单次事件等待上限,须小于 request_timeout;请求值较大时截到该上限,允许 timeout=0 |
| watch_idle_timeout | 60 秒 | 无活动轮询后的回收周期,活动轮询不因 idle 被清理 |
| page_size / max_page_size | 1,000 / 10,000 | 缺省与最大分页条数 |
请求先保留执行位或有限排队名额,再流式接收有大小上限的 body;等待者不能启动 JSON 解析或领域计算,满额请求不预读完整 body。每个已准入请求可保留一份最多 max_body_bytes 的缓冲,执行位和等待位之和限制这些缓冲数。解析、查询、验证和完整结果序列化使用受管理线程。取消等待者立即归还名额;已运行线程必须真正结束才释放执行位,不假称取消已停止计算。状态、指标和健康另有有限的轻量执行与序列化容量,不等待领域计算线程空闲。事件泵与事件页序列化另有独立的两个工作线程执行位;长轮询从准入至发送完成均计入独立 poll 名额,不占状态或领域请求名额。正常同步任务不占用 HTTP 准入队列。
资源饱和返回 429 resource_limit、retryable=true、Retry-After: 1;整批条数或 body 超量为 413。总截止超时为 503 request_timeout、retryable=true;清理仍等待已运行的线程。响应开始后若截止或断线,不发送截断 JSON 作为成功,远端看到传输失败。请求压缩不自动解压,非 identity Content-Encoding 拒绝;未知参数、重复查询键及重复 JSON 键均拒绝。JSON 必须是无 BOM 的 UTF-8;整数查询参数使用无符号 ASCII 十进制,不接受多余前导零、空白或下划线。timeout 接受非负 ASCII 十进制及科学计数法,不接受非有限值或额外空白。
request_timeout 限制正常请求工作并触发取消,不保证错误响应在该时刻到达。不可中断的计算仍须返回后才归还执行位;若 SDK 先到达自己的截止或关闭连接,它可能得到 TransportError 而未收到服务端的 503。增加 SDK 等待时间不会延长服务端正常响应的截止,也不会使取消自动停止线程。
最终导出或序列化线程返回后,发送每个应用响应消息前仍检查取消与原截止;底层发送不阻塞也不能绕过。截止后的错误响应不确认事件页交付,未成功交付的新 watch 必须关闭;已有 watch 保留未确认事件以供重试。
端点
| 方法与路径 | 输入 | 结果 |
|---|---|---|
| GET /v1/status | 无 | StatusReport |
| GET /v1/metrics | 无 | MetricsReport,首版 JSON,不绑定监控格式 |
| GET /v1/health/live | 无 | 进程与运行任务存活状态 |
| GET /v1/health/ready | required=vrp,aspa;可选 aspa_afi | 相应能力就绪;未就绪 503 |
| POST /v1/snapshots | 可选 required 数组、aspa_afi | 固定当前快照的 SnapshotRef;不等待未指定的能力 |
| POST /v1/validate/origin | prefix、asn、可选 snapshot_token、explain | OriginResult |
| POST /v1/validate/origins | items 数组及同上公共选项 | BatchResult |
| POST /v1/validate/aspa | path、context、可选 afi、snapshot_token、explain | AspaResult |
| POST /v1/validate/aspa-batch | items 数组、可选默认 afi 及公共选项 | BatchResult |
| POST /v1/analyze/bmp | path、BMP context、可选 afi、snapshot_token、explain | BmpAnalysis |
| GET /v1/vrps | snapshot_token、可选 covering/asn/source_id/family、limit/cursor | 固定快照的分页 VrpMatch |
| GET /v1/aspas | snapshot_token、可选 afi、source_id、limit/cursor | 固定快照分页的生效 AspaProviders,按 customer 排序 |
| GET /v1/aspa/{customer} | 可选 afi、snapshot_token | AspaProviders |
| POST /v1/sources/diff | left、right、snapshot_token | SourceDiff |
| GET /v1/export | snapshot_token | 完整版本化快照;与普通结果 envelope 区分 |
| POST /v1/watches | 无 | 原子 initial SnapshotRef、initial_status StatusReport、watch_id、cursor |
| GET /v1/watches/{watch_id}/events | cursor、timeout,默认 25 秒 | 有序事件页和下一 cursor |
| DELETE /v1/watches/{watch_id} | 无 | 幂等结束订阅 |
Origin ASN 的 JSON null 对应 NONE。path 明确为 segments 数组,每段含 kind 和整数 asns,不猜字符串路径方向。context 字段按模型和输入契约校验。首版不提供修改服务器来源、上传 SLURM 或管理凭据的远程端点。宿主进程使用 Client.apply_config 修改来源;新默认查询取得新视图,既有 token 保留原配置版本和期限,详见配置变更。
ASPA afi 的 JSON 表示为整数 1/2 或 null,查询参数仅接受 1/2;null/省略表示要求两族完整视图相同,不能把两族授权求并集。批次的 afi 是缺少该字段的条目的默认值,条目显式 afi 优先。readiness 的 aspa_afi 省略时分别检查两族可用,不要求授权相同;显式 aspa_afi 必须同时 required 包含 ASPA。POST snapshots 的 required 可省略、为 null 或空数组,均只固定当前快照;health ready 的 required 必须非空,省略默认 VRP。
快照和分页
SnapshotRef 包含 snapshot_id、config_revision、snapshot_token、capabilities、usable_until、active_groups、逐族 aspa_capabilities/aspa_usable_until/aspa_active_groups、retention_deadline、algorithm_versions、mode、evaluated_at 及可空 upstream_id。token 为服务生成的 opaque 值,调用方不解析、不比较大小;它引用服务器固定快照,不能单独证明数据仍有效。
未提供 token 的单条或批量验证在服务器请求处理开始时取得一个快照;整个批次固定该快照,并逐条检查期限。查询分页要求先取得 SnapshotRef,所有页使用同一个 token。
VRP/ASPA 遍历的 limit 默认 1,000,上限 10,000,可由 ServiceConfig 调整。服务从固定索引位置继续分页,不反复扫描先前页面。cursor 经服务会话密钥认证,绑定快照、全部规范化过滤条件(包括 afi)、分页 limit、排序和位置,不能只信任客户端传回的 offset。修改绑定字段后旧 cursor 为 invalid_input。分页 data 恰含 items、可空 next_cursor,envelope.snapshot_id 对应固定引用。
默认最多保留 4 个可定位版本、最长 600 秒,容量压力可以更早淘汰。快照淘汰返回 410 snapshot_gone;仍保留但数据期限已过返回 503 snapshot_expired。SDK 不静默换快照补齐后续页;调用方重启整次查询。同一快照重复固定可复用已有 token,沿用原 retention_deadline,不重新获得完整保留周期。retention_deadline 不承诺延长原始 usable_until,任一个边界先到都可以结束可查询性。
响应使用 Cache-Control: no-store,首版不让中间 HTTP 缓存延长验证结果的使用时间。请求 ID 用于诊断,不构成恰好一次保证。
事件长轮询
POST watches 与内存 watch 一样原子取得初始观察点和后续游标。initial 返回 SnapshotRef,消费者可固定该版本查询;initial_status 返回同一注册点的实时 StatusReport,避免连接状态仅产生 StatusEvent 而没有新载荷版本时遗漏现状。Snapshot.sources 始终是最后提交的固定元数据,不因实时状态观察改写同一快照。消费初始数据之前若初始 token 已淘汰,必须重建观察。服务把初始完整索引引用转交受限 token 池,watch pump 不额外永久保留每个初始版本。
事件窗口有界,cursor 包含发布身份、epoch、事件位置与 watch 身份。一个 watch 同时仅允许一个长轮询;重复同一 cursor 在窗口内可以返回重复事件,消费者按 event_id 去重。空超时返回 200 和空 events,cursor 不前进。事件页同时受 max_page_size 和 max_response_bytes 限制,只返回能完整序列化的事件前缀;单个事件已超过响应预算时返回 409 resync_required 并释放该 watch。已完整发送的页不再占待消费队列名额,但在有界事件窗口内仍可按原 cursor 重放;没有完成发送的页仍计入待消费积压。已发送历史被淘汰后,旧游标返回 resync_required,最新已交付游标仍可继续;不能仅因尚未收到下一次 poll 的确认而宣称已交付事件丢失。
HTTP watch 与宿主在同一 Client 上的嵌入式订阅共用 max_subscribers 上限;容量不足返回可重试的 429。把上限热改到当前使用量以下会拒绝配置,需要先关闭相应订阅。服务开始关闭后,已经准入但尚未完成注册的请求也不能创建新的 watch 或保留 token;关闭响应为 503 closed,并完成已开始的资源清理。
并发第二个 poll 返回 409 watch_conflict,retryable=true、retry_after=1,保留既有 poll、观察点和 cursor;不会自动顶替请求,也不映射成 ResyncRequiredError。取消或断开后清除 busy 状态,已取得的事件保留在有界重放窗口,可用原 cursor 重试。events data 为 {events:[{kind:"snapshot_event"|"status_event",data:领域事件}],cursor:字符串}。DELETE 对未知或已删除 watch 同样返回 200 watch_closed,data 为 {closed:true}。
watch 在 60 秒没有活动请求后可清理;清理、窗口溢出、epoch 改变或未知游标返回 409 resync_required。HTTP 连接断开本身不创建新初始点,重试可沿用游标;无法续接时明确失败。DELETE 释放资源,服务关闭唤醒等待者。CLI 服务在 Uvicorn 等待现有请求前停止准入并关闭 watches,待请求结束后由 lifespan 关闭 Client/数据库;嵌入 ASGI 的宿主也需在开始 drain 时发出关闭通知,例如先 await app.state.client.aclose(),不能只依赖 drain 之后的 lifespan 回调。阻塞工作仍需结束才能完成资源清理。
首版不提供永久游标、恰好一次交付、WebSocket 或无限事件历史。库的队列上限与服务端并发长轮询上限一致受配置约束。
错误映射
| HTTP 状态 | 含义 |
|---|---|
| 200 | 请求已执行;invalid、notfound、unknown 仍是正常验证结果 |
| 400 | 输入、配置形式、未知参数、游标过滤不匹配 |
| 401 / 403 | 未认证或无权限,不携带机密诊断 |
| 404 / 405 | 未知路径或不支持的方法;使用安全的 invalid_input 错误信封,不泄漏框架 detail |
| 409 | resync_required 要求重建观察;watch_conflict 可等待已有 poll 后重试 |
| 410 | snapshot_gone |
| 413 | 请求体或批量输入大小超过限制 |
| 429 | 有界并发/订阅资源已满,retryable=true 并提供 Retry-After;完整普通结果超出预算时 retryable=false |
| 503 | 数据/能力不可用、快照到期、服务正在退出 |
| 500 | 意外内部执行失败;安全 ErrorInfo,无 traceback |
统一转换框架默认校验错误,避免同类 InputError 有的返回 400、有的泄漏框架特定 422 结构。批次已完整处理但有逐条输入错误时返回 200,items 保存错误;整批容器无效才用 400。
远程 SDK
rpkiparrot.remote.RemoteClient(base_url, *, timeout=180.0, ...) 位于 http 可选依赖边界,仅用远程客户端不要求 FastAPI/Uvicorn。支持 async with,关闭自有 HTTP 连接及显式传入的 transport;构造不发请求。进入、使用与关闭须属于同一个事件循环;尚未完成的 watch 注册在发请求前也占 max_subscribers 名额。aclose 停止新请求,取消并等待已有注册、请求与不可放弃的解码线程,订阅 DELETE 并行执行且各自受总截止限制。已知 watch_id 的畸形响应也尝试 DELETE;无法取得 ID 或联系服务器时依赖服务端 idle 回收。Bearer 凭据仅允许 HTTPS 或明确的回环 HTTP 地址。
默认传输使用 HTTPX/HTTPcore 公共接口,TCP 连接竞赛和未完成 TLS 握手的资源在取消时释放;注入 transport 保留其自己的网络语义。进入上下文时在受管理线程中准备默认 TLS 信任配置,期间的关闭会等待准备结束并释放迟到传输,不遗弃线程或在事件循环上读取 CA 文件。
提供异步 get_snapshot、validate_origin(s)、validate_aspa(_batch)、analyze_bmp_path、covering_vrps/iter_vrps、iter_aspas、aspa_providers、compare_sources、export_snapshot、get_status、get_metrics,以及 watch 异步上下文。领域结果类型与嵌入式相同;SnapshotRef 代替本地 Snapshot,网络方法都需 await,远程迭代为异步迭代。iter_vrps、iter_aspas 和 covering_vrps 的 page_size 默认 None,使用服务端缺省页大小;显式值必须在服务器允许范围内。
所有验证和查询允许显式 snapshot 参数;不指定时由服务器取得当前快照。远程 wait_ready 使用有限截止时间查询能力状态,不能每次重试重置总超时。总请求截止包括请求编码、响应流读取、JSON 解析和领域类型解码;超时后仍等待已运行的线程释放资源。watch 产生 RemoteSubscription,initial 为 SnapshotRef,initial_status 为同一观察点的 StatusReport,事件和 ResyncRequired 与本地相同。ResyncRequiredError 使该订阅保持失效,不能靠下一次调用伪造恢复;取消保留上一完整解码页的 cursor。仅作为 HTTP 适配边界的 evidence 内部值采用不可变 JSON 表示;比较完整领域结果时使用相同规范序列化,不依赖解释值里的本地 Python 对象身份。
SDK 可以在总超时内重试无副作用查询,但显式 token、批次和 cursor 必须保持不变。服务返回未知结果枚举/主 schema 时报告兼容错误,不猜测为 valid;未知 ErrorInfo.code 保留原值。连接中断以及网关返回的非 JSON 502/503/504 产生可重试 TransportError,不伪装成完整批量结果。wait_ready 的 required 必须非空;纯观察未就绪状态使用 get_snapshot(required=())。
验收要求
同一快照和输入下,嵌入式、CLI、HTTP/SDK 的领域结果、理由及算法版本必须一致;时间字段允许请求实际采样差异。特别测试分页途中到期或淘汰、长轮询取消、慢消费者、无可用数据、空数据集和 owner lifespan 清理。服务框架测试不能代替核心 asyncio/Trio 测试。
批量错误建立边界:没有显式 token,或 token 在请求建立时有效,合法批次一律返回 200,条目逐个保存 InputError/DataUnavailableError/SnapshotExpiredError;初始全部不可用仍 complete=true。显式 token 在建立请求前已淘汰返回 410,已到期返回 503;建立后处理中到期则逐条返回错误,不改整批状态。容器无效或超量仍是整次请求失败。
ASPA 批次按有效条目实际请求的 AFI 检查建立时的固定快照期限;仅验证 IPv4 不会因 IPv6 先到期而整体拒绝,省略 AFI 仍要求公共授权视图。快照创建的 required 为 None 或空数组时不要求数据就绪,健康就绪检查的 required 必须非空。完整导出及来源差异若显式固定了 token,任一原始视图变更时效边界已到则拒绝,不能返回看似仍可使用的旧在线状态。
/openapi.json 从服务边界模型与领域数据类生成各路由请求、完整响应、分页参数、错误示例及 schema 2 导出结构;同样受认证/TLS 策略保护。运行 服务示例 可实际启动一个本机监听器,通过 SDK 完成固定快照查询和 watch 后清理全部任务。
AFI 作为字段值时使用整数 1、2;作为 JSON 对象映射键时使用字符串 "1"、"2"。生成的 OpenAPI 保留这一区别,包括快照引用、状态、来源差异、watch 初始状态与事件中的嵌套字段。验收使用独立 JSON Schema 2020-12 校验器检查各公开端点的真实成功和错误响应,以及分页、批量逐项错误与日期时间格式;不以服务自身的 Pydantic 或 SDK 解码成功代替结构一致性检查。