Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 6 additions & 26 deletions StackExchange.Redis.sln
Original file line number Diff line number Diff line change
Expand Up @@ -103,31 +103,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{00CA0876-DA9
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "toys", "toys", "{E25031D3-5C64-430D-B86F-697B66816FD8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{153A10E4-E668-41AD-9E0F-6785CE7EED66}"
ProjectSection(SolutionItems) = preProject
docs\Basics.md = docs\Basics.md
docs\Configuration.md = docs\Configuration.md
docs\Events.md = docs\Events.md
docs\ExecSync.md = docs\ExecSync.md
docs\index.md = docs\index.md
docs\KeysScan.md = docs\KeysScan.md
docs\KeysValues.md = docs\KeysValues.md
docs\PipelinesMultiplexers.md = docs\PipelinesMultiplexers.md
docs\Profiling.md = docs\Profiling.md
docs\Profiling_v1.md = docs\Profiling_v1.md
docs\Profiling_v2.md = docs\Profiling_v2.md
docs\PubSubOrder.md = docs\PubSubOrder.md
docs\ReleaseNotes.md = docs\ReleaseNotes.md
docs\Resp3.md = docs\Resp3.md
docs\RespLogging.md = docs\RespLogging.md
docs\Scripting.md = docs\Scripting.md
docs\Server.md = docs\Server.md
docs\Testing.md = docs\Testing.md
docs\ThreadTheft.md = docs\ThreadTheft.md
docs\Timeouts.md = docs\Timeouts.md
docs\Transactions.md = docs\Transactions.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestConsoleBaseline", "toys\TestConsoleBaseline\TestConsoleBaseline.csproj", "{D58114AE-4998-4647-AFCA-9353D20495AE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = ".github", ".github\.github.csproj", "{8FB98E7D-DAE2-4465-BD9A-104000E0A2D4}"
Expand All @@ -142,6 +117,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleTest", "tests\Consol
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleTestBaseline", "tests\ConsoleTestBaseline\ConsoleTestBaseline.csproj", "{69A0ACF2-DF1F-4F49-B554-F732DCA938A3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "docs", "docs\docs.csproj", "{64CF03B6-6B29-4C4C-88B8-7B9E317D631A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -192,6 +169,10 @@ Global
{69A0ACF2-DF1F-4F49-B554-F732DCA938A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{69A0ACF2-DF1F-4F49-B554-F732DCA938A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{69A0ACF2-DF1F-4F49-B554-F732DCA938A3}.Release|Any CPU.Build.0 = Release|Any CPU
{64CF03B6-6B29-4C4C-88B8-7B9E317D631A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{64CF03B6-6B29-4C4C-88B8-7B9E317D631A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{64CF03B6-6B29-4C4C-88B8-7B9E317D631A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{64CF03B6-6B29-4C4C-88B8-7B9E317D631A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -209,7 +190,6 @@ Global
{D082703F-1652-4C35-840D-7D377F6B9979} = {96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05}
{8375813E-FBAF-4DA3-A2C7-E4645B39B931} = {E25031D3-5C64-430D-B86F-697B66816FD8}
{3DA1EEED-E9FE-43D9-B293-E000CFCCD91A} = {E25031D3-5C64-430D-B86F-697B66816FD8}
{153A10E4-E668-41AD-9E0F-6785CE7EED66} = {3AD17044-6BFF-4750-9AC2-2CA466375F2A}
{D58114AE-4998-4647-AFCA-9353D20495AE} = {E25031D3-5C64-430D-B86F-697B66816FD8}
{A9F81DA3-DA82-423E-A5DD-B11C37548E06} = {96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05}
{A0F89B8B-32A3-4C28-8F1B-ADE343F16137} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A}
Expand Down
77 changes: 77 additions & 0 deletions docs/CancellationTimeout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Ambient Cancellation Support

StackExchange.Redis supports cancellation / timeout of operations. Because this feature impacts all operations, rather than add new parameters
to every method, it uses a declarative scope - an "ambient" context. This uses the `AsyncLocal<T>` feature, allowing for meaning:

- unrelated code-paths (threads or async-contexts) can have different values without conflicting with each-other
- the value is correctly propagated between `async`/`await` code

## Usage

### Timeout

Timeouts are probably the most common cancellation scenario, so is exposed directly:

```csharp
using (database.Multiplexer.WithTimeout(TimeSpan.FromSeconds(5)))
// using (database.Multiplexer.WithTimeout(5_000)) // identical
{
await database.StringSetAsync("key", "value");
var value = await database.StringGetAsync("key");
// operations will be cancelled when the *combined* time (i.e. from the `WithTimeout` call) exceeds 5 seconds
}
```

### Cancellation

You can also use `CancellationToken` to drive cancellation:

```csharp
CancellationToken token = ...; // for example, from HttpContext.RequestAborted
using (database.Multiplexer.WithCancellation(token))
{
await database.StringSetAsync("key", "value");
var value = await database.StringGetAsync("key");
// both operations use the cancellation token
}
```
### Combined Cancellation and Timeout

These two concepts can be combined:

```csharp
CancellationToken token = ...; // for example, from HttpContext.RequestAborted
using (database.Multiplexer.WithCancellationAndTimeout(token, TimeSpan.FromSeconds(10)))
// using (database.Multiplexer.WithCancellationAndTimeout(token, 10_000)) // identical
{
await database.StringSetAsync("key", "value");
var value = await database.StringGetAsync("key");
// operations use the cancellation token *and* observe the specified timeout
}
```

### Nested Scopes

Timeout/cancellation scopes can be nested, with the inner scope *replacing* the outer scope for that database:

```csharp
using (database.Multiplexer.WithCancellation(yourToken))
{
await database.StringSetAsync("key1", "value1"); // Uses yourToken

using (database.Multiplexer.WithTimeout(5000))
{
await database.StringSetAsync("key2", "value2"); // Uses 5s timeout, but does *not* observe yourToken
}

await database.StringSetAsync("key3", "value3"); // Uses yourToken
}
```

Consequently, timeout/cancellation can be suppressed by using `.WithCancellation(CancellationToken.None)`.

## Multiplexer scope

The scope of a `WithTimeout` (etc) call is tied to the *multiplexer*, hence the typical usage of `database.Multiplexer.WithTimeout(...)`.
Usually, there is only a single multiplexer in use, but this choice ensures that there are no surprises by library code outside of
your control / knowledge being impacted by your local cancellation / timeout choices.
1 change: 1 addition & 0 deletions docs/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Current package versions:
- Add support for new `BITOP` operations in CE 8.2 ([#2900 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2900))
- Package updates ([#2906 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2906))
- Fix handshake error with `CLIENT ID` ([#2909 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2909))
- Support scoped timeout and cancellation ([#2908 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2908))

## 2.8.41

Expand Down
6 changes: 6 additions & 0 deletions docs/docs.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Project Sdk="Microsoft.Build.NoTargets/3.3.0">
<!-- this is actually here just to serve as a hub for docs, so we don't need to keep editing the sln-->
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
</Project>
4 changes: 2 additions & 2 deletions src/StackExchange.Redis/ChannelMessageQueue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ private async Task OnMessageSyncImpl()
catch (ChannelClosedException) { break; } // expected
catch (Exception ex)
{
_parent?.multiplexer?.OnInternalError(ex);
_parent?.Multiplexer?.OnInternalError(ex);
break;
}

Expand Down Expand Up @@ -305,7 +305,7 @@ private async Task OnMessageAsyncImpl()
catch (ChannelClosedException) { break; } // expected
catch (Exception ex)
{
_parent?.multiplexer?.OnInternalError(ex);
_parent?.Multiplexer?.OnInternalError(ex);
break;
}

Expand Down
21 changes: 12 additions & 9 deletions src/StackExchange.Redis/Condition.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;

namespace StackExchange.Redis
{
Expand Down Expand Up @@ -391,7 +392,7 @@ private class ConditionMessage : Message.CommandKeyBase
private readonly RedisValue value1;

public ConditionMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value)
: base(db, flags, command, key)
: base(db, flags, command, key, CancellationToken.None) // once we're inside a transaction: just issue the commands
{
Condition = condition;
this.value = value; // note no assert here
Expand Down Expand Up @@ -468,7 +469,7 @@ public override string ToString() =>

internal override IEnumerable<Message> CreateMessages(int db, IResultBox? resultBox)
{
yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key);
yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key, CancellationToken.None);

var message = ConditionProcessor.CreateMessage(this, db, CommandFlags.None, cmd, key, expectedValue);
message.SetSource(ConditionProcessor.Default, resultBox);
Expand Down Expand Up @@ -537,7 +538,7 @@ public override string ToString() =>

internal sealed override IEnumerable<Message> CreateMessages(int db, IResultBox? resultBox)
{
yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key);
yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key, CancellationToken.None);

var message = ConditionProcessor.CreateMessage(this, db, CommandFlags.None, cmd, key, memberName);
message.SetSource(ConditionProcessor.Default, resultBox);
Expand Down Expand Up @@ -608,7 +609,7 @@ public override string ToString() =>

internal sealed override IEnumerable<Message> CreateMessages(int db, IResultBox? resultBox)
{
yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key);
yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key, CancellationToken.None);

var message = ConditionProcessor.CreateMessage(this, db, CommandFlags.None, RedisCommand.LINDEX, key, index);
message.SetSource(ConditionProcessor.Default, resultBox);
Expand Down Expand Up @@ -681,7 +682,7 @@ public LengthCondition(in RedisKey key, RedisType type, int compareToResult, lon

internal sealed override IEnumerable<Message> CreateMessages(int db, IResultBox? resultBox)
{
yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key);
yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key, CancellationToken.None);

var message = ConditionProcessor.CreateMessage(this, db, CommandFlags.None, cmd, key);
message.SetSource(ConditionProcessor.Default, resultBox);
Expand Down Expand Up @@ -738,7 +739,7 @@ public override string ToString() =>

internal sealed override IEnumerable<Message> CreateMessages(int db, IResultBox? resultBox)
{
yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key);
yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key, CancellationToken.None); // inside a tran: just issue the command

var message = ConditionProcessor.CreateMessage(this, db, CommandFlags.None, RedisCommand.ZCOUNT, key, min, max);
message.SetSource(ConditionProcessor.Default, resultBox);
Expand Down Expand Up @@ -794,7 +795,7 @@ public override string ToString() =>

internal sealed override IEnumerable<Message> CreateMessages(int db, IResultBox? resultBox)
{
yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key);
yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key, resultBox?.CancellationToken ?? CancellationToken.None);

var message = ConditionProcessor.CreateMessage(this, db, CommandFlags.None, RedisCommand.ZCOUNT, key, sortedSetScore, sortedSetScore);
message.SetSource(ConditionProcessor.Default, resultBox);
Expand Down Expand Up @@ -832,10 +833,12 @@ public sealed class ConditionResult

private volatile bool wasSatisfied;

internal ConditionResult(Condition condition)
internal CancellationToken CancellationToken => resultBox?.CancellationToken ?? CancellationToken.None;

internal ConditionResult(Condition condition, CancellationToken cancellationToken)
{
Condition = condition;
resultBox = SimpleResultBox<bool>.Create();
resultBox = SimpleResultBox<bool>.Create(cancellationToken);
}

/// <summary>
Expand Down
Loading
Loading