fix: geo 收口(模块单例/IPv6测试/降级日志)
- 导出模块级 resolver 单例:Task 6 起 import resolver 使用, 防多实例各持 ~11MB xdb 副本、lru_cache 按实例分片 - 补公网 IPv6→None 测试,锁定 M1 v4-only 决策 - __init__ 降级路径(文件过短/结构无法识别/except)均记 warning 日志,运维可区分「没部署 xdb」与「库 API 崩了」 - 删掉测试未使用的 monkeypatch 参数 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import functools
|
import functools
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
@@ -47,15 +48,22 @@ class GeoResolver:
|
|||||||
with open(settings.IP2REGION_XDB, "rb") as f:
|
with open(settings.IP2REGION_XDB, "rb") as f:
|
||||||
buff = f.read()
|
buff = f.read()
|
||||||
if len(buff) < ip2region_util.HeaderInfoLength:
|
if len(buff) < ip2region_util.HeaderInfoLength:
|
||||||
return # 文件过短,绝非合法 xdb
|
logging.getLogger(__name__).warning(
|
||||||
|
"GeoResolver 降级运行:xdb 文件过短(%d 字节)", len(buff))
|
||||||
|
return
|
||||||
header = ip2region_util.Header(buff[: ip2region_util.HeaderInfoLength])
|
header = ip2region_util.Header(buff[: ip2region_util.HeaderInfoLength])
|
||||||
version = ip2region_util.version_from_header(header)
|
version = ip2region_util.version_from_header(header)
|
||||||
if version is None:
|
if version is None:
|
||||||
return # 无法识别的 xdb 结构/IP 版本
|
logging.getLogger(__name__).warning(
|
||||||
|
"GeoResolver 降级运行:无法识别的 xdb 结构/IP 版本(header=%s)", header)
|
||||||
|
return
|
||||||
# 结构版本同时决定数据段布局:2.0 旧布局 / 3.0 新布局
|
# 结构版本同时决定数据段布局:2.0 旧布局 / 3.0 新布局
|
||||||
self._new_layout = header.version >= ip2region_util.XdbStructure30
|
self._new_layout = header.version >= ip2region_util.XdbStructure30
|
||||||
self._searcher = ip2region_searcher.new_with_buffer(version, buff)
|
self._searcher = ip2region_searcher.new_with_buffer(version, buff)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
# 运维可据此区分"没部署 xdb"与"库 API 崩了"(含堆栈)
|
||||||
|
logging.getLogger(__name__).warning(
|
||||||
|
"GeoResolver 降级运行(xdb 缺失或加载失败)", exc_info=True)
|
||||||
self._searcher = None
|
self._searcher = None
|
||||||
|
|
||||||
@functools.lru_cache(maxsize=10000)
|
@functools.lru_cache(maxsize=10000)
|
||||||
@@ -76,3 +84,8 @@ class GeoResolver:
|
|||||||
return parse_region(raw, self._new_layout)
|
return parse_region(raw, self._new_layout)
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# 进程级共享单例:Task 6 起 from tracker.services.geo import resolver 使用,
|
||||||
|
# 勿再各自实例化 GeoResolver()——每个实例会把 ~11MB xdb 完整读入内存,且 lru_cache 按实例分片。
|
||||||
|
resolver = GeoResolver()
|
||||||
|
|||||||
@@ -14,13 +14,19 @@ def test_private_ip_returns_local():
|
|||||||
assert r.resolve("127.0.0.1") is None
|
assert r.resolve("127.0.0.1") is None
|
||||||
|
|
||||||
|
|
||||||
def test_public_ip_parsed_or_none(monkeypatch):
|
def test_public_ip_parsed_or_none():
|
||||||
r = GeoResolver()
|
r = GeoResolver()
|
||||||
# xdb 未下载/解析失败时优雅降级返回 None,不抛异常
|
# xdb 未下载/解析失败时优雅降级返回 None,不抛异常
|
||||||
out = r.resolve("8.8.8.8")
|
out = r.resolve("8.8.8.8")
|
||||||
assert out is None or set(out) == {"country", "province", "city"}
|
assert out is None or set(out) == {"country", "province", "city"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_ipv6_returns_none():
|
||||||
|
# 锁定 M1 的 v4-only 决策:公网 IPv6 查 v4 库因字节长度不符抛 ValueError,
|
||||||
|
# 降级为 None(防库/数据未来变更悄悄改变行为)
|
||||||
|
assert GeoResolver().resolve("2001:4860:4860::8888") is None
|
||||||
|
|
||||||
|
|
||||||
def test_parse_region_two_layouts():
|
def test_parse_region_two_layouts():
|
||||||
# 旧版 2.0 结构:"国家|区域|省|市|ISP"
|
# 旧版 2.0 结构:"国家|区域|省|市|ISP"
|
||||||
assert parse_region("中国|0|江苏省|南京市|电信", new_layout=False) == {
|
assert parse_region("中国|0|江苏省|南京市|电信", new_layout=False) == {
|
||||||
|
|||||||
Reference in New Issue
Block a user