resolve_rolling_count builds a Valkey client per field resolution
- Dominant language
- Python
- Stars
- 670
- Forks
- 183
- Avg merge
- 17h 7m
- Merged PRs (30d)
- 358
Description
## Motivation
`KeyPair.resolve_rolling_count` builds and tears down a whole Valkey client on every field
resolution:
```python
# src/ai/backend/manager/api/gql_legacy/keypair.py:199
async def resolve_rolling_count(self, info: graphene.ResolveInfo) -> int:
ctx: GraphQueryContext = info.context
valkey_profile_target = ctx.config_provider.config.redis.to_valkey_profile_target()
valkey_target = valkey_profile_target.profile_target(RedisRole.RATE_LIMIT)
valkey_client = await ValkeyRateLimitClient.create(
valkey_target=valkey_target,
db_id=REDIS_RATE_LIMIT_DB,
human_readable_name="ratelimit",
)
try:
rlim_window = await valkey_client.get_user_rlim_window(UserID(self.user))
return rlim_window.count if rlim_window is not None else 0
finally:
await valkey_client.close()
```
`create()` goes through `create_valkey_client()`, which builds **two** GLIDE clients — an
operation client and a monitor client — and `MonitoringValkeyClient.connect()` then starts a
background monitor task. All of that serves a single read, and it is repeated for every keypair
in the result, on every query that selects `rolling_count`.
This is the only request-path caller of `ValkeyRateLimitClient.create()`. The sibling resolvers on
the same type read a long-lived client off the context (`ctx.valkey_stat` at lines 197 and 243),
and the manager already holds a long-lived rate-limit client
(`manager/dependencies/infrastructure/redis.py:142`) that is passed to the REST layer at
`api/rest/setup.py:75`.
## Required Features
Resolve `rolling_count` from the long-lived client instead of a per-request one:
1. Add a `valkey_rate_limit: ValkeyRateLimitClient` field to `GraphQueryContext`
(`api/gql_legacy/schema.py:318`), alongside `valkey_stat` / `valkey_live` / `valkey_image` /
`valkey_schedule`.
2. Carry `r.infrastructure.valkey.rate_limit` through `GQLContextDeps` (`api/rest/types.py:59`,
populated in `api/rest/setup.py`) into the `GraphQueryContext(...)` built in
`api/rest/admin/handler.py` — reuse the existing client rather than creating a second one.
3. Reduce `resolve_rolling_count` to a single `get_user_rlim_window()` call on
`ctx.valkey_rate_limit`.
## Impact
| Area | Effect |
|---|---|
| `manager/api/gql_legacy/keypair.py` | `resolve_rolling_count` loses its client lifecycle |
| `manager/api/gql_legacy/schema.py` | One new `GraphQueryContext` field |
| `manager/api/rest/types.py`, `setup.py`, `admin/handler.py` | Pass the existing rate-limit client through |
| GraphQL `keypair`/`keypairs` queries selecting `rolling_count` | Two fewer GLIDE connections and one fewer monitor task per keypair |
No schema, config, or API-surface change. `rolling_count` values are unaffected — same client
type, same DB, same command.
## Testing Scenarios
| # | Scenario | Expected |
|---|---|---|
| 1 | Query `rolling_count` for a keypair with Valkey healthy | Same value as before the change |
| 2 | Query `rolling_count` for N keypairs | No client is created or closed during resolution; the context client is used |
| 3 | Query the sibling fields (`num_queries`, `last_used`) | Unchanged — they already read from the context |
Contributor guide
Research direction
Start with resolve_rolling_count in manager/api/gql_legacy/keypair.py and the GraphQueryContext definition in api/gql_legacy/schema.py, then trace GQLContextDeps through api/rest/types.py, api/rest/setup.py, and api/rest/admin/handler.py. Confirm the existing rate-limit client reaches the GraphQL context, no client is created or closed during resolution, and the listed rolling_count and sibling-field scenarios retain their expected behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, redis
- Domain
- api, backend
- Issue type
- Refactor
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100