Skip to content

Commit

Permalink
Merge v0.22.0 (#156)
Browse files Browse the repository at this point in the history
* Add Expire method + tests
* Add error flag to obsolete props and methods
* Better FusionCacheProvider: less and more uniform code + support for lazy named cache instantiations
* Better DI-related tests
* Add a log message when a FusionCache instance is created + update related tests
* Better readonly methods
* Add multi-levels tests for Expire()
* Null handling fix per Yoshifumi's notes (MemoryPack serializer)
* Optimization for when no real factory
* Add XUnitLogger
* Add cache name to every log message
* Better logging tests
* Add SkipMemoryCache entry options + tests
* Handle zero duration and negative duration
* Add backplane notification for background factory executions
* Remove backplane and distributed cache when disposing a FusionCache instance
* Old unused code cleanup
* Add fluent methods to set with zero durations
* Xml docs
* Better adaptive caching
* Add NullFusionCache and related tests
* Add tests for adaptive caching and SkipMemoryCache working together
* Docs
* Add support for multiple cache instances in LoggingScenario
* Add backward compatibility tests for deserialization of old version payloads
* Timestamping + tests
* Better fail-safe activation logic and flow
* Add test for FusionCacheEntryOptions.Duplicate() method
* Add ext methods for common overloads of method Expire/ExpireAsync
  • Loading branch information
jodydonetti authored Jul 9, 2023
1 parent bfc2318 commit a8b1ce7
Show file tree
Hide file tree
Showing 101 changed files with 3,206 additions and 973 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ With [🦄 A Gentle Introduction](docs/AGentleIntroduction.md) you'll get yourse

Want to start using it immediately? There's a [⭐ Quick Start](#-quick-start) for you.

Curious about what you can achieve from start to finish? There's a [:woman_teacher: Step By Step ](docs/StepByStep.md) guide.
Curious about what you can achieve from start to finish? There's a [👩‍🏫 Step By Step ](docs/StepByStep.md) guide.

More into videos? The great Anna Hoffman has been so nice to listen to me mumble random stuff on [Data Exposed](https://learn.microsoft.com/en-us/shows/data-exposed/caching-made-easy-in-azure-sql-db-with-fusioncache-data-exposed).

Expand Down
7 changes: 7 additions & 0 deletions ZiggyCreatures.FusionCache.sln
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZiggyCreatures.FusionCache.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZiggyCreatures.FusionCache.Serialization.ServiceStackJson", "src\ZiggyCreatures.FusionCache.Serialization.ServiceStackJson\ZiggyCreatures.FusionCache.Serialization.ServiceStackJson.csproj", "{CE437FB2-510F-4DCE-8A1F-AED747DAA4EB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SerializerPayloadGenerator", "tests\SerializerPayloadGenerator\SerializerPayloadGenerator.csproj", "{5B1AF24E-90FC-4C21-AF9C-090FE32027E3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -99,6 +101,10 @@ Global
{CE437FB2-510F-4DCE-8A1F-AED747DAA4EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE437FB2-510F-4DCE-8A1F-AED747DAA4EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE437FB2-510F-4DCE-8A1F-AED747DAA4EB}.Release|Any CPU.Build.0 = Release|Any CPU
{5B1AF24E-90FC-4C21-AF9C-090FE32027E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5B1AF24E-90FC-4C21-AF9C-090FE32027E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5B1AF24E-90FC-4C21-AF9C-090FE32027E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5B1AF24E-90FC-4C21-AF9C-090FE32027E3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -117,6 +123,7 @@ Global
{FC2C7F03-69E0-4CCE-A582-1FB6D8D11560} = {34B53F49-F5C5-4850-B79E-59AD130379C6}
{919CDF8C-463A-4E82-AFFD-DF8A6B904600} = {34B53F49-F5C5-4850-B79E-59AD130379C6}
{CE437FB2-510F-4DCE-8A1F-AED747DAA4EB} = {34B53F49-F5C5-4850-B79E-59AD130379C6}
{5B1AF24E-90FC-4C21-AF9C-090FE32027E3} = {C6F3C570-C68C-4A95-960E-82778306BDBA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {92916FA2-FCAC-406E-BF3F-0A2CE9512EF0}
Expand Down

This file was deleted.

37 changes: 34 additions & 3 deletions benchmarks/ZiggyCreatures.FusionCache.Benchmarks/SamplePayload.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
namespace ZiggyCreatures.Caching.Fusion.Benchmarks
using System;
using System.Collections.Generic;

namespace ZiggyCreatures.Caching.Fusion.Benchmarks
{
public class SamplePayload
public class SamplePayload : IEquatable<SamplePayload?>
{
public SamplePayload()
{
Expand All @@ -12,5 +15,33 @@ public SamplePayload()
public string Foo { get; set; }
public string Bar { get; set; }
public int Baz { get; set; }

public override bool Equals(object? obj)
{
return Equals(obj as SamplePayload);
}

public bool Equals(SamplePayload? other)
{
return other is not null &&
Foo == other.Foo &&
Bar == other.Bar &&
Baz == other.Baz;
}

public override int GetHashCode()
{
return HashCode.Combine(Foo, Bar, Baz);
}

public static bool operator ==(SamplePayload? left, SamplePayload? right)
{
return EqualityComparer<SamplePayload>.Default.Equals(left, right);
}

public static bool operator !=(SamplePayload? left, SamplePayload? right)
{
return !(left == right);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ namespace ZiggyCreatures.Caching.Fusion.Benchmarks
[Config(typeof(Config))]
public class SequentialComparisonBenchmarkAsync
{

private class Config : ManualConfig
{
public Config()
Expand Down Expand Up @@ -160,6 +159,5 @@ await appcache.GetOrAddAsync<SamplePayload>(
cache.Compact(1);
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ namespace ZiggyCreatures.Caching.Fusion.Benchmarks
[Config(typeof(Config))]
public class SequentialComparisonBenchmarkSync
{

private class Config : ManualConfig
{
public Config()
Expand Down Expand Up @@ -158,6 +157,5 @@ public void LazyCache()
cache.Compact(1);
}
}

}
}
87 changes: 56 additions & 31 deletions docs/CoreMethods.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@

# :joystick: Core Methods

At a high level there are 5 core methods:
At a high level there are 6 core methods:

- `Set[Async]`
- `Remove[Async]`
- `TryGet[Async]`
- `GetOrDefault[Async]`
- `GetOrSet[Async]`
- `Expire[Async]`

All of them work **on both the memory cache and the distributed cache** (if any) in a transparent way: you don't have to do anything extra for it to coordinate the 2 layers.

Expand Down Expand Up @@ -53,33 +54,6 @@ cache.Remove("foo");
cache.Remove("foo");
```

## GetOrDefault[Async]

It is used to **GET** the value in the cache for the specified key and, if nothing is there, returns a **DEFAULT VALUE**.

With this method **NOTHING IS SET** in the cache.

It is useful if you want to use what's in the cache or some default value just for this call, but **you don't want to change the state** of the cache itself.

Examples:

```csharp
// THIS WILL GET BACK 42
foo = cache.GetOrDefault("foo", 42);

// IF WE IMMEDIATELY CALL THIS, WE WILL GET BACK 21
foo = cache.GetOrDefault("foo", 21);

// THIS WILL GET BACK 0, WHICH IS THE DEFAULT VALUE FOR THE TYPE int
foo = cache.GetOrDefault<int>("foo");

// ALSO USEFUL FOR USER PREFERENCES: WE CAN USE A DEFAULT VALUE WITHOUT SETTING ONE
var enableUnicorns = cache.GetOrDefault<bool>("flags.unicorns", false);

// AND SINCE false IS THE DEFAULT VALUE FOR THE TYPE bool WE CAN SIMPLY DO THIS
var enableUnicorns = cache.GetOrDefault<bool>("flags.unicorns");
```

## TryGet[Async]

It is used to **CHECK** if a value is in the cache for the specified key and, if so, to **GET** the value itself all at once.
Expand Down Expand Up @@ -122,6 +96,33 @@ int result = maybeFoo;

💡 It's not possible to use the classic method signature of `bool TryGet<TValue>(string key, out TValue value)` to set a value with an `out` parameter because .NET does not allow it on async methods (for good reasons) and I wanted to keep the same signature for every method in both sync/async versions.

## GetOrDefault[Async]

It is used to **GET** the value in the cache for the specified key and, if nothing is there, returns a **DEFAULT VALUE**.

With this method **NOTHING IS SET** in the cache.

It is useful if you want to use what's in the cache or some default value just for this call, but **you don't want to change the state** of the cache itself.

Examples:

```csharp
// THIS WILL GET BACK 42
foo = cache.GetOrDefault("foo", 42);

// IF WE IMMEDIATELY CALL THIS, WE WILL GET BACK 21
foo = cache.GetOrDefault("foo", 21);

// THIS WILL GET BACK 0, WHICH IS THE DEFAULT VALUE FOR THE TYPE int
foo = cache.GetOrDefault<int>("foo");

// ALSO USEFUL FOR USER PREFERENCES: WE CAN USE A DEFAULT VALUE WITHOUT SETTING ONE
var enableUnicorns = cache.GetOrDefault<bool>("flags.unicorns", false);

// AND SINCE false IS THE DEFAULT VALUE FOR THE TYPE bool WE CAN SIMPLY DO THIS
var enableUnicorns = cache.GetOrDefault<bool>("flags.unicorns");
```

## GetOrSet[Async]

This is the most important and powerful method available, and it does **a lot** for you.
Expand Down Expand Up @@ -204,16 +205,40 @@ var foo = cache.GetOrSet<int>(
var foo = cache.GetOrSet<int>("foo", _ => GetFooFromDb(), 42);
```

## Expire[Async]

It is used to explicitly **EXPIRE** the value in the cache for the specified key.

But wait, what is the difference between `Expire()` and `Remove()`? With `Remove()` the value is actually removed, totally. With `Expire()` instead, it depends on how [fail-safe](FailSafe.md) is configured:
- if fail-safe is enabled: the entry will marked as _logically_ expired, but will still be available as a fallback value in case of future problems
- if fail-safe is disabled: the entry will be effectively removed

This method may be is useful in case we want to threat something as remove, but with the ability to say _"better than nothing"_ in the future, in case of problems (thanks to fail-safe).

Examples:

```csharp
cache.Set("foo", 42, opt => opt.SetDuration(TimeSpan.FromSeconds(10)).SetFailSafe(true));

cache.Expire("foo", opt => opt.SetFailSafe(true));

// THIS WILL GET BACK 0, WHICH IS THE DEFAULT VALUE FOR THE TYPE int
foo = cache.GetOrDefault<int>("foo");

// THIS WILL GET BACK 42
foo = cache.GetOrDefault<int>("foo", opt => opt.SetFailSafe(true));
```

## :recycle: Common overloads

Every core method that needs a set of options (`FusionCacheEntryOptions`) for how to behave has different overloads to let you specify these options, for better ease of use.

You can choose between passing:

- **Nothing**: you don't pass anything, so the global `DefaultEntryOptions` will be used (also saves some memory allocations)
- **Nothing**: you don't pass anything, so the `cache.DefaultEntryOptions` will be used (also saves some memory allocations)
- **Options**: you directly pass a `FusionCacheEntryOptions` object. This gives you total control over each option, but you have to instantiate it yourself and **does not copy** the global `DefaultEntryOptions`
- **Setup action**: you pass a `lambda` that receives a duplicate of the `DefaultEntryOptions` so you start from there and modify it as you like (there's also a set of *fluent methods* to do that easily)
- **Duration**: you simply pass a `TimeSpan` value for the duration. This is the same as the previous one (start from the global default + lambda) but for the common scenario of when you only want to change the duration
- **Duration**: you simply pass a `TimeSpan` value for the duration. This is the same as the previous one (start from default + duplicate + lambda) but for the common scenario of when you only want to change the duration

## 🤷‍♂️ `MaybeValue<T>`
The special type `MaybeValue<T>` is similar to the standard `Nullable<T>` type in .NET, but usable for both value types and reference types.
Expand All @@ -222,7 +247,7 @@ The type has a `bool HasValue` property to know if it contains a value or not (l

The type also is implicitly convertible to and from values of type `T`, so that you don't have to manually do it yourself.

Finally, if you want to get the value inside of it or a default one if case it's not there, you can use the `GetValueOrDefault()` method, with or without the default value to use (again, just like standard nullables.
Finally, if you want to get the value inside of it or a default one in case it's not there, you can use the `GetValueOrDefault()` method, with or without the default value to use (again, just like standard nullables).

Currently this type is used inside FusionCache in 2 places:

Expand Down
17 changes: 11 additions & 6 deletions docs/Options.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ In general this can be used as a set of options that will act as the *baseline*,
| `CacheName` | `string` | `"FusionCache"` | The name of the cache: it can be used for identification, and in a multi-node scenario it is typically shared between nodes to create a logical association. |
| `DefaultEntryOptions` | `FusionCacheEntryOptions` | *see below* | This is the default entry options object that will be used when one is not passed to each method call that need one, and as a starting point when duplicating one, either via the explicit `FusionCache.CreateOptions(...)` method or in one of the *overloads* of each *core method*. |
| `DistributedCacheCircuitBreakerDuration` | `TimeSpan` | `none` | The duration of the circuit-breaker used when working with the distributed cache. |
| `CacheKeyPrefix` | `string?` | `null` | A prefix that will be added to each cache key for each call: it can be useful when working with multiple named caches. With the builder it can be set using the `WithCacheKeyPrefix(...)` method. |
| `DistributedCacheKeyModifierMode` | `CacheKeyModifierMode` | `Prefix` | Specify the mode in which cache key will be changed for the distributed cache (eg: to specify the wire format version). |
| `BackplaneCircuitBreakerDuration` | `TimeSpan` | `none` | The duration of the circuit-breaker used when working with the backplane. |
| `BackplaneChannelPrefix` | `string?` | `null` | The prefix to use in the backplane channel name: if not specified the `CacheName` will be used. |
Expand Down Expand Up @@ -100,10 +101,14 @@ For a better **developer experience** and to **consume less memory** (higher per
| `AllowTimedOutFactoryBackgroundCompletion` | `bool` | `true` | It enables a factory that has hit a synthetic timeout (both soft/hard) to complete in the background and update the cache with the new value. |
| 🧙‍♂️ `DistributedCacheDuration` | `TimeSpan?` | `null` | The custom duration to use for the distributed cache: this allows to have different duration between the 1st and 2nd layers. If `null`, the normal `Duration` will be used. |
| 🧙‍♂️ `DistributedCacheFailSafeMaxDuration` | `TimeSpan?` | `null` | The custom fail-safe max duration to use for the distributed cache: this allows to have different duration between the 1st and 2nd layers. If `null`, the normal `FailSafeMaxDuration` will be used. |
| `DistributedCacheSoftTimeout` | `TimeSpan` | `none` | The maximum execution time allowed for each operation on the distributed cache when is not problematic to simply timeout. |
| `DistributedCacheHardTimeout` | `TimeSpan` | `none` | The maximum execution time allowed for each operation on the distributed cache in any case, even if there is not a stale value to fallback to. |
| `AllowBackgroundDistributedCacheOperations` | `bool` | `false` | Normally operations on the distributed cache are executed in a blocking fashion: setting this flag to true let them run in the background in a kind of fire-and-forget way. This will give a perf boost, but watch out for rare side effects. |
| `ReThrowDistributedCacheExceptions` | `bool` | `false` | Set this to true to allow the bubble up of distributed cache exceptions (default is `false`). Please note that, even if set to true, in some cases you would also need `AllowBackgroundDistributedCacheOperations` set to false and no timeout (neither soft nor hard) specified. |
| `ReThrowSerializationExceptions` | `bool` | `true` | Set this to false to disable the bubble up of serialization exceptions (default is `true`). |
| 🧙‍♂️ `DistributedCacheSoftTimeout` | `TimeSpan` | `none` | The maximum execution time allowed for each operation on the distributed cache when is not problematic to simply timeout. |
| 🧙‍♂️ `DistributedCacheHardTimeout` | `TimeSpan` | `none` | The maximum execution time allowed for each operation on the distributed cache in any case, even if there is not a stale value to fallback to. |
| 🧙‍♂️ `AllowBackgroundDistributedCacheOperations` | `bool` | `false` | Normally operations on the distributed cache are executed in a blocking fashion: setting this flag to true let them run in the background in a kind of fire-and-forget way. This will give a perf boost, but watch out for rare side effects. |
| 🧙‍♂️ `ReThrowDistributedCacheExceptions` | `bool` | `false` | Set this to true to allow the bubble up of distributed cache exceptions (default is `false`). Please note that, even if set to true, in some cases you would also need `AllowBackgroundDistributedCacheOperations` set to false and no timeout (neither soft nor hard) specified. |
| 🧙 `ReThrowSerializationExceptions` | `bool` | `true` | Set this to false to disable the bubble up of serialization exceptions (default is `true`). |
| 🧙‍♂️ `SkipBackplaneNotifications` | `bool` | `false` | Skip sending backplane notifications after some operations, like a SET (via a Set/GetOrSet call) or a REMOVE (via a Remove call). |
| 🧙‍♂️ `AllowBackgroundBackplaneOperations` | `bool` | `true` | By default every operation on the backplane is non-blocking: that is to say the FusionCache method call would not wait for each backplane operation to be completed. Setting this flag to `false` will execute these operations in a blocking fashion, typically resulting in worse performance. |
| 🧙‍♂️ `AllowBackgroundBackplaneOperations` | `bool` | `true` | By default every operation on the backplane is non-blocking: that is to say the FusionCache method call would not wait for each backplane operation to be completed. Setting this flag to `false` will execute these operations in a blocking fashion, typically resulting in worse performance. |
| 🧙‍♂️ `EagerRefreshThreshold` | `float?` | `null` | The threshold to apply when deciding whether to refresh the cache entry eagerly (that is, before the actual expiration). |
| 🧙‍♂️ `SkipDistributedCache` | `bool` | `false` | Skip the usage of the distributed cache, if any. |
| 🧙‍♂️ `SkipDistributedCacheReadWhenStale` | `bool` | `false` | When a 2nd layer (distributed cache) is used and a cache entry in the 1st layer (memory cache) is found but is stale, a read is done on the distributed cache: the reason is that in a multi-node environment another node may have updated the cache entry, so we may found a newer version of it. |
| 🧙‍♂️ `SkipMemoryCache` | `bool` | `false` | Skip the usage of the memory cache. |
2 changes: 1 addition & 1 deletion docs/StepByStep.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

</div>

# :woman_teacher: Step By Step
# 👩‍🏫 Step By Step

What follows is an example scenario on which we can reason about: we've built a **service** that handle some **requests** by retrieving some data from a **database**, that's it.

Expand Down
2 changes: 1 addition & 1 deletion docs/Timeouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

There are different types of timeouts available and it may be useful to know them.

:bulb: For a complete example of how to use them and what results you can achieve there's the [:woman_teacher: Step By Step](StepByStep.md) guide.
:bulb: For a complete example of how to use them and what results you can achieve there's the [👩‍🏫 Step By Step](StepByStep.md) guide.
## Factory Timeouts

Sometimes your data source (database, webservice, etc) is overloaded, the network is congested or something else bad is happening and the end result is things start to get **:snail: very slow** to get a fresh piece of data.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,6 @@ public void Publish(BackplaneMessage message, FusionCacheEntryOptions options)
if (_backplanes is null)
throw new NullReferenceException("Something went wrong :-|");

if (_logger?.IsEnabled(LogLevel.Debug) ?? false)
_logger.Log(LogLevel.Debug, "A backplane notification has been sent for {CacheKey}", message.CacheKey);

foreach (var backplane in _backplanes)
{
if (backplane == this)
Expand Down
Loading

0 comments on commit a8b1ce7

Please sign in to comment.