> ## Documentation Index
> Fetch the complete documentation index at: https://docs.activeviam.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Cache routing

> Guide to service-driven DirectQuery cache routing in Atoti Market Risk 6.0.9, covering the IMarketShiftDirectQueryCachingPostProcessorEx interface, ICacheResolver, ServiceDispatchingCacheResolver, and MarketShiftCacheNameResolverUtils helpers.

For a narrative of the changes, see the [release notes](../../dev-release/release-notes#service-driven-directquery-cache-routing) and the [Migration guide](../../dev-release/migrate#service-driven-directquery-cache-routing).

## When to use it

The DirectQuery local cache is configured via `IMarketShiftDirectQueryCachingPostProcessor.CacheConfiguration` Spring beans. Each `CacheConfiguration` pairs a cache store name with an `ILocationToCachePartitionConverter` that maps a prefetch location to a cache partition.

In 6.0.8, exactly one `CacheConfiguration` could be wired into a shift post-processor at a time. 6.0.9 extends that contract in two backward-compatible ways:

* **Multi-store wiring.** Declare several `CacheConfiguration` beans and they are all wired into the same post-processor.
* **Service-driven dispatch.** Override `IMarketDataRetrievalService.resolveShiftCacheNames(...)` (or the symmetric method on `IFxShift`) so the post-processor picks the right store(s) for the current query coordinates instead of prefetching every registered cache on every query.

Both patterns are opt-in. Existing 6.0.8 wiring keeps working without any code change.

## `IMarketShiftDirectQueryCachingPostProcessorEx`

Package: `com.activeviam.mr.common.services`

Extends `IMarketShiftDirectQueryCachingPostProcessor` and is implemented by the three standard Atoti Market Risk shift post-processors:

* `AShiftVectorPostProcessor`
* `FXMarketShiftPostProcessor`
* `APnlVectorFromRiskSensiPostProcessor`

Customer subclasses of those classes pick up the new behavior automatically.

### Injection points

| Constant                                                             | Value                            | Purpose                                                                                                                                                                                                                                                                                               |
| -------------------------------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `IMarketShiftDirectQueryCachingPostProcessor.PROPERTY_NAME`          | `marketShiftCacheConfiguration`  | Legacy single-bean injection (6.0.7 / 6.0.8). Still honoured; wraps the bean in a singleton list.                                                                                                                                                                                                     |
| `IMarketShiftDirectQueryCachingPostProcessorEx.PROPERTY_NAME`        | `marketShiftCacheConfigurations` | New list-based injection. Spring autowires every `CacheConfiguration` `@Bean` into a `List<CacheConfiguration>`, which `RiskPostProcessorInjector` passes here.                                                                                                                                       |
| `IMarketShiftDirectQueryCachingPostProcessorEx.LEAF_EXPANSION_LIMIT` | `LEAF_EXPANSION_LIMIT` (key)     | Post-processor `parameters[]` key. Sets the upper bound on the number of leaf-level points the bundled router materializes before falling back to a single coarse call. Default `16`. Set `0` to disable bounded leaf expansion. This is **not** a Spring property: it is set on the measure builder. |

### Override points

The two methods customer code is expected to override:

```java theme={"languages":{"custom":["/engine/python-sdk/6.2/languages/pycon.tmLanguage.json"]}}
/** Cache name selection for the given query location. */
default @NonNull Set<String> getServiceNames(
        @NonNull ILocation location,
        @NonNull ICubeFilter filter,
        @NonNull IActivePivot pivot);

/** PP-side helper that bridges service-driven routing to the cache definition map. */
@Deprecated(since = "6.0.9", forRemoval = true)
default @NonNull Set<String> legacyFallbackIfEmpty(@NonNull Set<String> serviceNames);
```

The default `getServiceNames` returns the keys of `getMarketShiftCacheConfigurationMap()`, which preserves the 6.0.7 / 6.0.8 fixed-store behavior of prefetching every configured cache on every query.

Override `getServiceNames` to defer to an injected service. The recommended body delegates to one of the `routeVia*` helpers (see [`MarketShiftCacheNameResolverUtils`](#marketshiftcachenameresolverutils) below).

`legacyFallbackIfEmpty` is applied automatically inside the resolver and should not be called by override code. It is documented here only because it appears in the `addDatabaseCachePrefetcher` implementation.

### `legacyFallbackIfEmpty` behaviour matrix

The fallback is responsible for keeping 6.0.7 / 6.0.8 single-store wiring working when a project has not overridden `resolveShiftCacheNames` on its `IMarketDataRetrievalService`.

| Service returned | Map size | Helper returns     | Notes                                                                               |
| ---------------- | -------- | ------------------ | ----------------------------------------------------------------------------------- |
| `{}`             | 0        | `{}`               | no prefetch                                                                         |
| `{}`             | 1        | singleton from map | legacy single-store                                                                 |
| `{}`             | 2+       | `{}`               | ambiguous, no prefetch                                                              |
| `{A}`            | 0        | `{A}`              | passthrough; converter lookup will fail at query time                               |
| `{A}`            | 1 (`B`)  | `{B}`              | **1+1 override**: the map name wins. A one-shot WARN is logged on first occurrence. |
| `{A}`            | 2+       | `{A}`              | passthrough; service decides                                                        |
| `{A,B}`          | any      | `{A,B}`            | passthrough; service decides                                                        |

The **1+1 override** row is the bridge that keeps 6.0.7 wiring functional after the upgrade. If you see the WARN, your project is in that bridging state: either you have a single `CacheConfiguration` bean but your service returns a different name, or your service routing is incomplete. Either align the names or remove one side of the wiring once the migration is complete.

## `ICacheResolver`

Package: `com.activeviam.mr.common.services`

```java theme={"languages":{"custom":["/engine/python-sdk/6.2/languages/pycon.tmLanguage.json"]}}
public interface ICacheResolver {
    Map<String, Collection<CachePartition>> resolve(
            ILocation location, ICubeFilter cubeFilter, IActivePivot pivot);
}
```

A resolver is a function from `(location, filter, pivot)` to a map from cache store name to the `CachePartition`s the prefetcher should request for that store. An empty map means nothing needs to be prefetched. It is the unit the new `MRDatabaseCachePrefetcher` iterates over.

Most customer code does not implement `ICacheResolver` directly; the framework supplies a `ServiceDispatchingCacheResolver` that wraps the override of `getServiceNames`.

## `ServiceDispatchingCacheResolver`

Package: `com.activeviam.mr.common.services`

The framework-provided `ICacheResolver` that backs `IMarketShiftDirectQueryCachingPostProcessorEx`. It composes:

* A `CacheNameRouter` (`(location, filter, pivot) -> Set<String>`), normally bound to `getServiceNames` via `legacyFallbackIfEmpty`.
* A `Supplier<Map<String, ILocationToCachePartitionConverter>>` that reads the live `marketShiftCacheConfigurationMap` of the post-processor.

At query time, the resolver routes a name set, looks up each name in the map, and computes the partition via the registered converter. A name returned by the router with no matching converter raises `IllegalStateException` so that wiring bugs surface loudly rather than NPE deep inside the prefetcher.

## `MarketShiftCacheNameResolverUtils`

Package: `com.activeviam.mr.common.services`

Stateless helpers that produce the `CacheNameRouter` lambdas plugged into `ServiceDispatchingCacheResolver`. The two `routeVia*` methods are the recommended bodies for `getServiceNames` overrides.

### `routeViaMarketDataService`

```java theme={"languages":{"custom":["/engine/python-sdk/6.2/languages/pycon.tmLanguage.json"]}}
public static @NonNull Set<String> routeViaMarketDataService(
        @NonNull ILocation location, @NonNull ICubeFilter filter, @NonNull IActivePivot pivot,
        @NonNull IMarketDataRetrievalService service,
        @NonNull String sensitivityKind,
        @NonNull ILevelInfo riskClassLevel,
        @NonNull ILevelInfo sensitivityNameLevel,
        @Nullable LocationFunction leafCoordinatesFunction,
        int leafExpansionLimit);
```

What it does:

1. Wildcard-aware expansion of `location` over `riskClassLevel` × `sensitivityNameLevel`, dropping points the cube filter denies (via `PostProcessorUtils.grantedMembersCondition`).
2. For each surviving point: a bounded expansion over the levels reported by `leafCoordinatesFunction.getRequiredLevels()`. If the expanded count exceeds `leafExpansionLimit`, fall back to a single call against the coarse point and let the service overapproximate (typically by reading a `null` `leafCoordinates` argument).
3. Each surviving point fires one `service.resolveShiftCacheNames(sensitivityKind, riskClass, sensitivityName, leafCoordinates)` call. The union of returned names is the result.

`sensitivityKind` is a per-post-processor constant (for example `"Delta"` or `"Vega"`). `leafCoordinatesFunction` is a `LocationFunction` that produces the leaf-payload list; pass `null` if your post-processor does not route on leaf coordinates.

### `routeViaFxShift`

```java theme={"languages":{"custom":["/engine/python-sdk/6.2/languages/pycon.tmLanguage.json"]}}
public static @NonNull Set<String> routeViaFxShift(
        @NonNull ILocation location, @NonNull ICubeFilter filter, @NonNull IActivePivot pivot,
        @NonNull IFxShift fxShift,
        @Nullable LocationFunction leafCoordinatesFunction,
        int leafExpansionLimit);
```

The FX-shift equivalent. Because `IFxShift.resolveShiftCacheNames` does not take `(sensitivityKind, riskClass, sensitivityName)`, this helper only drives the optional bounded leaf expansion.

## `IFxShift.resolveShiftCacheNames`

Package: `com.activeviam.mr.common.services`

```java theme={"languages":{"custom":["/engine/python-sdk/6.2/languages/pycon.tmLanguage.json"]}}
default @NonNull Set<String> resolveShiftCacheNames(@Nullable List<Object> leafCoordinates) {
    // default delegates to the underlying IMarketDataRetrievalService
}
```

The default `FXShift` implementation delegates to the underlying `IMarketDataRetrievalService`, so overriding only the service method is sufficient in most cases. Override `IFxShift.resolveShiftCacheNames` directly only when the FX cache routing diverges from the market-data routing.

## `MRDatabaseCachePrefetcher`

Package: `com.activeviam.mr.common.services`

The `IPrefetcher` installed by `IMarketShiftDirectQueryCachingPostProcessorEx.addDatabaseCachePrefetcher`. It fans out across a list of `ICacheResolver`s and issues one `cacheManager.prefetchCacheAsync(...)` per `(cacheName, partition)` pair returned by each resolver.

It is functionally equivalent to the Atoti-core `DatabaseCachePrefetcher` for a single registered cache (same call shape, same partition derivation), but its `getName()` returns `MRDatabaseCachePrefetcher` instead of `DatabaseCachePrefetcher`. Monitoring that keys on the prefetcher class name, a Grafana panel or an APM query over the Atoti query logs for instance, matches the old value and shows nothing until the filter is updated.

`MRDatabaseCachePrefetcher.computePrefetches` no-ops on `IDistributedActivePivot` instances, matching the parent prefetcher contract.

## End-to-end example

```java theme={"languages":{"custom":["/engine/python-sdk/6.2/languages/pycon.tmLanguage.json"]}}
public class MyDeltaService extends DefaultMarketDataRetrievalService {

    @Override
    public Set<String> resolveShiftCacheNames(
            String sensitivityKind, String riskClass, String sensitivityName,
            @Nullable List<Object> leafCoordinates) {
        if ("FX".equals(riskClass)) {
            return Set.of("FxDeltaShift");
        }
        return Set.of("DeltaShift");
    }
}
```

```java theme={"languages":{"custom":["/engine/python-sdk/6.2/languages/pycon.tmLanguage.json"]}}
@Configuration
public class DeltaCacheConfig {

    @Bean
    IMarketShiftDirectQueryCachingPostProcessor.CacheConfiguration deltaShiftCache(
            ILocationToCachePartitionConverter converter) {
        return new IMarketShiftDirectQueryCachingPostProcessor.CacheConfiguration("DeltaShift", converter);
    }

    @Bean
    IMarketShiftDirectQueryCachingPostProcessor.CacheConfiguration fxDeltaShiftCache(
            ILocationToCachePartitionConverter fxConverter) {
        return new IMarketShiftDirectQueryCachingPostProcessor.CacheConfiguration("FxDeltaShift", fxConverter);
    }
}
```

At query time the post-processor picks the right store for each location: `FX` risk-class evaluations prefetch `FxDeltaShift`, everything else prefetches `DeltaShift`. No fork or subclass of the standard Atoti Market Risk post-processors is required.

The `MyDeltaService` and `DeltaCacheConfig` snippets above form a complete, self-contained example. Each store the service can return (`DeltaShift`, `FxDeltaShift`) has a matching `CacheConfiguration` bean. Spring autowires both beans into the post-processor's list-injection point.

To split routing across separate services instead of branching inside one, give each store its own `IMarketDataRetrievalService`. Each service's `resolveShiftCacheNames` then returns just that store's name, with a matching `CacheConfiguration` bean declared for each.
