128 lines
7.4 KiB
Markdown
128 lines
7.4 KiB
Markdown
# Location Resolver Shared Pipeline Plan
|
||
|
||
**状态**:已实现,当前用户流程见 [Earth 位置候选采集使用手册](/home/ray/dev/linkong/planet/docs/technical/zh/location-pipeline-user.md),开发接口见 [通用位置估算管线开发说明](/home/ray/dev/linkong/planet/docs/technical/zh/location-pipeline-development.md)。
|
||
|
||
## 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:
|
||
|
||
```python
|
||
class LocationResolver(Protocol):
|
||
name: str
|
||
def resolve(self, query: LocationQuery) -> ResolverOutput: ...
|
||
```
|
||
|
||
`LocationPipeline.collect_candidates()` 跑全部 resolver,聚合所有候选,按 `(source_rank, precision_rank, -confidence)` 排序去重;`resolve_best()` 选 top 候选。
|
||
|
||
### 各领域管线
|
||
|
||
```python
|
||
# 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. **修复隐藏 bug**:BGP 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.py`(16 用例)
|
||
- `backend/tests/test_bgp_collector_locations.py`(11 用例)
|
||
|
||
### 修改
|
||
- `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` —— `renderComputeCenterCollectSection` → `renderLocationCollectSection`,BGP collector 走通用化路径
|
||
- `frontend/public/earth/js/compute-centers.js` —— 新增通用 `collectLocationCandidates(endpoint, payload)`
|
||
- `frontend/public/earth/js/main.js` —— `previewComputeCenterCandidate` → `previewLocationCandidate`,事件名改为 `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 收敛解决
|