mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-29 01:42:19 +00:00
fix(proxy): evict email-only member caches and reset team members metric on delete
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
0a023a4e1a
commit
387cf21fff
2 changed files with 45 additions and 6 deletions
|
|
@ -4548,10 +4548,12 @@ async def delete_team(
|
|||
)
|
||||
await _invalidate_deleted_team_member_cache(
|
||||
teams=team_rows,
|
||||
prisma_client=prisma_client,
|
||||
user_api_key_cache=user_api_key_cache,
|
||||
)
|
||||
|
||||
for deleted_team in team_rows:
|
||||
_emit_team_members_metric(deleted_team.model_copy(update={"members_with_roles": []}))
|
||||
await sync_team_access_group_membership(prisma_client=prisma_client, team_id=deleted_team.team_id)
|
||||
|
||||
return deleted_teams
|
||||
|
|
@ -4628,16 +4630,23 @@ async def _invalidate_deleted_team_cache(
|
|||
|
||||
async def _invalidate_deleted_team_member_cache(
|
||||
teams: Sequence[LiteLLM_TeamTable],
|
||||
prisma_client: PrismaClient,
|
||||
user_api_key_cache: UserApiKeyCache,
|
||||
) -> None:
|
||||
for team in teams:
|
||||
await _evict_deleted_team_member_cache(team=team, user_api_key_cache=user_api_key_cache)
|
||||
await _evict_deleted_team_member_cache(
|
||||
team=team,
|
||||
prisma_client=prisma_client,
|
||||
user_api_key_cache=user_api_key_cache,
|
||||
)
|
||||
|
||||
|
||||
async def _evict_deleted_team_member_cache(team: LiteLLM_TeamTable, user_api_key_cache: UserApiKeyCache) -> None:
|
||||
member_user_ids: Final = tuple(
|
||||
sorted({member.user_id for member in team.members_with_roles if member.user_id is not None})
|
||||
)
|
||||
async def _evict_deleted_team_member_cache(
|
||||
team: LiteLLM_TeamTable,
|
||||
prisma_client: PrismaClient,
|
||||
user_api_key_cache: UserApiKeyCache,
|
||||
) -> None:
|
||||
member_user_ids: Final = await _deleted_team_member_user_ids(team=team, prisma_client=prisma_client)
|
||||
await evict_and_broadcast(cache_keys=member_user_ids, user_api_key_cache=user_api_key_cache)
|
||||
await asyncio.gather(
|
||||
*(
|
||||
|
|
@ -4651,6 +4660,25 @@ async def _evict_deleted_team_member_cache(team: LiteLLM_TeamTable, user_api_key
|
|||
)
|
||||
|
||||
|
||||
async def _deleted_team_member_user_ids(team: LiteLLM_TeamTable, prisma_client: PrismaClient) -> tuple[str, ...]:
|
||||
roster_user_ids: Final = frozenset(
|
||||
member.user_id for member in team.members_with_roles if member.user_id is not None
|
||||
)
|
||||
email_only_member_emails: Final = sorted(
|
||||
{
|
||||
member.user_email
|
||||
for member in team.members_with_roles
|
||||
if member.user_id is None and member.user_email is not None
|
||||
}
|
||||
)
|
||||
if not email_only_member_emails:
|
||||
return tuple(sorted(roster_user_ids))
|
||||
email_only_users: Final = await _user_db(prisma_client).find_many(
|
||||
where={"user_email": {"in": email_only_member_emails, "mode": "insensitive"}}
|
||||
)
|
||||
return tuple(sorted(roster_user_ids.union(user.user_id for user in email_only_users)))
|
||||
|
||||
|
||||
def _transform_teams_to_deleted_records(
|
||||
teams: list[LiteLLM_TeamTable],
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
|
|
|
|||
|
|
@ -9032,7 +9032,8 @@ async def test_delete_team_evicts_member_caches_with_one_transaction(
|
|||
team = LiteLLM_TeamTable(
|
||||
team_id="team-doomed",
|
||||
team_alias="doomed-team",
|
||||
members_with_roles=[Member(user_id=user_id, role="user") for user_id in member_user_ids],
|
||||
members_with_roles=[Member(user_id=user_id, role="user") for user_id in member_user_ids]
|
||||
+ [Member(user_id=None, user_email="invitee@example.com", role="user")],
|
||||
metadata={},
|
||||
model_max_budget={},
|
||||
model_spend={},
|
||||
|
|
@ -9046,6 +9047,9 @@ async def test_delete_team_evicts_member_caches_with_one_transaction(
|
|||
mock_prisma_client.db.litellm_verificationtoken.find_many = AsyncMock(return_value=[])
|
||||
mock_prisma_client.db.execute_raw = AsyncMock()
|
||||
mock_prisma_client.db.litellm_teammembership.delete_many = AsyncMock()
|
||||
mock_prisma_client.db.litellm_usertable.find_many = AsyncMock(
|
||||
return_value=[SimpleNamespace(user_id="invited-user", user_email="invitee@example.com")]
|
||||
)
|
||||
|
||||
mock_tx = AsyncMock()
|
||||
mock_tx.litellm_proxymodeltable.find_many = AsyncMock(return_value=[])
|
||||
|
|
@ -9058,6 +9062,7 @@ async def test_delete_team_evicts_member_caches_with_one_transaction(
|
|||
fresh_cache = UserApiKeyCache()
|
||||
for user_id in member_user_ids:
|
||||
fresh_cache.set_cache(key=user_id, value=UserAPIKeyAuth(user_id=user_id))
|
||||
fresh_cache.set_cache(key="invited-user", value=UserAPIKeyAuth(user_id="invited-user"))
|
||||
fresh_cache.set_cache(key="bystander-user", value=UserAPIKeyAuth(user_id="bystander-user"))
|
||||
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||
|
|
@ -9085,6 +9090,12 @@ async def test_delete_team_evicts_member_caches_with_one_transaction(
|
|||
assert fresh_cache.get_cache(key=user_id) is None, (
|
||||
f"member {user_id}'s cached user object survived the team delete"
|
||||
)
|
||||
assert fresh_cache.get_cache(key="invited-user") is None, (
|
||||
"the email-only roster entry resolves to invited-user, whose cached user object must be evicted too"
|
||||
)
|
||||
mock_prisma_client.db.litellm_usertable.find_many.assert_awaited_once_with(
|
||||
where={"user_email": {"in": ["invitee@example.com"], "mode": "insensitive"}}
|
||||
)
|
||||
assert fresh_cache.get_cache(key="bystander-user") is not None
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue