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:
Hermes Team 2026-08-30 20:04:52 +07:00
parent c35bc4868d
commit 7e83c38340
2 changed files with 74 additions and 5 deletions

View file

@ -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)

View file

@ -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}"