Commit e2d5e9e3 authored by Marc Gravell's avatar Marc Gravell

Merge branch 'master' of github.com:StackExchange/StackExchange.Redis into coreclr

parents c4eb409b 267bce16
...@@ -3,16 +3,20 @@ ...@@ -3,16 +3,20 @@
The central object in StackExchange.Redis is the `ConnectionMultiplexer` class in the `StackExchange.Redis` namespace; this is the object that hides away the details of multiple servers. Because the `ConnectionMultiplexer` does a lot, it is designed to be **shared and reused** between callers. You should not create a `ConnectionMultiplexer` per operation. It is fully thread-safe and ready for this usage. In all the subsequent examples it will be assumed that you have a `ConnectionMultiplexer` instance stored away for re-use. But for now, let's create one. This is done using `ConnectionMultiplexer.Connect` or `ConnectionMultiplexer.ConnectAsync`, passing in either a configuration string or a `ConfigurationOptions` object. The configuration string can take the form of a comma-delimited series of nodes, so let's just connect to an instance on the local machine on the default port (6379): The central object in StackExchange.Redis is the `ConnectionMultiplexer` class in the `StackExchange.Redis` namespace; this is the object that hides away the details of multiple servers. Because the `ConnectionMultiplexer` does a lot, it is designed to be **shared and reused** between callers. You should not create a `ConnectionMultiplexer` per operation. It is fully thread-safe and ready for this usage. In all the subsequent examples it will be assumed that you have a `ConnectionMultiplexer` instance stored away for re-use. But for now, let's create one. This is done using `ConnectionMultiplexer.Connect` or `ConnectionMultiplexer.ConnectAsync`, passing in either a configuration string or a `ConfigurationOptions` object. The configuration string can take the form of a comma-delimited series of nodes, so let's just connect to an instance on the local machine on the default port (6379):
using StackExchange.Redis; ```C#
... using StackExchange.Redis;
ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost"); ...
// ^^^ store and re-use this!!! ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost");
// ^^^ store and re-use this!!!
```
Note that `ConnectionMultiplexer` implements `IDisposable` and can be disposed when no longer required, but I am deliberately not showing `using` statement usage, because it is exceptionally rare that you would want to use a `ConnectionMultiplexer` briefly, as the idea is to re-use this object. Note that `ConnectionMultiplexer` implements `IDisposable` and can be disposed when no longer required, but I am deliberately not showing `using` statement usage, because it is exceptionally rare that you would want to use a `ConnectionMultiplexer` briefly, as the idea is to re-use this object.
A more complicated scenario might involve a master/slave setup; for this usage, simply specify all the desired nodes that make up that logical redis tier (it will automatically identify the master): A more complicated scenario might involve a master/slave setup; for this usage, simply specify all the desired nodes that make up that logical redis tier (it will automatically identify the master):
ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("server1:6379,server2:6379"); ```C#
ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("server1:6379,server2:6379");
```
If it finds both nodes are masters, a tie-breaker key can optionally be specified that can be used to resolve the issue, however such a condition is fortunately very rare. If it finds both nodes are masters, a tie-breaker key can optionally be specified that can be used to resolve the issue, however such a condition is fortunately very rare.
...@@ -27,30 +31,38 @@ Using a redis database ...@@ -27,30 +31,38 @@ Using a redis database
Accessing a redis database is as simple as: Accessing a redis database is as simple as:
IDatabase db = redis.GetDatabase(); ```C#
IDatabase db = redis.GetDatabase();
```
The object returned from `GetDatabase` is a cheap pass-thru object, and does not need to be stored. Note that redis supports multiple databases (although this is not supported on "cluster"); this can be optionally specified in the call to `GetDatabase`. Additionally, if you plan to make use of the asynchronous API and you require the [`Task.AsyncState`][2] to have a value, this can also be specified: The object returned from `GetDatabase` is a cheap pass-thru object, and does not need to be stored. Note that redis supports multiple databases (although this is not supported on "cluster"); this can be optionally specified in the call to `GetDatabase`. Additionally, if you plan to make use of the asynchronous API and you require the [`Task.AsyncState`][2] to have a value, this can also be specified:
int databaseNumber = ... ```C#
object asyncState = ... int databaseNumber = ...
IDatabase db = redis.GetDatabase(databaseNumber, asyncState); object asyncState = ...
IDatabase db = redis.GetDatabase(databaseNumber, asyncState);
```
Once you have the `IDatabase`, it is simply a case of using the [redis API](http://redis.io/commands). Note that all methods have both synchronous and asynchronous implementations. In line with Microsoft's naming guidance, the asynchronous methods all end `...Async(...)`, and are fully `await`-able etc. Once you have the `IDatabase`, it is simply a case of using the [redis API](http://redis.io/commands). Note that all methods have both synchronous and asynchronous implementations. In line with Microsoft's naming guidance, the asynchronous methods all end `...Async(...)`, and are fully `await`-able etc.
The simplest operation would be to store and retrieve a value: The simplest operation would be to store and retrieve a value:
string value = "abcdefg"; ```C#
db.StringSet("mykey", value); string value = "abcdefg";
... db.StringSet("mykey", value);
string value = db.StringGet("mykey"); ...
Console.WriteLine(value); // writes: "abcdefg" string value = db.StringGet("mykey");
Console.WriteLine(value); // writes: "abcdefg"
```
Note that the `String...` prefix here denotes the [String redis type](http://redis.io/topics/data-types), and is largely separate to the [.NET String type][3], although both can store text data. However, redis allows raw binary data for both keys and values - the usage is identical: Note that the `String...` prefix here denotes the [String redis type](http://redis.io/topics/data-types), and is largely separate to the [.NET String type][3], although both can store text data. However, redis allows raw binary data for both keys and values - the usage is identical:
byte[] key = ..., value = ...; ```C#
db.StringSet(key, value); byte[] key = ..., value = ...;
... db.StringSet(key, value);
byte[] value = db.StringGet(key); ...
byte[] value = db.StringGet(key);
```
The entire range of [redis database commands](http://redis.io/commands) covering all redis data types is available for use. The entire range of [redis database commands](http://redis.io/commands) covering all redis data types is available for use.
...@@ -59,17 +71,23 @@ Using redis pub/sub ...@@ -59,17 +71,23 @@ Using redis pub/sub
Another common use of redis is as a [pub/sub message](http://redis.io/topics/pubsub) distribution tool; this too is simple, and in the event of connection failure, the `ConnectionMultiplexer` will handle all the details of re-subscribing to the requested channels. Another common use of redis is as a [pub/sub message](http://redis.io/topics/pubsub) distribution tool; this too is simple, and in the event of connection failure, the `ConnectionMultiplexer` will handle all the details of re-subscribing to the requested channels.
ISubscriber sub = redis.GetSubscriber(); ```C#
ISubscriber sub = redis.GetSubscriber();
```
Again, the object returned from `GetSubscriber` is a cheap pass-thru object that does not need to be stored. The pub/sub API has no concept of databases, but as before we can optionally provide an async-state. Note that all subscriptions are global: they are not scoped to the lifetime of the `ISubscriber` instance. The pub/sub features in redis use named "channels"; channels do not need to be defined in advance on the server (an interesting use here is things like per-user notification channels, which is what drives parts of the realtime updates on [Stack Overflow](http://stackoverflow.com)). As is common in .NET, subscriptions take the form of callback delegates which accept the channel-name and the message: Again, the object returned from `GetSubscriber` is a cheap pass-thru object that does not need to be stored. The pub/sub API has no concept of databases, but as before we can optionally provide an async-state. Note that all subscriptions are global: they are not scoped to the lifetime of the `ISubscriber` instance. The pub/sub features in redis use named "channels"; channels do not need to be defined in advance on the server (an interesting use here is things like per-user notification channels, which is what drives parts of the realtime updates on [Stack Overflow](http://stackoverflow.com)). As is common in .NET, subscriptions take the form of callback delegates which accept the channel-name and the message:
sub.Subscribe("messages", (channel, message) => { ```C#
sub.Subscribe("messages", (channel, message) => {
Console.WriteLine((string)message); Console.WriteLine((string)message);
}); });
```
Separately (and often in a separate process on a separate machine) you can publish to this channel: Separately (and often in a separate process on a separate machine) you can publish to this channel:
sub.Publish("messages", "hello"); ```C#
sub.Publish("messages", "hello");
```
This will (virtually instantaneously) write `"hello"` to the console of the subscribed process. As before, both channel-names and messages can be binary. This will (virtually instantaneously) write `"hello"` to the console of the subscribed process. As before, both channel-names and messages can be binary.
...@@ -80,16 +98,22 @@ Accessing individual servers ...@@ -80,16 +98,22 @@ Accessing individual servers
For maintenance purposes, it is sometimes necessary to issue server-specific commands: For maintenance purposes, it is sometimes necessary to issue server-specific commands:
IServer server = redis.GetServer("localhost", 6379); ```C#
IServer server = redis.GetServer("localhost", 6379);
```
The `GetServer` method will accept an [`EndPoint`](http://msdn.microsoft.com/en-us/library/system.net.endpoint(v=vs.110).aspx) or the name/value pair that uniquely identify the server. As before, the object returned from `GetServer` is a cheap pass-thru object that does not need to be stored, and async-state can be optionally specified. Note that the set of available endpoints is also available: The `GetServer` method will accept an [`EndPoint`](http://msdn.microsoft.com/en-us/library/system.net.endpoint(v=vs.110).aspx) or the name/value pair that uniquely identify the server. As before, the object returned from `GetServer` is a cheap pass-thru object that does not need to be stored, and async-state can be optionally specified. Note that the set of available endpoints is also available:
EndPoint[] endpoints = redis.GetEndPoints(); ```C#
EndPoint[] endpoints = redis.GetEndPoints();
```
From the `IServer` instance, the [Server commands](http://redis.io/commands#server) are available; for example: From the `IServer` instance, the [Server commands](http://redis.io/commands#server) are available; for example:
DateTime lastSave = server.LastSave(); ```C#
ClientInfo[] clients = server.ClientList(); DateTime lastSave = server.LastSave();
ClientInfo[] clients = server.ClientList();
```
Sync vs Async vs Fire-and-Forget Sync vs Async vs Fire-and-Forget
--- ---
...@@ -107,15 +131,19 @@ The synchronous usage is already shown in the examples above. This is the simple ...@@ -107,15 +131,19 @@ The synchronous usage is already shown in the examples above. This is the simple
For asynchronous usage, the key difference is the `Async` suffix on methods, and (typically) the use of the `await` language feature. For example: For asynchronous usage, the key difference is the `Async` suffix on methods, and (typically) the use of the `await` language feature. For example:
string value = "abcdefg"; ```C#
await db.StringSetAsync("mykey", value); string value = "abcdefg";
... await db.StringSetAsync("mykey", value);
string value = await db.StringGetAsync("mykey"); ...
Console.WriteLine(value); // writes: "abcdefg" string value = await db.StringGetAsync("mykey");
Console.WriteLine(value); // writes: "abcdefg"
```
The fire-and-forget usage is accessed by the optional `CommandFlags flags` parameter on all methods (defaults to none). In this usage, the method returns the default value immediately (so a method that normally returns a `String` will always return `null`, and a method that normally returns an `Int64` will always return `0`). The operation will continue in the background. A typical use-case of this might be to increment page-view counts: The fire-and-forget usage is accessed by the optional `CommandFlags flags` parameter on all methods (defaults to none). In this usage, the method returns the default value immediately (so a method that normally returns a `String` will always return `null`, and a method that normally returns an `Int64` will always return `0`). The operation will continue in the background. A typical use-case of this might be to increment page-view counts:
db.StringIncrement(pageKey, flags: CommandFlags.FireAndForget); ```C#
db.StringIncrement(pageKey, flags: CommandFlags.FireAndForget);
```
......
...@@ -3,7 +3,9 @@ ...@@ -3,7 +3,9 @@
Because there are lots of different ways to configure redis, StackExchange.Redis offers a rich configuration model, which is invoked when calling `Connect` (or `ConnectAsync`): Because there are lots of different ways to configure redis, StackExchange.Redis offers a rich configuration model, which is invoked when calling `Connect` (or `ConnectAsync`):
var conn = ConnectionMultiplexer.Connect(configuration); ```C#
var conn = ConnectionMultiplexer.Connect(configuration);
```
The `configuration` here can be either: The `configuration` here can be either:
...@@ -17,32 +19,43 @@ Basic Configuration Strings ...@@ -17,32 +19,43 @@ Basic Configuration Strings
The *simplest* configuration example is just the host name: The *simplest* configuration example is just the host name:
var conn = ConnectionMultiplexer.Connect("localhost"); ```C#
var conn = ConnectionMultiplexer.Connect("localhost");
```
This will connect to a single server on the local machine using the default redis port (6379). Additional options are simply appended (comma-delimited). Ports are represented with a colon (`:`) as is usual. Configuration *options* include an `=` after the name. For example: This will connect to a single server on the local machine using the default redis port (6379). Additional options are simply appended (comma-delimited). Ports are represented with a colon (`:`) as is usual. Configuration *options* include an `=` after the name. For example:
var conn = ConnectionMultiplexer.Connect("redis0:6380,redis1:6380,allowAdmin=true"); ```C#
var conn = ConnectionMultiplexer.Connect("redis0:6380,redis1:6380,allowAdmin=true");
```
An overview of mapping between the `string` and `ConfigurationOptions` representation is shown below, but you can switch between them trivially: An overview of mapping between the `string` and `ConfigurationOptions` representation is shown below, but you can switch between them trivially:
ConfigurationOptions options = ConfigurationOptions.Parse(configString); ```C#
ConfigurationOptions options = ConfigurationOptions.Parse(configString);
```
or: or:
string configString = options.ToString(); ```C#
string configString = options.ToString();
```
A common usage is to store the *basic* details in a string, and then apply specific details at runtime: A common usage is to store the *basic* details in a string, and then apply specific details at runtime:
string configString = GetRedisConfiguration(); ```C#
var options = ConfigurationOptions.Parse(configString); string configString = GetRedisConfiguration();
options.ClientName = GetAppName(); // only known at runtime var options = ConfigurationOptions.Parse(configString);
options.AllowAdmin = true; options.ClientName = GetAppName(); // only known at runtime
conn = ConnectionMultiplexer.Connect(options); options.AllowAdmin = true;
conn = ConnectionMultiplexer.Connect(options);
```
Microsoft Azure Redis example with password Microsoft Azure Redis example with password
var conn = ConnectionMultiplexer.Connect("contoso5.redis.cache.windows.net,ssl=true,password=..."); ```C#
var conn = ConnectionMultiplexer.Connect("contoso5.redis.cache.windows.net,ssl=true,password=...");
```
Configuration Options Configuration Options
--- ---
...@@ -79,8 +92,9 @@ Automatic and Manual Configuration ...@@ -79,8 +92,9 @@ Automatic and Manual Configuration
In many common scenarios, StackExchange.Redis will automatically configure a lot of settings, including the server type and version, connection timeouts, and master/slave relationships. Sometimes, though, the commands for this have been disabled on the redis server. In this case, it is useful to provide more information: In many common scenarios, StackExchange.Redis will automatically configure a lot of settings, including the server type and version, connection timeouts, and master/slave relationships. Sometimes, though, the commands for this have been disabled on the redis server. In this case, it is useful to provide more information:
ConfigurationOptions config = new ConfigurationOptions ```C#
{ ConfigurationOptions config = new ConfigurationOptions
{
EndPoints = EndPoints =
{ {
{ "redis0", 6379 }, { "redis0", 6379 },
...@@ -94,41 +108,49 @@ In many common scenarios, StackExchange.Redis will automatically configure a lot ...@@ -94,41 +108,49 @@ In many common scenarios, StackExchange.Redis will automatically configure a lot
KeepAlive = 180, KeepAlive = 180,
DefaultVersion = new Version(2, 8, 8), DefaultVersion = new Version(2, 8, 8),
Password = "changeme" Password = "changeme"
}; };
```
Which is equivalent to the command string: Which is equivalent to the command string:
redis0:6379,redis1:6380,keepAlive=180,version=2.8.8,$CLIENT=,$CLUSTER=,$CONFIG=,$ECHO=,$INFO=,$PING= ```config
redis0:6379,redis1:6380,keepAlive=180,version=2.8.8,$CLIENT=,$CLUSTER=,$CONFIG=,$ECHO=,$INFO=,$PING=
```
Renaming Commands Renaming Commands
--- ---
A slightly unusual feature of redis is that you can disable and/or rename individual commands. As per the previous example, this is done via the `CommandMap`, but instead of passing a `HashSet<string>` to `Create()` (to indicate the available or unavailable commands), you pass a `Dictionary<string,string>`. All commands not mentioned in the dictionary are assumed to be enabled and not renamed. A `null` or blank value records that the command is disabled. For example: A slightly unusual feature of redis is that you can disable and/or rename individual commands. As per the previous example, this is done via the `CommandMap`, but instead of passing a `HashSet<string>` to `Create()` (to indicate the available or unavailable commands), you pass a `Dictionary<string,string>`. All commands not mentioned in the dictionary are assumed to be enabled and not renamed. A `null` or blank value records that the command is disabled. For example:
var commands = new Dictionary<string,string> { ```C#
var commands = new Dictionary<string,string> {
{ "info", null }, // disabled { "info", null }, // disabled
{ "select", "use" }, // renamed to SQL equivalent for some reason { "select", "use" }, // renamed to SQL equivalent for some reason
}; };
var options = new ConfigurationOptions { var options = new ConfigurationOptions {
// ... // ...
CommandMap = CommandMap.Create(commands), CommandMap = CommandMap.Create(commands),
// ... // ...
} }
```
The above is equivalent to (in the connection string): The above is equivalent to (in the connection string):
$INFO=,$SELECT=use ```config
$INFO=,$SELECT=use
```
Twemproxy Twemproxy
--- ---
[Twemproxy](https://github.com/twitter/twemproxy) is a tool that allows multiple redis instances to be used as though it were a single server, with inbuilt sharding and fault tolerance (much like redis cluster, but implemented separately). The feature-set available to Twemproxy is reduced. To avoid having to configure this manually, the `Proxy` option can be used: [Twemproxy](https://github.com/twitter/twemproxy) is a tool that allows multiple redis instances to be used as though it were a single server, with inbuilt sharding and fault tolerance (much like redis cluster, but implemented separately). The feature-set available to Twemproxy is reduced. To avoid having to configure this manually, the `Proxy` option can be used:
var options = new ConfigurationOptions ```C#
{ var options = new ConfigurationOptions
{
EndPoints = { "my-server" }, EndPoints = { "my-server" },
Proxy = Proxy.Twemproxy Proxy = Proxy.Twemproxy
}; };
```
Tiebreakers and Configuration Change Announcements Tiebreakers and Configuration Change Announcements
--- ---
......
...@@ -38,16 +38,18 @@ So how do I use them? ...@@ -38,16 +38,18 @@ So how do I use them?
Simple: start from a server, not a database. Simple: start from a server, not a database.
// get the target server ```C#
var server = conn.GetServer(someServer); // get the target server
var server = conn.GetServer(someServer);
// show all keys in database 0 that include "foo" in their name // show all keys in database 0 that include "foo" in their name
foreach(var key in server.Keys(pattern: "*foo*")) { foreach(var key in server.Keys(pattern: "*foo*")) {
Console.WriteLine(key); Console.WriteLine(key);
} }
// completely wipe ALL keys from database 0 // completely wipe ALL keys from database 0
server.FlushDatabase(); server.FlushDatabase();
```
Note that unlike the `IDatabase` API (where the target database has already been selected in the `GetDatabase()` call), these methods take an optional parameter for the database, or it defaults to `0`. Note that unlike the `IDatabase` API (where the target database has already been selected in the `GetDatabase()` call), these methods take an optional parameter for the database, or it defaults to `0`.
......
...@@ -18,48 +18,64 @@ Keys ...@@ -18,48 +18,64 @@ Keys
StackExchange.Redis represents keys by the `RedisKey` type. The good news, though, is that this has implicit conversions to and from both `string` and `byte[]`, allowing both text and binary keys to be used without any complication. For example, the `StringIncrement` method takes a `RedisKey` as the first parameter, but *you don't need to know that*; for example: StackExchange.Redis represents keys by the `RedisKey` type. The good news, though, is that this has implicit conversions to and from both `string` and `byte[]`, allowing both text and binary keys to be used without any complication. For example, the `StringIncrement` method takes a `RedisKey` as the first parameter, but *you don't need to know that*; for example:
string key = ... ```C#
db.StringIncrement(key); string key = ...
db.StringIncrement(key);
```
or or
byte[] key = ... ```C#
db.StringIncrement(key); byte[] key = ...
db.StringIncrement(key);
```
Likewise, there are operations that *return* keys as `RedisKey` - and again, it simply works: Likewise, there are operations that *return* keys as `RedisKey` - and again, it simply works:
string someKey = db.KeyRandom(); ```C#
string someKey = db.KeyRandom();
```
Values Values
--- ---
StackExchange.Redis represents values by the `RedisValue` type. As with `RedisKey`, there are implicit conversions in place which mean that most of the time you never see this type, for example: StackExchange.Redis represents values by the `RedisValue` type. As with `RedisKey`, there are implicit conversions in place which mean that most of the time you never see this type, for example:
db.StringSet("mykey", "myvalue"); ```C#
db.StringSet("mykey", "myvalue");
```
However, in addition to text and binary contents, values can also need to represent typed primitive data - most commonly (in .NET terms) `Int32`, `Int64`, `Double` or `Boolean`. Because of this, `RedisValue` provides a lot more conversion support than `RedisKey`: However, in addition to text and binary contents, values can also need to represent typed primitive data - most commonly (in .NET terms) `Int32`, `Int64`, `Double` or `Boolean`. Because of this, `RedisValue` provides a lot more conversion support than `RedisKey`:
db.StringSet("mykey", 123); // this is still a RedisKey and RedisValue ```C#
... db.StringSet("mykey", 123); // this is still a RedisKey and RedisValue
int i = (int)db.StringGet("mykey"); ...
int i = (int)db.StringGet("mykey");
```
Note that while the conversions from primitives to `RedisValue` are implicit, many of the conversions from `RedisValue` to primitives are explicit: this is because it is very possible that these conversions will fail if the data does not have an appropriate value. Note that while the conversions from primitives to `RedisValue` are implicit, many of the conversions from `RedisValue` to primitives are explicit: this is because it is very possible that these conversions will fail if the data does not have an appropriate value.
Note additionally that *when treated numerically*, redis treats a non-existent key as zero; for consistency with this, nil responses are treated as zero: Note additionally that *when treated numerically*, redis treats a non-existent key as zero; for consistency with this, nil responses are treated as zero:
db.KeyDelete("abc"); ```C#
int i = (int)db.StringGet("abc"); // this is ZERO db.KeyDelete("abc");
int i = (int)db.StringGet("abc"); // this is ZERO
```
If you need to detect the nil condition, then you can check for that: If you need to detect the nil condition, then you can check for that:
db.KeyDelete("abc"); ```C#
var value = db.StringGet("abc"); db.KeyDelete("abc");
bool isNil = value.IsNull; // this is true var value = db.StringGet("abc");
bool isNil = value.IsNull; // this is true
```
or perhaps more simply, just use the provided `Nullable<T>` support: or perhaps more simply, just use the provided `Nullable<T>` support:
db.KeyDelete("abc"); ```C#
var value = (int?)db.StringGet("abc"); // behaves as you would expect db.KeyDelete("abc");
var value = (int?)db.StringGet("abc"); // behaves as you would expect
```
Hashes Hashes
--- ---
...@@ -81,14 +97,18 @@ Scripting ...@@ -81,14 +97,18 @@ Scripting
Because of this, the `ScriptEvaluate` method accepts two separate input arrays: one `RedisKey[]` for the keys, one `RedisValue[]` for the values (both are optional, and are assumed to be empty if omitted). This is probably one of the few times that you'll actually need to type `RedisKey` or `RedisValue` in your code, and that is just because of array variance rules: Because of this, the `ScriptEvaluate` method accepts two separate input arrays: one `RedisKey[]` for the keys, one `RedisValue[]` for the values (both are optional, and are assumed to be empty if omitted). This is probably one of the few times that you'll actually need to type `RedisKey` or `RedisValue` in your code, and that is just because of array variance rules:
var result = db.ScriptEvaluate(TransferScript, ```C#
var result = db.ScriptEvaluate(TransferScript,
new RedisKey[] { from, to }, new RedisValue[] { quantity }); new RedisKey[] { from, to }, new RedisValue[] { quantity });
```
(where `TransferScript` is some `string` containing Lua, not shown for this example) (where `TransferScript` is some `string` containing Lua, not shown for this example)
The response uses the `RedisResult` type (this is unique to scripting; usually the API tries to represent the response as directly and clearly as possible). As before, `RedisResult` offers a range of conversion operations - more, in fact than `RedisValue`, because in addition to being interpreted as text, binary, primitives and nullable-primitives, the response can *also* be interpreted as *arrays* of such, for example: The response uses the `RedisResult` type (this is unique to scripting; usually the API tries to represent the response as directly and clearly as possible). As before, `RedisResult` offers a range of conversion operations - more, in fact than `RedisValue`, because in addition to being interpreted as text, binary, primitives and nullable-primitives, the response can *also* be interpreted as *arrays* of such, for example:
string[] items = db.ScriptEvaluate(...); ```C#
string[] items = db.ScriptEvaluate(...);
```
Conclusion Conclusion
--- ---
......
...@@ -3,8 +3,10 @@ ...@@ -3,8 +3,10 @@
Latency sucks. Modern computers can churn data at an alarming rate, and high speed networking (often with multiple parallel links between important servers) provides enormous bandwidth, but... that damned latency means that computers spend an awful lot of time *waiting for data* (one of the several reasons that continuation-based programming is becoming increasingly popular. Let's consider some regular procedural code: Latency sucks. Modern computers can churn data at an alarming rate, and high speed networking (often with multiple parallel links between important servers) provides enormous bandwidth, but... that damned latency means that computers spend an awful lot of time *waiting for data* (one of the several reasons that continuation-based programming is becoming increasingly popular. Let's consider some regular procedural code:
string a = db.StringGet("a"); ```C#
string b = db.StringGet("b"); string a = db.StringGet("a");
string b = db.StringGet("b");
```
In terms of steps involved, this looks like: In terms of steps involved, this looks like:
...@@ -40,10 +42,12 @@ Because of this, many redis clients allow you to make use of *pipelining*; this ...@@ -40,10 +42,12 @@ Because of this, many redis clients allow you to make use of *pipelining*; this
For example, to pipeline the two gets using procedural (blocking) code, we could use: For example, to pipeline the two gets using procedural (blocking) code, we could use:
var aPending = db.StringGetAsync("a"); ```C#
var bPending = db.StringGetAsync("b"); var aPending = db.StringGetAsync("a");
var a = db.Wait(aPending); var bPending = db.StringGetAsync("b");
var b = db.Wait(bPending); var a = db.Wait(aPending);
var b = db.Wait(bPending);
```
Note that I'm using `db.Wait` here because it will automatically apply the configured synchronous timeout, but you can use `aPending.Wait()` or `Task.WaitAll(aPending, bPending);` if you prefer. Using pipelining allows us to get both requests onto the network immediately, eliminating most of the latency. Additionally, it also helps reduce packet fragmentation: 20 requests sent individually (waiting for each response) will require at least 20 packets, but 20 requests sent in a pipeline could fit into much fewer packets (perhaps even just one). Note that I'm using `db.Wait` here because it will automatically apply the configured synchronous timeout, but you can use `aPending.Wait()` or `Task.WaitAll(aPending, bPending);` if you prefer. Using pipelining allows us to get both requests onto the network immediately, eliminating most of the latency. Additionally, it also helps reduce packet fragmentation: 20 requests sent individually (waiting for each response) will require at least 20 packets, but 20 requests sent in a pipeline could fit into much fewer packets (perhaps even just one).
...@@ -52,9 +56,11 @@ Fire and Forget ...@@ -52,9 +56,11 @@ Fire and Forget
A special-case of pipelining is when we expressly don't care about the response from a particular operation, which allows our code to continue immediately while the enqueued operation proceeds in the background. Often, this means that we can put concurrent work on the connection from a single caller. This is achieved using the `flags` parameter: A special-case of pipelining is when we expressly don't care about the response from a particular operation, which allows our code to continue immediately while the enqueued operation proceeds in the background. Often, this means that we can put concurrent work on the connection from a single caller. This is achieved using the `flags` parameter:
// sliding expiration ```C#
db.KeyExpire(key, TimeSpan.FromMinutes(5), flags: CommandFlags.FireAndForget); // sliding expiration
var value = (string)db.StringGet(key); db.KeyExpire(key, TimeSpan.FromMinutes(5), flags: CommandFlags.FireAndForget);
var value = (string)db.StringGet(key);
```
The `FireAndForget` flag causes the client library to queue the work as normal, but immediately return a default value (since `KeyExpire` returns a `bool`, this will return `false`, because `default(bool)` is `false` - however the return value is meaningless and should be ignored). This works for `*Async` methods too: an already-completed `Task<T>` is returned with the default value (or an already-completed `Task` is returned for `void` methods). The `FireAndForget` flag causes the client library to queue the work as normal, but immediately return a default value (since `KeyExpire` returns a `bool`, this will return `false`, because `default(bool)` is `false` - however the return value is meaningless and should be ignored). This works for `*Async` methods too: an already-completed `Task<T>` is returned with the default value (or an already-completed `Task` is returned for `void` methods).
...@@ -65,13 +71,15 @@ Pipelining is all well and good, but often any single block of code only want a ...@@ -65,13 +71,15 @@ Pipelining is all well and good, but often any single block of code only want a
For this reason, the only redis features that StackExchange.Redis does not offer (and *will not ever offer*) are the "blocking pops" ([BLPOP](http://redis.io/commands/blpop), [BRPOP](http://redis.io/commands/brpop) and [BRPOPLPUSH](http://redis.io/commands/brpoplpush)) - because this would allow a single caller to stall the entire multiplexer, blocking all other callers. The only other time that StackExchange.Redis needs to hold work is when verifying pre-conditions for a transaction, which is why StackExchange.Redis encapsulates such conditions into internally managed `Condition` instances. [Read more about transactions here](https://github.com/StackExchange/StackExchange.Redis/blob/master/Docs/Transactions.md). If you feel you want "blocking pops", then I strongly suggest you consider pub/sub instead: For this reason, the only redis features that StackExchange.Redis does not offer (and *will not ever offer*) are the "blocking pops" ([BLPOP](http://redis.io/commands/blpop), [BRPOP](http://redis.io/commands/brpop) and [BRPOPLPUSH](http://redis.io/commands/brpoplpush)) - because this would allow a single caller to stall the entire multiplexer, blocking all other callers. The only other time that StackExchange.Redis needs to hold work is when verifying pre-conditions for a transaction, which is why StackExchange.Redis encapsulates such conditions into internally managed `Condition` instances. [Read more about transactions here](https://github.com/StackExchange/StackExchange.Redis/blob/master/Docs/Transactions.md). If you feel you want "blocking pops", then I strongly suggest you consider pub/sub instead:
sub.Subscribe(channel, delegate { ```C#
sub.Subscribe(channel, delegate {
string work = db.ListRightPop(key); string work = db.ListRightPop(key);
if (work != null) Process(work); if (work != null) Process(work);
}); });
//... //...
db.ListLeftPush(key, newWork, flags: CommandFlags.FireAndForget); db.ListLeftPush(key, newWork, flags: CommandFlags.FireAndForget);
sub.Publish(channel, ""); sub.Publish(channel, "");
```
This achieves the same intent without requiring blocking operations. Notes: This achieves the same intent without requiring blocking operations. Notes:
...@@ -88,12 +96,14 @@ Concurrency ...@@ -88,12 +96,14 @@ Concurrency
It should be noted that the pipeline / multiplexer / future-value approach also plays very nicely with continuation-based asynchronous code; for example you could write: It should be noted that the pipeline / multiplexer / future-value approach also plays very nicely with continuation-based asynchronous code; for example you could write:
string value = await db.StringGet(key); ```C#
if (value == null) { string value = await db.StringGet(key);
if (value == null) {
value = await ComputeValueFromDatabase(...); value = await ComputeValueFromDatabase(...);
db.StringSet(key, value, flags: CommandFlags.FireAndForget); db.StringSet(key, value, flags: CommandFlags.FireAndForget);
} }
return value; return value;
```
[1]: http://msdn.microsoft.com/en-us/library/dd460717(v=vs.110).aspx [1]: http://msdn.microsoft.com/en-us/library/dd460717(v=vs.110).aspx
[2]: http://msdn.microsoft.com/en-us/library/system.threading.tasks.task(v=vs.110).aspx [2]: http://msdn.microsoft.com/en-us/library/system.threading.tasks.task(v=vs.110).aspx
......
...@@ -45,7 +45,7 @@ command is sent (via the `IProfiler` interface's `GetContext()` method). ...@@ -45,7 +45,7 @@ command is sent (via the `IProfiler` interface's `GetContext()` method).
A toy example of associating commands issued from many different threads together A toy example of associating commands issued from many different threads together
``` ```C#
class ToyProfiler : IProfiler class ToyProfiler : IProfiler
{ {
public ConcurrentDictionary<Thread, object> Contexts = new ConcurrentDictionary<Thread, object>(); public ConcurrentDictionary<Thread, object> Contexts = new ConcurrentDictionary<Thread, object>();
...@@ -59,7 +59,6 @@ class ToyProfiler : IProfiler ...@@ -59,7 +59,6 @@ class ToyProfiler : IProfiler
} }
} }
// ... // ...
ConnectionMultiplexer conn = /* initialization */; ConnectionMultiplexer conn = /* initialization */;
...@@ -107,7 +106,7 @@ At the end, `timings` will contain 16,000 `IProfiledCommand` objects - one for e ...@@ -107,7 +106,7 @@ At the end, `timings` will contain 16,000 `IProfiledCommand` objects - one for e
If instead you did the following: If instead you did the following:
``` ```C#
ConnectionMultiplexer conn = /* initialization */; ConnectionMultiplexer conn = /* initialization */;
var profiler = new ToyProfiler(); var profiler = new ToyProfiler();
...@@ -156,7 +155,7 @@ Moving away from toy examples, here's how you can profile StackExchange.Redis in ...@@ -156,7 +155,7 @@ Moving away from toy examples, here's how you can profile StackExchange.Redis in
First register the following `IProfiler` against your `ConnectionMultiplexer`: First register the following `IProfiler` against your `ConnectionMultiplexer`:
``` ```C#
public class RedisProfiler : IProfiler public class RedisProfiler : IProfiler
{ {
const string RequestContextKey = "RequestProfilingContext"; const string RequestContextKey = "RequestProfilingContext";
...@@ -184,7 +183,7 @@ public class RedisProfiler : IProfiler ...@@ -184,7 +183,7 @@ public class RedisProfiler : IProfiler
Then, add the following to your Global.asax.cs file: Then, add the following to your Global.asax.cs file:
``` ```C#
protected void Application_BeginRequest() protected void Application_BeginRequest()
{ {
var ctxObj = RedisProfiler.CreateContextForCurrentRequest(); var ctxObj = RedisProfiler.CreateContextForCurrentRequest();
......
...@@ -12,7 +12,9 @@ This works *particularly* well if messages are generally unrelated. ...@@ -12,7 +12,9 @@ This works *particularly* well if messages are generally unrelated.
For safety, **the default is sequential**; however, it is strongly recommended that you use concurrent processing whenever possible. This is a simple change: For safety, **the default is sequential**; however, it is strongly recommended that you use concurrent processing whenever possible. This is a simple change:
multiplexer.PreserveAsyncOrder = false; ```C#
multiplexer.PreserveAsyncOrder = false;
```
The reason that this is not a *configuration* option is that whether it is appropriate to do this depends *entirely* on the code that is subscribing to The reason that this is not a *configuration* option is that whether it is appropriate to do this depends *entirely* on the code that is subscribing to
messages. messages.
\ No newline at end of file
...@@ -11,11 +11,12 @@ all applied in a single unit (i.e. without other connections getting time betwee ...@@ -11,11 +11,12 @@ all applied in a single unit (i.e. without other connections getting time betwee
a `EXEC`, everything is thrown away. Because the commands inside the transaction are queued, you can't make decisions *inside* a `EXEC`, everything is thrown away. Because the commands inside the transaction are queued, you can't make decisions *inside*
the transaction. For example, in a SQL database you might do the following (pseudo-code - illustrative only): the transaction. For example, in a SQL database you might do the following (pseudo-code - illustrative only):
// assign a new unique id only if they don't already ```C#
// have one, in a transaction to ensure no thread-races // assign a new unique id only if they don't already
var newId = CreateNewUniqueID(); // optimistic // have one, in a transaction to ensure no thread-races
using(var tran = conn.BeginTran()) var newId = CreateNewUniqueID(); // optimistic
{ using(var tran = conn.BeginTran())
{
var cust = GetCustomer(conn, custId, tran); var cust = GetCustomer(conn, custId, tran);
var uniqueId = cust.UniqueID; var uniqueId = cust.UniqueID;
if(uniqueId == null) if(uniqueId == null)
...@@ -24,7 +25,8 @@ the transaction. For example, in a SQL database you might do the following (pseu ...@@ -24,7 +25,8 @@ the transaction. For example, in a SQL database you might do the following (pseu
SaveCustomer(conn, cust, tran); SaveCustomer(conn, cust, tran);
} }
tran.Complete(); tran.Complete();
} }
```
So how do you do it in Redis? So how do you do it in Redis?
--- ---
...@@ -39,14 +41,16 @@ you *can* do is: `WATCH` a key, check data from that key in the normal way, then ...@@ -39,14 +41,16 @@ you *can* do is: `WATCH` a key, check data from that key in the normal way, then
If, when you check the data, you discover that you don't actually need the transaction, you can use `UNWATCH` to If, when you check the data, you discover that you don't actually need the transaction, you can use `UNWATCH` to
forget all the watched keys. Note that watched keys are also reset during `EXEC` and `DISCARD`. So *at the Redis layer*, this is conceptually: forget all the watched keys. Note that watched keys are also reset during `EXEC` and `DISCARD`. So *at the Redis layer*, this is conceptually:
WATCH {custKey} ```
HEXISTS {custKey} "UniqueId" WATCH {custKey}
(check the reply, then either:) HEXISTS {custKey} "UniqueId"
MULTI (check the reply, then either:)
HSET {custKey} "UniqueId" {newId} MULTI
EXEC HSET {custKey} "UniqueId" {newId}
(or, if we find there was already an unique-id:) EXEC
UNWATCH (or, if we find there was already an unique-id:)
UNWATCH
```
This might look odd - having a `MULTI`/`EXEC` that only spans a single operation - but the important thing This might look odd - having a `MULTI`/`EXEC` that only spans a single operation - but the important thing
is that we are now also tracking changes to `{custKey}` from all other connections - if anyone else is that we are now also tracking changes to `{custKey}` from all other connections - if anyone else
...@@ -62,12 +66,14 @@ basically pre-canned tests involving `WATCH`, some kind of test, and a check on ...@@ -62,12 +66,14 @@ basically pre-canned tests involving `WATCH`, some kind of test, and a check on
pass, the `MULTI`/`EXEC` is issued; otherwise `UNWATCH` is issued. This is all done in a way that prevents the commands being pass, the `MULTI`/`EXEC` is issued; otherwise `UNWATCH` is issued. This is all done in a way that prevents the commands being
mixed together with other callers. So our example becomes: mixed together with other callers. So our example becomes:
var newId = CreateNewId(); ```C#
var tran = db.CreateTransaction(); var newId = CreateNewId();
tran.AddCondition(Condition.HashNotExists(custKey, "UniqueID")); var tran = db.CreateTransaction();
tran.HashSetAsync(custKey, "UniqueID", newId); tran.AddCondition(Condition.HashNotExists(custKey, "UniqueID"));
bool committed = tran.Execute(); tran.HashSetAsync(custKey, "UniqueID", newId);
// ^^^ if true: it was applied; if false: it was rolled back bool committed = tran.Execute();
// ^^^ if true: it was applied; if false: it was rolled back
```
Note that the object returned from `CreateTransaction` only has access to the *async* methods - because the result of Note that the object returned from `CreateTransaction` only has access to the *async* methods - because the result of
each operation will not be known until after `Execute` (or `ExecuteAsync`) has completed. If the operations are not applied, all the `Task`s each operation will not be known until after `Execute` (or `ExecuteAsync`) has completed. If the operations are not applied, all the `Task`s
...@@ -82,8 +88,10 @@ Inbuilt operations via `When` ...@@ -82,8 +88,10 @@ Inbuilt operations via `When`
It should also be noted that many common scenarios (in particular: key/hash existence, like in the above) have been anticipated by Redis, and single-operation It should also be noted that many common scenarios (in particular: key/hash existence, like in the above) have been anticipated by Redis, and single-operation
atomic commands exist. These are accessed via the `When` parameter - so our previous example can *also* be written as: atomic commands exist. These are accessed via the `When` parameter - so our previous example can *also* be written as:
var newId = CreateNewId(); ```C#
bool wasSet = db.HashSet(custKey, "UniqueID", newId, When.NotExists); var newId = CreateNewId();
bool wasSet = db.HashSet(custKey, "UniqueID", newId, When.NotExists);
```
(here, the `When.NotExists` causes the `HSETNX` command to be used, rather than `HSET`) (here, the `When.NotExists` causes the `HSETNX` command to be used, rather than `HSET`)
...@@ -96,11 +104,15 @@ between the caller and the server, but the trade-off is that it monopolises the ...@@ -96,11 +104,15 @@ between the caller and the server, but the trade-off is that it monopolises the
At the Redis layer (and assuming `HSETNX` did not exist) this could be implemented as: At the Redis layer (and assuming `HSETNX` did not exist) this could be implemented as:
EVAL "if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end" 1 {custKey} {newId} ```
EVAL "if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end" 1 {custKey} {newId}
```
This can be used in StackExchange.Redis via: This can be used in StackExchange.Redis via:
var wasSet = (bool) db.ScriptEvaluate(@"if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end", ```C#
var wasSet = (bool) db.ScriptEvaluate(@"if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end",
new RedisKey[] { custKey }, new RedisValue[] { newId }); new RedisKey[] { custKey }, new RedisValue[] { newId });
```
(note that the response from `ScriptEvaluate` and `ScriptEvaluateAsync` is variable depending on your exact script; the response can be interpreted by casting - in this case as a `bool`) (note that the response from `ScriptEvaluate` and `ScriptEvaluateAsync` is variable depending on your exact script; the response can be interpreted by casting - in this case as a `bool`)
...@@ -187,6 +187,49 @@ public void CreateDisconnectedNonsenseConnection_DNS() ...@@ -187,6 +187,49 @@ public void CreateDisconnectedNonsenseConnection_DNS()
} }
} }
[Test]
public void AbortConnectFalseForAzure()
{
var options = ConfigurationOptions.Parse("contoso.redis.cache.windows.net");
Assert.IsFalse(options.AbortOnConnectFail);
}
[Test]
public void AbortConnectTrueForAzureWhenSpecified()
{
var options = ConfigurationOptions.Parse("contoso.redis.cache.windows.net,abortConnect=true");
Assert.IsTrue(options.AbortOnConnectFail);
}
[Test]
public void AbortConnectFalseForAzureChina()
{
// added a few upper case chars to validate comparison
var options = ConfigurationOptions.Parse("contoso.REDIS.CACHE.chinacloudapi.cn");
Assert.IsFalse(options.AbortOnConnectFail);
}
[Test]
public void AbortConnectFalseForAzureUSGov()
{
var options = ConfigurationOptions.Parse("contoso.redis.cache.usgovcloudapi.net");
Assert.IsFalse(options.AbortOnConnectFail);
}
[Test]
public void AbortConnectTrueForNonAzure()
{
var options = ConfigurationOptions.Parse("redis.contoso.com");
Assert.IsTrue(options.AbortOnConnectFail);
}
[Test]
public void AbortConnectDefaultWhenNoEndpointsSpecifiedYet()
{
var options = new ConfigurationOptions();
Assert.IsTrue(options.AbortOnConnectFail);
}
internal static void AssertNearlyEqual(double x, double y) internal static void AssertNearlyEqual(double x, double y)
{ {
if (Math.Abs(x - y) > 0.00001) Assert.AreEqual(x, y); if (Math.Abs(x - y) > 0.00001) Assert.AreEqual(x, y);
......
...@@ -19,11 +19,15 @@ Installation ...@@ -19,11 +19,15 @@ Installation
StackExchange.Redis can be installed via the nuget UI (as [StackExchange.Redis](https://www.nuget.org/packages/StackExchange.Redis/)), or via the nuget package manager console: StackExchange.Redis can be installed via the nuget UI (as [StackExchange.Redis](https://www.nuget.org/packages/StackExchange.Redis/)), or via the nuget package manager console:
PM> Install-Package StackExchange.Redis ```PowerShell
PM> Install-Package StackExchange.Redis
```
If you require a strong-named package (because your project is strong-named), then you may wish to use instead: If you require a strong-named package (because your project is strong-named), then you may wish to use instead:
PM> Install-Package StackExchange.Redis.StrongName ```PowerShell
PM> Install-Package StackExchange.Redis.StrongName
```
([for further reading, see here](http://blog.marcgravell.com/2014/06/snk-we-need-to-talk.html)) ([for further reading, see here](http://blog.marcgravell.com/2014/06/snk-we-need-to-talk.html))
......
...@@ -182,9 +182,17 @@ internal ClusterConfiguration(ServerSelectionStrategy serverSelectionStrategy, s ...@@ -182,9 +182,17 @@ internal ClusterConfiguration(ServerSelectionStrategy serverSelectionStrategy, s
{ {
if (string.IsNullOrWhiteSpace(line)) continue; if (string.IsNullOrWhiteSpace(line)) continue;
var node = new ClusterNode(this, line, origin); var node = new ClusterNode(this, line, origin);
// Be resilient to ":0 {master,slave},fail,noaddr" nodes // Be resilient to ":0 {master,slave},fail,noaddr" nodes
if (node.IsNoAddr) if (node.IsNoAddr)
continue; continue;
// Override the origin value with the endpoint advertised with the target node to
// make sure that things like clusterConfiguration[clusterConfiguration.Origin]
// will work as expected.
if (node.IsMyself)
this.origin = node.EndPoint;
if (nodeLookup.ContainsKey(node.EndPoint)) if (nodeLookup.ContainsKey(node.EndPoint))
{ {
// Deal with conflicting node entries for the same endpoint // Deal with conflicting node entries for the same endpoint
...@@ -288,6 +296,8 @@ public sealed class ClusterNode : IEquatable<ClusterNode>, IComparable<ClusterN ...@@ -288,6 +296,8 @@ public sealed class ClusterNode : IEquatable<ClusterNode>, IComparable<ClusterN
private readonly EndPoint endpoint; private readonly EndPoint endpoint;
private readonly bool isMyself;
private readonly bool isSlave; private readonly bool isSlave;
private readonly bool isNoAddr; private readonly bool isNoAddr;
...@@ -315,6 +325,16 @@ internal ClusterNode(ClusterConfiguration configuration, string raw, EndPoint or ...@@ -315,6 +325,16 @@ internal ClusterNode(ClusterConfiguration configuration, string raw, EndPoint or
var flags = parts[2].Split(StringSplits.Comma); var flags = parts[2].Split(StringSplits.Comma);
endpoint = Format.TryParseEndPoint(parts[1]); endpoint = Format.TryParseEndPoint(parts[1]);
if (flags.Contains("myself"))
{
isMyself = true;
if (endpoint == null)
{
// Unconfigured cluster nodes might report themselves as endpoint ":{port}",
// hence the origin fallback value to make sure that we can address them
endpoint = origin;
}
}
nodeId = parts[0]; nodeId = parts[0];
isSlave = flags.Contains("slave"); isSlave = flags.Contains("slave");
...@@ -363,6 +383,11 @@ public IList<ClusterNode> Children ...@@ -363,6 +383,11 @@ public IList<ClusterNode> Children
/// </summary> /// </summary>
public EndPoint EndPoint { get { return endpoint; } } public EndPoint EndPoint { get { return endpoint; } }
/// <summary>
/// Gets whether this is the node which responded to the CLUSTER NODES request
/// </summary>
public bool IsMyself { get { return isMyself; } }
/// <summary> /// <summary>
/// Gets whether this node is a slave /// Gets whether this node is a slave
/// </summary> /// </summary>
......
...@@ -132,7 +132,7 @@ public static string TryNormalize(string value) ...@@ -132,7 +132,7 @@ public static string TryNormalize(string value)
/// <summary> /// <summary>
/// Gets or sets whether connect/configuration timeouts should be explicitly notified via a TimeoutException /// Gets or sets whether connect/configuration timeouts should be explicitly notified via a TimeoutException
/// </summary> /// </summary>
public bool AbortOnConnectFail { get { return abortOnConnectFail ?? true; } set { abortOnConnectFail = value; } } public bool AbortOnConnectFail { get { return abortOnConnectFail ?? GetDefaultAbortOnConnectFailSetting(); } set { abortOnConnectFail = value; } }
/// <summary> /// <summary>
/// Indicates whether admin operations should be allowed /// Indicates whether admin operations should be allowed
...@@ -621,6 +621,38 @@ private void DoParse(string configuration, bool ignoreUnknown) ...@@ -621,6 +621,38 @@ private void DoParse(string configuration, bool ignoreUnknown)
} }
} }
private bool GetDefaultAbortOnConnectFailSetting()
{
// Microsoft Azure team wants abortConnect=false by default
if (IsAzureEndpoint())
return false;
return true;
}
private bool IsAzureEndpoint()
{
var result = false;
var dnsEndpoints = endpoints.Select(endpoint => endpoint as DnsEndPoint).Where(ep => ep != null);
foreach(var ep in dnsEndpoints)
{
int firstDot = ep.Host.IndexOf('.');
if (firstDot >= 0)
{
var domain = ep.Host.Substring(firstDot).ToLowerInvariant();
switch(domain)
{
case ".redis.cache.windows.net":
case ".redis.cache.chinacloudapi.cn":
case ".redis.cache.usgovcloudapi.net":
return true;
}
}
}
return result;
}
private string InferSslHostFromEndpoints() { private string InferSslHostFromEndpoints() {
var dnsEndpoints = endpoints.Select(endpoint => endpoint as DnsEndPoint); var dnsEndpoints = endpoints.Select(endpoint => endpoint as DnsEndPoint);
string dnsHost = dnsEndpoints.First() != null ? dnsEndpoints.First().Host : null; string dnsHost = dnsEndpoints.First() != null ? dnsEndpoints.First().Host : null;
......
...@@ -521,6 +521,12 @@ public bool TryComplete(bool isAsync) ...@@ -521,6 +521,12 @@ public bool TryComplete(bool isAsync)
if (resultBox != null) if (resultBox != null)
{ {
var ret = resultBox.TryComplete(isAsync); var ret = resultBox.TryComplete(isAsync);
if (ret && isAsync)
{
resultBox = null; // in async mode TryComplete will have unwrapped and recycled resultBox; ensure we no longer reference it via this message
}
if (performance != null) if (performance != null)
{ {
performance.SetCompleted(); performance.SetCompleted();
......
...@@ -413,7 +413,7 @@ int IComparable.CompareTo(object obj) ...@@ -413,7 +413,7 @@ int IComparable.CompareTo(object obj)
return value == null ? @null : (RedisValue)value.GetValueOrDefault(); return value == null ? @null : (RedisValue)value.GetValueOrDefault();
} }
/// <summary> /// <summary>
/// Creates a new RedisValue from a Boolean /// Converts the value to a Boolean
/// </summary> /// </summary>
public static explicit operator bool (RedisValue value) public static explicit operator bool (RedisValue value)
{ {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment