Skip to content

Add IOutputCacheBufferWriterStore  #48854

Closed
@mgravell

Description

@mgravell

Ideally for preview 6...

Background and Motivation

We wish to implement an official SE.Redis output cache implementation.

The existing IOutputCacheStore API is byte[]-focused, which is fine for the in-process implementation which constantly returns the same byte[], but is hugely suboptimal for out-of-process implementations, as we would have to return a right-sized byte[] every call (in either direction), causing GC load. Additionally, since output-cache is an entire page load plus headers, this can easily be large enough to spill into LOH.

Context: #48450

IMPORTANT: this API was approved for RC6, but an unforeseen problem arose (also #50402) where-by this change added a framework reference into a previously non-framework-reference OOB package; this is undesirable, so a secondary change is proposed to resolve this. The obsolete parts of this proposal have been struck, and the new changes are in italics.

For this, we propose three things:

  1. a new SetAsync method similar to the existing, but taking ReadOnlySequence<byte> instead of byte[]; this API to be implemented in the existing IOutputCacheStore interface as a DIM, to avoid back-compat problems
  2. a new interface IOutputCacheBufferWriterStore which extends IOutputCacheStore (same assembly/namespace), allowing implementations to optionally use a new GetAsync API that takes an IBufferWriter<byte> - thus allowing implementations to pass data to the consumer while site-stepping the topic of "data lifetime" : the receiver can pass a recycling-enabled buffer-writer, and the cache implementation does not need to know about those details - it just asks for space to write data, and writes
  3. a new API to register the SE.Redis output cache implementation, in the existing Microsoft.Extensions.Caching.StackExchangeRedis OOB package in a new Microsoft.AspNetCore.OutputCaching.StackExchangeRedis OOB package

Proposed API

  namespace Microsoft.AspNetCore.OutputCaching;

  public interface IOutputCacheStore
  {
      // pre-existing
      ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan validFor, CancellationToken cancellationToken);

    
+     ValueTask SetAsync(string key, ReadOnlySequence<byte> value, ReadOnlyMemory<string> tags, TimeSpan validFor, CancellationToken cancellationToken)
+     {
+         // compatibility implementation using the original API
+         return SetAsync(key, value.ToArray(), tags.IsEmpty ? null : tags.ToArray(), validFor, cancellationToken);
+     }
  }

+ public interface IOutputCacheBufferWriterStore : IOutputCacheStore
+ {
+     ValueTask<bool> GetAsync(string key, IBufferWriter<byte> destination, CancellationToken cancellationToken);
+ }

This second interface is optional / separate because not all types will wish to opt in; in particular, in the case of the existing in-proc implementation, it will be preferable to just keep returning the existing byte[]; as such, the output cache middleware will test for this API and use the most appropriate.

Additionally, to implement the SE.Redis cache, we propose the addition of

package: `Microsoft.Extensions.Caching.StackExchangeRedis` (pre-existing) type `StackExchangeRedisCacheServiceCollectionExtensions` (pre-existing)
+ public static IServiceCollection AddStackExchangeRedisOutputCache(
+     this IServiceCollection services, Action<RedisCacheOptions> setupAction)

package: Microsoft.AspNetCore.OutputCaching.StackExchangeRedis (new)
type Microsoft.AspNetCore.OutputCaching.StackExchangeRedis (new)

+ public static IServiceCollection AddStackExchangeRedisOutputCache(
+     this IServiceCollection services, Action<RedisOutputCacheOptions> setupAction)

where RedisOutputCacheOptions is a new clone of the existing RedisCacheOptions, but in the new package/namespace (otherwise: 1-1 feature parity with RedisCacheOptions):

+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.Configuration.get -> string?
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.Configuration.set -> void
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.ConfigurationOptions.get -> StackExchange.Redis.ConfigurationOptions?
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.ConfigurationOptions.set -> void
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.ConnectionMultiplexerFactory.get -> System.Func<System.Threading.Tasks.Task<StackExchange.Redis.IConnectionMultiplexer!>!>?
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.ConnectionMultiplexerFactory.set -> void
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.InstanceName.get -> string?
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.InstanceName.set -> void
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.ProfilingSession.get -> System.Func<StackExchange.Redis.Profiling.ProfilingSession!>?
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.ProfilingSession.set -> void
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.RedisOutputCacheOptions() -> void

which is the new API to register SE.Redis into output cache

Alternatives considered

  1. eating the GC overhead, zero API change - hugely undesirable in this context
  2. have a new GetAsync API that returns some kind of "data with lifetime" - however, there's no good metaphor for this, and it pushes the buffer management into each individual buffer implementation, rather than having one buffer implementation and having the cache implementations just worry about cache

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarea-middlewareIncludes: URL rewrite, redirect, response cache/compression, session, and other general middlewaresfeature-output-caching

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions