├── CODE_OF_CONDUCT.md
├── stylecop.json
├── .github
└── workflows
│ └── dotnet.yml
├── src
├── GroupCacheStubImpl
│ ├── GroupCacheStubImpl.csproj
│ ├── Log4NetLoggerAdapter.cs
│ ├── OwinClient.cs
│ ├── OwinServer.cs
│ ├── OwinPool.cs
│ └── OwinPoolPicker.cs
└── GroupCache
│ ├── GroupCache.csproj
│ ├── LinkedListExtensions.cs
│ ├── ReaderWriterLockSlimExtensions.cs
│ ├── JumpHasher.cs
│ ├── ICacheEntryValidator.cs
│ ├── ExhaustedRetryException.cs
│ ├── IGroupStat.cs
│ ├── ICache.cs
│ ├── KeyPrefixCacheDecorator.cs
│ ├── DefaultGroupStat.cs
│ ├── ILogger.cs
│ ├── IGetter.cs
│ ├── ReaderLockHolder.cs
│ ├── WriterLockHolder.cs
│ ├── SingleFlight.cs
│ ├── UpgradeableReaderLockHolder.cs
│ ├── NullCacheEntryValidator.cs
│ ├── RetryContext.cs
│ ├── NullLogger.cs
│ ├── IGroupCacheClient.cs
│ ├── SimpleRetryPolicy.cs
│ ├── SemaphoreHolder.cs
│ ├── MemoryCache.cs
│ ├── Retry.cs
│ ├── IPeerPicker.cs
│ ├── CircuitBreakerClient.cs
│ ├── Argument.cs
│ ├── GroupCache.cs
│ ├── LRUCache.cs
│ └── DiskCache.cs
├── test
└── GroupCacheStubTest
│ ├── GroupCacheStubTest.csproj
│ ├── UnitTestGroupStats.cs
│ ├── TestDiskCache.cs
│ ├── TestStub.cs
│ └── TestGroupCache.cs
├── README.md
├── LICENSE
├── Directory.Packages.props
├── Directory.Build.props
├── GroupCacheDotNet.sln
├── SECURITY.md
├── .editorconfig
└── .gitignore
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Microsoft Open Source Code of Conduct
2 |
3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
4 |
5 | Resources:
6 |
7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
10 |
--------------------------------------------------------------------------------
/stylecop.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
3 | "settings": {
4 | "documentationRules": {
5 | "companyName": "Microsoft Corporation",
6 | "copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the {licenseName} license. See {licenseFile} file in the project root for full license information.",
7 | "variables": {
8 | "licenseName": "MIT",
9 | "licenseFile": "LICENSE"
10 | },
11 | "headerDecoration": "--------------------------------------------------------------------------------------------------------------------"
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.github/workflows/dotnet.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a .NET project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
3 |
4 | name: .NET
5 |
6 | on:
7 | push:
8 | branches: [ "master" ]
9 | pull_request:
10 | branches: [ "master" ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: windows-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 | - name: Setup .NET
20 | uses: actions/setup-dotnet@v4
21 | with:
22 | dotnet-version: 8.0.x
23 | - name: Restore dependencies
24 | run: dotnet restore
25 | - name: Build
26 | run: dotnet build --no-restore
27 | - name: Test
28 | run: dotnet test --no-build --verbosity normal
29 |
--------------------------------------------------------------------------------
/src/GroupCacheStubImpl/GroupCacheStubImpl.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net472
4 | GroupCacheStub.GroupCacheStubImpl
5 | GroupCacheStub.GroupCacheStubImpl
6 | GroupCacheStub
7 | true
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/GroupCache/GroupCache.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netstandard2.0
4 | GroupCacheDotNet.GroupCache
5 | GroupCacheDotNet.GroupCache
6 | GroupCacheDotNet
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/test/GroupCacheStubTest/GroupCacheStubTest.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net472
5 | GroupCacheStubTest
6 | GroupCacheStubTest
7 | true
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Contributing
3 |
4 | This project welcomes contributions and suggestions. Most contributions require you to agree to a
5 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
6 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
7 |
8 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide
9 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
10 | provided by the bot. You will only need to do this once across all repos using our CLA.
11 |
12 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
13 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
14 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE
22 |
--------------------------------------------------------------------------------
/src/GroupCache/LinkedListExtensions.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 | using System.Collections.Generic;
12 |
13 | public static class LinkedListExtensions
14 | {
15 | public static void MoveToFront(this LinkedList list, LinkedListNode node)
16 | {
17 | if (node.List != list)
18 | {
19 | throw new InvalidOperationException("The provided LinkedList should own the LinkedListNode");
20 | }
21 |
22 | if (list.First == node)
23 | {
24 | return;
25 | }
26 |
27 | list.Remove(node);
28 | list.AddFirst(node);
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/GroupCache/ReaderWriterLockSlimExtensions.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System.Threading;
11 |
12 | public static class ReaderWriterLockSlimExtensions
13 | {
14 | public static ReaderLockHolder GetReaderLock(this ReaderWriterLockSlim rwLock)
15 | {
16 | return new ReaderLockHolder(rwLock);
17 | }
18 |
19 | public static WriterLockHolder GetWriterLock(this ReaderWriterLockSlim rwLock)
20 | {
21 | return new WriterLockHolder(rwLock);
22 | }
23 |
24 | public static UpgradeableReaderLockHolder GetUpgradeableReaderLock(this ReaderWriterLockSlim rwLock)
25 | {
26 | return new UpgradeableReaderLockHolder(rwLock);
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/src/GroupCache/JumpHasher.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | ///
11 | /// A C# implementation of the jump consistent hash from Lamping and Veach.
12 | /// Paper: http://arxiv.org/ftp/arxiv/papers/1406/1406.2294.pdf.
13 | ///
14 | public class JumpHasher
15 | {
16 | private const long JUMP = 1L << 31;
17 | private const ulong HASHSeed = 2862933555777941757;
18 | private const int BitShiftAmount = 33;
19 |
20 | // Hash returns the integer hash for the given key.
21 | public int Hash(ulong key, int n)
22 | {
23 | long b = -1;
24 | long j = 0;
25 | while (j < n)
26 | {
27 | b = j;
28 | key = (key * HASHSeed) + 1;
29 | j = (long)((b + 1L) * (JUMP / (double)((key >> BitShiftAmount) + 1L)));
30 | }
31 |
32 | return (int)b;
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/GroupCache/ICacheEntryValidator.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 | using System.IO;
12 | using System.Threading;
13 | using System.Threading.Tasks;
14 |
15 | public class CacheEntryValidationFailedException : Exception
16 | {
17 | public CacheEntryValidationFailedException()
18 | : base()
19 | {
20 | }
21 |
22 | public CacheEntryValidationFailedException(string message)
23 | : base(message)
24 | {
25 | }
26 |
27 | public CacheEntryValidationFailedException(string message, Exception innerException)
28 | : base(message, innerException)
29 | {
30 | }
31 | }
32 |
33 | public abstract class ValidationStream : Stream
34 | {
35 | public abstract Task ValidateAsync(CancellationToken ct);
36 | }
37 |
38 | public interface ICacheEntryValidator
39 | {
40 | ValidationStream ValidateEntryPassThrough(string key, Stream streamPayload);
41 | }
42 | }
--------------------------------------------------------------------------------
/src/GroupCache/ExhaustedRetryException.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 |
12 | public class ExhaustedRetryException : Exception
13 | {
14 | ///
15 | /// Initializes a new instance of the class.
16 | ///
17 | public ExhaustedRetryException()
18 | {
19 | }
20 |
21 | ///
22 | /// Initializes a new instance of the class.
23 | ///
24 | ///
25 | public ExhaustedRetryException(string message)
26 | : base(message)
27 | {
28 | }
29 |
30 | ///
31 | /// Initializes a new instance of the class.
32 | ///
33 | ///
34 | ///
35 | public ExhaustedRetryException(string message, Exception inner)
36 | : base(message, inner)
37 | {
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/src/GroupCache/IGroupStat.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 |
12 | ///
13 | /// Interface used to observe GroupCache internal operations
14 | /// This would be used to collect counters for monitoring.
15 | ///
16 | public interface IGroupStat
17 | {
18 | void TraceGets(string groupName); // any Get request, including from peers
19 |
20 | void TraceCacheHits(string groupName); // either cache was good
21 |
22 | void TracePeerLoads(string groupName); // either remote load or remote cache hit (not an error)
23 |
24 | void TraceLoadsDeduped(string groupName); // after singleflight
25 |
26 | void TraceLocalLoads(string groupName); // total good local loads
27 |
28 | void TraceServerRequests(string groupName); // gets that came over the network from peers
29 |
30 | void TraceRoundtripLatency(string groupName, TimeSpan ts);
31 |
32 | void TraceRetry(string groupName);
33 |
34 | void TraceItemOverCapacity(string groupName);
35 |
36 | void TraceConcurrentServerRequests(int v);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/GroupCache/ICache.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 | using System.IO;
12 | using System.Threading;
13 | using System.Threading.Tasks;
14 |
15 | public interface ICache
16 | {
17 | Task GetOrAddAsync(string key, Func valueFactory, ICacheControl cacheControl, CancellationToken ct);
18 |
19 | event Action ItemOverCapacity;
20 |
21 | Task RemoveAsync(string key, CancellationToken ct);
22 | }
23 |
24 | public interface ICacheEntry
25 | {
26 | Stream Value();
27 |
28 | void Ref();
29 |
30 | Task DisposeAsync();
31 | }
32 |
33 | public class EmptyCacheEntry : ICacheEntry
34 | {
35 | ///
36 | public Task DisposeAsync()
37 | {
38 | return Task.CompletedTask;
39 | }
40 |
41 | ///
42 | public void Ref()
43 | {
44 | }
45 |
46 | ///
47 | public Stream Value()
48 | {
49 | throw new NotImplementedException("Empty entry have no stream");
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Directory.Packages.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | true
5 | true
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | true
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | true
21 |
22 |
23 |
24 |
25 | all
26 | runtime; build; native; contentfiles; analyzers
27 |
28 |
29 | all
30 | runtime; build; native; contentfiles; analyzers
31 |
32 |
33 | all
34 | runtime; build; native; contentfiles; analyzers
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/GroupCache/KeyPrefixCacheDecorator.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 | using System.IO;
12 | using System.Threading;
13 | using System.Threading.Tasks;
14 |
15 | public class KeyPrefixCacheDecorator : ICache
16 | {
17 | private readonly ICache decorated;
18 | private readonly string prefix;
19 |
20 | public KeyPrefixCacheDecorator(string prefix, ICache cache)
21 | {
22 | this.prefix = prefix;
23 | this.decorated = cache;
24 | this.decorated.ItemOverCapacity += (key) => this.ItemOverCapacity?.Invoke(key);
25 | }
26 |
27 | ///
28 | public event Action ItemOverCapacity;
29 |
30 | ///
31 | public Task GetOrAddAsync(string key, Func valueFactory, ICacheControl cacheControl, CancellationToken ct)
32 | {
33 | var newkey = this.prefix + key;
34 | return this.decorated.GetOrAddAsync(newkey, (str, stream, cc) => valueFactory(key, stream, cc), cacheControl, ct);
35 | }
36 |
37 | ///
38 | public Task RemoveAsync(string key, CancellationToken ct)
39 | {
40 | var newkey = this.prefix + key;
41 | return this.decorated.RemoveAsync(newkey, ct);
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/GroupCache/DefaultGroupStat.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 |
12 | ///
13 | /// For unit-testing only.
14 | ///
15 | public class NullGroupStat : IGroupStat
16 | {
17 | ///
18 | public void TraceCacheHits(string groupName)
19 | {
20 | }
21 |
22 | ///
23 | public void TraceGets(string groupName)
24 | {
25 | }
26 |
27 | ///
28 | public void TraceLoadsDeduped(string groupName)
29 | {
30 | }
31 |
32 | ///
33 | public void TraceLocalLoads(string groupName)
34 | {
35 | }
36 |
37 | ///
38 | public void TracePeerLoads(string groupName)
39 | {
40 | }
41 |
42 | ///
43 | public void TraceServerRequests(string groupName)
44 | {
45 | }
46 |
47 | ///
48 | public void TraceRoundtripLatency(string groupName, TimeSpan ts)
49 | {
50 | }
51 |
52 | ///
53 | public void TraceRetry(string groupName)
54 | {
55 | }
56 |
57 | ///
58 | public void TraceItemOverCapacity(string groupName)
59 | {
60 | }
61 |
62 | ///
63 | public void TraceConcurrentServerRequests(int v)
64 | {
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/GroupCache/ILogger.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 |
12 | public interface ILogger
13 | {
14 | /// Logs a message with severity Debug.
15 | void Debug(string message);
16 |
17 | /// Logs a formatted message with severity Debug.
18 | void Debug(string format, params object[] formatArgs);
19 |
20 | /// Logs a message with severity Info.
21 | void Info(string message);
22 |
23 | /// Logs a formatted message with severity Info.
24 | void Info(string format, params object[] formatArgs);
25 |
26 | /// Logs a message with severity Warning.
27 | void Warning(string message);
28 |
29 | /// Logs a formatted message with severity Warning.
30 | void Warning(string format, params object[] formatArgs);
31 |
32 | /// Logs a message and an associated exception with severity Warning.
33 | void Warning(Exception exception, string message);
34 |
35 | /// Logs a message with severity Error.
36 | void Error(string message);
37 |
38 | /// Logs a formatted message with severity Error.
39 | void Error(string format, params object[] formatArgs);
40 |
41 | /// Logs a message and an associated exception with severity Error.
42 | void Error(Exception exception, string message);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/GroupCache/IGetter.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 | using System.IO;
12 | using System.Threading;
13 | using System.Threading.Tasks;
14 |
15 | public interface ICacheControl
16 | {
17 | // Whether cache must not store any response.
18 | bool NoStore { get; set; }
19 | }
20 |
21 | public class CacheControl : ICacheControl
22 | {
23 | // Whether cache must not store any response.
24 | ///
25 | public bool NoStore { get; set; } = false;
26 | }
27 |
28 | ///
29 | /// A Getter loads data for identified by key.
30 | ///
31 | /// The returned data must be unversioned.
32 | /// That is, key must uniquely describe the loaded data
33 | /// without an implicit current time, and without relying on cache expiration mechanisms.
34 | ///
35 | public interface IGetter
36 | {
37 | Task GetAsync(string key, Stream sink, ICacheControl cacheControl, CancellationToken ct);
38 | }
39 |
40 | public sealed class GetterFunc : IGetter
41 | {
42 | private readonly Func func;
43 |
44 | public GetterFunc(Func func)
45 | {
46 | this.func = func;
47 | }
48 |
49 | ///
50 | public Task GetAsync(string key, Stream sink, ICacheControl cacheControl, CancellationToken ct)
51 | {
52 | return this.func(key, sink, cacheControl, ct);
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/GroupCache/ReaderLockHolder.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 | using System.Threading;
12 |
13 | ///
14 | /// Helper class that makes it easier to ensure proper usage of a for readers by providing support for
16 | /// and the using keyword.
17 | ///
18 | public sealed class ReaderLockHolder : IDisposable
19 | {
20 | private readonly ReaderWriterLockSlim rwLock;
21 |
22 | // Track whether Dispose has been called.
23 | private bool disposed = false;
24 |
25 | ///
26 | /// Initializes a new instance of the class.
27 | /// Acquires a reader lock on the rwLock received with no timeout specified.
28 | ///
29 | public ReaderLockHolder(ReaderWriterLockSlim rwLock)
30 | {
31 | if (rwLock == null)
32 | {
33 | throw new ArgumentNullException("rwLock");
34 | }
35 |
36 | this.rwLock = rwLock;
37 | this.rwLock.EnterReadLock();
38 | }
39 |
40 | ///
41 | public void Dispose()
42 | {
43 | this.Dispose(true);
44 | }
45 |
46 | private void Dispose(bool disposing)
47 | {
48 | if (disposing && !this.disposed)
49 | {
50 | if (this.rwLock.IsReadLockHeld)
51 | {
52 | this.rwLock.ExitReadLock();
53 | }
54 |
55 | this.disposed = true;
56 | }
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/src/GroupCache/WriterLockHolder.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 | using System.Threading;
12 |
13 | ///
14 | /// Helper class that makes it easier to ensure proper usage of a
15 | /// for writers by providing support for and the using keyword.
16 | ///
17 | public sealed class WriterLockHolder : IDisposable
18 | {
19 | private readonly ReaderWriterLockSlim rwLock;
20 |
21 | // Track whether Dispose has been called.
22 | private bool disposed = false;
23 |
24 | ///
25 | /// Initializes a new instance of the class.
26 | /// Acquires a reader lock on the rwLock received
27 | /// with no timeout specified.
28 | ///
29 | public WriterLockHolder(ReaderWriterLockSlim rwLock)
30 | {
31 | if (rwLock == null)
32 | {
33 | throw new ArgumentNullException("rwLock");
34 | }
35 |
36 | this.rwLock = rwLock;
37 | this.rwLock.EnterWriteLock();
38 | }
39 |
40 | ///
41 | public void Dispose()
42 | {
43 | this.Dispose(true);
44 | }
45 |
46 | private void Dispose(bool disposing)
47 | {
48 | if (disposing && !this.disposed)
49 | {
50 | if (this.rwLock.IsWriteLockHeld)
51 | {
52 | this.rwLock.ExitWriteLock();
53 | }
54 |
55 | this.disposed = true;
56 | }
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/src/GroupCache/SingleFlight.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 | using System.Collections.Concurrent;
12 | using System.Threading.Tasks;
13 |
14 | ///
15 | /// Provides a duplicate function call suppression mechanism.
16 | ///
17 | /// Return type of the callback.
18 | public sealed class SingleFlight
19 | {
20 | private readonly ConcurrentDictionary> flights = new ConcurrentDictionary>();
21 |
22 | ///
23 | /// Initializes a new instance of the class.
24 | ///
25 | public SingleFlight()
26 | {
27 | }
28 |
29 | ///
30 | ///
31 | ///
32 | ///
33 | ///
34 | /// A representing the result of the asynchronous operation.
35 | public async Task DoAsync(string key, Func> valueFactory)
36 | {
37 | TaskCompletionSource tcs = new TaskCompletionSource();
38 | var result = this.flights.GetOrAdd(key, tcs);
39 | if (result != tcs)
40 | {
41 | return await result.Task.ConfigureAwait(false);
42 | }
43 |
44 | try
45 | {
46 | var value = await valueFactory().ConfigureAwait(false);
47 | tcs.TrySetResult(value);
48 | }
49 | catch (Exception ex)
50 | {
51 | tcs.SetException(ex);
52 | }
53 |
54 | this.flights.TryRemove(key, out _);
55 | return await tcs.Task.ConfigureAwait(false);
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/test/GroupCacheStubTest/UnitTestGroupStats.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCacheStubTest
9 | {
10 | using System;
11 | using System.Threading;
12 | using GroupCache;
13 |
14 | ///
15 | /// For unit-testing only.
16 | ///
17 | public class UnitTestGroupStat : IGroupStat
18 | {
19 | public int CacheHits = 0;
20 | public int Gets = 0;
21 | public int LoadsDeduped = 0;
22 | public int LocalLoads = 0;
23 | public int PeerLoads = 0;
24 | public int ServerRequests = 0;
25 |
26 | public void TraceCacheHits(string groupName)
27 | {
28 | Interlocked.Increment(ref this.CacheHits);
29 | }
30 |
31 | public void TraceGets(string groupName)
32 | {
33 | Interlocked.Increment(ref this.Gets);
34 | }
35 |
36 | public void TraceLoadsDeduped(string groupName)
37 | {
38 | Interlocked.Increment(ref this.LoadsDeduped);
39 | }
40 |
41 | public void TraceLocalLoads(string groupName)
42 | {
43 | Interlocked.Increment(ref this.LocalLoads);
44 | }
45 |
46 | public void TracePeerLoads(string groupName)
47 | {
48 | Interlocked.Increment(ref this.PeerLoads);
49 | }
50 |
51 | public void TraceServerRequests(string groupName)
52 | {
53 | Interlocked.Increment(ref this.ServerRequests);
54 | }
55 |
56 | public void TraceRoundtripLatency(string groupName, TimeSpan ts)
57 | {
58 | }
59 |
60 | public void TraceRetry(string groupName)
61 | {
62 | }
63 |
64 | public void TraceItemOverCapacity(string groupName)
65 | {
66 | }
67 |
68 | public void TraceConcurrentServerRequests(int v)
69 | {
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/GroupCache/UpgradeableReaderLockHolder.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 | using System.Threading;
12 |
13 | ///
14 | /// Helper class that makes it easier to ensure proper usage of a
15 | /// for readers by providing support for and the using keyword.
16 | ///
17 | public sealed class UpgradeableReaderLockHolder : IDisposable
18 | {
19 | ///
20 | /// Initializes a new instance of the class.
21 | /// Acquires a reader lock on the rwLock received
22 | /// with no timeout specified.
23 | ///
24 | public UpgradeableReaderLockHolder(ReaderWriterLockSlim rwLock)
25 | {
26 | if (rwLock == null)
27 | {
28 | throw new ArgumentNullException("rwLock");
29 | }
30 |
31 | this.rwLock = rwLock;
32 | this.rwLock.EnterUpgradeableReadLock();
33 | }
34 |
35 | ///
36 | public void Dispose()
37 | {
38 | this.Dispose(true);
39 | }
40 |
41 | public WriterLockHolder GetWriterLock()
42 | {
43 | return this.rwLock.GetWriterLock();
44 | }
45 |
46 | private void Dispose(bool disposing)
47 | {
48 | if (disposing && !this.disposed)
49 | {
50 | if (this.rwLock.IsUpgradeableReadLockHeld)
51 | {
52 | this.rwLock.ExitUpgradeableReadLock();
53 | }
54 |
55 | this.disposed = true;
56 | }
57 | }
58 |
59 | private readonly ReaderWriterLockSlim rwLock;
60 |
61 | // Track whether Dispose has been called.
62 | private bool disposed = false;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/GroupCacheDotNet.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.13.35931.197
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
7 | ProjectSection(SolutionItems) = preProject
8 | .editorconfig = .editorconfig
9 | Directory.Build.props = Directory.Build.props
10 | Directory.Packages.props = Directory.Packages.props
11 | stylecop.json = stylecop.json
12 | EndProjectSection
13 | EndProject
14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GroupCache", "src\GroupCache\GroupCache.csproj", "{8D9CF82B-1DAA-9E8A-8535-E14122FB27FD}"
15 | EndProject
16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GroupCacheStubImpl", "src\GroupCacheStubImpl\GroupCacheStubImpl.csproj", "{C62F2717-4EBF-DFAD-CC07-920214115635}"
17 | EndProject
18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GroupCacheStubTest", "test\GroupCacheStubTest\GroupCacheStubTest.csproj", "{94E451B2-27D2-A6BF-9613-C10541EEBFDB}"
19 | EndProject
20 | Global
21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
22 | Debug|Any CPU = Debug|Any CPU
23 | Release|Any CPU = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
26 | {8D9CF82B-1DAA-9E8A-8535-E14122FB27FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {8D9CF82B-1DAA-9E8A-8535-E14122FB27FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {8D9CF82B-1DAA-9E8A-8535-E14122FB27FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {8D9CF82B-1DAA-9E8A-8535-E14122FB27FD}.Release|Any CPU.Build.0 = Release|Any CPU
30 | {C62F2717-4EBF-DFAD-CC07-920214115635}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31 | {C62F2717-4EBF-DFAD-CC07-920214115635}.Debug|Any CPU.Build.0 = Debug|Any CPU
32 | {C62F2717-4EBF-DFAD-CC07-920214115635}.Release|Any CPU.ActiveCfg = Release|Any CPU
33 | {C62F2717-4EBF-DFAD-CC07-920214115635}.Release|Any CPU.Build.0 = Release|Any CPU
34 | {94E451B2-27D2-A6BF-9613-C10541EEBFDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35 | {94E451B2-27D2-A6BF-9613-C10541EEBFDB}.Debug|Any CPU.Build.0 = Debug|Any CPU
36 | {94E451B2-27D2-A6BF-9613-C10541EEBFDB}.Release|Any CPU.ActiveCfg = Release|Any CPU
37 | {94E451B2-27D2-A6BF-9613-C10541EEBFDB}.Release|Any CPU.Build.0 = Release|Any CPU
38 | EndGlobalSection
39 | GlobalSection(SolutionProperties) = preSolution
40 | HideSolutionNode = FALSE
41 | EndGlobalSection
42 | GlobalSection(ExtensibilityGlobals) = postSolution
43 | SolutionGuid = {2D1F54F4-4937-4DFA-A5D3-94013FC1975A}
44 | EndGlobalSection
45 | EndGlobal
46 |
--------------------------------------------------------------------------------
/test/GroupCacheStubTest/TestDiskCache.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCacheStubTest
9 | {
10 | using System.Collections.Generic;
11 | using System.Threading;
12 | using System.Threading.Tasks;
13 | using GroupCache;
14 | using Xunit;
15 |
16 | public class TestDiskCache
17 | {
18 | // Test have random failure disabling for now
19 | [Fact]
20 | public void TestConcurrentCacheAccess()
21 | {
22 | var mockFileSystem = new MockFileSystem();
23 | var diskCache = new DiskCache("c:\\CacheRoot", 10, mockFileSystem);
24 | long cacheFill = 0;
25 | List tasks = new List();
26 | for (int i = 0; i < 24; i++)
27 | {
28 | var t = Task.Run(async () =>
29 | {
30 | for (int k = 0; k < 100; k++)
31 | {
32 | for (int j = 0; j < 40; j++)
33 | {
34 | var keyString = j.ToString();
35 | var cacheEntry = await diskCache.GetOrAddAsync(
36 | keyString,
37 | (key, sink, cacheControl) =>
38 | {
39 | Interlocked.Increment(ref cacheFill);
40 | var keystream = key.StringToStream();
41 | return keystream.CopyToAsync(sink);
42 | }, new CacheControl(), CancellationToken.None);
43 | try
44 | {
45 | var cacheEntryString = cacheEntry.Value().StreamToString();
46 | Xunit.Assert.Equal(keyString, cacheEntryString);
47 | }
48 | finally
49 | {
50 | await cacheEntry.DisposeAsync();
51 | }
52 | }
53 | }
54 | });
55 | tasks.Add(t);
56 | }
57 |
58 | Task.WaitAll(tasks.ToArray());
59 | Xunit.Assert.Equal(10, mockFileSystem.DirectoryGetFiles("c:\\CacheRoot\\tmp").Length);
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Security
4 |
5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
6 |
7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below.
8 |
9 | ## Reporting Security Issues
10 |
11 | **Please do not report security vulnerabilities through public GitHub issues.**
12 |
13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).
14 |
15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).
16 |
17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
18 |
19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
20 |
21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
22 | * Full paths of source file(s) related to the manifestation of the issue
23 | * The location of the affected source code (tag/branch/commit or direct URL)
24 | * Any special configuration required to reproduce the issue
25 | * Step-by-step instructions to reproduce the issue
26 | * Proof-of-concept or exploit code (if possible)
27 | * Impact of the issue, including how an attacker might exploit the issue
28 |
29 | This information will help us triage your report more quickly.
30 |
31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs.
32 |
33 | ## Preferred Languages
34 |
35 | We prefer all communications to be in English.
36 |
37 | ## Policy
38 |
39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).
40 |
41 |
--------------------------------------------------------------------------------
/src/GroupCache/NullCacheEntryValidator.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System.IO;
11 | using System.Threading;
12 | using System.Threading.Tasks;
13 |
14 | public class NullCacheEntryValidator : ICacheEntryValidator
15 | {
16 | ///
17 | public ValidationStream ValidateEntryPassThrough(string key, Stream sink)
18 | {
19 | return new NullValidationStream(sink);
20 | }
21 | }
22 |
23 | public class NullValidationStream : ValidationStream
24 | {
25 | private readonly Stream sink;
26 |
27 | public NullValidationStream(Stream sink)
28 | {
29 | this.sink = sink;
30 | }
31 |
32 | ///
33 | public override bool CanRead => this.sink.CanRead;
34 |
35 | ///
36 | public override bool CanSeek => this.sink.CanSeek;
37 |
38 | ///
39 | public override bool CanWrite => this.sink.CanWrite;
40 |
41 | ///
42 | public override long Length => this.sink.Length;
43 |
44 | ///
45 | public override long Position { get => this.sink.Position; set => this.sink.Position = value; }
46 |
47 | ///
48 | public override void Flush()
49 | {
50 | this.sink.Flush();
51 | }
52 |
53 | ///
54 | public override int Read(byte[] buffer, int offset, int count)
55 | {
56 | return this.sink.Read(buffer, offset, count);
57 | }
58 |
59 | ///
60 | public override long Seek(long offset, SeekOrigin origin)
61 | {
62 | return this.sink.Seek(offset, origin);
63 | }
64 |
65 | ///
66 | public override void SetLength(long value)
67 | {
68 | this.sink.SetLength(value);
69 | }
70 |
71 | ///
72 | public override Task ValidateAsync(CancellationToken ct)
73 | {
74 | return Task.CompletedTask;
75 | }
76 |
77 | ///
78 | public override void Write(byte[] buffer, int offset, int count)
79 | {
80 | this.sink.Write(buffer, offset, count);
81 | }
82 | }
83 | }
--------------------------------------------------------------------------------
/src/GroupCacheStubImpl/Log4NetLoggerAdapter.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCacheStub
9 | {
10 | using System;
11 |
12 | public class Log4NetLoggerAdapter : GroupCache.ILogger
13 | {
14 | private readonly log4net.ILog innerLogger;
15 |
16 | ///
17 | /// Initializes a new instance of the class.
18 | ///
19 | ///
20 | public Log4NetLoggerAdapter(log4net.ILog log4NetLogger)
21 | {
22 | this.innerLogger = log4NetLogger;
23 | }
24 |
25 | ///
26 | public void Debug(string message)
27 | {
28 | this.innerLogger.Debug(message);
29 | }
30 |
31 | ///
32 | public void Debug(string format, params object[] formatArgs)
33 | {
34 | this.innerLogger.DebugFormat(format, formatArgs);
35 | }
36 |
37 | ///
38 | public void Error(string message)
39 | {
40 | this.innerLogger.Error(message);
41 | }
42 |
43 | ///
44 | public void Error(string format, params object[] formatArgs)
45 | {
46 | this.innerLogger.ErrorFormat(format, formatArgs);
47 | }
48 |
49 | ///
50 | public void Error(Exception exception, string message)
51 | {
52 | this.innerLogger.Error(message, exception);
53 | }
54 |
55 | ///
56 | public void Info(string message)
57 | {
58 | this.innerLogger.Info(message);
59 | }
60 |
61 | ///
62 | public void Info(string format, params object[] formatArgs)
63 | {
64 | this.innerLogger.InfoFormat(format, formatArgs);
65 | }
66 |
67 | ///
68 | public void Warning(string message)
69 | {
70 | this.innerLogger.Warn(message);
71 | }
72 |
73 | ///
74 | public void Warning(string format, params object[] formatArgs)
75 | {
76 | this.innerLogger.WarnFormat(format, formatArgs);
77 | }
78 |
79 | ///
80 | public void Warning(Exception exception, string message)
81 | {
82 | this.innerLogger.Warn(message, exception);
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/GroupCache/RetryContext.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 |
12 | ///
13 | /// Low-level access to ongoing retry operation.
14 | /// Normally not needed by clients, but can be used to alter the course of the retry,.
15 | ///
16 | public class RetryContext
17 | {
18 | private Exception exception;
19 | private int count = 0;
20 | private TimeSpan backOffPeriod;
21 | private readonly int maxAttempts;
22 |
23 | ///
24 | /// Gets the number of attempts before a retry becomes impossible.
25 | ///
26 | public int RetryCount
27 | {
28 | get { return this.count; }
29 | }
30 |
31 | ///
32 | /// Gets accessor for the exception object that caused the current retry.
33 | ///
34 | public Exception LastException
35 | {
36 | get { return this.exception; }
37 | }
38 |
39 | ///
40 | /// Gets accessor for the backoff period of the current policy.
41 | ///
42 | public TimeSpan BackOffPeriod
43 | {
44 | get { return this.backOffPeriod; }
45 | }
46 |
47 | ///
48 | /// Gets accessor for the max attempts of the current policy.
49 | ///
50 | public int MaxAttempts
51 | {
52 | get { return this.maxAttempts; }
53 | }
54 |
55 | ///
56 | /// Gets or sets a value indicating whether signal to the framework that no more attempts should be made to try or retry the current RetryCallback.
57 | ///
58 | public bool IsExhausted { get; set; }
59 |
60 | ///
61 | /// Initializes a new instance of the class.
62 | ///
63 | ///
64 | ///
65 | public RetryContext(int maxAttempts, TimeSpan backOffPeriod)
66 | {
67 | this.maxAttempts = maxAttempts;
68 | this.backOffPeriod = backOffPeriod;
69 | this.IsExhausted = false;
70 | }
71 |
72 | public void RegisterException(Exception ex)
73 | {
74 | this.exception = ex;
75 | if (ex != null)
76 | {
77 | this.count++;
78 | }
79 | }
80 | }
81 | }
--------------------------------------------------------------------------------
/src/GroupCache/NullLogger.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 |
12 | ///
13 | /// Logger which doesn't log any information anywhere.
14 | ///
15 | public sealed class NullLogger : ILogger
16 | {
17 | ///
18 | /// As with all logging calls on this logger, this method is a no-op.
19 | ///
20 | public void Debug(string message)
21 | {
22 | }
23 |
24 | ///
25 | /// As with all logging calls on this logger, this method is a no-op.
26 | ///
27 | public void Debug(string format, params object[] formatArgs)
28 | {
29 | }
30 |
31 | ///
32 | /// As with all logging calls on this logger, this method is a no-op.
33 | ///
34 | public void Error(string message)
35 | {
36 | }
37 |
38 | ///
39 | /// As with all logging calls on this logger, this method is a no-op.
40 | ///
41 | public void Error(Exception exception, string message)
42 | {
43 | }
44 |
45 | ///
46 | /// As with all logging calls on this logger, this method is a no-op.
47 | ///
48 | public void Error(string format, params object[] formatArgs)
49 | {
50 | }
51 |
52 | ///
53 | /// Returns a reference to the instance on which the method is called, as
54 | /// instances aren't associated with specific types.
55 | ///
56 | ///
57 | public ILogger ForType()
58 | {
59 | return this;
60 | }
61 |
62 | ///
63 | /// As with all logging calls on this logger, this method is a no-op.
64 | ///
65 | public void Info(string message)
66 | {
67 | }
68 |
69 | ///
70 | /// As with all logging calls on this logger, this method is a no-op.
71 | ///
72 | public void Info(string format, params object[] formatArgs)
73 | {
74 | }
75 |
76 | ///
77 | /// As with all logging calls on this logger, this method is a no-op.
78 | ///
79 | public void Warning(string message)
80 | {
81 | }
82 |
83 | ///
84 | /// As with all logging calls on this logger, this method is a no-op.
85 | ///
86 | public void Warning(Exception exception, string message)
87 | {
88 | }
89 |
90 | ///
91 | /// As with all logging calls on this logger, this method is a no-op.
92 | ///
93 | public void Warning(string format, params object[] formatArgs)
94 | {
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/GroupCache/IGroupCacheClient.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 | using System.IO;
12 | using System.Runtime.Serialization;
13 | using System.Threading;
14 | using System.Threading.Tasks;
15 |
16 | public interface IGroupCacheClient : IDisposable
17 | {
18 | Task GetAsync(string group, string key, Stream sink, ICacheControl cacheControl, CancellationToken ct);
19 |
20 | PeerEndpoint Endpoint { get; }
21 |
22 | ///
23 | /// Gets a value indicating whether this is true if this client is not making a request to a remote machine but simply read from local cache.
24 | ///
25 | bool IsLocal { get; }
26 | }
27 |
28 | [Serializable]
29 | public class InternalServerErrorException : Exception
30 | {
31 | public InternalServerErrorException(string message)
32 | : base(message)
33 | {
34 | }
35 |
36 | public InternalServerErrorException(string message, Exception innerException)
37 | : base(message, innerException)
38 | {
39 | }
40 |
41 | protected InternalServerErrorException(SerializationInfo info, StreamingContext context)
42 | : base(info, context)
43 | {
44 | }
45 | }
46 |
47 | [Serializable]
48 | public class ConnectFailureException : Exception
49 | {
50 | public ConnectFailureException()
51 | {
52 | }
53 |
54 | public ConnectFailureException(string message)
55 | : base(message)
56 | {
57 | }
58 |
59 | public ConnectFailureException(string message, Exception innerException)
60 | : base(message, innerException)
61 | {
62 | }
63 |
64 | protected ConnectFailureException(SerializationInfo info, StreamingContext context)
65 | : base(info, context)
66 | {
67 | }
68 | }
69 |
70 | [Serializable]
71 | public class ServerBusyException : Exception
72 | {
73 | public ServerBusyException()
74 | {
75 | }
76 |
77 | public ServerBusyException(string message)
78 | : base(message)
79 | {
80 | }
81 | }
82 |
83 | [Serializable]
84 | public class GroupNotFoundException : Exception
85 | {
86 | public GroupNotFoundException()
87 | {
88 | }
89 |
90 | public GroupNotFoundException(string message)
91 | : base(message)
92 | {
93 | }
94 |
95 | public GroupNotFoundException(string message, Exception innerException)
96 | : base(message, innerException)
97 | {
98 | }
99 |
100 | protected GroupNotFoundException(SerializationInfo info, StreamingContext context)
101 | : base(info, context)
102 | {
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/GroupCache/SimpleRetryPolicy.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 | using System.Linq;
12 | using System.Threading;
13 | using System.Threading.Tasks;
14 |
15 | public class SimpleRetryPolicy
16 | {
17 | public const int DEFAULTMAXATTEMPTS = 3;
18 | public static readonly TimeSpan DEFAULTBACKOFFPERIOD = TimeSpan.FromMilliseconds(1000);
19 | private volatile int maxAttempts;
20 | private volatile int backOffPeriod;
21 | private readonly Type[] retryableExceptionTypes;
22 |
23 | public SimpleRetryPolicy()
24 | : this(DEFAULTMAXATTEMPTS, DEFAULTBACKOFFPERIOD)
25 | {
26 | }
27 |
28 | public SimpleRetryPolicy(int maxAttempts, TimeSpan backOffPeriod)
29 | : this(maxAttempts, backOffPeriod, new Type[] { typeof(Exception) })
30 | {
31 | }
32 |
33 | public SimpleRetryPolicy(int maxAttempts, TimeSpan backOffPeriod, Type[] retryableExceptions)
34 | {
35 | this.maxAttempts = maxAttempts;
36 | this.backOffPeriod = (int)backOffPeriod.TotalMilliseconds;
37 | this.retryableExceptionTypes = retryableExceptions;
38 | }
39 |
40 | private bool RetryForException(Exception classifiable)
41 | {
42 | if (classifiable == null)
43 | {
44 | return false;
45 | }
46 |
47 | var exceptionClass = classifiable.GetType();
48 |
49 | if (this.retryableExceptionTypes == null)
50 | {
51 | return false;
52 | }
53 | else
54 | {
55 | // Determines whether the exception is a subclass of any element of the retryable exception type.
56 | return this.retryableExceptionTypes.Any((retryableType) => { return retryableType.IsAssignableFrom(exceptionClass); });
57 | }
58 | }
59 |
60 | ///
61 | /// Gets or sets the maximum retry count.
62 | ///
63 | /// The maximum retry count.
64 | public int MaxAttempts
65 | {
66 | get { return this.maxAttempts; }
67 | set { this.maxAttempts = value; }
68 | }
69 |
70 | ///
71 | /// Gets or sets the delay time span to sleep after an exception is thrown and a rety is
72 | /// attempted.
73 | ///
74 | /// The delay time span.
75 | public TimeSpan BackOffPeriod
76 | {
77 | get { return TimeSpan.FromMilliseconds(this.backOffPeriod); }
78 | set { this.backOffPeriod = (int)value.TotalMilliseconds; }
79 | }
80 |
81 | public virtual bool CanRetry(RetryContext context)
82 | {
83 | Exception ex = context.LastException;
84 |
85 | // N.B. the contract is defined to include the initial attempt in the count.
86 | return (ex == null || this.RetryForException(ex)) && (context.RetryCount < this.maxAttempts);
87 | }
88 |
89 | public virtual void RegisterThrowable(RetryContext context, Exception exception)
90 | {
91 | context.RegisterException(exception);
92 | }
93 |
94 | public virtual void BackOff(RetryContext context)
95 | {
96 | Thread.Sleep(this.BackOffPeriod);
97 | }
98 |
99 | public virtual Task BackOffAsync(RetryContext context)
100 | {
101 | return Task.Delay(this.BackOffPeriod);
102 | }
103 |
104 | public virtual T HandleRetryExhausted(RetryContext context)
105 | {
106 | throw new ExhaustedRetryException("Retry exhausted after last attempt with no recovery path.", context.LastException);
107 | }
108 | }
109 | }
--------------------------------------------------------------------------------
/src/GroupCache/SemaphoreHolder.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 | using System.Threading;
12 | using System.Threading.Tasks;
13 |
14 | public static class SemaphoreExtensions
15 | {
16 | private const string TimeOutError = "Failed to Aquire lock within timeout";
17 |
18 | public static SemaphoreHolder Acquire(this SemaphoreSlim semaphore)
19 | {
20 | semaphore.Wait();
21 | return new SemaphoreHolder(semaphore);
22 | }
23 |
24 | public static SemaphoreHolder Acquire(this SemaphoreSlim semaphore, CancellationToken cancellationToken)
25 | {
26 | semaphore.Wait(cancellationToken);
27 | return new SemaphoreHolder(semaphore);
28 | }
29 |
30 | public static SemaphoreHolder Acquire(this SemaphoreSlim semaphore, TimeSpan timeout)
31 | {
32 | if (!semaphore.Wait(timeout))
33 | {
34 | throw new TimeoutException(TimeOutError);
35 | }
36 |
37 | return new SemaphoreHolder(semaphore);
38 | }
39 |
40 | public static SemaphoreHolder Acquire(this SemaphoreSlim semaphore, TimeSpan timeout, CancellationToken cancellationToken)
41 | {
42 | if (!semaphore.Wait(timeout, cancellationToken))
43 | {
44 | throw new TimeoutException(TimeOutError);
45 | }
46 |
47 | return new SemaphoreHolder(semaphore);
48 | }
49 |
50 | public static async Task AcquireAsync(this SemaphoreSlim semaphore)
51 | {
52 | await semaphore.WaitAsync().ConfigureAwait(false);
53 | return new SemaphoreHolder(semaphore);
54 | }
55 |
56 | public static async Task AcquireAsync(this SemaphoreSlim semaphore, CancellationToken cancellationToken)
57 | {
58 | await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
59 | return new SemaphoreHolder(semaphore);
60 | }
61 |
62 | public static async Task AcquireAsync(this SemaphoreSlim semaphore, TimeSpan timeout)
63 | {
64 | if (!await semaphore.WaitAsync(timeout).ConfigureAwait(false))
65 | {
66 | throw new TimeoutException(TimeOutError);
67 | }
68 |
69 | return new SemaphoreHolder(semaphore);
70 | }
71 |
72 | public static async Task AcquireAsync(this SemaphoreSlim semaphore, TimeSpan timeout, CancellationToken cancellationToken)
73 | {
74 | if (!await semaphore.WaitAsync(timeout, cancellationToken).ConfigureAwait(false))
75 | {
76 | throw new TimeoutException(TimeOutError);
77 | }
78 |
79 | return new SemaphoreHolder(semaphore);
80 | }
81 | }
82 |
83 | ///
84 | /// Helper class that makes it easier to ensure proper usage of a for
85 | /// readers by providing support for and the using keyword.
86 | ///
87 | public sealed class SemaphoreHolder : IDisposable
88 | {
89 | private readonly SemaphoreSlim semaphore;
90 |
91 | // Track whether Dispose has been called.
92 | private bool disposed = false;
93 |
94 | public SemaphoreHolder(SemaphoreSlim semaphore)
95 | {
96 | Argument.NotNull(semaphore, "semaphore");
97 | this.semaphore = semaphore;
98 | }
99 |
100 | public void Dispose()
101 | {
102 | this.Dispose(true);
103 | }
104 |
105 | private void Dispose(bool disposing)
106 | {
107 | if (disposing && !this.disposed)
108 | {
109 | this.semaphore.Release();
110 | this.disposed = true;
111 | }
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/GroupCacheStubImpl/OwinClient.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCacheStub
9 | {
10 | using System;
11 | using System.Collections.Generic;
12 | using System.IO;
13 | using System.Net;
14 | using System.Net.Http;
15 | using System.Threading;
16 | using System.Threading.Tasks;
17 | using GroupCache;
18 |
19 | public class OwinClient : IGroupCacheClient, IDisposable
20 | {
21 | private readonly HttpClient client;
22 |
23 | ///
24 | public PeerEndpoint Endpoint { get; private set; }
25 |
26 | ///
27 | public bool IsLocal
28 | {
29 | get { return false; }
30 | }
31 |
32 | public OwinClient(PeerEndpoint endpoint, int minuteTimeout = 2)
33 | {
34 | this.Endpoint = endpoint;
35 | this.client = new HttpClient()
36 | {
37 | Timeout = TimeSpan.FromMinutes(minuteTimeout),
38 | BaseAddress = new UriBuilder("http", this.Endpoint.HostName, this.Endpoint.Port).Uri,
39 | };
40 | this.client.DefaultRequestHeaders.ConnectionClose = true;
41 | }
42 |
43 | ///
44 | public async Task GetAsync(string group, string key, Stream sink, ICacheControl cacheControl, CancellationToken ct)
45 | {
46 | var formContent = new FormUrlEncodedContent(new[]
47 | {
48 | new KeyValuePair(OwinServer.GROUPNAME, group),
49 | new KeyValuePair(OwinServer.KEY, key),
50 | });
51 |
52 | HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Post, "Get")
53 | {
54 | Content = formContent,
55 | Version = HttpVersion.Version10,
56 | };
57 |
58 | // GetAsync block until the response’s (headers + content) is fully downloaded/read to the memory
59 | // Unless we use HttpCompletionOption.ResponseHeadersRead
60 | HttpResponseMessage response;
61 | try
62 | {
63 | response = await this.client.SendAsync(msg, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
64 | }
65 | catch (HttpRequestException ex)
66 | {
67 | throw new ConnectFailureException($"fail to send request to {this.Endpoint.ToString()}", ex);
68 | }
69 |
70 | using (response)
71 | {
72 | if (!response.IsSuccessStatusCode)
73 | {
74 | if (response.StatusCode == HttpStatusCode.InternalServerError)
75 | {
76 | throw new InternalServerErrorException(response.ReasonPhrase);
77 | }
78 |
79 | if (response.StatusCode == HttpStatusCode.ServiceUnavailable)
80 | {
81 | throw new ServerBusyException(response.ReasonPhrase);
82 | }
83 |
84 | if (response.StatusCode == HttpStatusCode.NotFound)
85 | {
86 | throw new GroupNotFoundException(response.ReasonPhrase);
87 | }
88 |
89 | response.EnsureSuccessStatusCode();
90 | }
91 |
92 | // if we reach her we can safely read response.Content
93 | if (response.Content == null)
94 | {
95 | throw new InternalServerErrorException("response body is null");
96 | }
97 |
98 | if (response.Headers.CacheControl != null && response.Headers.CacheControl.NoStore)
99 | {
100 | cacheControl.NoStore = true;
101 | }
102 |
103 | // using ct.Register to force CopyToAsync to return early and throw OperationCanceledException
104 | using (ct.Register(response.Dispose))
105 | {
106 | try
107 | {
108 | await response.Content.CopyToAsync(sink).ConfigureAwait(false);
109 | }
110 | catch (ObjectDisposedException e) when (ct.IsCancellationRequested)
111 | {
112 | throw new OperationCanceledException(null, e, ct);
113 | }
114 | }
115 | }
116 | }
117 |
118 | ///
119 | public void Dispose()
120 | {
121 | this.client.Dispose();
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/GroupCache/MemoryCache.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 | using System.Collections.Generic;
12 | using System.IO;
13 | using System.Threading;
14 | using System.Threading.Tasks;
15 |
16 | public sealed class MemoryCacheEntry : ICacheEntry
17 | {
18 | private ArraySegment segment;
19 |
20 | public MemoryCacheEntry(ArraySegment segment)
21 | {
22 | this.segment = segment;
23 | }
24 |
25 | ///
26 | public Task DisposeAsync()
27 | {
28 | return Task.CompletedTask;
29 | }
30 |
31 | ///
32 | public void Ref()
33 | {
34 | }
35 |
36 | ///
37 | public Stream Value()
38 | {
39 | return new MemoryStream(this.segment.Array, this.segment.Offset, this.segment.Count);
40 | }
41 | }
42 |
43 | public sealed class MemoryCache : ICache
44 | {
45 | private const int DefaultCopyBufferSize = 81920;
46 | private readonly SingleFlight createEntry = new SingleFlight();
47 | private readonly LRUCache> cache;
48 |
49 | public MemoryCache(int maxItemCount, TimeSpan ttl)
50 | {
51 | this.cache = new LRUCache>(maxItemCount, ttl);
52 | this.cache.ItemOverCapacity += this.TriggerItemCapacity;
53 | }
54 |
55 | public MemoryCache(int maxItemCount = 200, ulong capacity = 0)
56 | {
57 | this.cache = new LRUCache>(maxItemCount, capacity);
58 | this.cache.ItemOverCapacity += this.TriggerItemCapacity;
59 | }
60 |
61 | public MemoryCache(int maxItemCount, ulong capacity, TimeSpan ttl)
62 | {
63 | this.cache = new LRUCache>(maxItemCount, EqualityComparer.Default, capacity, ttl);
64 | this.cache.ItemOverCapacity += this.TriggerItemCapacity;
65 | }
66 |
67 | ///
68 | public event Action ItemOverCapacity;
69 |
70 | /// valueFactory
71 | /// Get item from cache of call valueFactory if missing.
72 | ///
73 | /// The key of the value that need to be filled in.
74 | /// valueFactory should write result to the stream but not close it.
75 | /// Cancellation token.
76 | /// A representing the asynchronous operation.
77 | public Task GetOrAddAsync(string key, Func valueFactory, ICacheControl cacheControl, CancellationToken ct)
78 | {
79 | return this.createEntry.DoAsync(key, async () =>
80 | {
81 | ICacheEntry found = await this.GetAsync(key, ct).ConfigureAwait(false);
82 | if (!(found is EmptyCacheEntry))
83 | {
84 | return found;
85 | }
86 |
87 | ArraySegment buffer;
88 | using (var memStream = new MemoryStream())
89 | {
90 | await valueFactory(key, memStream, cacheControl).ConfigureAwait(false);
91 | memStream.Position = 0;
92 | buffer = new ArraySegment(memStream.ToArray());
93 | }
94 |
95 | if (!cacheControl.NoStore)
96 | {
97 | this.cache.Add(key, buffer, (ulong)buffer.Count);
98 | }
99 |
100 | return new MemoryCacheEntry(buffer);
101 | });
102 | }
103 |
104 | public Task GetAsync(string key, CancellationToken ct)
105 | {
106 | var found = this.cache.TryGetValue(key, out ArraySegment bytes);
107 | if (!found)
108 | {
109 | return Task.FromResult(new EmptyCacheEntry());
110 | }
111 |
112 | return Task.FromResult(new MemoryCacheEntry(bytes));
113 | }
114 |
115 | private void TriggerItemCapacity(KeyValuePair> kv)
116 | {
117 | this.ItemOverCapacity?.Invoke(kv.Key);
118 | }
119 |
120 | ///
121 | public Task RemoveAsync(string key, CancellationToken ct)
122 | {
123 | this.cache.Remove(key);
124 | return Task.CompletedTask;
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/GroupCacheStubImpl/OwinServer.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCacheStub
9 | {
10 | using System;
11 | using System.Linq;
12 | using GroupCache;
13 | using Microsoft.Owin;
14 | using Microsoft.Owin.Hosting;
15 | using Owin;
16 |
17 | public class OwinServer : IDisposable
18 | {
19 | public const string GROUPNAME = "groupName";
20 | public const string KEY = "key";
21 | private const string BASEURL = "/";
22 | private IDisposable server;
23 | private readonly IGroupCacheClient handler;
24 | private readonly string host;
25 |
26 | // Admin rights are not needed for port values of 5000 and higher
27 | private readonly int port;
28 |
29 | ///
30 | /// Initializes a new instance of the class.
31 | /// Creates a new server.
32 | ///
33 | /// the host.
34 | /// the port. If zero, an unused port is chosen automatically.
35 | public OwinServer(IGroupCacheClient handler, int port, string host = "*")
36 | {
37 | this.host = host;
38 | this.port = port;
39 | this.handler = handler;
40 | }
41 |
42 | public void Start()
43 | {
44 | var baseUrl = new UriBuilder("http", this.host, this.port).ToString();
45 | var startOption = new StartOptions(baseUrl) { ServerFactory = "Microsoft.Owin.Host.HttpListener" };
46 |
47 | // WebApp class is from Microsoft.Owin.Hosting
48 | this.server = WebApp.Start(startOption, this.Configuration);
49 | }
50 |
51 | public void Dispose()
52 | {
53 | if (this.server != null)
54 | {
55 | this.server.Dispose();
56 | }
57 | }
58 |
59 | public void Configuration(IAppBuilder app)
60 | {
61 | app.Run(async context =>
62 | {
63 | var request = context.Request;
64 | var response = context.Response;
65 | var callCancelled = context.Request.CallCancelled;
66 |
67 | var form = await request.ReadFormAsync();
68 | var groupName = form.Get(GROUPNAME);
69 | var key = form.Get(KEY);
70 |
71 | if (groupName == null || key == null)
72 | {
73 | response.StatusCode = 400; // BadRequest;
74 | return;
75 | }
76 |
77 | response.ContentType = "application/octet-stream";
78 | try
79 | {
80 | var cacheControl = new OwinCacheControl(context.Response.Headers);
81 | await this.handler.GetAsync(groupName, key, response.Body, cacheControl, callCancelled);
82 | }
83 | catch (GroupNotFoundException ex)
84 | {
85 | response.StatusCode = 404;
86 | response.ReasonPhrase = ex.ToString();
87 | }
88 | catch (ServerBusyException busy)
89 | {
90 | response.StatusCode = 503;
91 | response.ReasonPhrase = busy.ToString();
92 | }
93 | catch (Exception ex)
94 | {
95 | response.StatusCode = 500;
96 | response.ReasonPhrase = ex.ToString();
97 | }
98 | });
99 | }
100 | }
101 |
102 | public class OwinCacheControl : ICacheControl
103 | {
104 | private const string CacheControlHeaderName = "Cache-Control";
105 | private const string NoStoreHeaderValue = "no-store";
106 | private readonly IHeaderDictionary headers;
107 |
108 | public OwinCacheControl(IHeaderDictionary headers)
109 | {
110 | this.headers = headers;
111 | }
112 |
113 | public bool NoStore
114 | {
115 | get
116 | {
117 | var values = this.headers.GetCommaSeparatedValues(CacheControlHeaderName);
118 | return values != null && values.Contains(NoStoreHeaderValue);
119 | }
120 |
121 | set
122 | {
123 | if (value == true)
124 | {
125 | this.headers.AppendCommaSeparatedValues(CacheControlHeaderName, new string[] { NoStoreHeaderValue });
126 | }
127 | else
128 | {
129 | var values = this.headers.GetCommaSeparatedValues(CacheControlHeaderName);
130 | if (values != null)
131 | {
132 | var valuesMinusNoStore = values.Where((val) => val != NoStoreHeaderValue).ToArray();
133 | this.headers.SetCommaSeparatedValues(CacheControlHeaderName, valuesMinusNoStore);
134 | }
135 | }
136 | }
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/GroupCacheStubImpl/OwinPool.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCacheStub
9 | {
10 | using System;
11 | using System.Collections.Concurrent;
12 | using System.Collections.Generic;
13 | using System.IO;
14 | using System.Threading;
15 | using System.Threading.Tasks;
16 | using GroupCache;
17 |
18 | ///
19 | /// OwinPool implements IPeerPicker for a pool of HTTP peers.
20 | ///
21 | public sealed class OwinPool : IGroupCacheClient, IDisposable
22 | {
23 | private readonly object @lock = new object();
24 | private readonly OwinServer server;
25 | private PeerEndpoint self;
26 | private readonly ConcurrentDictionary> peerPickers = new ConcurrentDictionary>(StringComparer.InvariantCultureIgnoreCase);
27 | private readonly ConcurrentDictionary> cacheClients = new ConcurrentDictionary>();
28 | private readonly SemaphoreSlim concurrencyLimiter = null;
29 | private readonly int concurrencyLimit;
30 |
31 | ///
32 | /// Initializes a new instance of the class.
33 | ///
34 | ///
35 | ///
36 | public OwinPool(PeerEndpoint self, int concurrencyLimit = 24)
37 | {
38 | this.self = self;
39 | this.concurrencyLimit = concurrencyLimit;
40 | this.concurrencyLimiter = new SemaphoreSlim(initialCount: this.concurrencyLimit);
41 | this.server = new OwinServer(this, self.Port, self.HostName);
42 | this.server.Start();
43 | }
44 |
45 | ///
46 | public PeerEndpoint Endpoint
47 | {
48 | get { return this.self; }
49 | }
50 |
51 | ///
52 | public bool IsLocal
53 | {
54 | get { return true; }
55 | }
56 |
57 | ///
58 | /// Handle request from Peers and forward to the correct local Group instance.
59 | ///
60 | /// A representing the asynchronous operation.
61 | public async Task GetAsync(string groupName, string key, Stream sink, ICacheControl cacheControl, CancellationToken ct)
62 | {
63 | SemaphoreHolder limiter;
64 | try
65 | {
66 | limiter = await this.concurrencyLimiter.AcquireAsync(TimeSpan.Zero).ConfigureAwait(false);
67 | }
68 | catch (TimeoutException)
69 | {
70 | throw new ServerBusyException("Too many concurrent connection");
71 | }
72 |
73 | using (limiter)
74 | {
75 | var groupKey = new GroupKey { GroupName = groupName, Endpoint = this.Endpoint };
76 | var found = GroupCache.GetGroup(groupKey, out Group group);
77 | if (!found)
78 | {
79 | throw new GroupNotFoundException($"no such group: {groupName}");
80 | }
81 |
82 | group.Stats.TraceConcurrentServerRequests(this.concurrencyLimit - this.concurrencyLimiter.CurrentCount);
83 | group.Stats.TraceServerRequests(groupName);
84 |
85 | // We received a request from a peer, we need to download it locally and not forward to peer
86 | // because forwarding to peer would cause infinite loop if using different peer list
87 | await group.GetAsyncLocallyAsync(key, sink, cacheControl, ct);
88 | }
89 | }
90 |
91 | public IGroupCacheClient GetClient(PeerEndpoint endpoint)
92 | {
93 | var lazy = new Lazy(
94 | () =>
95 | {
96 | return new CircuitBreakerClient(new OwinClient(endpoint));
97 | },
98 | LazyThreadSafetyMode.ExecutionAndPublication);
99 | return this.cacheClients.GetOrAdd(endpoint, lazy).Value;
100 | }
101 |
102 | public IPeerPicker GetPicker(string groupName, IEqualityComparer keyHasher = null)
103 | {
104 | var lazy = new Lazy(
105 | () =>
106 | {
107 | var hasher = keyHasher;
108 | if (hasher == null)
109 | {
110 | hasher = EqualityComparer.Default;
111 | }
112 |
113 | var pool = new OwinPoolPicker(groupName, this);
114 | pool.KeyHasher = hasher;
115 | return pool;
116 | },
117 | LazyThreadSafetyMode.ExecutionAndPublication);
118 | return this.peerPickers.GetOrAdd(groupName, lazy).Value;
119 | }
120 |
121 | ///
122 | public void Dispose()
123 | {
124 | this.server.Dispose();
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/GroupCacheStubImpl/OwinPoolPicker.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCacheStub
9 | {
10 | using System;
11 | using System.Collections.Generic;
12 | using System.Linq;
13 | using GroupCache;
14 |
15 | public class OwinPoolPicker : IPeerPicker
16 | {
17 | private readonly object @lock = new object();
18 | private readonly OwinPool locaHandler;
19 | private readonly string groupName;
20 | private readonly JumpHasher consistentHasher = new JumpHasher();
21 | private List peerEndpoints;
22 | private Dictionary clients;
23 |
24 | public IEqualityComparer KeyHasher { get; set; } = EqualityComparer.Default;
25 |
26 | public OwinPoolPicker(string groupName, OwinPool locaHandler)
27 | : this(groupName, locaHandler, new List())
28 | {
29 | }
30 |
31 | public OwinPoolPicker(string groupName, OwinPool localHandler, List peerEndpoints)
32 | {
33 | this.groupName = groupName;
34 | this.locaHandler = localHandler;
35 | this.Set(peerEndpoints);
36 | }
37 |
38 | ///
39 | /// Gets endpoint metadata of the local machine.
40 | ///
41 | public PeerEndpoint Self
42 | {
43 | get
44 | {
45 | return this.locaHandler.Endpoint;
46 | }
47 | }
48 |
49 | ///
50 | public List PickPeers(string key, int numPeers)
51 | {
52 | lock (this.@lock)
53 | {
54 | numPeers = Math.Min(numPeers, this.peerEndpoints.Count);
55 | List replica = new List(numPeers);
56 | List buckets = new List(this.peerEndpoints);
57 |
58 | for (int i = 0; i < numPeers; i++)
59 | {
60 | var keyHash = this.KeyHasher.GetHashCode(key);
61 | var index = this.consistentHasher.Hash((ulong)keyHash, buckets.Count);
62 | replica.Add(this.clients[buckets[index]]);
63 |
64 | // Remove this node from buckets to guarantee its note added twice to replica list
65 | buckets.RemoveAt(index);
66 | }
67 |
68 | return replica;
69 | }
70 | }
71 |
72 | ///
73 | public void Set(List peerEndpoints)
74 | {
75 | if (peerEndpoints == null)
76 | {
77 | return;
78 | }
79 |
80 | lock (this.@lock)
81 | {
82 | this.peerEndpoints = new List(peerEndpoints);
83 | this.peerEndpoints.Sort();
84 | var peerClients = new Dictionary();
85 | foreach (var peerEndpoint in this.peerEndpoints)
86 | {
87 | if (this.Self == peerEndpoint)
88 | {
89 | peerClients.Add(peerEndpoint, this.locaHandler);
90 | }
91 | else
92 | {
93 | var client = this.locaHandler.GetClient(peerEndpoint);
94 | peerClients.Add(peerEndpoint, client);
95 | }
96 | }
97 |
98 | this.clients = peerClients;
99 | }
100 | }
101 |
102 | ///
103 | public void Add(List peerEndpoints)
104 | {
105 | lock (this.@lock)
106 | {
107 | HashSet peerSet = new HashSet(this.peerEndpoints);
108 | peerSet.UnionWith(peerEndpoints);
109 | var newList = peerSet.ToList();
110 | newList.Sort();
111 | this.peerEndpoints = newList;
112 | var peerClients = new Dictionary();
113 | foreach (var peerEndpoint in this.peerEndpoints)
114 | {
115 | bool isSelf = this.Self == peerEndpoint;
116 | if (isSelf)
117 | {
118 | peerClients.Add(peerEndpoint, this.locaHandler);
119 | }
120 | else
121 | {
122 | var client = this.locaHandler.GetClient(peerEndpoint);
123 | peerClients.Add(peerEndpoint, client);
124 | }
125 | }
126 |
127 | this.clients = peerClients;
128 | }
129 | }
130 |
131 | ///
132 | public int Count
133 | {
134 | get
135 | {
136 | lock (this.@lock)
137 | {
138 | return this.peerEndpoints.Count;
139 | }
140 | }
141 | }
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/GroupCache/Retry.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 | using System.Threading.Tasks;
12 |
13 | ///
14 | /// Template class that simplifies the execution of operations with retry semantics.
15 | /// This class is thread-safe and suitable for concurrent access when executing operations and when performing configuration changes.
16 | ///
17 | public class Retry
18 | {
19 | private readonly SimpleRetryPolicy retryPolicy;
20 |
21 | public Action OnRetry
22 | {
23 | get;
24 | set;
25 | }
26 |
27 | ///
28 | /// Initializes a new instance of the class.
29 | ///
30 | ///
31 | public Retry(SimpleRetryPolicy retryPolicy)
32 | {
33 | this.OnRetry = null;
34 | this.retryPolicy = retryPolicy;
35 | }
36 |
37 | ///
38 | /// Keep executing the callback until it eiether succeeds or the policy dictates that we stop,
39 | /// in which case the most recent exception thrown by the callback will be rethrown.
40 | ///
41 | /// lambda for an operation that can be retried.
42 | public void Execute(Action action)
43 | {
44 | this.Execute((context) =>
45 | {
46 | action(context);
47 | return true;
48 | });
49 | }
50 |
51 | ///
52 | /// Keep executing the callback until it eiether succeeds or the policy dictates that we stop,
53 | /// in which case the most recent exception thrown by the callback will be rethrown.
54 | ///
55 | /// Callback interface for an operation that can be retried.
56 | /// Return value of the callback.
57 | /// Return type.
58 | public T Execute(Func callback)
59 | {
60 | Exception lastException = null;
61 | RetryContext context = new RetryContext(this.retryPolicy.MaxAttempts, this.retryPolicy.BackOffPeriod);
62 | while (this.retryPolicy.CanRetry(context) && !context.IsExhausted)
63 | {
64 | try
65 | {
66 | lastException = null;
67 | return callback(context);
68 | }
69 | catch (Exception ex)
70 | {
71 | lastException = ex;
72 | this.retryPolicy.RegisterThrowable(context, ex);
73 |
74 | if (!this.retryPolicy.CanRetry(context))
75 | {
76 | // Rethrow last exception
77 | throw;
78 | }
79 | }
80 |
81 | var onRetryHandler = this.OnRetry;
82 | if (onRetryHandler != null)
83 | {
84 | onRetryHandler(context);
85 | }
86 |
87 | this.retryPolicy.BackOff(context);
88 | }
89 |
90 | return this.retryPolicy.HandleRetryExhausted(context);
91 | }
92 |
93 | ///
94 | /// Keep executing the callback until it eiether succeeds or the policy dictates that we stop,
95 | /// in which case the most recent exception thrown by the callback will be rethrown.
96 | ///
97 | /// lambda for an operation that can be retried.
98 | /// A representing the asynchronous operation.
99 | public Task ExecuteAsync(Func func)
100 | {
101 | return this.ExecuteAsync(async (context) =>
102 | {
103 | await func(context).ConfigureAwait(false);
104 | return true;
105 | });
106 | }
107 |
108 | ///
109 | /// Keep executing the callback until it eiether succeeds or the policy dictates that we stop,
110 | /// in which case the most recent exception thrown by the callback will be rethrown.
111 | ///
112 | /// Callback interface for an operation that can be retried.
113 | /// Return value of the callback.
114 | /// Return type.
115 | public async Task ExecuteAsync(Func> callback)
116 | {
117 | Exception lastException = null;
118 | RetryContext context = new RetryContext(this.retryPolicy.MaxAttempts, this.retryPolicy.BackOffPeriod);
119 | while (this.retryPolicy.CanRetry(context) && !context.IsExhausted)
120 | {
121 | try
122 | {
123 | lastException = null;
124 | return await callback(context).ConfigureAwait(false);
125 | }
126 | catch (Exception ex)
127 | {
128 | lastException = ex;
129 | this.retryPolicy.RegisterThrowable(context, ex);
130 |
131 | if (!this.retryPolicy.CanRetry(context))
132 | {
133 | // Rethrow last exception
134 | throw;
135 | }
136 | }
137 |
138 | var onRetryHandler = this.OnRetry;
139 | if (onRetryHandler != null)
140 | {
141 | onRetryHandler(context);
142 | }
143 |
144 | await this.retryPolicy.BackOffAsync(context).ConfigureAwait(false);
145 | }
146 |
147 | return this.retryPolicy.HandleRetryExhausted(context);
148 | }
149 | }
150 | }
--------------------------------------------------------------------------------
/src/GroupCache/IPeerPicker.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 | using System.Collections.Generic;
12 |
13 | ///
14 | /// PeerPicker is the interface that must be implemented to locate
15 | /// the peer that owns a specific key.
16 | ///
17 | public interface IPeerPicker
18 | {
19 | ///
20 | /// PickPeer returns the peer that owns the specific key.
21 | /// Property IsLocal on IGroupCacheClient will be false
22 | /// to indicate that a remote peer was nominated.
23 | ///
24 | ///
25 | List PickPeers(string key, int numPeers);
26 |
27 | ///
28 | /// Set the peer list this picker need to select from.
29 | ///
30 | /// List peer Endpoint struct.
31 | void Set(List peerEndpoints);
32 |
33 | ///
34 | /// Set the peer list to the union of the existing list and the provided list.
35 | ///
36 | /// List of peer to add if not already present.
37 | void Add(List peerEndpoints);
38 |
39 | ///
40 | /// Gets endpoint metadata of the local machine.
41 | ///
42 | PeerEndpoint Self { get; }
43 |
44 | ///
45 | /// Gets the total number of peer this PeerPicker select from.
46 | ///
47 | int Count { get; }
48 | }
49 |
50 | public struct PeerEndpoint : IEquatable, IComparable
51 | {
52 | private string hostName;
53 |
54 | public string HostName
55 | {
56 | get
57 | {
58 | return this.hostName;
59 | }
60 |
61 | set
62 | {
63 | this.hostName = value.ToLower();
64 | }
65 | }
66 |
67 | public int Port { get; set; }
68 |
69 | public override bool Equals(object obj)
70 | {
71 | return obj is PeerEndpoint && this.Equals((PeerEndpoint)obj);
72 | }
73 |
74 | public bool Equals(PeerEndpoint other)
75 | {
76 | return this.HostName.Equals(other.HostName, StringComparison.OrdinalIgnoreCase) &&
77 | this.Port == other.Port;
78 | }
79 |
80 | public override int GetHashCode()
81 | {
82 | var hashCode = 1180852050;
83 | hashCode = (hashCode * -1521134295) + base.GetHashCode();
84 | hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(this.HostName);
85 | hashCode = (hashCode * -1521134295) + this.Port.GetHashCode();
86 | return hashCode;
87 | }
88 |
89 | public static bool operator ==(PeerEndpoint endpoint1, PeerEndpoint endpoint2)
90 | {
91 | return endpoint1.Equals(endpoint2);
92 | }
93 |
94 | public static bool operator !=(PeerEndpoint endpoint1, PeerEndpoint endpoint2)
95 | {
96 | return !endpoint1.Equals(endpoint2);
97 | }
98 |
99 | public static bool operator <=(PeerEndpoint endpoint1, PeerEndpoint endpoint2)
100 | {
101 | return endpoint1.CompareTo(endpoint2) <= 0;
102 | }
103 |
104 | public static bool operator >=(PeerEndpoint endpoint1, PeerEndpoint endpoint2)
105 | {
106 | return endpoint1.CompareTo(endpoint2) >= 0;
107 | }
108 |
109 | public static bool operator <(PeerEndpoint endpoint1, PeerEndpoint endpoint2)
110 | {
111 | return endpoint1.CompareTo(endpoint2) < 0;
112 | }
113 |
114 | public static bool operator >(PeerEndpoint endpoint1, PeerEndpoint endpoint2)
115 | {
116 | return endpoint1.CompareTo(endpoint2) > 0;
117 | }
118 |
119 | public static bool TryCreate(string endPointStr, out PeerEndpoint endpoint)
120 | {
121 | bool canParse = Uri.TryCreate($"http://{endPointStr}", UriKind.Absolute, out Uri uri);
122 | if (!canParse)
123 | {
124 | endpoint = default(PeerEndpoint);
125 | return false;
126 | }
127 |
128 | endpoint = new PeerEndpoint { HostName = uri.Host, Port = uri.Port };
129 | return true;
130 | }
131 |
132 | public int CompareTo(PeerEndpoint other)
133 | {
134 | return this.HostName.CompareTo(other.HostName);
135 | }
136 |
137 | public override string ToString()
138 | {
139 | return $"{this.hostName}:{this.Port}";
140 | }
141 | }
142 |
143 | ///
144 | /// Key to uniquely identify a cache on local machine.
145 | /// this include the cache name and the endpoint metadata.
146 | ///
147 | public struct GroupKey : IEquatable
148 | {
149 | public string GroupName;
150 | public PeerEndpoint Endpoint;
151 |
152 | public override bool Equals(object obj)
153 | {
154 | return obj is GroupKey && this.Equals((GroupKey)obj);
155 | }
156 |
157 | public bool Equals(GroupKey other)
158 | {
159 | return this.GroupName == other.GroupName &&
160 | this.Endpoint.Equals(other.Endpoint);
161 | }
162 |
163 | public override int GetHashCode()
164 | {
165 | var hashCode = -1870784389;
166 | hashCode = (hashCode * -1521134295) + base.GetHashCode();
167 | hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(this.GroupName);
168 | hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(this.Endpoint);
169 | return hashCode;
170 | }
171 |
172 | public static bool operator ==(GroupKey key1, GroupKey key2)
173 | {
174 | return key1.Equals(key2);
175 | }
176 |
177 | public static bool operator !=(GroupKey key1, GroupKey key2)
178 | {
179 | return !(key1 == key2);
180 | }
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Top-most EditorConfig file
2 | root = true
3 |
4 | # Global settings (applies to all files)
5 | [*]
6 | charset = utf-8
7 | end_of_line = crlf
8 | insert_final_newline = true
9 | indent_style = space
10 | indent_size = 4
11 | trim_trailing_whitespace = true
12 |
13 | # C# files
14 | [*.cs]
15 | dotnet_sort_system_directives_first = true
16 | dotnet_style_qualification_for_field = false:suggestion
17 | dotnet_style_qualification_for_property = false:suggestion
18 | dotnet_style_qualification_for_method = false:suggestion
19 | dotnet_style_qualification_for_event = false:suggestion
20 | dotnet_style_prefer_inferred_tuple_names = true:suggestion
21 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
22 | dotnet_style_prefer_auto_properties = true:suggestion
23 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
24 | csharp_new_line_before_open_brace = all
25 | csharp_indent_case_contents = true
26 | csharp_indent_switch_labels = true
27 | csharp_prefer_braces = true:suggestion
28 | csharp_style_var_for_built_in_types = true:suggestion
29 | csharp_style_var_when_type_is_apparent = true:suggestion
30 | csharp_style_var_elsewhere = false:suggestion
31 | csharp_style_prefer_switch_expression = true:suggestion
32 | csharp_style_prefer_pattern_matching = true:suggestion
33 | csharp_style_prefer_throw_expression = true:suggestion
34 | csharp_style_prefer_conditional_delegate_call = true:suggestion
35 | csharp_using_directive_placement = inside_namespace:suggestion
36 |
37 | # Header template for team
38 | [*.cs]
39 | csharp_prefer_simple_using_statement = true:suggestion
40 | csharp_style_namespace_declarations = block_scoped:silent
41 | csharp_style_prefer_method_group_conversion = true:silent
42 | csharp_style_prefer_top_level_statements = true:silent
43 | csharp_style_prefer_primary_constructors = true:suggestion
44 | csharp_prefer_system_threading_lock = true:suggestion
45 | csharp_style_expression_bodied_methods = false:silent
46 | csharp_style_expression_bodied_constructors = false:silent
47 | csharp_style_expression_bodied_operators = false:silent
48 | csharp_style_expression_bodied_properties = true:silent
49 | csharp_style_expression_bodied_indexers = true:silent
50 | csharp_style_expression_bodied_accessors = true:silent
51 | csharp_style_expression_bodied_lambdas = true:silent
52 | csharp_style_expression_bodied_local_functions = false:silent
53 | csharp_indent_labels = one_less_than_current
54 | csharp_style_throw_expression = true:suggestion
55 | csharp_style_prefer_null_check_over_type_check = true:suggestion
56 | csharp_prefer_simple_default_expression = true:suggestion
57 | csharp_style_prefer_local_over_anonymous_function = true:suggestion
58 | csharp_style_prefer_index_operator = true:suggestion
59 | csharp_style_prefer_range_operator = true:suggestion
60 | csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
61 | csharp_style_prefer_tuple_swap = true:suggestion
62 | csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion
63 | csharp_style_prefer_utf8_string_literals = true:suggestion
64 | csharp_style_inlined_variable_declaration = true:suggestion
65 | csharp_style_deconstructed_variable_declaration = true:suggestion
66 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion
67 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent
68 |
69 | [*.{cs,vb}]
70 | #### Naming styles ####
71 |
72 | # Naming rules
73 |
74 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
75 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
76 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
77 |
78 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
79 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types
80 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
81 |
82 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
83 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
84 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
85 |
86 | # Symbol specifications
87 |
88 | dotnet_naming_symbols.interface.applicable_kinds = interface
89 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
90 | dotnet_naming_symbols.interface.required_modifiers =
91 |
92 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
93 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
94 | dotnet_naming_symbols.types.required_modifiers =
95 |
96 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
97 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
98 | dotnet_naming_symbols.non_field_members.required_modifiers =
99 |
100 | # Naming styles
101 |
102 | dotnet_naming_style.begins_with_i.required_prefix = I
103 | dotnet_naming_style.begins_with_i.required_suffix =
104 | dotnet_naming_style.begins_with_i.word_separator =
105 | dotnet_naming_style.begins_with_i.capitalization = pascal_case
106 |
107 | dotnet_naming_style.pascal_case.required_prefix =
108 | dotnet_naming_style.pascal_case.required_suffix =
109 | dotnet_naming_style.pascal_case.word_separator =
110 | dotnet_naming_style.pascal_case.capitalization = pascal_case
111 |
112 | dotnet_naming_style.pascal_case.required_prefix =
113 | dotnet_naming_style.pascal_case.required_suffix =
114 | dotnet_naming_style.pascal_case.word_separator =
115 | dotnet_naming_style.pascal_case.capitalization = pascal_case
116 | dotnet_style_coalesce_expression = true:suggestion
117 | dotnet_style_null_propagation = true:suggestion
118 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
119 | dotnet_style_prefer_auto_properties = true:suggestion
120 | dotnet_style_object_initializer = true:suggestion
121 | dotnet_style_collection_initializer = true:suggestion
122 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
123 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent
124 | dotnet_style_operator_placement_when_wrapping = beginning_of_line
125 | tab_width = 4
126 | dotnet_style_prefer_conditional_expression_over_return = true:silent
127 | dotnet_style_explicit_tuple_names = true:suggestion
128 | dotnet_style_prefer_inferred_tuple_names = true:suggestion
129 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
130 | dotnet_style_prefer_simplified_interpolation = true:suggestion
131 | dotnet_style_prefer_compound_assignment = true:suggestion
132 | dotnet_style_namespace_match_folder = true:suggestion
133 | dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
134 | dotnet_style_readonly_field = true:suggestion
135 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent
136 | dotnet_style_predefined_type_for_member_access = true:silent
137 |
--------------------------------------------------------------------------------
/src/GroupCache/CircuitBreakerClient.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 | using System.IO;
12 | using System.Runtime.Serialization;
13 | using System.Threading;
14 | using System.Threading.Tasks;
15 |
16 | ///
17 | /// Usually when some server is slow/down/overloaded, we want to simply send request to a failover destination.
18 | /// Once server is marked bad, failover becomes instant, we don't need to do 2 round trips
19 | /// (one to known slow/broken box, one to failover destination) for a request.
20 | ///
21 | /// The CircuitBreaker will open after maxRetry request fail in a row.
22 | /// What happens after a client CircuitBreaker is open:
23 | /// 1- Every request sent to the Circuit breaker is immediately discarded with CircuitBreakerOpenException.
24 | /// 2- After backOff time have elapsed since last failed request circuit breaker will try sending one request.
25 | /// a- if that request succeed the Circuit Breaker is closed and let all request go through.
26 | ///
27 | public sealed class CircuitBreakerClient : IGroupCacheClient
28 | {
29 | private readonly object @lock = new object();
30 | private readonly IGroupCacheClient client;
31 | private readonly TimeSpan backOff;
32 | private int sequentialFailure;
33 | private DateTimeOffset lastTry = DateTimeOffset.UtcNow;
34 | private readonly int maxRetry;
35 |
36 | ///
37 | /// Initializes a new instance of the class.
38 | ///
39 | ///
40 | public CircuitBreakerClient(IGroupCacheClient client)
41 | : this(client, TimeSpan.FromMinutes(10), 3)
42 | {
43 | }
44 |
45 | ///
46 | /// Initializes a new instance of the class.
47 | ///
48 | ///
49 | ///
50 | ///
51 | public CircuitBreakerClient(IGroupCacheClient client, TimeSpan backOff, int maxRetry)
52 | {
53 | this.client = client;
54 | this.backOff = backOff;
55 | this.maxRetry = maxRetry;
56 | }
57 |
58 | ///
59 | public PeerEndpoint Endpoint
60 | {
61 | get
62 | {
63 | return this.client.Endpoint;
64 | }
65 | }
66 |
67 | ///
68 | public bool IsLocal
69 | {
70 | get
71 | {
72 | return this.client.IsLocal;
73 | }
74 | }
75 |
76 | private bool IsOpen
77 | {
78 | get
79 | {
80 | return this.sequentialFailure >= this.maxRetry;
81 | }
82 | }
83 |
84 | private TimeSpan TimeSinceLastTry
85 | {
86 | get
87 | {
88 | return DateTimeOffset.Now - this.lastTry;
89 | }
90 | }
91 |
92 | ///
93 | public void Dispose()
94 | {
95 | this.client.Dispose();
96 | }
97 |
98 | ///
99 | public async Task GetAsync(string group, string key, Stream sink, ICacheControl cacheControl, CancellationToken ct)
100 | {
101 | try
102 | {
103 | this.TripIfNeeded();
104 | await this.client.GetAsync(group, key, sink, cacheControl, ct).ConfigureAwait(false);
105 | }
106 | catch (ServerBusyException)
107 | {
108 | // Dont count busy server as bad
109 | throw;
110 | }
111 | catch
112 | {
113 | this.CountFailure();
114 | throw;
115 | }
116 |
117 | this.ResetCount();
118 | }
119 |
120 | private void CountFailure()
121 | {
122 | lock (this.@lock)
123 | {
124 | if (!this.IsOpen)
125 | {
126 | this.sequentialFailure++;
127 | }
128 | }
129 | }
130 |
131 | private void ResetCount()
132 | {
133 | lock (this.@lock)
134 | {
135 | this.sequentialFailure = 0;
136 | }
137 | }
138 |
139 | private void TripIfNeeded()
140 | {
141 | lock (this.@lock)
142 | {
143 | if (this.IsOpen)
144 | {
145 | if (this.TimeSinceLastTry < this.backOff)
146 | {
147 | throw new CircuitBreakerOpenException();
148 | }
149 | }
150 |
151 | this.lastTry = DateTimeOffset.UtcNow;
152 | }
153 | }
154 | }
155 |
156 | [Serializable]
157 | public class CircuitBreakerOpenException : Exception
158 | {
159 | ///
160 | /// Initializes a new instance of the class.
161 | ///
162 | public CircuitBreakerOpenException()
163 | {
164 | }
165 |
166 | ///
167 | /// Initializes a new instance of the class.
168 | ///
169 | ///
170 | public CircuitBreakerOpenException(string message)
171 | : base(message)
172 | {
173 | }
174 |
175 | ///
176 | /// Initializes a new instance of the class.
177 | ///
178 | ///
179 | ///
180 | public CircuitBreakerOpenException(string message, Exception innerException)
181 | : base(message, innerException)
182 | {
183 | }
184 |
185 | ///
186 | /// Initializes a new instance of the class.
187 | ///
188 | ///
189 | ///
190 | protected CircuitBreakerOpenException(SerializationInfo info, StreamingContext context)
191 | : base(info, context)
192 | {
193 | }
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Aa][Rr][Mm]/
27 | [Aa][Rr][Mm]64/
28 | bld/
29 | [Bb]in/
30 | [Oo]bj/
31 | [Ll]og/
32 | [Ll]ogs/
33 |
34 | # Visual Studio 2015/2017 cache/options directory
35 | .vs/
36 | # Uncomment if you have tasks that create the project's static files in wwwroot
37 | #wwwroot/
38 |
39 | # Visual Studio 2017 auto generated files
40 | Generated\ Files/
41 |
42 | # MSTest test Results
43 | [Tt]est[Rr]esult*/
44 | [Bb]uild[Ll]og.*
45 |
46 | # NUnit
47 | *.VisualState.xml
48 | TestResult.xml
49 | nunit-*.xml
50 |
51 | # Build Results of an ATL Project
52 | [Dd]ebugPS/
53 | [Rr]eleasePS/
54 | dlldata.c
55 |
56 | # Benchmark Results
57 | BenchmarkDotNet.Artifacts/
58 |
59 | # .NET Core
60 | project.lock.json
61 | project.fragment.lock.json
62 | artifacts/
63 |
64 | # StyleCop
65 | StyleCopReport.xml
66 |
67 | # Files built by Visual Studio
68 | *_i.c
69 | *_p.c
70 | *_h.h
71 | *.ilk
72 | *.meta
73 | *.obj
74 | *.iobj
75 | *.pch
76 | *.pdb
77 | *.ipdb
78 | *.pgc
79 | *.pgd
80 | *.rsp
81 | *.sbr
82 | *.tlb
83 | *.tli
84 | *.tlh
85 | *.tmp
86 | *.tmp_proj
87 | *_wpftmp.csproj
88 | *.log
89 | *.vspscc
90 | *.vssscc
91 | .builds
92 | *.pidb
93 | *.svclog
94 | *.scc
95 |
96 | # Chutzpah Test files
97 | _Chutzpah*
98 |
99 | # Visual C++ cache files
100 | ipch/
101 | *.aps
102 | *.ncb
103 | *.opendb
104 | *.opensdf
105 | *.sdf
106 | *.cachefile
107 | *.VC.db
108 | *.VC.VC.opendb
109 |
110 | # Visual Studio profiler
111 | *.psess
112 | *.vsp
113 | *.vspx
114 | *.sap
115 |
116 | # Visual Studio Trace Files
117 | *.e2e
118 |
119 | # TFS 2012 Local Workspace
120 | $tf/
121 |
122 | # Guidance Automation Toolkit
123 | *.gpState
124 |
125 | # ReSharper is a .NET coding add-in
126 | _ReSharper*/
127 | *.[Rr]e[Ss]harper
128 | *.DotSettings.user
129 |
130 | # TeamCity is a build add-in
131 | _TeamCity*
132 |
133 | # DotCover is a Code Coverage Tool
134 | *.dotCover
135 |
136 | # AxoCover is a Code Coverage Tool
137 | .axoCover/*
138 | !.axoCover/settings.json
139 |
140 | # Visual Studio code coverage results
141 | *.coverage
142 | *.coveragexml
143 |
144 | # NCrunch
145 | _NCrunch_*
146 | .*crunch*.local.xml
147 | nCrunchTemp_*
148 |
149 | # MightyMoose
150 | *.mm.*
151 | AutoTest.Net/
152 |
153 | # Web workbench (sass)
154 | .sass-cache/
155 |
156 | # Installshield output folder
157 | [Ee]xpress/
158 |
159 | # DocProject is a documentation generator add-in
160 | DocProject/buildhelp/
161 | DocProject/Help/*.HxT
162 | DocProject/Help/*.HxC
163 | DocProject/Help/*.hhc
164 | DocProject/Help/*.hhk
165 | DocProject/Help/*.hhp
166 | DocProject/Help/Html2
167 | DocProject/Help/html
168 |
169 | # Click-Once directory
170 | publish/
171 |
172 | # Publish Web Output
173 | *.[Pp]ublish.xml
174 | *.azurePubxml
175 | # Note: Comment the next line if you want to checkin your web deploy settings,
176 | # but database connection strings (with potential passwords) will be unencrypted
177 | *.pubxml
178 | *.publishproj
179 |
180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
181 | # checkin your Azure Web App publish settings, but sensitive information contained
182 | # in these scripts will be unencrypted
183 | PublishScripts/
184 |
185 | # NuGet Packages
186 | *.nupkg
187 | # NuGet Symbol Packages
188 | *.snupkg
189 | # The packages folder can be ignored because of Package Restore
190 | **/[Pp]ackages/*
191 | # except build/, which is used as an MSBuild target.
192 | !**/[Pp]ackages/build/
193 | # Uncomment if necessary however generally it will be regenerated when needed
194 | #!**/[Pp]ackages/repositories.config
195 | # NuGet v3's project.json files produces more ignorable files
196 | *.nuget.props
197 | *.nuget.targets
198 |
199 | # Microsoft Azure Build Output
200 | csx/
201 | *.build.csdef
202 |
203 | # Microsoft Azure Emulator
204 | ecf/
205 | rcf/
206 |
207 | # Windows Store app package directories and files
208 | AppPackages/
209 | BundleArtifacts/
210 | Package.StoreAssociation.xml
211 | _pkginfo.txt
212 | *.appx
213 | *.appxbundle
214 | *.appxupload
215 |
216 | # Visual Studio cache files
217 | # files ending in .cache can be ignored
218 | *.[Cc]ache
219 | # but keep track of directories ending in .cache
220 | !?*.[Cc]ache/
221 |
222 | # Others
223 | ClientBin/
224 | ~$*
225 | *~
226 | *.dbmdl
227 | *.dbproj.schemaview
228 | *.jfm
229 | *.pfx
230 | *.publishsettings
231 | orleans.codegen.cs
232 |
233 | # Including strong name files can present a security risk
234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
235 | #*.snk
236 |
237 | # Since there are multiple workflows, uncomment next line to ignore bower_components
238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
239 | #bower_components/
240 |
241 | # RIA/Silverlight projects
242 | Generated_Code/
243 |
244 | # Backup & report files from converting an old project file
245 | # to a newer Visual Studio version. Backup files are not needed,
246 | # because we have git ;-)
247 | _UpgradeReport_Files/
248 | Backup*/
249 | UpgradeLog*.XML
250 | UpgradeLog*.htm
251 | ServiceFabricBackup/
252 | *.rptproj.bak
253 |
254 | # SQL Server files
255 | *.mdf
256 | *.ldf
257 | *.ndf
258 |
259 | # Business Intelligence projects
260 | *.rdl.data
261 | *.bim.layout
262 | *.bim_*.settings
263 | *.rptproj.rsuser
264 | *- [Bb]ackup.rdl
265 | *- [Bb]ackup ([0-9]).rdl
266 | *- [Bb]ackup ([0-9][0-9]).rdl
267 |
268 | # Microsoft Fakes
269 | FakesAssemblies/
270 |
271 | # GhostDoc plugin setting file
272 | *.GhostDoc.xml
273 |
274 | # Node.js Tools for Visual Studio
275 | .ntvs_analysis.dat
276 | node_modules/
277 |
278 | # Visual Studio 6 build log
279 | *.plg
280 |
281 | # Visual Studio 6 workspace options file
282 | *.opt
283 |
284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
285 | *.vbw
286 |
287 | # Visual Studio LightSwitch build output
288 | **/*.HTMLClient/GeneratedArtifacts
289 | **/*.DesktopClient/GeneratedArtifacts
290 | **/*.DesktopClient/ModelManifest.xml
291 | **/*.Server/GeneratedArtifacts
292 | **/*.Server/ModelManifest.xml
293 | _Pvt_Extensions
294 |
295 | # Paket dependency manager
296 | .paket/paket.exe
297 | paket-files/
298 |
299 | # FAKE - F# Make
300 | .fake/
301 |
302 | # CodeRush personal settings
303 | .cr/personal
304 |
305 | # Python Tools for Visual Studio (PTVS)
306 | __pycache__/
307 | *.pyc
308 |
309 | # Cake - Uncomment if you are using it
310 | # tools/**
311 | # !tools/packages.config
312 |
313 | # Tabs Studio
314 | *.tss
315 |
316 | # Telerik's JustMock configuration file
317 | *.jmconfig
318 |
319 | # BizTalk build output
320 | *.btp.cs
321 | *.btm.cs
322 | *.odx.cs
323 | *.xsd.cs
324 |
325 | # OpenCover UI analysis results
326 | OpenCover/
327 |
328 | # Azure Stream Analytics local run output
329 | ASALocalRun/
330 |
331 | # MSBuild Binary and Structured Log
332 | *.binlog
333 |
334 | # NVidia Nsight GPU debugger configuration file
335 | *.nvuser
336 |
337 | # MFractors (Xamarin productivity tool) working folder
338 | .mfractor/
339 |
340 | # Local History for Visual Studio
341 | .localhistory/
342 |
343 | # BeatPulse healthcheck temp database
344 | healthchecksdb
345 |
346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
347 | MigrationBackup/
348 |
349 | # Ionide (cross platform F# VS Code tools) working folder
350 | .ionide/
351 |
--------------------------------------------------------------------------------
/src/GroupCache/Argument.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 | using System.Diagnostics;
12 |
13 | public static class Argument
14 | {
15 | ///
16 | /// Helper function to allow simple if check ArgumentNull exception.
17 | ///
18 | /// parameter value to check for null.
19 | /// name of the parameter.
20 | ///
21 | /// DoSomething.(this IEnumerable list, Action action)
22 | /// {
23 | /// ArgumetValidator.NotNull(list,"list");
24 | /// ArgumetValidator.NotNull(action,"action");
25 | ///
26 | ///
27 | public static void NotNull(object param, string parameterName)
28 | {
29 | if (param == null)
30 | {
31 | throw new ArgumentNullException(parameterName);
32 | }
33 | }
34 |
35 | ///
36 | /// Helper function to allow simple if check validations and exception throwing.
37 | ///
38 | /// Condition. if false, Throws ArgumentException.
39 | /// Message for the ArgumentException.
40 | /// optional parameter name for the ArgumentException.
41 | ///
42 | /// DoSomething.(this IEnumerable list, Action action)
43 | /// {
44 | /// ArgumetValidator.IsTrue(list.Contains(s=>s==1), "Enumerable does not contain 1","list.");
45 | /// ArgumetValidator.IsTrue(list.Contains(s=>s==2), "Enumerable does not contain 1");
46 | ///
47 | public static void IsTrue(bool condition, string message, string parameterName = null)
48 | {
49 | if (!condition)
50 | {
51 | if (!string.IsNullOrEmpty(parameterName))
52 | {
53 | throw new ArgumentException(message, parameterName);
54 | }
55 | else
56 | {
57 | throw new ArgumentException(message);
58 | }
59 | }
60 | }
61 |
62 | ///
63 | /// Helper function to allow simple if check validations and exception throwing.
64 | ///
65 | /// Condition. if true, Throws ArgumentException.
66 | /// Message for the ArgumentException.
67 | /// optional parameter name for the ArgumentException.
68 | ///
69 | /// DoSomething.(this IEnumerable list, Action action)
70 | /// {
71 | /// ArgumetValidator.IsFalse(list.Contains(s=>s==1), "Enumerable contains 1","list.");
72 | ///
73 | public static void IsFalse(bool condition, string message, string parameterName = null)
74 | {
75 | if (condition)
76 | {
77 | if (!string.IsNullOrEmpty(parameterName))
78 | {
79 | throw new ArgumentException(message, parameterName);
80 | }
81 | else
82 | {
83 | throw new ArgumentException(message);
84 | }
85 | }
86 | }
87 |
88 | ///
89 | /// Compares two types and returns if value is out of range.
90 | ///
91 | /// numeric type (works for some other types but definitely works for numeric types.
92 | /// Name of parameter.
93 | /// Value of parameter.
94 | /// Minimum value, inclusive.
95 | /// Maximum value, exclusive.
96 | ///
97 | /// someVal.InRange(minValue, maxValue, "SomeParam");.
98 | ///
99 | public static void InRange(IComparable value, T maxExclusive, string paramName)
100 | {
101 | InRange(value, minInclusive: default(T), maxExclusive: maxExclusive, paramName: paramName);
102 | }
103 |
104 | ///
105 | /// Compares two types and returns if value is out of range.
106 | ///
107 | /// numeric type (works for some other types but definitely works for numeric types.
108 | /// Name of parameter.
109 | /// Value of parameter.
110 | /// Minimum value, inclusive.
111 | /// Maximum value, exclusive.
112 | ///
113 | /// someVal.InRange(minValue, maxValue, "SomeParam");.
114 | ///
115 | public static void InRange(IComparable value, T minInclusive, T maxExclusive, string paramName)
116 | {
117 | NotNull(value, paramName);
118 |
119 | if (value.CompareTo(minInclusive) < 0 || value.CompareTo(maxExclusive) >= 0)
120 | {
121 | throw new ArgumentOutOfRangeException(
122 | paramName,
123 | FormatRangeErrorMsg(value, minInclusive, maxExclusive));
124 | }
125 | }
126 |
127 | public static string FormatRangeErrorMsg(IComparable value, T minInclusive, T maxExclusive)
128 | {
129 | return string.Format("Value {2} should be in range [{0}..{1})", minInclusive, maxExclusive, value);
130 | }
131 |
132 | public static void NotNullOrEmpty(string name, string argName)
133 | {
134 | if (string.IsNullOrEmpty(name))
135 | {
136 | throw new ArgumentNullException(argName + " cannot be null or empty");
137 | }
138 | }
139 |
140 | public static void NotNullOrWhiteSpace(string name, string argName)
141 | {
142 | if (string.IsNullOrWhiteSpace(name))
143 | {
144 | throw new ArgumentNullException(argName, argName + " cannot be null or whitespace");
145 | }
146 | }
147 |
148 | public static void Requires(bool condition)
149 | where TException : Exception, new()
150 | {
151 | if (!condition)
152 | {
153 | throw new TException();
154 | }
155 | }
156 |
157 | public static void Requires(bool condition, string what)
158 | where TException : Exception, new()
159 | {
160 | if (!condition)
161 | {
162 | var type = typeof(TException);
163 | var ctor = type.GetConstructor(new[] { typeof(string) });
164 | if (ctor != null)
165 | {
166 | var ex = ctor.Invoke(new object[] { what });
167 | throw ex as TException;
168 | }
169 | else
170 | {
171 | throw new TException();
172 | }
173 | }
174 | }
175 |
176 | [Conditional("DEBUG")]
177 | public static void RequiresDebugOnly(bool condition)
178 | where TException : Exception, new()
179 | {
180 | Requires(condition);
181 | }
182 |
183 | [Conditional("DEBUG")]
184 | public static void RequiresDebugOnly(bool condition, string what)
185 | where TException : Exception, new()
186 | {
187 | Requires(condition, what);
188 | }
189 | }
190 | }
--------------------------------------------------------------------------------
/test/GroupCacheStubTest/TestStub.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCacheStubTest
9 | {
10 | using System;
11 | using System.IO;
12 | using System.Threading;
13 | using System.Threading.Tasks;
14 | using GroupCache;
15 | using GroupCacheStub;
16 | using Xunit;
17 |
18 | public sealed class GroupCacheHandler : IGroupCacheClient
19 | {
20 | ///
21 | public void Dispose()
22 | {
23 | }
24 |
25 | ///
26 | public bool IsLocal
27 | {
28 | get { return true; }
29 | }
30 |
31 | ///
32 | public PeerEndpoint Endpoint => throw new NotImplementedException();
33 |
34 | ///
35 | public async Task GetAsync(string group, string key, Stream sink, ICacheControl cacheControl, CancellationToken ct)
36 | {
37 | using (var resultStream = "HelloWorld".StringToStream())
38 | {
39 | await resultStream.CopyToAsync(sink);
40 | }
41 |
42 | return;
43 | }
44 | }
45 |
46 | public sealed class GroupCacheHandlerNoStore : IGroupCacheClient
47 | {
48 | ///
49 | public void Dispose()
50 | {
51 | }
52 |
53 | ///
54 | public bool IsLocal
55 | {
56 | get { return true; }
57 | }
58 |
59 | ///
60 | public PeerEndpoint Endpoint => throw new NotImplementedException();
61 |
62 | ///
63 | public async Task GetAsync(string group, string key, Stream sink, ICacheControl cacheControl, CancellationToken ct)
64 | {
65 | cacheControl.NoStore = true;
66 | using (var resultStream = "HelloWorld".StringToStream())
67 | {
68 | await resultStream.CopyToAsync(sink);
69 | }
70 | }
71 | }
72 |
73 | public sealed class BadGroupCacheHandler : IGroupCacheClient
74 | {
75 | ///
76 | public void Dispose()
77 | {
78 | }
79 |
80 | ///
81 | public bool IsLocal
82 | {
83 | get { return true; }
84 | }
85 |
86 | ///
87 | public PeerEndpoint Endpoint => throw new NotImplementedException();
88 |
89 | ///
90 | public Task GetAsync(string group, string key, Stream sink, ICacheControl cacheControl, CancellationToken ct)
91 | {
92 | return Task.FromException(new InvalidOperationException("test"));
93 | }
94 | }
95 |
96 | public class TestStub
97 | {
98 | [Fact]
99 | public async Task TestHelloWorldAsync()
100 | {
101 | var host = "localhost";
102 | var port = 60051;
103 | var handler = new GroupCacheHandler();
104 | using (var server = new OwinServer(handler, port, host))
105 | {
106 | server.Start();
107 | var endpoint = new PeerEndpoint() { HostName = host, Port = port };
108 | using (var client = new OwinClient(endpoint))
109 | {
110 | using (var stream = new MemoryStream())
111 | {
112 | var cacheControl = new CacheControl();
113 | await client.GetAsync("groupA", "key1", stream, cacheControl, CancellationToken.None);
114 | var str = stream.StreamToString();
115 | Xunit.Assert.Equal("HelloWorld", str);
116 | Xunit.Assert.False(cacheControl.NoStore);
117 | }
118 | }
119 | }
120 | }
121 |
122 | [Fact]
123 | public async Task TestHelloWorldNoStoreAsync()
124 | {
125 | var host = "localhost";
126 | var port = 60051;
127 | var handler = new GroupCacheHandlerNoStore();
128 | using (var server = new OwinServer(handler, port, host))
129 | {
130 | server.Start();
131 | var endpoint = new PeerEndpoint() { HostName = host, Port = port };
132 | using (var client = new OwinClient(endpoint))
133 | {
134 | using (var stream = new MemoryStream())
135 | {
136 | var cacheControl = new CacheControl();
137 | await client.GetAsync("groupA", "key1", stream, cacheControl, CancellationToken.None);
138 | var str = stream.StreamToString();
139 | Xunit.Assert.Equal("HelloWorld", str);
140 | Xunit.Assert.True(cacheControl.NoStore);
141 | }
142 | }
143 | }
144 | }
145 |
146 | [Fact]
147 | public async Task TestCancellationAsync()
148 | {
149 | var host = "localhost";
150 | var port = 60051;
151 | var handler = new GroupCacheHandler();
152 |
153 | using (var tokenSource = new CancellationTokenSource())
154 | {
155 | tokenSource.Cancel();
156 | var ct = tokenSource.Token;
157 | using (var server = new OwinServer(handler, port, host))
158 | {
159 | server.Start();
160 | var endpoint = new PeerEndpoint() { HostName = host, Port = port };
161 | using (var client = new OwinClient(endpoint))
162 | {
163 | using (var stream = new MemoryStream())
164 | {
165 | await Xunit.Assert.ThrowsAsync(async () =>
166 | {
167 | await client.GetAsync("groupA", "key1", stream, new CacheControl(), ct);
168 | var str = stream.StreamToString();
169 | Xunit.Assert.Fail("Should have thrown");
170 | });
171 | }
172 | }
173 | }
174 | }
175 | }
176 |
177 | [Fact]
178 | public async Task TestServerSideExceptionAsync()
179 | {
180 | var host = "localhost";
181 | var port = 60051;
182 | var handler = new BadGroupCacheHandler();
183 |
184 | using (var server = new OwinServer(handler, port, host))
185 | {
186 | server.Start();
187 | var endpoint = new PeerEndpoint() { HostName = host, Port = port };
188 | using (var client = new OwinClient(endpoint))
189 | {
190 | using (var stream = new MemoryStream())
191 | {
192 | await Xunit.Assert.ThrowsAsync(async () =>
193 | {
194 | await client.GetAsync("groupA", "key1", stream, new CacheControl(), CancellationToken.None);
195 | var str = stream.StreamToString();
196 | Xunit.Assert.Fail("Should have thrown");
197 | });
198 | }
199 | }
200 | }
201 | }
202 |
203 | [Fact]
204 | public async Task TestUnavailableAsync()
205 | {
206 | var host = "localhost";
207 | var badPort = 666;
208 | var handler = new GroupCacheHandler();
209 | var endpoint = new PeerEndpoint() { HostName = host, Port = badPort };
210 | using (var client = new OwinClient(endpoint))
211 | {
212 | using (var stream = new MemoryStream())
213 | {
214 | await Xunit.Assert.ThrowsAsync(async () =>
215 | {
216 | await client.GetAsync("groupA", "key1", stream, new CacheControl(), CancellationToken.None);
217 | var str = stream.StreamToString();
218 | Xunit.Assert.Fail("Should have thrown");
219 | });
220 | }
221 | }
222 | }
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/test/GroupCacheStubTest/TestGroupCache.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCacheStubTest
9 | {
10 | using System;
11 | using System.Collections.Generic;
12 | using System.IO;
13 | using System.Text;
14 | using System.Threading;
15 | using System.Threading.Tasks;
16 | using GroupCache;
17 | using Xunit;
18 |
19 | public static class StreamHelper
20 | {
21 | public static string StreamToString(this Stream stream)
22 | {
23 | stream.Position = 0;
24 | using (StreamReader reader = new StreamReader(stream, Encoding.UTF8))
25 | {
26 | return reader.ReadToEnd();
27 | }
28 | }
29 |
30 | public static Stream StringToStream(this string src)
31 | {
32 | byte[] byteArray = Encoding.UTF8.GetBytes(src);
33 | return new MemoryStream(byteArray);
34 | }
35 | }
36 |
37 | public class HelloGetter : IGetter
38 | {
39 | public string PeerName;
40 | public int CallCount = 0;
41 |
42 | public async Task GetAsync(string key, Stream sink, ICacheControl cacheControl, CancellationToken ct)
43 | {
44 | Interlocked.Increment(ref this.CallCount);
45 | using (var stream = key.StringToStream())
46 | {
47 | await stream.CopyToAsync(sink);
48 | }
49 | }
50 | }
51 |
52 | public class FibGetter : IGetter
53 | {
54 | public IGetter Getter;
55 | public int CallCount = 0;
56 |
57 | public async Task GetAsync(string key, Stream sink, ICacheControl cacheControl, CancellationToken ct)
58 | {
59 | Interlocked.Increment(ref this.CallCount);
60 | try
61 | {
62 | long number = long.Parse(key);
63 | long result;
64 | if (number == 0)
65 | {
66 | result = 0;
67 | }
68 | else if (number == 1)
69 | {
70 | result = 1;
71 | }
72 | else
73 | {
74 | // Make to call to n-1 and n-2 in parallel
75 | using (var firstStream = new MemoryStream())
76 | using (var secondStream = new MemoryStream())
77 | {
78 | await this.Getter.GetAsync((number - 1).ToString(), firstStream, new CacheControl(), CancellationToken.None).ConfigureAwait(false);
79 | await this.Getter.GetAsync((number - 2).ToString(), secondStream, new CacheControl(), CancellationToken.None).ConfigureAwait(false);
80 | var firstStr = firstStream.StreamToString();
81 | var secondStr = secondStream.StreamToString();
82 |
83 | // return the sum
84 | var firstLong = long.Parse(firstStr);
85 | var secondLong = long.Parse(secondStr);
86 | result = firstLong + secondLong;
87 | }
88 | }
89 |
90 | var resultStream = result.ToString().StringToStream();
91 | resultStream.Position = 0;
92 | await resultStream.CopyToAsync(sink);
93 | }
94 | catch (Exception ex)
95 | {
96 | Console.WriteLine(ex.ToString());
97 | throw;
98 | }
99 | }
100 | }
101 |
102 | public class TestGroupCache : IDisposable
103 | {
104 | private readonly GroupCacheStub.OwinPool peer1Pool;
105 | private readonly GroupCacheStub.OwinPool peer2Pool;
106 |
107 | public TestGroupCache()
108 | {
109 | var peer1 = new PeerEndpoint { HostName = "localhost", Port = 60053 };
110 | var peer2 = new PeerEndpoint { HostName = "localhost", Port = 60054 };
111 | var peerList = new List { peer1, peer2 };
112 |
113 | // server listening on port 50053
114 | this.peer1Pool = new GroupCacheStub.OwinPool(peer1);
115 | this.peer1Pool.GetPicker("TestGroupForwarding").Add(peerList);
116 | this.peer1Pool.GetPicker("Fibonacci").Add(peerList);
117 |
118 | // server listening on port 50054
119 | this.peer2Pool = new GroupCacheStub.OwinPool(peer2);
120 | this.peer2Pool.GetPicker("TestGroupForwarding").Add(peerList);
121 | this.peer2Pool.GetPicker("Fibonacci").Add(peerList);
122 | }
123 |
124 | public void Dispose()
125 | {
126 | this.peer1Pool.Dispose();
127 | this.peer2Pool.Dispose();
128 | }
129 |
130 | [Fact]
131 | public void TestGroupForwarding()
132 | {
133 | var getter1 = new HelloGetter { PeerName = "peer1" };
134 | var getter2 = new HelloGetter { PeerName = "peer2" };
135 | var fooResponse = "foo";
136 | var barResponse = "bar";
137 | var peer1Group = GroupCache.NewGroup("TestGroupForwarding", getter1, this.peer1Pool.GetPicker("TestGroupForwarding"));
138 | var peer2Group = GroupCache.NewGroup("TestGroupForwarding", getter2, this.peer2Pool.GetPicker("TestGroupForwarding"));
139 | var group1Stat = new UnitTestGroupStat();
140 | var group2Stat = new UnitTestGroupStat();
141 | peer1Group.Stats = group1Stat;
142 | peer2Group.Stats = group2Stat;
143 | using (var result1a = new MemoryStream())
144 | using (var result1b = new MemoryStream())
145 | using (var result2a = new MemoryStream())
146 | using (var result2b = new MemoryStream())
147 | {
148 | peer1Group.GetAsync("foo", result1a, new CacheControl(), CancellationToken.None).Wait();
149 | peer1Group.GetAsync("bar", result1b, new CacheControl(), CancellationToken.None).Wait();
150 | peer2Group.GetAsync("foo", result2a, new CacheControl(), CancellationToken.None).Wait();
151 | peer2Group.GetAsync("bar", result2b, new CacheControl(), CancellationToken.None).Wait();
152 | var result1aStr = result1a.StreamToString();
153 | Xunit.Assert.Equal(fooResponse, result1aStr);
154 | var result1bStr = result1b.StreamToString();
155 | Xunit.Assert.Equal(barResponse, result1bStr);
156 | var result2aStr = result2a.StreamToString();
157 | Xunit.Assert.Equal(fooResponse, result2aStr);
158 | var result2bStr = result2b.StreamToString();
159 | Xunit.Assert.Equal(barResponse, result2bStr);
160 | }
161 |
162 | using (var result3a = new MemoryStream())
163 | using (var result3b = new MemoryStream())
164 | using (var result4a = new MemoryStream())
165 | using (var result4b = new MemoryStream())
166 | {
167 | peer1Group.GetAsync("foo", result3a, new CacheControl(), CancellationToken.None).Wait();
168 | result3a.Position = 0;
169 | peer1Group.GetAsync("bar", result3b, new CacheControl(), CancellationToken.None).Wait();
170 | result3b.Position = 0;
171 | peer2Group.GetAsync("foo", result4a, new CacheControl(), CancellationToken.None).Wait();
172 | result4a.Position = 0;
173 | peer2Group.GetAsync("bar", result4b, new CacheControl(), CancellationToken.None).Wait();
174 | result4b.Position = 0;
175 | }
176 |
177 | Xunit.Assert.Equal(2, getter1.CallCount + getter2.CallCount);
178 | Xunit.Assert.Equal(2, group1Stat.LocalLoads + group2Stat.LocalLoads);
179 | }
180 |
181 | [Fact]
182 | public async Task TestFibonacciAsync()
183 | {
184 | var fibGetter1 = new FibGetter { CallCount = 0 };
185 | var fibGetter2 = new FibGetter { CallCount = 0 };
186 | var groupName = "Fibonacci";
187 | var peer1Group = GroupCache.NewGroup(groupName, fibGetter1, this.peer1Pool.GetPicker(groupName));
188 | var peer2Group = GroupCache.NewGroup(groupName, fibGetter2, this.peer2Pool.GetPicker(groupName));
189 | var group1Stat = new UnitTestGroupStat();
190 | var group2Stat = new UnitTestGroupStat();
191 | peer1Group.Stats = group1Stat;
192 | peer2Group.Stats = group2Stat;
193 | fibGetter1.Getter = peer1Group;
194 | fibGetter2.Getter = peer2Group;
195 | using (var result1a = new MemoryStream())
196 | {
197 | await peer1Group.GetAsync("90", result1a, new CacheControl(), CancellationToken.None);
198 | var result1aStr = result1a.StreamToString();
199 | Xunit.Assert.Equal("2880067194370816120", result1aStr);
200 | }
201 |
202 | Xunit.Assert.Equal(group1Stat.PeerLoads + group2Stat.PeerLoads, group1Stat.ServerRequests + group2Stat.ServerRequests);
203 | Xunit.Assert.Equal(91, group1Stat.LocalLoads + group2Stat.LocalLoads);
204 | Xunit.Assert.Equal(91, fibGetter1.CallCount + fibGetter2.CallCount);
205 | using (var result1b = new MemoryStream())
206 | {
207 | await peer2Group.GetAsync("90", result1b, new CacheControl(), CancellationToken.None);
208 | var result1bStr = result1b.StreamToString();
209 | Xunit.Assert.Equal("2880067194370816120", result1bStr);
210 | }
211 |
212 | Xunit.Assert.Equal(91, fibGetter1.CallCount + fibGetter2.CallCount);
213 | }
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/src/GroupCache/GroupCache.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 | using System.Collections.Concurrent;
12 | using System.Diagnostics;
13 | using System.IO;
14 | using System.Threading;
15 | using System.Threading.Tasks;
16 |
17 | ///
18 | /// Groupcache provides a data loading mechanism with caching and de-duplication
19 | /// that works across a set of peer processes.
20 | ///
21 | /// Each data Get first consults its local cache, otherwise delegates to the requested key's canonical owner,
22 | /// which then checks its cache or finally gets the data.
23 | ///
24 | /// In the common case, many concurrent cache misses across
25 | /// a set of peers for the same key result in just one cache fill.
26 | ///
27 | /// In a nutshell, a groupcache lookup of Get("foo") looks like:
28 | /// (On machine #5 of a set of N machines running the same code)
29 | ///
30 | /// 1- Is the value of "foo" in local memory because peer #5 (the current peer) is the owner of it? If so, use it.
31 | ///
32 | /// 2- Amongst the peers in my set of N, am I the owner of the key "foo"? (e.g.does it hash to 5?) If so, load it.
33 | /// If other callers come in, via the same process or via RPC requests from peers, they block waiting for the load to finish and get the same answer.
34 | /// If not, RPC to the peer that's the owner and get to the answer.
35 | /// If the RPC fails, just try again next canonical owner for that key (still with local dup suppression).
36 | ///
37 | public static class GroupCache
38 | {
39 | private static readonly ConcurrentDictionary GROUPS = new ConcurrentDictionary();
40 |
41 | ///
42 | /// returns the named group previously created with NewGroup,
43 | /// Used by server callback to forward received request to the correct Group instance.
44 | ///
45 | ///
46 | public static bool GetGroup(GroupKey key, out Group group)
47 | {
48 | return GROUPS.TryGetValue(key, out group);
49 | }
50 |
51 | public static Group NewGroup(string groupName, Func getterFunc, IPeerPicker peers)
52 | {
53 | Argument.NotNull(getterFunc, "getterFunc");
54 | var getter = new GetterFunc(getterFunc);
55 | return NewGroup(groupName, getter, peers);
56 | }
57 |
58 | public static Group NewGroup(string name, IGetter getter, ICache cache, IPeerPicker peers)
59 | {
60 | Argument.NotNull(name, "name");
61 | Argument.NotNull(getter, "getter");
62 | Argument.NotNull(cache, "cache");
63 | Argument.NotNull(peers, "peers");
64 | var groupKey = new GroupKey { GroupName = name, Endpoint = peers.Self };
65 | var group = new Group(name, getter, peers, cache);
66 | return GROUPS.GetOrAdd(groupKey, group);
67 | }
68 |
69 | public static Group NewGroup(string name, IGetter getter, IPeerPicker peers)
70 | {
71 | Argument.NotNull(name, "name");
72 | Argument.NotNull(getter, "getter");
73 | Argument.NotNull(peers, "peers");
74 | var groupKey = new GroupKey { GroupName = name, Endpoint = peers.Self };
75 | var group = new Group(name, getter, peers);
76 | return GROUPS.GetOrAdd(groupKey, group);
77 | }
78 | }
79 |
80 | ///
81 | /// A Group is a cache namespace and associated data loaded spread over a group of 1 or more machines.
82 | ///
83 | public sealed class Group : IGetter
84 | {
85 | private readonly string groupName;
86 | private readonly IGetter getter;
87 | private readonly IPeerPicker peers;
88 | private readonly ICache localCache;
89 | private IGroupStat stats;
90 | private ILogger logger;
91 |
92 | public IGroupStat Stats
93 | {
94 | get
95 | {
96 | return this.stats;
97 | }
98 |
99 | set
100 | {
101 | Argument.NotNull(value, "Stats");
102 | this.stats = value;
103 | }
104 | }
105 |
106 | public ILogger Logger
107 | {
108 | get
109 | {
110 | return this.logger;
111 | }
112 |
113 | set
114 | {
115 | Argument.NotNull(value, "Logger");
116 | this.logger = value;
117 | }
118 | }
119 |
120 | public int MaxRetry { get; set; } = 5;
121 |
122 | public ICacheEntryValidator CacheEntryValidator { get; set; }
123 |
124 | internal Group(string groupName, IGetter getter, IPeerPicker peers)
125 | : this(groupName, getter, peers, new MemoryCache())
126 | {
127 | }
128 |
129 | internal Group(string groupName, IGetter getter, IPeerPicker peers, ICache localCache)
130 | {
131 | this.groupName = groupName;
132 | this.getter = getter;
133 | this.peers = peers;
134 | this.localCache = localCache;
135 | this.Stats = new NullGroupStat();
136 | this.Logger = new NullLogger();
137 | this.localCache.ItemOverCapacity += this.ItemWasOverCapacity;
138 | this.CacheEntryValidator = new NullCacheEntryValidator();
139 | }
140 |
141 | private void ItemWasOverCapacity(string obj)
142 | {
143 | this.Stats.TraceItemOverCapacity(this.groupName);
144 | }
145 |
146 | ///
147 | /// Read the data from Peer or local machine and dedup in flight call.
148 | ///
149 | /// A representing the asynchronous operation.
150 | public async Task GetAsync(string key, Stream sink, ICacheControl cacheControl, CancellationToken ct)
151 | {
152 | if (cacheControl == null)
153 | {
154 | cacheControl = new CacheControl();
155 | }
156 |
157 | this.Stats.TraceGets(this.groupName);
158 | var watch = Stopwatch.StartNew();
159 | await this.LoadFromGetterOrPeerAsync(key, sink, cacheControl, ct).ConfigureAwait(false);
160 | this.Stats.TraceRoundtripLatency(this.groupName, watch.Elapsed);
161 | }
162 |
163 | ///
164 | /// Read the data from localMachine or from IGetter and dedup in flight call.
165 | ///
166 | /// A representing the asynchronous operation.
167 | public async Task GetAsyncLocallyAsync(string key, Stream sink, ICacheControl cacheControl, CancellationToken ct)
168 | {
169 | this.Stats.TraceGets(this.groupName);
170 |
171 | // Dipose validatingSink but leave sink open, to release validatingSink ressources
172 | using (var validatingSink = this.CacheEntryValidator.ValidateEntryPassThrough(key, sink))
173 | {
174 | try
175 | {
176 | await this.LoadLocallyAsync(key, validatingSink, cacheControl, ct).ConfigureAwait(false);
177 | await validatingSink.ValidateAsync(ct).ConfigureAwait(false);
178 | }
179 | catch (CacheEntryValidationFailedException)
180 | {
181 | await this.RemoveAsync(key, ct).ConfigureAwait(false);
182 | throw;
183 | }
184 | }
185 | }
186 |
187 | public Task RemoveAsync(string key, CancellationToken ct)
188 | {
189 | return this.localCache.RemoveAsync(key, ct);
190 | }
191 |
192 | private async Task LoadFromGetterOrPeerAsync(string key, Stream sink, ICacheControl cacheControl, CancellationToken ct)
193 | {
194 | this.Stats.TraceLoadsDeduped(this.groupName);
195 | var replicasForKey = this.peers.PickPeers(key, this.peers.Count);
196 | var retryCount = Math.Min(this.MaxRetry, replicasForKey.Count);
197 | var retryableException = new Type[] { typeof(InternalServerErrorException), typeof(ServerBusyException), typeof(GroupNotFoundException), typeof(ConnectFailureException) };
198 | var retry = new Retry(new SimpleRetryPolicy(retryCount, TimeSpan.Zero, retryableException));
199 | try
200 | {
201 | await retry.ExecuteAsync(async (RetryContext ctx) =>
202 | {
203 | if (ctx.RetryCount > 0)
204 | {
205 | this.Stats.TraceRetry(this.groupName);
206 | }
207 |
208 | var peerClient = replicasForKey[ctx.RetryCount];
209 | if (peerClient.IsLocal) // we need to read from peer
210 | {
211 | this.Logger.Debug(string.Format("Call to LoadLocally for Cache {0} key: {1} retryCount: {2}", this.groupName, key, ctx.RetryCount));
212 | await this.LoadLocallyAsync(key, sink, cacheControl, ct).ConfigureAwait(false);
213 | }
214 | else
215 | {
216 | this.Logger.Debug(string.Format("Call to LoadFromPeer for Cache {0} key: {1} retryCount: {2}", this.groupName, key, ctx.RetryCount));
217 | await this.LoadFromPeerAsync(key, sink, cacheControl, peerClient, ct).ConfigureAwait(false);
218 | }
219 | })
220 | .ConfigureAwait(false);
221 | }
222 | catch
223 | {
224 | this.Logger.Info($"{this.groupName}:{key} failed to download from peer. trying to download locally from source directly.");
225 | await this.LoadLocallyAsync(key, sink, cacheControl, ct).ConfigureAwait(false);
226 | }
227 | }
228 |
229 | private async Task LoadLocallyAsync(string key, Stream sink, ICacheControl cacheControl, CancellationToken ct)
230 | {
231 | async Task Func(string cachekey, Stream factorySink, ICacheControl factoryCacheControl)
232 | {
233 | try
234 | {
235 | this.Stats.TraceLocalLoads(this.groupName);
236 | await this.getter.GetAsync(cachekey, factorySink, factoryCacheControl, ct).ConfigureAwait(false);
237 | }
238 | catch (Exception ex)
239 | {
240 | this.Logger.Error(ex, string.Format("Call to LoadLocally for Cache {0} key: {1} failed ", this.groupName, key));
241 | throw;
242 | }
243 | }
244 |
245 | this.Stats.TraceCacheHits(this.groupName);
246 |
247 | var cacheEntry = await this.localCache.GetOrAddAsync(key, Func, cacheControl, ct).ConfigureAwait(false);
248 | try
249 | {
250 | Stream cacheEntryStream = cacheEntry.Value();
251 | await cacheEntryStream.CopyToAsync(sink).ConfigureAwait(false);
252 | }
253 | finally
254 | {
255 | await cacheEntry.DisposeAsync().ConfigureAwait(false);
256 | }
257 | }
258 |
259 | private async Task LoadFromPeerAsync(string key, Stream sink, ICacheControl cacheControl, IGroupCacheClient peerClient, CancellationToken ct)
260 | {
261 | try
262 | {
263 | this.Stats.TracePeerLoads(this.groupName);
264 | using (var validatingSink = this.CacheEntryValidator.ValidateEntryPassThrough(key, sink))
265 | {
266 | await peerClient.GetAsync(this.groupName, key, validatingSink, cacheControl, ct).ConfigureAwait(false);
267 | await validatingSink.ValidateAsync(ct).ConfigureAwait(false);
268 | }
269 | }
270 | catch (CircuitBreakerOpenException)
271 | {
272 | throw; // Dont log CircuitBreakerOpenException
273 | }
274 | catch (InternalServerErrorException internalErrorEx)
275 | {
276 | this.Logger.Error(string.Format("Call to LoadFromPeer to {0} for Cache {1} key: {2} failed on InternalServerErrorException {3}", peerClient.Endpoint.ToString(), this.groupName, key, internalErrorEx.Message));
277 | throw;
278 | }
279 | catch (Exception ex)
280 | {
281 | this.Logger.Error(ex, string.Format("Call to LoadFromPeer to {0} for Cache {1} key: {2} failed ", peerClient.Endpoint.ToString(), this.groupName, key));
282 | throw;
283 | }
284 | }
285 | }
286 | }
287 |
--------------------------------------------------------------------------------
/src/GroupCache/LRUCache.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 | using System.Collections;
12 | using System.Collections.Generic;
13 | using System.Linq;
14 | using System.Threading;
15 |
16 | ///
17 | /// Implements a Generic fixed-size thread safe LRU (Least Recently Used) cache.
18 | /// The object maintains a doubly-linked list of elements.
19 | /// When an element is accessed, it is promoted to the head of the list.
20 | /// When space is needed, the element at the tail of the list (the least recently used element) is evicted.
21 | ///
22 | /// The type of the keys in the cache.
23 | /// The type of the values in the cache.
24 | public class LRUCache : IEnumerable>, IDisposable
25 | {
26 | private readonly ReaderWriterLockSlim @lock;
27 | private readonly LinkedList> lruList;
28 | private readonly Dictionary>> map;
29 | private readonly int maxEntries;
30 | private ulong capacity;
31 | private TimeSpan ttl = TimeSpan.MaxValue;
32 | private ulong usage = 0;
33 |
34 | public ulong Capacity
35 | {
36 | get
37 | {
38 | using (this.@lock.GetReaderLock())
39 | {
40 | return this.capacity;
41 | }
42 | }
43 |
44 | set
45 | {
46 | using (this.@lock.GetWriterLock())
47 | {
48 | this.capacity = value;
49 | }
50 | }
51 | }
52 |
53 | public event Action> ItemEvicted;
54 |
55 | public event Action> ItemOverCapacity;
56 |
57 | ///
58 | /// Gets returns the number of items in the cache.
59 | ///
60 | public int Count
61 | {
62 | get
63 | {
64 | using (this.@lock.GetReaderLock())
65 | {
66 | return this.lruList.Count;
67 | }
68 | }
69 | }
70 |
71 | public ulong TotalCharge
72 | {
73 | get
74 | {
75 | using (this.@lock.GetReaderLock())
76 | {
77 | return this.usage;
78 | }
79 | }
80 | }
81 |
82 | ///
83 | /// Initializes a new instance of the class.
84 | /// Creates an LRU of the given size.
85 | /// maxEntries is zero, the cache has no limit and it's assumed that eviction is done by the caller.
86 | ///
87 | ///
88 | public LRUCache(int maxEntries, IEqualityComparer comparer, ulong capacity, TimeSpan ttl)
89 | {
90 | this.maxEntries = maxEntries;
91 | this.@lock = new ReaderWriterLockSlim();
92 | this.lruList = new LinkedList>();
93 | this.map = new Dictionary>>(comparer);
94 | this.capacity = capacity;
95 | this.ttl = ttl;
96 | }
97 |
98 | public LRUCache(int maxEntries, TimeSpan ttl)
99 | : this(maxEntries, EqualityComparer.Default, 0, ttl)
100 | {
101 | }
102 |
103 | public LRUCache(int maxEntries, IEqualityComparer comparer)
104 | : this(maxEntries, comparer, 0, TimeSpan.MaxValue)
105 | {
106 | }
107 |
108 | public LRUCache(int maxEntries = 0, ulong capacity = 0)
109 | : this(maxEntries, EqualityComparer.Default, capacity, TimeSpan.MaxValue)
110 | {
111 | }
112 |
113 | ///
114 | /// Adds a an element to the cache.
115 | ///
116 | /// The object to use as the key of the element to add.
117 | /// The object to use as the value of the element to add.
118 | /// key is null.
119 | /// Returns true if an eviction occured.
120 | public bool Add(TKey key, TValue value, ulong charge = 0)
121 | {
122 | return this.Add(key, ref value, true, charge);
123 | }
124 |
125 | ///
126 | /// Removes the element with the specified key from the cache.
127 | ///
128 | ///
129 | /// true if the element is successfully removed; otherwise, false.
130 | /// This method also returns false if key was not found.
131 | ///
132 | /// true if the item existed.
133 | public bool Remove(TKey key)
134 | {
135 | if (key == null)
136 | {
137 | throw new ArgumentNullException("key");
138 | }
139 |
140 | using (this.@lock.GetWriterLock())
141 | {
142 | LinkedListNode> node;
143 | if (this.map.TryGetValue(key, out node))
144 | {
145 | this.RemoveElement(node);
146 | return true;
147 | }
148 |
149 | return false;
150 | }
151 | }
152 |
153 | ///
154 | /// Check if a key is in the cache, without updating the recent-ness or deleting it for being stale.
155 | ///
156 | ///
157 | public bool ContainsKey(TKey key)
158 | {
159 | if (key == null)
160 | {
161 | throw new ArgumentNullException("key");
162 | }
163 |
164 | using (this.@lock.GetReaderLock())
165 | {
166 | return this.map.ContainsKey(key);
167 | }
168 | }
169 |
170 | ///
171 | /// Gets the value associated with the specified key.
172 | ///
173 | /// The key whose value to get.
174 | ///
175 | /// The value associated with the specified key, if the key is found;
176 | /// otherwise, the default value for the type of the value parameter.
177 | ///
178 | /// true if the item was found.
179 | public bool TryGetValue(TKey key, out TValue value)
180 | {
181 | if (key == null)
182 | {
183 | throw new ArgumentNullException("key");
184 | }
185 |
186 | using (this.@lock.GetWriterLock())
187 | {
188 | LinkedListNode> node;
189 | if (this.map.TryGetValue(key, out node))
190 | {
191 | if (DateTimeOffset.Now.Subtract(node.Value.CreationTime) <= this.ttl)
192 | {
193 | this.lruList.MoveToFront(node);
194 | value = node.Value.Value;
195 | return true;
196 | }
197 | else
198 | {
199 | this.RemoveElement(node);
200 | }
201 | }
202 |
203 | value = default(TValue);
204 | return false;
205 | }
206 | }
207 |
208 | ///
209 | /// Gets the value associated with the specified key.
210 | ///
211 | /// The key whose value to get.
212 | ///
213 | /// If the key is not found, this is the value that will be added and returned.
214 | ///
215 | /// the value associated to the key.
216 | public TValue GetOrAdd(TKey key, Func valueFactory)
217 | {
218 | if (key == null)
219 | {
220 | throw new ArgumentNullException("key");
221 | }
222 |
223 | if (valueFactory == null)
224 | {
225 | throw new ArgumentNullException("valueFactory");
226 | }
227 |
228 | TValue outValue;
229 | if (!this.TryGetValue(key, out outValue))
230 | {
231 | outValue = valueFactory(key);
232 | this.Add(key, ref outValue, false, 0);
233 | }
234 |
235 | return outValue;
236 | }
237 |
238 | public void Clear()
239 | {
240 | using (this.@lock.GetWriterLock())
241 | {
242 | this.lruList.Clear();
243 | this.map.Clear();
244 | }
245 | }
246 |
247 | ///
248 | public IEnumerator> GetEnumerator()
249 | {
250 | using (this.@lock.GetReaderLock())
251 | {
252 | return this.lruList.Select(e => new KeyValuePair(e.Key, e.Value)).ToList().GetEnumerator();
253 | }
254 | }
255 |
256 | ///
257 | IEnumerator IEnumerable.GetEnumerator()
258 | {
259 | return this.GetEnumerator();
260 | }
261 |
262 | ///
263 | public void Dispose()
264 | {
265 | this.@lock.Dispose();
266 | }
267 |
268 |
269 |
270 | // note: must hold _lock
271 | private void RemoveOldest()
272 | {
273 | var oldestNode = this.lruList.Last;
274 | if (oldestNode != null)
275 | {
276 | this.RemoveElement(oldestNode);
277 | this.ItemEvicted?.Invoke(new KeyValuePair(oldestNode.Value.Key, oldestNode.Value.Value));
278 | }
279 | }
280 |
281 | // note: must hold _lock
282 | private void RemoveElement(LinkedListNode> element)
283 | {
284 | // Remove using LinkedListNode is an O(1) operation.
285 | this.lruList.Remove(element);
286 | this.map.Remove(element.Value.Key);
287 | this.usage -= element.Value.Charge;
288 | }
289 |
290 | ///
291 | /// Add or update an entry to the LRUCache and put it in the front of the lru list.
292 | ///
293 | ///
294 | ///
295 | /// whether to replace the value if already exist.
296 | /// charge for capacity.
297 | ///
298 | private bool Add(TKey key, ref TValue value, bool replace = true, ulong charge = 0)
299 | {
300 | bool evicted = false;
301 | bool itemLargerThanCapacity = false;
302 | if (key == null)
303 | {
304 | throw new ArgumentNullException("key");
305 | }
306 |
307 | using (this.@lock.GetWriterLock())
308 | {
309 | LinkedListNode> node;
310 |
311 | // Check for existing item
312 | if (this.map.TryGetValue(key, out node))
313 | {
314 | this.lruList.MoveToFront(node);
315 | if (replace)
316 | {
317 | node.Value.Value = value;
318 | }
319 | else
320 | {
321 | value = node.Value.Value;
322 | }
323 |
324 | return false;
325 | }
326 |
327 | if (this.capacity == 0 || charge <= this.capacity)
328 | {
329 | // Add new item
330 | var entry = new Entry { Key = key, Value = value, Charge = charge, CreationTime = DateTimeOffset.Now };
331 | this.lruList.AddFirst(entry);
332 | this.map[key] = this.lruList.First;
333 | this.usage += charge;
334 | }
335 | else
336 | {
337 | itemLargerThanCapacity = true;
338 | }
339 |
340 | // Verify size not exceeded
341 | while ((this.maxEntries != 0 && this.lruList.Count > this.maxEntries) || (this.capacity != 0 && this.usage > this.capacity))
342 | {
343 | evicted = true;
344 | this.RemoveOldest();
345 | }
346 | }
347 |
348 | if (itemLargerThanCapacity)
349 | {
350 | this.ItemOverCapacity?.Invoke(new KeyValuePair(key, value));
351 | }
352 |
353 | return evicted;
354 | }
355 |
356 | ///
357 | /// Defines a key/value pair that can be set or retrieved.
358 | ///
359 | /// The type of the key.
360 | /// The type of the value.
361 | private class Entry
362 | {
363 | public ulong Charge
364 | {
365 | get;
366 | set;
367 | }
368 |
369 | public DateTimeOffset CreationTime
370 | {
371 | get;
372 | set;
373 | }
374 |
375 | public TEKey Key
376 | {
377 | get;
378 | set;
379 | }
380 |
381 | public TEValue Value
382 | {
383 | get;
384 | set;
385 | }
386 | }
387 | }
388 | }
389 |
--------------------------------------------------------------------------------
/src/GroupCache/DiskCache.cs:
--------------------------------------------------------------------------------
1 | // --------------------------------------------------------------------------------------------------------------------
2 | //
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information.
5 | //
6 | // --------------------------------------------------------------------------------------------------------------------
7 |
8 | namespace GroupCache
9 | {
10 | using System;
11 | using System.Collections.Concurrent;
12 | using System.Collections.Generic;
13 | using System.Diagnostics;
14 | using System.IO;
15 | using System.Linq;
16 | using System.Threading;
17 | using System.Threading.Tasks;
18 | using Microsoft.VisualStudio.Threading;
19 |
20 | public interface IFileSystem
21 | {
22 | Stream FileOpenRead(string path);
23 |
24 | void FileDelete(string sourcePath, string tmpPath);
25 |
26 | Task WriteAtomicAsync(Func copyAsync, string tmpPath, CancellationToken ct);
27 |
28 | string[] DirectoryGetFiles(string directoryPath);
29 |
30 | void DirectoryCreate(string directoryPath);
31 |
32 | void DirectoryReCreate(string directoryPath);
33 | }
34 |
35 | public class MockFileSystem : IFileSystem
36 | {
37 | private readonly Dictionary> dirs = new Dictionary>();
38 | private readonly object @lock = new object();
39 |
40 | ///
41 | public void DirectoryCreate(string directoryPath)
42 | {
43 | lock (this.@lock)
44 | {
45 | this.dirs.Add(directoryPath, new Dictionary());
46 | }
47 | }
48 |
49 | ///
50 | public string[] DirectoryGetFiles(string directoryPath)
51 | {
52 | lock (this.@lock)
53 | {
54 | return this.dirs[directoryPath].Keys.ToArray();
55 | }
56 | }
57 |
58 | ///
59 | public void DirectoryReCreate(string directoryPath)
60 | {
61 | lock (this.@lock)
62 | {
63 | this.dirs.Remove(directoryPath);
64 | this.dirs.Add(directoryPath, new Dictionary());
65 | }
66 | }
67 |
68 | ///
69 | public void FileDelete(string sourcePath, string tmpPath)
70 | {
71 | lock (this.@lock)
72 | {
73 | var directory = Path.GetDirectoryName(sourcePath);
74 | var file = Path.GetFileName(sourcePath);
75 | var dir = this.dirs[directory];
76 | Debug.Assert(dir.ContainsKey(file));
77 | dir.Remove(file);
78 | }
79 | }
80 |
81 | ///
82 | public Stream FileOpenRead(string path)
83 | {
84 | lock (this.@lock)
85 | {
86 | var srcFileName = Path.GetFileName(path);
87 | var srcDir = Path.GetDirectoryName(path);
88 | return new MemoryStream(this.dirs[srcDir][srcFileName]);
89 | }
90 | }
91 |
92 | ///
93 | public async Task WriteAtomicAsync(Func copyAsync, string tmpPath, CancellationToken ct)
94 | {
95 | using (var memStream = new MemoryStream())
96 | {
97 | await copyAsync(memStream, ct).ConfigureAwait(false);
98 | var array = memStream.ToArray();
99 | lock (this.@lock)
100 | {
101 | var randomPath = this.GenerateRandomFilePath(tmpPath);
102 | var srcFileName = Path.GetFileName(randomPath);
103 | var srcDir = Path.GetDirectoryName(randomPath);
104 | this.dirs[srcDir].Add(srcFileName, array);
105 | return randomPath;
106 | }
107 | }
108 | }
109 |
110 | private bool Exists(string path)
111 | {
112 | lock (this.@lock)
113 | {
114 | var srcFileName = Path.GetFileName(path);
115 | var srcDir = Path.GetDirectoryName(path);
116 | return this.dirs[srcDir].ContainsKey(srcFileName);
117 | }
118 | }
119 |
120 | private string GenerateRandomFilePath(string tmpPath)
121 | {
122 | string filePath;
123 | do
124 | {
125 | filePath = Path.Combine(tmpPath, Guid.NewGuid().ToString());
126 | }
127 | while (this.Exists(filePath));
128 | return filePath;
129 | }
130 | }
131 |
132 | public class FileSystem : IFileSystem
133 | {
134 | ///
135 | public Stream FileOpenRead(string path)
136 | {
137 | return File.OpenRead(path);
138 | }
139 |
140 | ///
141 | public void FileDelete(string sourcePath, string tmpPath)
142 | {
143 | File.Delete(sourcePath);
144 | }
145 |
146 | public bool FileExists(string path)
147 | {
148 | return File.Exists(path);
149 | }
150 |
151 | private string GenerateRandomFilePath(string tmpPath)
152 | {
153 | string filePath;
154 | do
155 | {
156 | filePath = Path.Combine(tmpPath, Path.GetRandomFileName());
157 | }
158 | while (File.Exists(filePath));
159 | return filePath;
160 | }
161 |
162 | ///
163 | /// 1- create a new file at tmpPath;
164 | /// 2- send data to the operating system kernel for writing to tmpPath;
165 | /// 3- close the file;
166 | /// 4- flush the writing of data to tmpPath;
167 | /// 5- rename tmpPath on top of path.
168 | ///
169 | /// A representing the asynchronous operation.
170 | public async Task WriteAtomicAsync(Func copyAsync, string tmpPath, CancellationToken ct)
171 | {
172 | while (true)
173 | {
174 | try
175 | {
176 | var randomPath = this.GenerateRandomFilePath(tmpPath);
177 | using (var fileStream = File.Create(randomPath))
178 | {
179 | await copyAsync(fileStream, ct).ConfigureAwait(false);
180 | }
181 |
182 | return randomPath;
183 | }
184 | catch
185 | {
186 | }
187 | }
188 | }
189 |
190 | ///
191 | public string[] DirectoryGetFiles(string directoryPath)
192 | {
193 | return Directory.GetFiles(directoryPath);
194 | }
195 |
196 | ///
197 | public void DirectoryCreate(string directoryPath)
198 | {
199 | if (!Directory.Exists(directoryPath))
200 | {
201 | Directory.CreateDirectory(directoryPath);
202 | }
203 | }
204 |
205 | ///
206 | public void DirectoryReCreate(string directoryPath)
207 | {
208 | if (Directory.Exists(directoryPath))
209 | {
210 | // discard all entry
211 | Directory.Delete(directoryPath, true);
212 | }
213 |
214 | Directory.CreateDirectory(directoryPath);
215 | }
216 | }
217 |
218 | // Cache entries have an "in_cache" boolean indicating whether the cache has a
219 | // reference on the entry.
220 | //
221 | // The cache keeps two Dictionary of items in the cache.
222 | // All items in the cache are in one or the other, and never both.
223 | // Items still referenced by clients but erased from the cache are in neither list.
224 | public class DiskCacheEntry : ICacheEntry
225 | {
226 | private readonly DiskCache ownerCache;
227 |
228 | public string Key { get; private set; }
229 |
230 | // file path to the file storing this cache entry data
231 | public string EntryPath { get; internal set; }
232 |
233 | internal int Refs = 0; // References, including cache reference
234 | internal bool InCache = true; // Whether entry is in the cache.
235 |
236 | public DiskCacheEntry(string key, string entryPath, DiskCache owner)
237 | {
238 | Debug.Assert(entryPath != string.Empty, "entryPath should not be empty");
239 | this.EntryPath = entryPath;
240 | this.ownerCache = owner;
241 | this.Key = key;
242 | }
243 |
244 | ///
245 | /// Read the file from disk each time this is called.
246 | ///
247 | ///
248 | public Stream Value()
249 | {
250 | return this.ownerCache.ReadEntry(this);
251 | }
252 |
253 | ///
254 | public void Ref()
255 | {
256 | Interlocked.Increment(ref this.Refs);
257 | }
258 |
259 | internal int Unref()
260 | {
261 | Debug.Assert(this.Refs > 0, "disk entry should have 1+ reference");
262 | return Interlocked.Decrement(ref this.Refs);
263 | }
264 |
265 | public void Delete()
266 | {
267 | Debug.Assert(this.Refs <= 0, "refcount must be 0");
268 | this.ownerCache.DeleteEntry(this);
269 | }
270 |
271 | ///
272 | public Task DisposeAsync()
273 | {
274 | return this.ownerCache.ReleaseAsync(this);
275 | }
276 | }
277 |
278 | public class DiskCache : ICache
279 | {
280 | private readonly SingleFlight concurrentRequest = new SingleFlight();
281 | public static readonly ICacheEntry EMPTY = new EmptyCacheEntry();
282 | private readonly AsyncReaderWriterLock rwLock = new AsyncReaderWriterLock();
283 |
284 | // Store (string -> DiskCacheEntry) mapping for entry that are on disk and not currently referenced by clients, in LRU order
285 | // Entries have refs==1 and in_cache==true.
286 | private readonly LRUCache lruCache;
287 |
288 | // Store (string -> DiskCacheEntry) mapping for entry that are on disk and currently referenced by clients.
289 | // Entries have refs >= 2 and in_cache==true.
290 | private readonly ConcurrentDictionary inUse = new ConcurrentDictionary();
291 |
292 | // Root cache folder
293 | private readonly string cacheRootPath;
294 |
295 | // Folder for entry that are being written to disk but are not in the cache yet
296 | private readonly string cacheTmpPath;
297 |
298 | private readonly IFileSystem fs;
299 |
300 | // Event trigger when an attempt was made to add an item to the cache
301 | // but the item was larger than the cache maximum size
302 | ///
303 | public event Action ItemOverCapacity;
304 |
305 | public DiskCache(string cacheRootPath, int maxEntryCount, IFileSystem fs = null)
306 | {
307 | this.cacheRootPath = cacheRootPath;
308 | this.cacheTmpPath = Path.Combine(this.cacheRootPath, "tmp");
309 | this.fs = fs;
310 | if (this.fs == null)
311 | {
312 | this.fs = new FileSystem();
313 | }
314 |
315 | this.fs.DirectoryCreate(this.cacheRootPath);
316 | this.fs.DirectoryReCreate(this.cacheTmpPath);
317 |
318 | this.lruCache = new LRUCache(maxEntryCount);
319 | this.lruCache.ItemEvicted += (KeyValuePair pair) => this.FinishErase(pair.Key, pair.Value);
320 | this.lruCache.ItemOverCapacity += (kv) => this.ItemOverCapacity?.Invoke(kv.Key);
321 | }
322 |
323 | public async Task ReleaseAsync(DiskCacheEntry entry)
324 | {
325 | int refCount;
326 |
327 | // If InCache == false this is called from FinishErase with _lock already held and cause deadlock
328 | if (Volatile.Read(ref entry.InCache))
329 | {
330 | using (await this.rwLock.WriteLockAsync())
331 | {
332 | // entry is in "_inUse" and refcount is between 1 .. N
333 | // Call Unref inside the lock to avoid other thread incrementing concurrently
334 | refCount = entry.Unref();
335 | if (refCount == 1)
336 | {
337 | // Entry is still in cache but no longer in use.
338 | if (this.inUse.TryRemove(entry.Key, out _))
339 | {
340 | this.lruCache.Add(entry.Key, entry);
341 | }
342 | }
343 |
344 | if (refCount <= 0)
345 | {
346 | this.inUse.TryRemove(entry.Key, out _);
347 | entry.Delete();
348 | return;
349 | }
350 | }
351 | }
352 | else
353 | {
354 | // its safe to decrment unref because Entry is not in cache anymore and refcount can only decrease now
355 | if (entry.Refs > 0)
356 | {
357 | refCount = entry.Unref();
358 | if (refCount == 0)
359 | {
360 | this.inUse.TryRemove(entry.Key, out _);
361 | entry.Delete();
362 | return;
363 | }
364 | }
365 | }
366 | }
367 |
368 | ///
369 | public async Task GetOrAddAsync(string key, Func valueFactory, ICacheControl cacheControl, CancellationToken ct)
370 | {
371 | using (await this.rwLock.ReadLockAsync())
372 | {
373 | ICacheEntry found = this.GetInternal(key);
374 | if (!(found is EmptyCacheEntry))
375 | {
376 | found.Ref(); // this entry remain valid after releasing the lock
377 | return found;
378 | }
379 | }
380 |
381 | using (await this.rwLock.WriteLockAsync())
382 | {
383 | ICacheEntry found = this.GetInternal(key);
384 | if (!(found is EmptyCacheEntry))
385 | {
386 | found.Ref(); // this entry remain valid after releasing the lock
387 | return found;
388 | }
389 |
390 | var randomPath = await this.fs.WriteAtomicAsync((sink, cancel) => valueFactory(key, sink, cacheControl), this.cacheTmpPath, ct).ConfigureAwait(false);
391 | var tmpEntry = new DiskCacheEntry(key, randomPath, this);
392 | if (!cacheControl.NoStore)
393 | {
394 | this.SetInternal(key, tmpEntry);
395 | }
396 |
397 | var entry = this.GetInternal(key);
398 | entry.Ref();
399 | return entry;
400 | }
401 | }
402 |
403 | // Calling thread need to be first lock "_lock"
404 | private ICacheEntry GetInternal(string key)
405 | {
406 | var inUse = this.inUse.TryGetValue(key, out DiskCacheEntry entry);
407 | if (inUse)
408 | {
409 | return entry;
410 | }
411 |
412 | var found = this.lruCache.TryGetValue(key, out entry);
413 | if (found)
414 | {
415 | // Move from _lruCache to _inUse
416 | this.lruCache.Remove(key);
417 | this.inUse.TryAdd(key, entry);
418 | return entry;
419 | }
420 | else
421 | {
422 | return new EmptyCacheEntry();
423 | }
424 | }
425 |
426 | public async Task GetAsync(string key, CancellationToken ct)
427 | {
428 | using (await this.rwLock.ReadLockAsync())
429 | {
430 | ICacheEntry entry = this.GetInternal(key);
431 | entry.Ref();
432 | return entry;
433 | }
434 | }
435 |
436 | ///
437 | public async Task RemoveAsync(string key, CancellationToken ct)
438 | {
439 | using (await this.rwLock.WriteLockAsync())
440 | {
441 | var found = this.lruCache.TryGetValue(key, out DiskCacheEntry entry);
442 | if (found)
443 | {
444 | this.lruCache.Remove(key);
445 | this.FinishErase(key, entry);
446 | }
447 |
448 | var inUse = this.inUse.TryGetValue(key, out entry);
449 | if (inUse)
450 | {
451 | this.FinishErase(key, entry);
452 | }
453 | }
454 | }
455 |
456 | // Calling thread need to be first take writelock
457 | private bool SetInternal(string key, DiskCacheEntry diskEntry)
458 | {
459 | if (this.inUse.ContainsKey(key))
460 | {
461 | return false;
462 | }
463 |
464 | if (this.lruCache.ContainsKey(key))
465 | {
466 | return false;
467 | }
468 |
469 | Volatile.Write(ref diskEntry.InCache, true);
470 |
471 | diskEntry.Ref(); // for the LRU cache's reference.
472 | if (diskEntry.Refs == 1)
473 | {
474 | this.lruCache.Add(key, diskEntry);
475 | }
476 | else if (diskEntry.Refs > 1)
477 | {
478 | this.inUse.TryAdd(key, diskEntry);
479 | }
480 |
481 | Debug.Assert(diskEntry.Refs > 0, "disk entry should have at least 1 reference");
482 |
483 | return true;
484 | }
485 |
486 | // Entry has already been removed from the hash table
487 | // Finish removing from cache
488 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "")]
489 | private void FinishErase(string key, DiskCacheEntry entry)
490 | {
491 | if (entry != null)
492 | {
493 | Volatile.Write(ref entry.InCache, false);
494 | entry.DisposeAsync().Wait(); // not inside LRU cache anymore
495 | }
496 | }
497 |
498 | internal void DeleteEntry(DiskCacheEntry diskCacheEntry)
499 | {
500 | var refs = diskCacheEntry.Refs;
501 | Debug.Assert(refs == 0, "disk entry should have 0 reference");
502 | this.fs.FileDelete(diskCacheEntry.EntryPath, this.cacheTmpPath);
503 | diskCacheEntry.EntryPath = string.Empty;
504 | }
505 |
506 | internal Stream ReadEntry(DiskCacheEntry diskCacheEntry)
507 | {
508 | Debug.Assert(diskCacheEntry.Refs > 0, "disk entry should have at least 1 reference");
509 | Debug.Assert(diskCacheEntry.EntryPath != string.Empty, "disk entry have been deleted");
510 | return this.fs.FileOpenRead(diskCacheEntry.EntryPath);
511 | }
512 | }
513 | }
514 |
--------------------------------------------------------------------------------