diff --git a/src/antigravity_provider/router/adapters/antigravity_adapter.py b/src/antigravity_provider/router/adapters/antigravity_adapter.py index 3bf94ff..c2b8951 100644 --- a/src/antigravity_provider/router/adapters/antigravity_adapter.py +++ b/src/antigravity_provider/router/adapters/antigravity_adapter.py @@ -27,9 +27,20 @@ from ..router_config import RouterProfileConfig from .base_adapter import BaseProviderAdapter, ErrorCategory, ErrorClassification -import threading - -_AGY_INVOCATION_LOCK = threading.RLock() +# Глобального мьютекса здесь больше нет. +# +# Он был введён в 50fde5f, чтобы защитить ГЛОБАЛЬНУЮ подмену учётных данных +# gemini:antigravity от гонки между подпроцессами. С тех пор подмена перестала +# быть глобальной: agy_subprocess пишет в profile_dir/.gemini/oauth_creds.json, +# а HOME, USERPROFILE, HOMEPATH и HOMEDRIVE подменяются на каталог профиля. +# Общего состояния между профилями не осталось, и мьютекс охранял пустоту. +# +# Стоил он при этом дорого: измерено, что три параллельных вызова по одной +# секунде занимали 3.01 с. То есть из десяти аккаунтов Antigravity +# одновременно работал ровно один, чем обесценивалась вся мультиаккаунтность. +# +# Ограничение одновременности остаётся за LeaseManager, который считает лизы +# ПО ПРОФИЛЮ и настраивается через max_concurrency. def get_profile_env_dir(profile_id: str) -> Path: @@ -77,8 +88,7 @@ class AntigravityAdapter(BaseProviderAdapter): profile_id=profile.profile_id, ) - with _AGY_INVOCATION_LOCK: - res = agy_generate(req, custom_env=custom_env, profile_id=profile.profile_id) + res = agy_generate(req, custom_env=custom_env, profile_id=profile.profile_id) else: res = agy_generate(req, custom_env=custom_env, profile_id=profile.profile_id) diff --git a/tests/test_antigravity_concurrency.py b/tests/test_antigravity_concurrency.py index 78e9ba5..803fd76 100644 --- a/tests/test_antigravity_concurrency.py +++ b/tests/test_antigravity_concurrency.py @@ -77,3 +77,62 @@ def test_credential_restoration_on_subprocess_exception(tmp_path, monkeypatch): # Must be cleanly restored assert True + +def test_antigravity_profiles_run_in_parallel(): + """Вызовы разных профилей Antigravity идут одновременно, а не по очереди. + + В adapters/antigravity_adapter.py жил модульный _AGY_INVOCATION_LOCK. Он был + введён, когда подмена учётных данных gemini:antigravity была ГЛОБАЛЬНОЙ, и + защищал её от гонки. Позже подмена стала попрофильной — agy_subprocess + пишет в profile_dir/.gemini/oauth_creds.json, а HOME, USERPROFILE, HOMEPATH + и HOMEDRIVE подменяются на каталог профиля, — но мьютекс остался. + + Цена измерялась: три параллельных вызова по одной секунде занимали 3.01 с. + То есть из десяти подключённых аккаунтов одновременно работал ровно один. + + Тест удерживает это свойство: три вызова по 0.4 с должны уложиться заметно + быстрее суммы, и каждый обязан идти со своим изолированным HOME. + """ + import threading + import time + from unittest.mock import patch + + import antigravity_provider.router.adapters.antigravity_adapter as adapter_mod + + auth = {"token": {"access_token": "a", "refresh_token": "r", "expires_at": 9999999999}} + + class _Profile: + def __init__(self, pid): + self.profile_id = pid + self.preferred_models = ["gemini-3.7-flash"] + self.provider = "antigravity" + + homes = [] + + def _slow(req, custom_env=None, profile_id=None): + env = custom_env or {} + homes.append(env.get("HOME") or env.get("USERPROFILE")) + time.sleep(0.4) + return {"content": "ok"} + + with patch.object(adapter_mod, "agy_generate", side_effect=_slow), patch.object( + adapter_mod.ProfileAuthManager, "load_profile_auth", return_value=auth + ): + started = time.time() + threads = [ + threading.Thread(target=lambda pid=f"ag-w{i}": adapter_mod.AntigravityAdapter().invoke( + _Profile(pid), {"messages": []} + )) + for i in range(1, 4) + ] + for t in threads: + t.start() + for t in threads: + t.join() + elapsed = time.time() - started + + assert elapsed < 0.9, ( + f"вызовы разных профилей сериализуются ({elapsed:.2f} с при трёх по 0.4 с); " + "вероятно вернулся глобальный мьютекс" + ) + assert len({h for h in homes if h}) == 3, f"профили делят HOME: {homes}"