Currently working on some PRs to address the non-trivial latency regression of watch vs the faster query(). A few optimizations with tradeoffs in WatcherInterceptor that were out of scope and/or require discussion are listed below:
Normalizing to derive keys does a lot of discarded work.
We only need record.key + fields.keys, yet Normalizer also runs metadataGenerator.metadataForObject per field, materializes every field value through replaceObjects, calls mergeWith for objects appearing twice, and does two mapValues + a filterValues per record. A key-only traversal could skip all of that. Two things it must keep: cache-key generation (which reads field values, e.g. id), and the maxAge == Duration.ZERO exclusion — fields dropped from the record must also be dropped from the key set, so that check can't be skipped. Divergence from real normalization is the risk.
Bursts of cache changes cause serial refetches, and can backpressure cache writes. Worth proposing upstream.
flattenConcat collects refetches sequentially, so while one is in flight no new changedKeys events are pulled. They fill the SharedFlow's 64-slot buffer and then, per BufferOverflow.SUSPEND, cache writes start suspending app-wide — that's what the "potential code smell" comment in DefaultCacheManager is about. Conflating after the filter would bound this: every event still reaches the filter so no match is missed, and each refetch reads current cache state, so collapsing consecutive positive matches loses nothing. Note you can't conflate before the filter — that would drop key sets and miss matches.
Currently working on some PRs to address the non-trivial latency regression of watch vs the faster query(). A few optimizations with tradeoffs in WatcherInterceptor that were out of scope and/or require discussion are listed below:
Normalizing to derive keys does a lot of discarded work.
We only need record.key + fields.keys, yet Normalizer also runs metadataGenerator.metadataForObject per field, materializes every field value through replaceObjects, calls mergeWith for objects appearing twice, and does two mapValues + a filterValues per record. A key-only traversal could skip all of that. Two things it must keep: cache-key generation (which reads field values, e.g. id), and the maxAge == Duration.ZERO exclusion — fields dropped from the record must also be dropped from the key set, so that check can't be skipped. Divergence from real normalization is the risk.
Bursts of cache changes cause serial refetches, and can backpressure cache writes. Worth proposing upstream.
flattenConcat collects refetches sequentially, so while one is in flight no new changedKeys events are pulled. They fill the SharedFlow's 64-slot buffer and then, per BufferOverflow.SUSPEND, cache writes start suspending app-wide — that's what the "potential code smell" comment in DefaultCacheManager is about. Conflating after the filter would bound this: every event still reaches the filter so no match is missed, and each refetch reads current cache state, so collapsing consecutive positive matches loses nothing. Note you can't conflate before the filter — that would drop key sets and miss matches.