Files
AlphaPulse/scripts/smoke_ai_recovery.py
T
eezd 8766e829f9
Development CI / check-and-build (push) Successful in 3m3s
GitHub Git mirror / mirror (push) Successful in 6s
Development CI / deploy-development (push) Successful in 2m29s
Coordinate AI retries and recover shared provider outages
Retry transient requests after one and three minutes and coordinate provider recovery across processes with bounded probe intervals. Preserve completed results when recovery bookkeeping fails, keep concurrent failure accounting accurate, and expose retry state in the UI.
2026-11-06 16:25:16 +08:00

76 lines
3.9 KiB
Python

"""在明确指定的隔离 Redis 上验证接入恢复与租约互斥。"""
import asyncio
import os
import uuid
def main():
url = os.environ['AI_RECOVERY_TEST_REDIS_URL']
from redis.asyncio import Redis
from alphapulse.ai.recovery import SCRIPT
async def run():
redis = Redis.from_url(url, decode_responses=True)
import json
key = 'ap:ai:test:' + uuid.uuid4().hex
# 仅替换时钟,实际执行生产 Lua,检查跨客户端的租约互斥。
script = SCRIPT.replace("tonumber(redis.call('TIME')[1])", 'tonumber(ARGV[4])')
async def call(op, identity='', epoch=-1, now=1000):
return json.loads(await redis.eval(script, 1, key, op, identity, epoch, now))
try:
for _ in range(4):
assert (await call('failure', 'same', 0))['level'] == 0
# 四个并发请求先成功一个,其后其他三个失败必须正常计数。
admissions = [await call('enter', name) for name in ('good', 'a', 'b', 'c')]
epoch = admissions[0]['epoch']
await call('success', 'good', epoch)
assert (await call('read'))['epoch'] == epoch
await call('failure', 'a', epoch, 1000)
await call('failure', 'b', epoch, 1001)
opened = await call('failure', 'c', epoch, 1002)
assert opened['level'] == 1 and opened['retry_at'] == 1182
assert not (await call('enter', 'early', now=1181))['admitted']
claims = await asyncio.gather(*(call('enter', str(i), now=1182) for i in range(20)))
assert sum(s['admitted'] for s in claims) == 1
winner = str(next(i for i, s in enumerate(claims) if s['admitted']))
epoch = claims[int(winner)]['epoch']
# 旧失败不推进;失效的并发成功不解除当前故障。
assert (await call('failure', 'late', epoch-1, 1183))['level'] == 1
assert (await call('success', 'late', epoch-1, 1183))['level'] == 1
second = await call('failure', winner, epoch, 1183)
assert second['level'] == 2 and second['retry_at'] == 1783
third = await call('enter', 'probe2', now=1783)
state = await call('failure', 'probe2', third['epoch'], 1784)
assert state['level'] == 3 and state['retry_at'] == 2684
third = await call('enter', 'probe3', now=2684)
state = await call('failure', 'probe3', third['epoch'], 2685)
assert state['retry_at'] == 3585
probe = await call('enter', 'crashed', now=3585)
await call('heartbeat', 'crashed', probe['epoch'], 4000)
assert not (await call('enter', 'other', now=4200))['admitted']
recovered = await call('enter', 'other', now=4661)
assert recovered['admitted']
assert (await call('success', 'other', recovered['epoch'], 4662))['level'] == 0
# 窗口边界:过期失败不积累,显式释放试探不冒充恢复成功。
epoch = (await call('read'))['epoch']
await call('failure', 'a', epoch, 5000)
await call('failure', 'b', epoch, 5061)
assert (await call('failure', 'c', epoch, 5062))['level'] == 0
await call('failure', 'd', epoch, 5063)
probe = await call('enter', 'cancelled', now=5243)
assert probe['admitted']
await call('release', 'cancelled', probe['epoch'], 5244)
assert (await call('enter', 'replacement', now=5244))['admitted']
# 另一个接入不受此故障影响。
independent = await redis.eval(script, 1, key+'-other', 'enter', 'fresh', -1, 5244)
assert json.loads(independent)['admitted']
finally:
await redis.delete(key)
await redis.aclose()
asyncio.run(run())
if __name__ == "__main__":
main()
print("AI recovery atomic acceptance passed")