Files
planet/docs/plans/location-resolver-shared-pipeline-plan.md
linkong e1984c7a35 release: bump version to 0.49.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 17:42:27 +08:00

7.4 KiB
Raw Blame History

Location Resolver Shared Pipeline Plan

状态:已实现,当前用户流程见 Earth 位置候选采集使用手册,开发接口见 通用位置估算管线开发说明

Goal

把"给定一条记录,决定它的 lat/lon"这件事抽象成一条统一的可插拔管线让算力中心、BGP 观测站、BGP 事件——以及未来任何需要位置估算的实体——共用同一套接口。新算法peeringdb 设施查询、IXP 表、用户认领的精确点位等)通过实现一个 Resolver 类即可挂入,不需要改任何上层调用方。

Background

实施前现状

  • 算力中心 (backend/app/services/compute_center_locations.py) 早期曾使用源坐标 → 本地 JSON 注册表 → 城市兜底 → Nominatim 在线地理编码。后续为避免硬编码位置污染事实链路,算力中心本地注册表已移除;主地图只使用源坐标,手动候选采集使用 ROR 和 Nominatim。
  • BGP 观测站 (collectors/bgp_common.py:RIPE_RIS_COLLECTOR_COORDS) 是一张写死的字典26 个 RIPE RIS collector 的城市级坐标。新增 collector / 升级到设施级精度都得改 Python。
  • BGP 事件继承所属 collector 的城市级坐标(BGPObservation.collector_geo)。
  • 用户原本以为 BGP 观测站位置是通过 iptoasn 推断的——其实 iptoasn 只用于前缀级国家归属(bgp_enrichment.py),不影响 marker 坐标。

痛点

  1. 算力中心那条 4 层链路写死在算力中心模块里BGP 想用得复制一遍。
  2. 三类实体各走各的坐标策略,缺统一抽象。
  3. 未来要插更精的算法peeringdb / IXP / 用户认领),现在没有挂入点。

Design

接口契约

backend/app/services/location/

  • models.py —— LocationQuery(输入)、LocationCandidate(候选)、ResolverOutput(单 resolver 输出)、ResolutionResult/ResolutionDiagnostic(管线最终结果)
  • pipeline.py —— LocationResolver Protocol、LocationPipeline 编排器
  • resolvers/source_coordinates.py —— 记录自带 lat/lon 时直通
  • resolvers/registry.py —— 本地 JSON 注册表locations + city_fallbacks按别名得分
  • resolvers/nominatim.py —— 通用 Nominatim 客户端rate-limited + LRU 缓存)+ 可注入 query plan
  • resolvers/inherit.py —— 从外部回调取候选(事件继承 collector 用)
  • text.py —— 文本规范化共享工具

核心 Protocol

class LocationResolver(Protocol):
    name: str
    def resolve(self, query: LocationQuery) -> ResolverOutput: ...

LocationPipeline.collect_candidates() 跑全部 resolver聚合所有候选(source_rank, precision_rank, -confidence) 排序去重;resolve_best() 选 top 候选。

各领域管线

# compute_center_locations.py重构后公共 API 不变)
COMPUTE_CENTER_PIPELINE = LocationPipeline([
    SourceCoordinatesResolver(),
])

COMPUTE_CENTER_COLLECTION_PIPELINE = LocationPipeline([
    SourceCoordinatesResolver(),
    ROROrganizationResolver(),
    NominatimResolver(query_plan_builder=_compute_center_query_plan,
                      geocoder=lambda q: _geocode_online(q)),
])

# bgp_collector_locations.py
BGP_COLLECTOR_PIPELINE = LocationPipeline([
    SourceCoordinatesResolver(),
    StoredCollectorLocationResolver(),
])

BGP_COLLECTOR_COLLECTION_PIPELINE = LocationPipeline([
    SourceCoordinatesResolver(),
    NominatimResolver(query_plan_builder=_bgp_collector_query_plan,
                      geocoder=lambda q: _geocode_online(q)),
])

# bgp_event_locations.py
BGP_EVENT_PIPELINE = LocationPipeline([
    SourceCoordinatesResolver(),
    InheritFromAnotherEntityResolver(source_lookup=_inherit_from_owning_collector),
    # 占位:将来插 ASNFacilityResolver / PrefixGeoResolver
])

关键设计决策

  1. 算力中心公共 API 完全不变resolve_compute_center_location()collect_location_candidates()ComputeCenterLocation dataclass、_geocode_online 模块级符号都保留,前端 / 上层调用方零改动;现有 19 个回归测试全绿。
  2. _geocode_online 用 lambda 晚绑定NominatimResolver(geocoder=lambda q: _geocode_online(q)) 能让测试 monkeypatch.setattr(module, "_geocode_online", fake) 继续生效。
  3. RIPE_RIS_COLLECTOR_COORDS 自动从 DB-backed cache 重建:启动时 seed/refresh bgp_collector_locations 维表,再原地刷新旧 {rrcXX → {city, country, lat, lon}} 字典。下游消费者(bgp_collectors.py、序列化、detector不动即可获得新元数据。
  4. 修复隐藏 bugBGP collector 不再通过 registry/operator 模糊匹配晋升候选,避免 operator="RIPE NCC" 让每个事件都落到 rrc00
  5. 事件继承走严格名字查询:事件继承不跑 collector 的完整 pipeline改成直接查 DB-backed cache。"改进位置"用户触发流程只跑源坐标和在线地理编码候选。

Files

新增

  • backend/app/services/location/__init__.py
  • backend/app/services/location/models.py
  • backend/app/services/location/pipeline.py
  • backend/app/services/location/text.py
  • backend/app/services/location/resolvers/__init__.py
  • backend/app/services/location/resolvers/source_coordinates.py
  • backend/app/services/location/resolvers/registry.py
  • backend/app/services/location/resolvers/nominatim.py
  • backend/app/services/location/resolvers/inherit.py
  • backend/app/services/bgp_collector_locations.py
  • backend/app/services/bgp_event_locations.py
  • backend/app/models/bgp_collector_location.py
  • backend/tests/test_location_pipeline.py16 用例)
  • backend/tests/test_bgp_collector_locations.py11 用例)

修改

  • backend/app/services/compute_center_locations.py —— 改为薄包装
  • backend/app/services/collectors/bgp_common.py —— 删除写死字典,改调 resolve_bgp_event_geo_dict()
  • backend/app/api/v1/bgp.py —— 新增 POST /api/v1/bgp/collectors/{collector_id}/collect-location
  • frontend/public/earth/js/info-card.js —— renderComputeCenterCollectSectionrenderLocationCollectSectionBGP collector 走通用化路径
  • frontend/public/earth/js/compute-centers.js —— 新增通用 collectLocationCandidates(endpoint, payload)
  • frontend/public/earth/js/main.js —— previewComputeCenterCandidatepreviewLocationCandidate,事件名改为 earth:preview-location-candidate

Verification

  • uv run pytest backend/tests/test_visualization_compute_centers.py —— 19 个用例全绿(公共 API 未改)
  • uv run pytest backend/tests/test_location_pipeline.py backend/tests/test_bgp_collector_locations.py —— 16 + 11 用例全绿
  • 抽象可插拔性测试:test_pluggability_custom_resolver_works_without_changing_pipeline —— 临时实现 _PeeringDBStubResolver 直接接入 LocationPipeline,验证管线不需要改一行就能识别新 source

Out of scope

  • 持久化用户认领的精确坐标(写回 JSON 注册表)—— suggested_registry_entry 字段已就绪,工作流单独立项
  • 真正实现 ASNFacilityResolver / PrefixGeoResolver —— 接口已留好具体算法peeringdb / IXP 表 / iptoasn 升级)单独立项
  • 算力中心 / 观测站 marker 合并避让 —— 上一轮已用 SURFACE_AVOIDANCE_PROFILES.city + halo 收敛解决