1717regions (``/settings/regions`` is 404, as it is for cloud-api)."""
1818
1919import asyncio
20- from typing import Callable , List
20+ import socket
21+ from typing import Callable , List , Optional
2122
2223import aiohttp
2324import pytest
2425from aiohttp import web
2526from aiohttp .test_utils import TestServer
2627
2728from livekit .api import CreateRoomRequest , Room , ServerError
29+ from livekit .api ._failover import FAILOVER_MAX_ATTEMPTS , failover_attempts
2830from livekit .api .twirp_client import TwirpClient
2931
3032Handler = Callable [[int , web .Request ], "web.StreamResponse | None" ]
3133
3234
33- async def _call_single_host (behave : Handler , attempts : List [int ]) -> Room :
34- """Runs one CreateRoom against a server whose only origin is itself and
35- appends each attempt index to ``attempts``. ``behave(attempt, request)``
35+ class _StaticResolver (aiohttp .abc .AbstractResolver ):
36+ """Resolves every hostname to the loopback address so a test server can be
37+ reached under an arbitrary name."""
38+
39+ async def resolve (self , host : str , port : int = 0 , family : int = socket .AF_INET ) -> list :
40+ return [
41+ {
42+ "hostname" : host ,
43+ "host" : "127.0.0.1" ,
44+ "port" : port ,
45+ "family" : socket .AF_INET ,
46+ "proto" : 0 ,
47+ "flags" : 0 ,
48+ }
49+ ]
50+
51+ async def close (self ) -> None :
52+ pass
53+
54+
55+ async def _call_single_host (
56+ behave : Handler ,
57+ attempts : List [int ],
58+ * ,
59+ host : str = "127.0.0.1" ,
60+ discovery_hits : Optional [List [None ]] = None ,
61+ ) -> Room :
62+ """Runs one CreateRoom against a server, reached as ``host``, whose only
63+ origin is itself; appends each attempt index to ``attempts`` and each
64+ ``/settings/regions`` hit to ``discovery_hits``. ``behave(attempt, request)``
3665 returns a response, or None to drop the connection (a transport error with
3766 no HTTP response)."""
3867
68+ async def regions (request : web .Request ) -> web .StreamResponse :
69+ if discovery_hits is not None :
70+ discovery_hits .append (None )
71+ raise web .HTTPNotFound ()
72+
3973 async def twirp (request : web .Request ) -> web .StreamResponse :
4074 attempt = len (attempts )
4175 attempts .append (attempt )
@@ -48,13 +82,15 @@ async def twirp(request: web.Request) -> web.StreamResponse:
4882
4983 app = web .Application ()
5084 app .router .add_post ("/twirp/livekit.RoomService/CreateRoom" , twirp )
85+ app .router .add_get ("/settings/regions" , regions )
5186 async with TestServer (app ) as server :
52- async with aiohttp .ClientSession () as session :
87+ connector = aiohttp .TCPConnector (resolver = _StaticResolver ())
88+ async with aiohttp .ClientSession (connector = connector ) as session :
5389 client = TwirpClient (
5490 session ,
55- str ( server .make_url ( "" )) ,
91+ f"http:// { host } : { server .port } " ,
5692 "livekit" ,
57- _failover_force = True ,
93+ _failover_force = host == "127.0.0.1" ,
5894 _failover_backoff = 0.001 ,
5995 )
6096 return await client .request ("RoomService" , "CreateRoom" , CreateRoomRequest (), {}, Room )
@@ -76,6 +112,22 @@ def behave(attempt: int, request: web.Request):
76112 assert len (attempts ) == 2
77113
78114
115+ def test_cloud_api_host_never_consults_region_discovery ():
116+ """A Cloud API host retries the same host without any /settings/regions request."""
117+
118+ def behave (attempt : int , request : web .Request ):
119+ return None if attempt == 0 else _ok (request )
120+
121+ attempts : List [int ] = []
122+ hits : List [None ] = []
123+ room = asyncio .run (
124+ _call_single_host (behave , attempts , host = "cloud-api.livekit.io" , discovery_hits = hits )
125+ )
126+ assert room .name == "r"
127+ assert len (attempts ) == 2
128+ assert hits == []
129+
130+
79131def test_no_regions_does_not_retry_5xx ():
80132 """Without a fallback origin, a 5xx is surfaced, never retried."""
81133
@@ -87,3 +139,23 @@ def behave(attempt: int, request: web.Request):
87139 asyncio .run (_call_single_host (behave , attempts ))
88140 assert exc .value .status == 502
89141 assert len (attempts ) == 1
142+
143+
144+ @pytest .mark .parametrize (
145+ "host, expected" ,
146+ [
147+ ("myproject.livekit.cloud" , FAILOVER_MAX_ATTEMPTS ),
148+ ("myproject.region.livekit.cloud" , FAILOVER_MAX_ATTEMPTS ),
149+ ("myproject.livekit.io" , 1 ),
150+ # The LiveKit Cloud API hosts fail over too (same-host retry).
151+ ("cloud-api.livekit.io" , FAILOVER_MAX_ATTEMPTS ),
152+ ("cloud-api.staging.livekit.io" , FAILOVER_MAX_ATTEMPTS ),
153+ ("CLOUD-API.LIVEKIT.IO" , FAILOVER_MAX_ATTEMPTS ),
154+ ("cloud-api.example.com" , 1 ),
155+ ("example.com" , 1 ),
156+ ("127.0.0.1" , 1 ),
157+ ("notlivekit.cloud" , 1 ),
158+ ],
159+ )
160+ def test_failover_attempts (host : str , expected : int ):
161+ assert failover_attempts (True , host ) == expected
0 commit comments