106 lines
3.2 KiB
Python
106 lines
3.2 KiB
Python
#!/usr/bin/env python3
|
|||
|
|
"""When Resi says "too many requests" (429), the bridge must wait, not ask again next poll.
|
||
|
|
|
||
|
|
Seen at North Attleboro: two tools polling one Resi account every second hit its rate
|
||
|
|
limit, and because neither backed off, Resi kept refusing and the ProPresenter
|
||
|
|
countdown stopped. Runs the real loop on a fake clock against a fake Resi; touches
|
||
|
|
nothing real. Takes a second.
|
||
|
|
|
||
|
|
python3 test_rate_limit.py
|
||
|
|
"""
|
||
|
|
import io
|
||
|
|
import urllib.error
|
||
|
|
|
||
|
|
import resi_to_propresenter as R
|
||
|
|
|
||
|
|
clock = [0.0]
|
||
|
|
asks = []
|
||
|
|
sleeps = []
|
||
|
|
|
||
|
|
|
||
|
|
class Done(BaseException):
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
def fake_sleep(s):
|
||
|
|
sleeps.append(s)
|
||
|
|
clock[0] += s
|
||
|
|
if clock[0] > 600: # ten simulated minutes is plenty
|
||
|
|
raise Done()
|
||
|
|
|
||
|
|
|
||
|
|
def too_many():
|
||
|
|
return urllib.error.HTTPError("https://central.resi.io/x", 429, "Too Many Requests",
|
||
|
|
{"Retry-After": "Wed, 21 Oct 2026 07:28:00 GMT"}, io.BytesIO())
|
||
|
|
|
||
|
|
|
||
|
|
class LimitedResi:
|
||
|
|
limited_until = 300 # Resi refuses for the first five minutes
|
||
|
|
|
||
|
|
def __init__(self, *a, **k):
|
||
|
|
self.no_cache_bust = False
|
||
|
|
|
||
|
|
def login(self):
|
||
|
|
pass
|
||
|
|
|
||
|
|
def venues(self):
|
||
|
|
return [{"uuid": "V1"}]
|
||
|
|
|
||
|
|
def players(self, venue_uuid):
|
||
|
|
asks.append(clock[0])
|
||
|
|
if clock[0] < self.limited_until:
|
||
|
|
raise too_many()
|
||
|
|
return [{"playerHardwareId": "DEC1", "userName": "Test", "position": "00:10:00",
|
||
|
|
"eventId": None}]
|
||
|
|
|
||
|
|
|
||
|
|
class QuietPP:
|
||
|
|
def __init__(self, *a, **k):
|
||
|
|
pass
|
||
|
|
|
||
|
|
def check(self):
|
||
|
|
pass
|
||
|
|
|
||
|
|
def __getattr__(self, name):
|
||
|
|
return lambda *a, **k: True
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
R.time = type("T", (), {"time": staticmethod(lambda: clock[0]),
|
||
|
|
"sleep": staticmethod(fake_sleep),
|
||
|
|
"strftime": staticmethod(lambda f: "00:00:00")})
|
||
|
|
R.now_ms = lambda: clock[0] * 1000
|
||
|
|
R.Resi = LimitedResi
|
||
|
|
R.ProPresenter = QuietPP
|
||
|
|
R.keychain_password = lambda account: "x"
|
||
|
|
R.find_player = lambda resi, wanted: ("V1", {"playerHardwareId": "DEC1", "userName": "Test"})
|
||
|
|
R.log = lambda msg: None
|
||
|
|
cfg = dict(R.DEFAULTS)
|
||
|
|
cfg.update({"resi_user": "t@example.com", "player": "DEC1", "propresenter": "http://x"})
|
||
|
|
try:
|
||
|
|
R.run(cfg, False)
|
||
|
|
except Done:
|
||
|
|
pass
|
||
|
|
|
||
|
|
failures = []
|
||
|
|
limited = [t for t in asks if t < LimitedResi.limited_until]
|
||
|
|
print("asked Resi %d times in the 5 rate-limited minutes (every poll would be %d)"
|
||
|
|
% (len(limited), int(LimitedResi.limited_until / cfg["poll_seconds"])))
|
||
|
|
if len(limited) > 12:
|
||
|
|
failures.append("kept asking while rate-limited: %d times" % len(limited))
|
||
|
|
after = [t for t in asks if t >= LimitedResi.limited_until]
|
||
|
|
if not after:
|
||
|
|
failures.append("never asked again once Resi would answer")
|
||
|
|
elif after[0] - LimitedResi.limited_until > 125:
|
||
|
|
failures.append("waited %.0fs too long to recover" % (after[0] - LimitedResi.limited_until))
|
||
|
|
steady = [b - a for a, b in zip(after, after[1:])]
|
||
|
|
if steady and max(steady) > cfg["poll_seconds"] + 0.5:
|
||
|
|
failures.append("did not return to the normal poll once answered (gaps %s)" % steady[:5])
|
||
|
|
if failures:
|
||
|
|
raise SystemExit("FAILED:\n - " + "\n - ".join(failures))
|
||
|
|
print("rate limit tests passed")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|