perf(antigravity): из десяти аккаунтов одновременно работал один
В adapters/antigravity_adapter.py жил модульный _AGY_INVOCATION_LOCK, общий
для ВСЕХ профилей Antigravity. Он брался при каждом вызове, у которого есть
учётные данные, то есть при каждом рабочем. Ветка без мьютекса срабатывала
только у профиля без учётки — у вызова, который и так упадёт.
Измерено на живом адаптере: три параллельных вызова по одной секунде
занимали 3.01 с. При десяти подключённых аккаунтах одновременно работал ровно
один, чем обесценивалась вся мультиаккаунтность — то, ради чего хаб и делался.
Мьютекс охранял пустоту. Он был введён в 50fde5f со словами «guarded global
gemini:antigravity credential swap ... to eliminate concurrent subprocess
race», когда подмена учётных данных была ГЛОБАЛЬНОЙ. С тех пор она стала
попрофильной: agy_subprocess пишет в profile_dir/.gemini/oauth_creds.json, а
HOME, USERPROFILE, HOMEPATH и HOMEDRIVE подменяются на каталог профиля.
Общего состояния между профилями не осталось — проверено поиском глобальных
путей и обращений к keyring, их нет.
После снятия: те же три вызова занимают 1.00 с, и каждый идёт со своим HOME
(ag-w1, ag-w2, ag-w3) — изоляция не пострадала.
Ограничение одновременности остаётся за LeaseManager: он считает лизы по
профилю и настраивается через max_concurrency, в том числе значением 1 для
локальных моделей с --parallel 1.
Добавлен тест, удерживающий это свойство: он падает, если вызовы разных
профилей снова начнут сериализоваться. Существующий тест изоляции учётных
данных проходит без изменений.
Найдено при разборе анализа, который Antigravity провёл на сервере владельца
(hermes-muliacount). 459 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
c35bc4868d
commit
7e83c38340
2 changed files with 74 additions and 5 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
Loading…
Reference in a new issue