Skip to content

refactor(ai-core): typesafe handlers decoupled from service impl#74

Merged
Schmarvinius merged 46 commits into
review-fixesfrom
refactor/typesafe-handlers
Jun 15, 2026
Merged

refactor(ai-core): typesafe handlers decoupled from service impl#74
Schmarvinius merged 46 commits into
review-fixesfrom
refactor/typesafe-handlers

Conversation

@Schmarvinius

Copy link
Copy Markdown
Contributor

Applies idiomatic CAP Java patterns to the AICore service architecture:

  • Service API methods emit typed EventContext events; handlers provide the implementation
  • CRUD handlers are fully decoupled from AICoreServiceImpl (no stored reference)
  • Generated constants and typed contexts replace string literals

Changes

Service emits, handler implements:

  • AICoreServiceImpl.deploymentId(), .inferenceClient(), .resourceGroup() now create a typed EventContext, call emit(), and return the result
  • New AICoreApiHandler (@on handler) contains all business logic: caching, retry, SDK calls, deployment polling
  • Apps can hook @Before/@After on deploymentId, inferenceClient, resourceGroup events

Handler decoupling:

  • Removed AICoreServiceImpl field from AbstractCrudHandler
  • Handlers receive stateless SDK API clients via constructor injection
  • At invocation time, handlers obtain the service from EventContext.getService()

Typed contexts & compile-safe constants:

  • New DeploymentIdContext, InferenceClientContext, ResourceGroupContext in api package
  • ActionHandler uses generated DeploymentsStopContext instead of raw EventContext
  • Entity annotations use Deployments_.CDS_NAME, ResourceGroups_.CDS_NAME, Configurations_.CDS_NAME

Naming:

  • DEFAULT_NAME renamed from "AICore$Default" to "AICoreService$Default" (standard CAP convention)

lisajulia and others added 30 commits June 10, 2026 11:32
- Replace resourceGroupForTenant(String) with resourceGroup() on the
  public AICoreService interface. The implementation reads the tenant
  from the current RequestContext internally.
- Remove isMultiTenancyEnabled() and getRetry() from the public
  interface; they remain accessible on AbstractAICoreService for
  internal callers.
- Remove the CDS function 'resourceGroupForTenant' from index.cds
  and its action handler.
- Detect multi-tenancy via standard CAP Java cds.multiTenancy.sidecar.url
  property and DeploymentService presence instead of custom flag.
- Update RecommendationClientResolver to drop tenantId parameter.
- Update samples, tests, and javadoc accordingly.

Addresses review comments from PR #49 (Issue 2).
- Add tenant ownership verification on ResourceGroupHandler for
  READ (by-key), UPDATE, and DELETE operations. Returns 404 if the
  resource group belongs to a different tenant.
- Scope list queries (READ without key) to the current tenant's
  resource groups via the tenant label filter in multi-tenancy mode.
- Add ensureResourceGroupAccessible() guard to DeploymentHandler and
  ConfigurationHandler, validating the addressed resource group
  belongs to the current tenant before forwarding to AI Core.
- Provider/system users are exempt from tenant restrictions and can
  access all resource groups (useful for ops/debug scenarios).
- Add isProviderUser() and currentTenantId() as public helpers on
  AbstractAICoreService for use by handler classes.

Addresses review comments from PR #49 (Issue 3a).
- Rename all configuration properties from cds.requires.AICore.* to
  cds.ai.core.* to align with CAP Java property naming conventions.
- Rename cds.requires.recommendations.contextRowLimit to
  cds.ai.recommendations.contextRowLimit.
- Drop the cds.requires.AICore.multiTenancy flag entirely; multi-
  tenancy is now auto-detected from standard CAP Java properties.
- Update README with new configuration namespace and examples.

Addresses review comments from PR #49 (Issue 3b).
When resourceGroupForTenant is called with a null tenantId (which
happens when currentTenantId() returns null in single-tenant or
non-tenant-scoped RequestContexts), fall back to the default resource
group instead of passing null to the Caffeine cache (which throws NPE).

This fixes integration test failures in the CI pipeline where the
ApplicationServiceDelegation and Recommendation tests run without an
explicit tenant in the RequestContext.
The cleanup step previously only deleted resource groups matching the
exact current run_id AND run_attempt. When a run failed and was re-run,
the previous attempt's resource groups were never cleaned up, eventually
hitting the AI Core resource group limit (50).

Changes:
- Match prefix 'itest-{run_id}-' (all attempts) instead of the exact
  'itest-{run_id}-{run_attempt}' string.
- Same for 'sonar-{run_id}-' prefix.
- Also delete 'cds-itest-' prefixed resource groups which are created
  by the multi-tenancy integration tests via resourceGroupForTenant()
  and were never cleaned up by the pipeline.
The source code (commit c30080b) renamed properties from
cds.requires.AICore.* to cds.ai.core.*, but the integration test
application.yaml files were not updated. This meant the
CDS_AICORE_TEST_RESOURCE_GROUP env var set by CI was silently ignored
and tests always ran against the literal default resource group.

- spring/application.yaml: cds.requires.AICore -> cds.ai.core
- mtx-local/application.yaml: remove obsolete cds.requires.AICore.multiTenancy
  (now auto-detected from cds.multi-tenancy.sidecar.url)
Cover new code paths introduced by the tenant-scoping branch:

- TenantScopingTest (7 tests): exercises every branch of
  AbstractCrudHandler.ensureResourceGroupAccessible() — provider bypass,
  single-tenancy bypass, null tenant, matching/non-matching labels, 404.

- MockAICoreServiceImplTest (9 tests): both constructors,
  MT enabled/disabled, resourceGroupForTenant, cache isolation,
  clearTenantCache, getRetry, config property reads.

- AICoreServiceImplDeploymentIdTest (+2 tests):
  resourceGroupForTenant(null) returns default even with MT enabled;
  single-tenancy always returns default.
Document the missing E2E coverage for RecommendationModelChangedHandler.
The proper test requires an extensibility-enabled sidecar with extension
JSON that adds prediction columns — not yet set up in mtx-local.

The cache-invalidation logic itself is covered by the existing unit test
FioriRecommendationHandlerTest.invalidateTenant_removesOnlyThatTenantsEntries.
Each parallel CI job (Java 17, Java 21, SonarQube) was using broad
prefixes in its cleanup step, deleting resource groups belonging to
sibling jobs still in progress. This caused intermittent 403 Forbidden
errors when the affected jobs tried to use their now-deleted resource
groups.

Narrow the cleanup prefixes so each job only deletes its own:
- integration-tests: itest-{run_id}-{attempt}-j{version}*
- scan-with-sonar: sonar-{run_id}-{attempt}*
Both still clean up itest-rg-* (ResourceGroupTest leftovers).
- DeploymentHandler: test onCreate (with/without TTL) and onUpdate
  happy path (targetStatus and configurationId branches)
- ResourceGroupHandler: test onUpdate with/without labels,
  buildTenantLabelSelector branches (tenantId filter, MT non-provider,
  MT null tenant, single tenancy), ensureOwnedByCurrentTenant branches
  (provider, single tenant, wrong tenant, matching tenant)
- AICoreServiceConfiguration: test eventHandlers() MockAICoreServiceImpl
  branch (with and without multi-tenancy), test detectMultiTenancy via
  services() for sidecarUrl branch and no-MT fallback
The MultiTenancyTest creates per-tenant resource groups with names like
cds-itest-mt-a-{timestamp} (from resourceGroupPrefix 'cds-' + tenant
name 'itest-*'). These are unique per test run (timestamped) and safe
to clean up from any job without cross-job interference.
…ce-migration-v2

# Conflicts:
#	.github/actions/integration-tests/action.yml
#	.github/actions/scan-with-sonar/action.yml
#	cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/AbstractAICoreService.java
#	cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/AICoreServiceConfigurationTest.java
#	cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/AICoreServiceImplDeploymentIdTest.java
#	cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/MockAICoreServiceImplTest.java
Follow standard CAP Java naming convention for service instances
(ServiceInterface$Default). The CDS definition name stays 'AICore'
(matching the CDS model); only the registered instance name changes.
… API

Add typed EventContext interfaces in the api package for the three
programmatic API methods:
- DeploymentIdContext: for deploymentId(resourceGroupId, spec)
- InferenceClientContext: for inferenceClient(resourceGroupId, deploymentId)
- ResourceGroupContext: for resourceGroup()

These enable the idiomatic CAP pattern where service methods emit events
and ON handlers provide the implementation, allowing extensibility via
@Before/@after hooks.
…o handler

Apply idiomatic CAP Java pattern: service methods create typed EventContext,
emit(), and return result. A separately-registered AICoreApiHandler provides
the ON implementation with the actual business logic.

- AICoreServiceImpl.deploymentId/inferenceClient/resourceGroup/
  resourceGroupForTenant now emit typed contexts instead of doing work directly
- New AICoreApiHandler handles DeploymentIdContext, InferenceClientContext,
  ResourceGroupContext with all caching, retry, and SDK logic
- ResourceGroupContext extended with optional tenantId for explicit-tenant path
- AICoreServiceConfiguration registers AICoreApiHandler
- AICoreServiceImpl retains shared state (caches, config, APIs) accessed by
  handlers via EventContext.getService()

This enables extensibility: apps can register @Before/@after handlers on
deploymentId, inferenceClient, and resourceGroup events.
…d contexts

- Remove AICoreServiceImpl field from AbstractCrudHandler; handlers now
  obtain the service from EventContext.getService() at invocation time.
- Pass SDK API clients (DeploymentApi, ResourceGroupApi, ConfigurationApi)
  directly via constructor injection — stateless clients don't need the
  service reference.
- ActionHandler uses generated DeploymentsStopContext instead of raw
  EventContext with string-based key extraction.
- All handlers use generated entity name constants (Deployments_.CDS_NAME,
  ResourceGroups_.CDS_NAME, Configurations_.CDS_NAME) instead of
  hand-written strings.
- Update AICoreServiceConfiguration to pass API clients to handler
  constructors.
- Update all handler unit tests for the new constructor signatures and
  EventContext-based service access pattern.

Addresses issue #70: typesafe handlers decoupled from service impl.
@Schmarvinius Schmarvinius requested a review from a team as a code owner June 12, 2026 12:26
@hyperspace-insights

Copy link
Copy Markdown

Summary

The following content is AI-generated and provides a summary of the pull request:


Refactor AICore Service to Idiomatic CAP Java Event-Driven Architecture

♻️ Refactor: Applies idiomatic CAP Java patterns to the AICore service architecture by introducing typed EventContext classes and decoupling CRUD handlers from AICoreServiceImpl.

Changes

The core change separates business logic from the service implementation: AICoreServiceImpl now acts as a thin event emitter, while a new AICoreApiHandler provides the actual ON-handler implementations. CRUD handlers are fully decoupled from the service implementation via constructor injection of SDK API clients.

  • AICoreService.java: Renamed DEFAULT_NAME from "AICore$Default" to "AICoreService$Default" (standard CAP convention).
  • DeploymentIdContext.java (new): Typed EventContext for the deploymentId event, carrying resource group ID, deployment spec, and the resolved deployment ID result.
  • InferenceClientContext.java (new): Typed EventContext for the inferenceClient event, carrying resource group ID, deployment ID, and the resulting ApiClient.
  • ResourceGroupContext.java (new): Typed EventContext for the resourceGroup event, carrying an optional explicit tenant ID and the resolved resource group ID result.
  • AICoreServiceImpl.java: Converted resourceGroup(), deploymentId(), and inferenceClient() into thin emitters that create a typed context, call emit(), and return ctx.getResult(). All business logic (caching, SDK calls, deployment polling, resource group creation) moved to AICoreApiHandler. Exposed new accessors (getSdkService(), getDeploymentLocks(), getMaxRetries(), getInitialDelayMs(), Caffeine cache accessors) for handler use. Made deploymentCacheKey() and notReadyYet() public static.
  • AICoreApiHandler.java (new): @On handler for resourceGroup, deploymentId, and inferenceClient events. Contains the implementation logic previously in AICoreServiceImpl (caching, retry, configuration/deployment creation, polling).
  • AICoreServiceConfiguration.java: Updated to instantiate AICoreApiHandler and pass SDK API clients directly to CRUD handlers instead of the service reference.
  • AbstractCrudHandler.java: Removed stored AICoreServiceImpl reference; handlers now receive ResourceGroupApi via constructor and obtain the service at invocation time via EventContext.getService(). Methods resolveResourceGroup and ensureResourceGroupAccessible updated to accept EventContext.
  • ActionHandler.java, ConfigurationHandler.java, DeploymentHandler.java, ResourceGroupHandler.java: Updated constructors to accept SDK API clients directly. Entity annotations updated to use generated *_.CDS_NAME constants (e.g., Deployments_.CDS_NAME) and ActionHandler uses the typed DeploymentsStopContext.
  • Test files (AICoreServiceImplDeploymentIdTest, ConfigurationHandlerTest, DeploymentHandlerTest, ResourceGroupHandlerTest, TenantScopingTest): Updated to reflect the new constructor signatures, handler registration with AICoreApiHandler, and updated method signatures requiring EventContext.

  • 🔄 Regenerate and Update Summary
  • ✏️ Insert as PR Description (deletes this comment)
  • 🗑️ Delete comment
PR Bot Information

Version: 1.22.8

  • LLM: anthropic--claude-4.6-sonnet
  • Summary Prompt: Default Prompt
  • Correlation ID: c0bca913-e7cb-4743-acc4-e8cf0b65b902
  • Event Trigger: pull_request.opened
  • File Content Strategy: Full file content
  • Output Template: Default Template

@hyperspace-insights hyperspace-insights Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Summary

The refactor successfully applies CAP Java's event-driven patterns — thin emitters, typed EventContext classes, and decoupled handlers receiving API clients via constructor injection. The structural improvement is clear and the generated-constant migration (Deployments_.CDS_NAME etc.) is a solid correctness improvement.

Three substantive issues were identified and commented:

  1. Null results after emit()deploymentId(), inferenceClient(), and resourceGroup() return ctx.getResult() without guarding against null, which silently propagates to callers if no handler is registered.
  2. Unchecked AICoreServiceImpl cast in AICoreApiHandler(AICoreServiceImpl) context.getService() will throw ClassCastException for any non-AICoreServiceImpl service instance with no descriptive error.
  3. Retry instance recreated per pollUntilRunning call — a new Retry.of("pollDeployment", …) is constructed on every invocation, which conflicts with any RetryRegistry usage and loses observability.

Please address these before merging.

PR Bot Information

Version: 1.22.8

  • Correlation ID: c0bca913-e7cb-4743-acc4-e8cf0b65b902
  • File Content Strategy: Full file content
  • LLM: anthropic--claude-4.6-sonnet
  • Event Trigger: pull_request.opened

Replace heavily-mocked unit tests with integration-style tests that boot
a real CdsRuntime, register real handlers, and dispatch CQN through the
full handler pipeline. Only SDK API clients remain mocked.

- DeploymentHandlerTest: tests CREATE, UPDATE via service.run()
- ConfigurationHandlerTest: tests READ, CREATE via service.run()
- ResourceGroupHandlerTest: tests CRUD + MT label filtering
- TenantScopingTest: tests tenant isolation through actual CQN operations
  with different RequestContext tenants
@Schmarvinius Schmarvinius changed the title Refactor/typesafe handlers refactor(ai-core): typesafe handlers decoupled from service impl Jun 12, 2026
Immutable record for config values and holder for SDK API clients.
Encapsulates caches, locks, retry, and validation behind
resolveResourceGroup, resolveDeployment, invalidateTenant.
Handlers receive dependencies via constructor. Use
context.getUserInfo() for tenant/provider checks directly.
Zero fields, zero accessors. Delete AbstractAICoreService and
MockAICoreServiceImpl. Add resourceGroupForTenant to interface.
Configuration creates AICoreConfig, AICoreClients, DeploymentResolver
and injects them into handlers at registration time.
Single-arg constructor, no dependency on service internals.
Remove AbstractAICoreService casts from all consumers.
Adapt all unit and integration tests to use AICoreConfig,
AICoreClients, DeploymentResolver instead of service accessors.
Only retry the deployment creation call. Polling is handled
separately so a poll timeout does not re-create deployments.
Handlers use DeploymentResolver.resolveResourceGroup() directly
instead of casting context.getService(). Zero service references.
Add @HandlerOrder to setup handlers for DeploymentService events.
Wire MockAICoreSetupHandler to actually call clearTenantCache().
Use ServiceException in mock inference handler.
Fail fast on invalid cds.ai.core.* property values.
Document AbstractCdsDefinedService dependency rationale.
Pass ResourceGroupApi to DeploymentResolver constructor.
Pass DeploymentResolver to CRUD handler constructors.
Base automatically changed from refactor/remote-service-migration-v2 to review-fixes June 15, 2026 13:09
…safe-handlers

# Conflicts:
#	.github/workflows/pr.yml
#	cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/AICoreService.java
#	cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/AICoreServiceConfiguration.java
#	cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/AbstractAICoreService.java
#	cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/AICoreServiceConfigurationTest.java
#	cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/AICoreServiceImplDeploymentIdTest.java
#	cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/MockAICoreServiceImplTest.java
@Schmarvinius Schmarvinius merged commit f48c554 into review-fixes Jun 15, 2026
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants