mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-30 19:33:45 +08:00
192 lines
7.6 KiB
Python
192 lines
7.6 KiB
Python
"""Offline quota and request-coalescing regressions."""
|
|
import importlib.util
|
|
import io
|
|
import threading
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from pathlib import Path
|
|
from urllib.error import HTTPError
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def history():
|
|
path = Path(__file__).resolve().parents[1] / 'version_history.py'
|
|
spec = importlib.util.spec_from_file_location('version_rate_limits_under_test', path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
@pytest.mark.parametrize('transport', ['api', 'raw'])
|
|
@pytest.mark.parametrize('headers,deadline', [({'Retry-After': '180'}, 1180),
|
|
({'X-RateLimit-Remaining': '0', 'X-RateLimit-Reset': '4600'}, 4600), ({}, 1060)])
|
|
def test_full_server_backoff_blocks_all_urls_until_deadline(history, monkeypatch, transport, headers, deadline):
|
|
clock = [1000.0]
|
|
monkeypatch.setattr(history.time, 'time', lambda: clock[0])
|
|
monkeypatch.setattr(history.time, 'monotonic', lambda: clock[0])
|
|
calls = []
|
|
def limited(request, **kwargs):
|
|
calls.append(request.full_url)
|
|
if len(calls) == 1:
|
|
raise HTTPError(request.full_url, 429, 'Limited', headers, io.BytesIO())
|
|
return io.BytesIO(b'{}' if transport == 'api' else b'STARPILOT_DISPLAY_VERSION = "6.7.7"')
|
|
monkeypatch.setattr(history, '_open_url' if transport == 'api' else '_open_raw_url', limited)
|
|
fetch = history._get_json if transport == 'api' else history._get_display_version
|
|
with pytest.raises(history.HistoryUnavailable):
|
|
fetch('https://example.test/first')
|
|
clock[0] = deadline - 1
|
|
with pytest.raises(history.HistoryUnavailable):
|
|
fetch('https://example.test/second')
|
|
assert len(calls) == 1
|
|
clock[0] = deadline
|
|
fetch('https://example.test/second')
|
|
assert len(calls) == 2
|
|
|
|
|
|
@pytest.mark.parametrize('transport', ['api', 'raw'])
|
|
def test_concurrent_identical_cache_misses_download_once(history, monkeypatch, transport):
|
|
start = threading.Barrier(3)
|
|
entered, duplicate, release = threading.Event(), threading.Event(), threading.Event()
|
|
calls = []
|
|
def response(*args, **kwargs):
|
|
calls.append(1)
|
|
entered.set()
|
|
if len(calls) > 1:
|
|
duplicate.set()
|
|
assert release.wait(timeout=0.8)
|
|
return {} if transport == 'api' else '6.7.7'
|
|
monkeypatch.setattr(history, '_get_json' if transport == 'api' else '_get_display_version', response)
|
|
def caller():
|
|
start.wait(timeout=0.8)
|
|
if transport == 'api':
|
|
return history._json('https://api.github.com/repos/a/b/commits')
|
|
return history._display_version('https://api.github.com/repos/a/b', 'a' * 40)
|
|
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
futures = [executor.submit(caller) for _ in range(2)]
|
|
start.wait(timeout=0.8)
|
|
try:
|
|
assert entered.wait(timeout=0.8)
|
|
duplicate.wait(timeout=0.1)
|
|
finally:
|
|
release.set()
|
|
assert futures[0].result(timeout=0.8) == futures[1].result(timeout=0.8)
|
|
assert len(calls) == 1
|
|
|
|
|
|
def test_raw_quota_does_not_block_api_or_cached_raw(history, monkeypatch):
|
|
base, sha = 'https://api.github.com/repos/a/b', 'a' * 40
|
|
monkeypatch.setattr(history, '_open_raw_url', lambda *a, **kw: io.BytesIO(b'STARPILOT_DISPLAY_VERSION = "6.7.7"'))
|
|
assert history._display_version(base, sha) == '6.7.7'
|
|
def limited(request, **kwargs):
|
|
raise HTTPError(request.full_url, 429, 'Limited', {'Retry-After': '180'}, io.BytesIO())
|
|
monkeypatch.setattr(history, '_open_raw_url', limited)
|
|
with pytest.raises(history.HistoryUnavailable):
|
|
history._display_version(base, 'b' * 40)
|
|
assert history._display_version(base, sha) == '6.7.7'
|
|
monkeypatch.setattr(history, '_open_url', lambda *a, **kw: io.BytesIO(b'{}'))
|
|
assert history._get_json(base) == {}
|
|
|
|
|
|
def test_fresh_api_calls_do_not_reuse_cached_head(history, monkeypatch):
|
|
calls = []
|
|
def response(url):
|
|
calls.append(url)
|
|
return {'call': len(calls)}
|
|
monkeypatch.setattr(history, '_get_json', response)
|
|
assert history._json('head') == {'call': 1}
|
|
assert history._json('head', fresh=True) == {'call': 2}
|
|
assert history._json('head', fresh=True) == {'call': 3}
|
|
|
|
|
|
def test_later_shorter_raw_response_cannot_shorten_active_backoff(history, monkeypatch):
|
|
clock = [1000.0]
|
|
monkeypatch.setattr(history.time, 'time', lambda: clock[0])
|
|
monkeypatch.setattr(history.time, 'monotonic', lambda: clock[0])
|
|
started, long_finished = threading.Barrier(2), threading.Event()
|
|
calls = []
|
|
def limited(request, **kwargs):
|
|
calls.append(request.full_url)
|
|
started.wait(timeout=0.8)
|
|
if request.full_url.endswith('short'):
|
|
assert long_finished.wait(timeout=0.8)
|
|
wait = '180' if request.full_url.endswith('long') else '60'
|
|
raise HTTPError(request.full_url, 429, 'Limited', {'Retry-After': wait}, io.BytesIO())
|
|
monkeypatch.setattr(history, '_open_raw_url', limited)
|
|
def caller(suffix):
|
|
with pytest.raises(history.HistoryUnavailable):
|
|
history._get_display_version('https://raw.githubusercontent.com/' + suffix)
|
|
if suffix == 'long':
|
|
long_finished.set()
|
|
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
futures = [executor.submit(caller, suffix) for suffix in ['long', 'short']]
|
|
for future in futures:
|
|
future.result(timeout=0.8)
|
|
clock[0] = 1179
|
|
with pytest.raises(history.HistoryUnavailable):
|
|
history._get_display_version('https://raw.githubusercontent.com/third')
|
|
assert len(calls) == 2
|
|
|
|
|
|
def test_raw_worker_waiting_for_slot_observes_new_quota(history, monkeypatch):
|
|
entered, queued, release = threading.Event(), threading.Event(), threading.Event()
|
|
slots, gate_lock = threading.Semaphore(1), threading.Lock()
|
|
counts = {'attempts': 0, 'http': 0}
|
|
class Gate:
|
|
def __enter__(self):
|
|
with gate_lock:
|
|
counts['attempts'] += 1
|
|
if counts['attempts'] == 2:
|
|
queued.set()
|
|
assert slots.acquire(timeout=0.8)
|
|
def __exit__(self, *args):
|
|
slots.release()
|
|
monkeypatch.setattr(history, '_version_slots', Gate())
|
|
def limited(request, **kwargs):
|
|
counts['http'] += 1
|
|
entered.set()
|
|
assert release.wait(timeout=0.8)
|
|
raise HTTPError(request.full_url, 429, 'Limited', {'Retry-After': '180'}, io.BytesIO())
|
|
monkeypatch.setattr(history, '_open_raw_url', limited)
|
|
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
first = executor.submit(history._display_version, 'https://api.github.com/repos/a/b', 'a' * 40)
|
|
assert entered.wait(timeout=0.8)
|
|
second = executor.submit(history._display_version, 'https://api.github.com/repos/a/b', 'b' * 40)
|
|
try:
|
|
assert queued.wait(timeout=0.8)
|
|
finally:
|
|
release.set()
|
|
for future in (first, second):
|
|
with pytest.raises(history.HistoryUnavailable):
|
|
future.result(timeout=0.8)
|
|
assert counts['http'] == 1
|
|
|
|
|
|
def test_distinct_api_requests_serialize_and_observe_first_quota(history, monkeypatch):
|
|
start = threading.Barrier(3)
|
|
entered, duplicate, release = threading.Event(), threading.Event(), threading.Event()
|
|
calls = []
|
|
def limited(request, **kwargs):
|
|
calls.append(request.full_url)
|
|
entered.set()
|
|
if len(calls) > 1:
|
|
duplicate.set()
|
|
assert release.wait(timeout=0.8)
|
|
raise HTTPError(request.full_url, 429, 'Limited', {'Retry-After': '180'}, io.BytesIO())
|
|
monkeypatch.setattr(history, '_open_url', limited)
|
|
def caller(suffix):
|
|
start.wait(timeout=0.8)
|
|
return history._get_json('https://api.github.com/repos/a/b/' + suffix)
|
|
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
futures = [executor.submit(caller, suffix) for suffix in ['branches/main', 'commits']]
|
|
start.wait(timeout=0.8)
|
|
try:
|
|
assert entered.wait(timeout=0.8)
|
|
duplicate.wait(timeout=0.1)
|
|
finally:
|
|
release.set()
|
|
for future in futures:
|
|
with pytest.raises(history.HistoryUnavailable):
|
|
future.result(timeout=0.8)
|
|
assert len(calls) == 1
|