Skip to content

Conversation

@HyeockJinKim
Copy link
Collaborator

Replace individual fetch_*() calls with core entity DataLoaders (domain_loader, project_loader, user_loader) in fair share GQL types to prevent N+1 queries. Also document cross-entity reference resolver pattern with strawberry.lazy() in api-guide skill.

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com

resolves #NNN (BA-MMM)

Checklist: (if applicable)

  • Milestone metadata specifying the target backport version
  • Mention to the original issue
  • Installer updates including:
    • Fixtures for db schema changes
    • New mandatory config options
  • Update of end-to-end CLI integration tests in ai.backend.test
  • API server-client counterparts (e.g., manager API -> client SDK)
  • Test case(s) to:
    • Demonstrate the difference of before/after
    • Demonstrate the flow of abstract/conceptual models with a concrete implementation
  • Documentation
    • Contents in the docs directory
    • docstrings in public interfaces and type annotations

…resolvers

Replace individual fetch_*() calls with core entity DataLoaders
(domain_loader, project_loader, user_loader) in fair share GQL types
to prevent N+1 queries. Also document cross-entity reference resolver
pattern with strawberry.lazy() in api-guide skill.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 9, 2026 23:19
@github-actions github-actions bot added size:L 100~500 LoC comp:manager Related to Manager component labels Feb 9, 2026
Co-authored-by: octodog <mu001@lablup.com>
@github-actions github-actions bot added the area:docs Documentations label Feb 9, 2026
@HyeockJinKim HyeockJinKim added this pull request to the merge queue Feb 9, 2026
github-merge-queue bot pushed a commit that referenced this pull request Feb 9, 2026
…resolvers (#8702)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: octodog <mu001@lablup.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the fair share GraphQL node back-reference resolvers (domain/project/user) to use core entity DataLoaders to prevent N+1 query patterns, and documents the recommended cross-entity resolver pattern using strawberry.lazy().

Changes:

  • Replace fetch_*() calls with info.context.data_loaders.{domain,project,user}_loader.load(...) in fair share type resolvers.
  • Convert loaded data into *V2GQL nodes via from_data(...) constructors.
  • Add an API guide section documenting cross-entity reference resolver patterns (lazy types + DataLoaders).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 9 comments.

File Description
src/ai/backend/manager/api/gql/fair_share/types/user.py Switch fair-share → user back-reference resolver to DataLoader-based loading.
src/ai/backend/manager/api/gql/fair_share/types/project.py Switch fair-share → project back-reference resolver to DataLoader-based loading.
src/ai/backend/manager/api/gql/fair_share/types/domain.py Switch fair-share → domain back-reference resolver to DataLoader-based loading.
.claude/skills/api-guide/SKILL.md Document cross-entity resolver pattern (strawberry.lazy() + DataLoader).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +69 to +80
) -> (
Annotated[
DomainV2GQL,
strawberry.lazy("ai.backend.manager.api.gql.domain_v2.types.node"),
]
| None
):
from ai.backend.manager.api.gql.domain_v2.types.node import DomainV2GQL

domain_data = await info.context.data_loaders.domain_loader.load(self.domain_name)
if domain_data is None:
return None
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DataLoader returns DomainData | None; calling DomainV2GQL.from_data() is only safe when data is present. Instead of returning None here, raise a not-found error (matching the previous fetch_domain() behavior) so missing domains don’t get silently swallowed.

Suggested change
) -> (
Annotated[
DomainV2GQL,
strawberry.lazy("ai.backend.manager.api.gql.domain_v2.types.node"),
]
| None
):
from ai.backend.manager.api.gql.domain_v2.types.node import DomainV2GQL
domain_data = await info.context.data_loaders.domain_loader.load(self.domain_name)
if domain_data is None:
return None
) -> Annotated[
DomainV2GQL,
strawberry.lazy("ai.backend.manager.api.gql.domain_v2.types.node"),
]:
from ai.backend.manager.api.gql.domain_v2.types.node import DomainV2GQL
domain_data = await info.context.data_loaders.domain_loader.load(self.domain_name)
if domain_data is None:
raise KeyError(f"Domain not found: {self.domain_name}")

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +245
When a GQL node references another entity node, use `strawberry.lazy()` to avoid circular imports. Strawberry requires runtime type resolution, so `TYPE_CHECKING` imports alone are insufficient.

Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section says "TYPE_CHECKING imports alone are insufficient" but earlier in the document it states "NEVER use TYPE_CHECKING for Strawberry types"; these statements conflict, and the codebase uses TYPE_CHECKING for cross-entity node types alongside strawberry.lazy(). Consider rewording to clarify that TYPE_CHECKING is fine for static typing, but runtime schema generation must rely on strawberry.lazy() / forward references (and adjust the earlier rule accordingly).

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +83
| None
):
from ai.backend.manager.api.gql.user_v2.types.node import UserV2GQL

user_data = await info.context.data_loaders.user_loader.load(self.user_uuid)
if user_data is None:
return None
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user field used to be non-nullable (and fetch_user() would raise UserNotFound on missing users). Making the return type ... | None changes the GraphQL schema/contract and can silently hide dangling references. Keep the field non-nullable and, if the DataLoader returns None, raise the same not-found error as the old fetcher.

Suggested change
| None
):
from ai.backend.manager.api.gql.user_v2.types.node import UserV2GQL
user_data = await info.context.data_loaders.user_loader.load(self.user_uuid)
if user_data is None:
return None
):
from ai.backend.manager.api.gql.user_v2.types.node import UserNotFound, UserV2GQL
user_data = await info.context.data_loaders.user_loader.load(self.user_uuid)
if user_data is None:
raise UserNotFound(str(self.user_uuid))

Copilot uses AI. Check for mistakes.

user_data = await info.context.data_loaders.user_loader.load(self.user_uuid)
if user_data is None:
return None
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning None when the loader can’t find the user changes behavior compared to the previous fetch_user() call (which raises). If you keep the DataLoader approach, translate None into the appropriate not-found exception so clients get a consistent error instead of a null field.

Suggested change
return None
raise strawberry.exceptions.GraphQLError(
f"User with UUID {self.user_uuid} not found."
)

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +83
) -> (
Annotated[
ProjectV2GQL,
strawberry.lazy("ai.backend.manager.api.gql.project_v2.types.node"),
]
| None
):
from ai.backend.manager.api.gql.project_v2.types.node import ProjectV2GQL

project_data = await info.context.data_loaders.project_loader.load(self.project_id)
if project_data is None:
return None
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The project field previously returned a non-null ProjectV2GQL (and fetch_project() raises on missing projects). Changing the return type to ... | None alters the GraphQL schema and can break clients. Prefer keeping it non-nullable and raising ProjectNotFound (or the same error raised by the old fetcher) when the DataLoader yields None.

Suggested change
) -> (
Annotated[
ProjectV2GQL,
strawberry.lazy("ai.backend.manager.api.gql.project_v2.types.node"),
]
| None
):
from ai.backend.manager.api.gql.project_v2.types.node import ProjectV2GQL
project_data = await info.context.data_loaders.project_loader.load(self.project_id)
if project_data is None:
return None
) -> Annotated[
ProjectV2GQL,
strawberry.lazy("ai.backend.manager.api.gql.project_v2.types.node"),
]:
from ai.backend.manager.api.gql.project_v2.types.node import ProjectV2GQL
project_data = await info.context.data_loaders.project_loader.load(self.project_id)
if project_data is None:
raise RuntimeError(
f"Project not found for fair share record (project_id={self.project_id})"
)

Copilot uses AI. Check for mistakes.

project_data = await info.context.data_loaders.project_loader.load(self.project_id)
if project_data is None:
return None
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning None on missing project data will silently null out the back-reference instead of surfacing an error like the old fetch_project() path. Consider raising the appropriate not-found exception when project_loader.load() returns None to preserve prior semantics.

Suggested change
return None
raise KeyError(f"Project not found for id {self.project_id}")

Copilot uses AI. Check for mistakes.
Comment on lines +257 to +259
]:
from ai.backend.manager.api.gql.domain_v2.types.node import DomainV2GQL
data = await info.context.data_loaders.domain_loader.load(self.domain_name)
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example resolver uses DomainV2GQL.from_data(data) but domain_loader.load() returns an optional type; as written, the snippet will fail if data is None. Update the guide to either return None (with a nullable field type) or raise DomainNotFound when the loader yields None, matching the recommended pattern.

Suggested change
]:
from ai.backend.manager.api.gql.domain_v2.types.node import DomainV2GQL
data = await info.context.data_loaders.domain_loader.load(self.domain_name)
] | None:
from ai.backend.manager.api.gql.domain_v2.types.node import DomainV2GQL
data = await info.context.data_loaders.domain_loader.load(self.domain_name)
if data is None:
return None

Copilot uses AI. Check for mistakes.
Comment on lines +269 to 273
**DataLoaders** (`info.context.data_loaders`): Use DataLoaders instead of individual fetch functions to prevent N+1 queries. See `api/gql/data_loader/data_loaders.py` for available loaders.

**See examples:**
- `api/gql/fair_share/types/domain.py` - Cross-entity reference with DataLoader
- `api/gql/domain_v2/types/node.py` - DomainV2GQL with fair_shares/usage_buckets
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file paths in this new section are inconsistent with earlier “Key Files” paths (e.g., src/ai/backend/manager/...). api/gql/data_loader/data_loaders.py and the example paths look like they’re missing the src/ai/backend/manager/ prefix; please make the path style consistent so readers can locate the files reliably.

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +80
) -> (
Annotated[
DomainV2GQL,
strawberry.lazy("ai.backend.manager.api.gql.domain_v2.types.node"),
]
| None
):
from ai.backend.manager.api.gql.domain_v2.types.node import DomainV2GQL

domain_data = await info.context.data_loaders.domain_loader.load(self.domain_name)
if domain_data is None:
return None
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The domain back-reference was previously non-null and would error if the domain didn’t exist via fetch_domain(). Making it nullable (... | None) changes the GraphQL schema/contract. Keep it non-nullable and map a None DataLoader result to DomainNotFound (or the equivalent error from the old fetcher).

Suggested change
) -> (
Annotated[
DomainV2GQL,
strawberry.lazy("ai.backend.manager.api.gql.domain_v2.types.node"),
]
| None
):
from ai.backend.manager.api.gql.domain_v2.types.node import DomainV2GQL
domain_data = await info.context.data_loaders.domain_loader.load(self.domain_name)
if domain_data is None:
return None
) -> Annotated[
DomainV2GQL,
strawberry.lazy("ai.backend.manager.api.gql.domain_v2.types.node"),
]:
from ai.backend.manager.api.gql.domain_v2.types.node import DomainV2GQL
domain_data = await info.context.data_loaders.domain_loader.load(self.domain_name)
if domain_data is None:
raise RuntimeError("Domain not found")

Copilot uses AI. Check for mistakes.
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Feb 9, 2026
@HyeockJinKim HyeockJinKim added this pull request to the merge queue Feb 9, 2026
Merged via the queue into main with commit 20edeae Feb 9, 2026
33 checks passed
@HyeockJinKim HyeockJinKim deleted the feat/dataloader-fairshare branch February 9, 2026 23:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:docs Documentations comp:manager Related to Manager component size:L 100~500 LoC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant