dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.17k stars 9.93k forks source link

Add ability to leverage StackExchange.Redis.CommandFlags via IDistributedCache or IDistributedRedisCache (new) #41948

Open udlose opened 3 years ago

udlose commented 3 years ago

Is your feature request related to a problem? Please describe.

I'd like to be able to specify the usage of a Replica or Slave endpoint instead of always going to the Master. This would allow me to offload traffic for read-only cache operations where the latest data is not critical resulting in improved performance.

Currently, the StackExchange.Redis.IDatabase API allows for this by use of optional StackExchange.Redis.CommandFlag enum that can be specified in API calls. However, the implementation of Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache doesn't provide these optional params to be passed thru.

Describe the solution you'd like

I would like for the implementation of Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache to provide the ability to pass a flag that allows me to specify replica usage (master, slave, replica, etc.). You would probably need to create a new interface to expose this which Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache would implement e.g. IDistributedRedisCache.

Describe alternatives you've considered

Using StackExchange.Redis.IDatabase directly instead of Microsoft's IDistributedCache wrapper. Unfortunately, it would be a lot of work to change for our applications and we do prefer the IDistributedCache wrapper in general. We also use this with Polly for caching and Polly provides a nice integration with IDistributedCache already.

dotnet-issue-labeler[bot] commented 3 years ago

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

udlose commented 3 years ago

I don't have write permissions but ill tag the lead for the area: @ericstj , @eerhardt

ghost commented 3 years ago

Tagging subscribers to this area: @eerhardt, @maryamariyan, @michaelgsharp See info in area-owners.md if you want to be subscribed.

Issue Details
### Is your feature request related to a problem? Please describe. I'd like to be able to specify the usage of a Replica or Slave endpoint instead of always going to the Master. This would allow me to offload traffic for read-only cache operations where the latest data is not critical resulting in improved performance. Currently, the `StackExchange.Redis.IDatabase` API allows for this by use of optional [`StackExchange.Redis.CommandFlag` enum](https://github.com/StackExchange/StackExchange.Redis/blob/8612fb8a9278822c87d5476819325a7438e596ca/src/StackExchange.Redis/Enums/CommandFlags.cs#L47) that can be specified in API calls. However, the implementation of `Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache` doesn't provide these optional params to be passed thru. ### Describe the solution you'd like I would like for the implementation of IDistributedCache to provide the ability to pass a flag that allows me to specify replica usage (master, slave, replica, etc.) ### Describe alternatives you've considered Using `StackExchange.Redis.IDatabase` directly instead of Microsoft's `IDistributedCache` wrapper. Unfortunately, it would be a lot of work to change for our applications and we do prefer the `IDistributedCache` wrapper in general. We also use this with [Polly](https://github.com/App-vNext/Polly) for caching and Polly provides a nice integration with `IDistributedCache` already.
Author: udlose
Assignees: -
Labels: `area-Extensions-Caching`, `untriaged`
Milestone: -
pranavacharya commented 2 years ago

Can adding support for an extra optional parameters to IDistributedCache interface's methods solve this ?

maryamariyan commented 2 years ago

IDistributedCache wouldnt be getting Redis or StackExchange related logic in. Transferring to aspnetcore repo to carry conversation forward.

adityamandaleeka commented 2 years ago

@udlose Can you share more about why you need this? It seems strange to modify the interface to support implementation-specific parameters.

ghost commented 2 years ago

Hi @udlose. We have added the "Needs: Author Feedback" label to this issue, which indicates that we have an open question for you before we can take further action. This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

udlose commented 2 years ago

@adityamandaleeka, @maryamariyan - perhaps the confusion is that I misspoke by asking that the implementation-agnostic interface of IDistributedCache be modified to include implementation-specific parameters pertaining to [StackExchange.Redis.CommandFlag](https://github.com/StackExchange/StackExchange.Redis/blob/8612fb8a9278822c87d5476819325a7438e596ca/src/StackExchange.Redis/Enums/CommandFlags.cs#L47).

I've updated the title and feature request details to help clarify.

To rephrase, for performance reasons, I'd like to be able to specify whether my Redis API call is sent to a Replica, Slave, or Master instance. As I mention, the Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache implementation doesn't allow for specifying these valuable params. So at a minimum, I think that ability should be added. It would at least give users the ability to create an interface (e.g. IDistributedRedisCache) that exposes those params on the API interface. Though I'd argue that the IDistributedRedisCache interface should be implemented in the Microsoft.Extensions.Caching.StackExchangeRedis nuget and not place the responsibility on devs to have to create the interface. IMO, an API should be able to be consumed without additional work on behalf of the API consumer.

I am just throwing out an idea of what the implementation might look like. If you have an alternative that your more amenable to but still provides the ability to specify the [StackExchange.Redis.CommandFlag], I'd be happy just the same.

Please let me know if you have any more questions as I'd really like to see this added. It seems important from a performance perspective.

adityamandaleeka commented 2 years ago

Thanks @udlose. Adding this to the RedisCache implementation seems reasonable.

We don't want to create another new interface but as you mentioned, users can create that if they want it.

adityamandaleeka commented 2 years ago

We'd take a contribution to add this if you're interested. Feel free to copy/paste the issue template here into a comment on this issue and we will review it.

ghost commented 2 years ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

udlose commented 2 years ago

Thanks @udlose. Adding this to the RedisCache implementation seems reasonable.

We don't want to create another new interface but as you mentioned, users can create that if they want it.

@adityamandaleeka why are you not open to adding this interface to the Microsoft.Extensions.Caching.StackExchangeRedis nuget?

adityamandaleeka commented 2 years ago

To clarify, we are open to updating Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache with the added parameter. The thing we don't think is useful is creating and consuming another new interface like IDistributedRedisCache.

udlose commented 2 years ago

@adityamandaleeka, @maryamariyan so I'm working on submitting a PR for this change and I have a question. I'm making changes as follows to Microsoft.Extensions.Caching.StackExchangeRedis.RedisCache:

// THE METHOD BELOW IS EXISTING
    /// <inheritdoc />
    public byte[]? Get(string key)
    {
        if (key == null)
        {
            throw new ArgumentNullException(nameof(key));
        }

        return GetAndRefresh(key, getData: true);
    }

// THE METHOD BELOW IS NEW
    /// <inheritdoc />
    public byte[]? Get(string key, CommandFlags commandFlags)
    {
        if (key == null)
        {
            throw new ArgumentNullException(nameof(key));
        }

        return GetAndRefresh(key, getData: true, commandFlags: commandFlags);
    }

// THE METHOD BELOW IS EXISTING
    /// <inheritdoc />
    public async Task<byte[]?> GetAsync(string key, CancellationToken token = default(CancellationToken), CommandFlags commandFlags = CommandFlags.None)
    {
        //code elided
    }

When I add my default params, to the end of the GetAsync method (so that I do not violate RS0027 (https://github.com/dotnet/roslyn/blob/main/docs/Adding%20Optional%20Parameters%20in%20Public%20API.md), I get a CompilerError: CS 1068 (CancellationToken parameters must come last).

If I have the new CommandFlag param before the optional CancellationToken param to satisfy CA1068, I violate binary compatibility and RS0027. I don't want to break binary compatibility but I don't like ignoring compiler warnings either.

What are your suggestions?

udlose commented 2 years ago

Actually it looks as if I won't be able to add the CommandFlags param to this signature because it then no longer implements the IDistributedCache signature of:

public async Task<byte[]?> GetAsync(string key, CancellationToken token = default(CancellationToken))

If I add another signature to not break the IDistributedCache contract, I then break RS0027 - Adding optional parameters to public methods

public async Task<byte[]?> GetAsync(string key, CommandFlags commandFlags, CancellationToken token)

Not sure where to go from here.

udlose commented 1 year ago

@adityamandaleeka @maryamariyan any updates or suggestions on how to proceed? (see above comment)

peterdrier commented 7 months ago

If I understand this right, since the IDistributedCache is using the IConnectionMultiplexer directly.. Perhaps a way forward would be to configure (or likely add a config option to ...) SE.Redis to PreferReplica on read operations when the multiplexer is created initially.

And/or adding a parameter to the RedisCacheOptions passed into AddStackExchangeRedisCache, specifying PreferReplica on reads..

That way IDistributedCache's interface is none the wiser, but the underlying implementation can direct reads/writes appropriately.

Could that work?

udlose commented 7 months ago

If I understand this right, since the IDistributedCache is using the IConnectionMultiplexer directly.. Perhaps a way forward would be to configure (or likely add a config option to ...) SE.Redis to PreferReplica on read operations when the multiplexer is created initially.

And/or adding a parameter to the RedisCacheOptions passed into AddStackExchangeRedisCache, specifying PreferReplica on reads..

That way IDistributedCache's interface is none the wiser, but the underlying implementation can direct reads/writes appropriately.

Could that work?

At a minimum, that would provide at least some way to customize the behavior. It would be nice to be able to have flexibility to change behavior via API parameterss as the StackExchangeRedis API's have.

davidfowl commented 7 months ago

You can't change the interface (it's a breaking change), you also can't add redis specific APIs to the base interface, it would break the abstraction. It seems like you want a redis interface, IDistributedCache isn't it.

peterdrier commented 7 months ago

You can't change the interface (it's a breaking change), you also can't add redis specific APIs to the base interface, it would break the abstraction. It seems like you want a redis interface, IDistributedCache isn't it.

Not sure which "You" that you're replying to here, so your message is ambiguous.

davidfowl commented 7 months ago

My bad, I was looking referring to this comment. My read of the issue is that we're trying to flow a per call (Get/Set) flag from IDistributedCache to the underlying redis implementation and that breaks the abstraction.

It seems like there's a desire for a more lightweight redis specific interface.

rwasef1830 commented 7 months ago

There's an awkward workaround to use the factory option of redis distributed cache implementation to return a decorated connection multiplexer that returns decorated redis clients that can then take hints from the key name and set the appropriate flag...

Awkward and tedious but would work.

Mindex-Brandon-Key commented 2 months ago

This is also causing us grief. All traffic is going to the writer and no load is able to go to the read replicas. Instead of modifying the interface, what if the CommandFlags are passed in the options during registration, then used by the RedisCache implementation?

In RedisCacheOptions.cs add public CommandFlags ReadCommandFlags { get; set; }. In RedisCache.GetAndRefresh and RedisCache.GetAndRefreshAsync add _options.ReadCommandFlags to the cache.HashGet call.

The scope should be pretty small. Existing uses won't be impacted - the default CommandFlags value is None, matching the HashGet's default value. Users that want to use read replicas can set RedisCacheOptions.ReadCommandFlags = CommandFlags.PreferReplica.

acasciani commented 2 months ago

This is a pain point for us as well. The changes that @Mindex-Brandon-Key suggested would fit our use case as well.

Mindex-Brandon-Key commented 2 months ago

I just found https://github.com/dotnet/aspnetcore/issues/28375 which seems to echo the missing behavior