-
-
Notifications
You must be signed in to change notification settings - Fork 31
Add request-scoped service contexts for embedded memory applications #204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
bb122d1
feat: support request-scoped memory service contexts
Coding-Dev-Tools 401f787
Test service contexts independently of optional HTTP dependencies
Coding-Dev-Tools d9c8c4f
Merge remote-tracking branch 'origin/main' into codex/hosted-team-con…
Coding-Dev-Tools a8d11c7
Merge remote-tracking branch 'origin/main' into codex/hosted-team-con…
Coding-Dev-Tools File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| """Request-scoped service injection for applications embedding the local engine. | ||
|
|
||
| The standalone entry points retain their local default. A hosted application must | ||
| enter ``bind_service`` for each operation, with a validated principal. Contexts | ||
| are restored even when an operation fails, and never mutate module singletons. | ||
| """ | ||
| from __future__ import annotations | ||
|
|
||
| from contextlib import contextmanager | ||
| from contextvars import ContextVar | ||
| from typing import TYPE_CHECKING, Iterator, Optional | ||
|
|
||
| if TYPE_CHECKING: | ||
| from engraphis.service import MemoryService | ||
|
|
||
| _BOUND: ContextVar[Optional["MemoryService"]] = ContextVar("engraphis_bound_service", default=None) | ||
| _REQUIRED: ContextVar[bool] = ContextVar("engraphis_bound_service_required", default=False) | ||
|
|
||
|
|
||
| def bound_service() -> Optional["MemoryService"]: | ||
| """Resolve an injected service, refusing an absent required binding.""" | ||
| result = _BOUND.get() | ||
| if result is None and _REQUIRED.get(): | ||
| raise RuntimeError("An authenticated service context is required") | ||
| return result | ||
|
|
||
|
|
||
| @contextmanager | ||
| def require_service_context() -> Iterator[None]: | ||
| """Disable the standalone fallback for the duration of a hosted request.""" | ||
| token = _REQUIRED.set(True) | ||
| try: | ||
| yield | ||
| finally: | ||
| _REQUIRED.reset(token) | ||
|
|
||
|
|
||
| @contextmanager | ||
| def bind_service(service: "MemoryService", *, principal: dict) -> Iterator["MemoryService"]: | ||
| """Bind an explicit service and principal, restoring the enclosing context.""" | ||
| from engraphis.service import _CURRENT_USER, set_current_user | ||
|
|
||
| if service is None or not principal: | ||
| raise ValueError("An explicit service and authenticated principal are required") | ||
| previous_user = _CURRENT_USER.get() | ||
| try: | ||
| set_current_user(principal) | ||
| except Exception: | ||
| _CURRENT_USER.set(previous_user) | ||
| raise | ||
| service_token = _BOUND.set(service) | ||
| required_token = _REQUIRED.set(True) | ||
| try: | ||
| yield service | ||
| finally: | ||
| _REQUIRED.reset(required_token) | ||
| _BOUND.reset(service_token) | ||
| _CURRENT_USER.set(previous_user) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| """Concurrent embedding contexts must never inherit another tenant's service.""" | ||
| from concurrent.futures import ThreadPoolExecutor | ||
| from threading import Barrier | ||
|
|
||
| import pytest | ||
|
|
||
| from engraphis.service import MemoryService, current_user | ||
| from engraphis.service_context import bind_service, bound_service, require_service_context | ||
|
|
||
|
|
||
| def test_required_context_cannot_fall_back_to_local_service(): | ||
| with require_service_context(), pytest.raises(RuntimeError, match="context is required"): | ||
| bound_service() | ||
| assert bound_service() is None | ||
|
|
||
|
|
||
| def test_contexts_keep_identical_workspace_names_isolated(): | ||
| services = [MemoryService.create(":memory:", extractor="none") for _ in range(2)] | ||
| barrier = Barrier(2) | ||
|
|
||
| def work(index): | ||
| principal = {"id": "member_%d" % index, "email": "u%d@example.test" % index, | ||
| "role": "member"} | ||
| with bind_service(services[index], principal=principal): | ||
| bound_service().remember("Only tenant %d" % index, workspace="shared") | ||
| barrier.wait(timeout=5) | ||
| assert bound_service() is services[index] | ||
| assert current_user()["id"] == principal["id"] | ||
| assert bound_service() is None | ||
| assert current_user() is None | ||
|
|
||
| try: | ||
| with ThreadPoolExecutor(max_workers=2) as pool: | ||
| list(pool.map(work, range(2))) | ||
| finally: | ||
| for instance in services: | ||
| instance.close() | ||
|
|
||
|
|
||
| def test_http_adapter_requires_explicit_context_when_requested(): | ||
| pytest.importorskip("fastapi", reason="HTTP adapter requires the optional server extra") | ||
| from engraphis.routes.v2_api import service | ||
|
|
||
| with require_service_context(), pytest.raises(RuntimeError, match="context is required"): | ||
| service() | ||
| instance = MemoryService.create(":memory:", extractor="none") | ||
| principal = {"id": "member_http", "email": "http@example.test", "role": "member"} | ||
| try: | ||
| with bind_service(instance, principal=principal): | ||
| assert service() is instance | ||
| finally: | ||
| instance.close() | ||
|
|
||
|
|
||
| def test_exception_and_nested_binding_restore_outer_identity(): | ||
| first = MemoryService.create(":memory:", extractor="none") | ||
| second = MemoryService.create(":memory:", extractor="none") | ||
| user = {"id": "member_outer", "email": "outer@example.test", "role": "member"} | ||
| other = {"id": "member_inner", "email": "inner@example.test", "role": "viewer"} | ||
| try: | ||
| with bind_service(first, principal=user): | ||
| with pytest.raises(ValueError, match="operation failed"): | ||
| with bind_service(second, principal=other): | ||
| raise ValueError("operation failed") | ||
| assert bound_service() is first | ||
| assert current_user()["id"] == user["id"] | ||
| assert current_user() is None | ||
| finally: | ||
| first.close() | ||
| second.close() |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.