Cache Aspect Concurrency

jonyadamit's Avatar

jonyadamit

17 Aug, 2017 09:36 AM

Hi, I don't know if this is a bug or a feature request.
If a Cache decorated method is accessed concurrently before the result is cached - the method is executed twice, but is cached correctly of course.
When I used my own version of CacheAttribute, I had a ConcurrentDictionary that held keys and TaskCompletionSources.
Upon entry I would create or get that promise and return it. Once cache was set I would fulfil the promise and this way it was executed only once.

Is it possible to have the same behavior with the Cache Aspect?

  1. Support Staff 1 Posted by PostSharp Techn... on 17 Aug, 2017 12:16 PM

    PostSharp Technologies's Avatar

    Hi,

    this is not implemented out of the box, but you can implement your own CachingBackendEnhancer which would manage this. See http://doc.postsharp.net/t_postsharp_patterns_caching_implementatio...

    Then you add your enhancer to the chain of responsibility. For example if you're using MemoryCacheBackend via

    CachingServices.DefaultBackend = new MemoryCachingBackend();,

    you'd change this to

    CachingServices.DefaultBackend = new ConcurrentCachingBackendEnhancer(new MemoryCachingBackend());.

    You can also ask for this feature at https://postsharp.uservoice.com/.

    Best regards,
    -tony

  2. 2 Posted by jonyadamit on 20 Aug, 2017 05:52 AM

    jonyadamit's Avatar

    OK, thank you very much!

  3. jonyadamit closed this discussion on 20 Aug, 2017 05:52 AM.

  4. jonyadamit re-opened this discussion on 29 Aug, 2017 02:46 PM

  5. 3 Posted by jonyadamit on 29 Aug, 2017 02:46 PM

    jonyadamit's Avatar

    Hi, I have followed your advice (both for the enhancer and uservoice feature request).
    The problem is that there is little documentation regarding the behavior of CachingBackendEnhancer.
    Specifically, the ContainsDependencyAsyncCore and ContainsItemAsyncCore methods never get called (neither their synchronous counterparts).
    So by trial and error I gathered that SetItemAsyncCore only gets called when GetItemAsyncCore returns null.
    This leaves me with very few hooks to enhance the backend with.
    Below is what I have come up with based on these restrictions. The problem is that this code probably doesn't support SlidingExpiration policy since I invoke GetItemAsyncCore only once - but, again, that class doesn't provide me with relevant hooks to achieve my goal.

      public class ConcurrentCachingBackendEnhancer : CachingBackendEnhancer
        {
            private CachingBackend underlyingBackend;
            private ConcurrentDictionary<string, TaskCompletionSource<CacheValue>> values = new ConcurrentDictionary<string, TaskCompletionSource<CacheValue>>();
    
            public ConcurrentCachingBackendEnhancer([Required] CachingBackend underlyingBackend) : base(underlyingBackend)
            {
                this.underlyingBackend = underlyingBackend;
                underlyingBackend.ItemRemoved += UnderlyingBackend_ItemRemoved;
            }
    
            protected override void DisposeCore()
            {
                base.DisposeCore();
                underlyingBackend.ItemRemoved -= UnderlyingBackend_ItemRemoved;
                underlyingBackend = null;
            }   
    
            private void UnderlyingBackend_ItemRemoved(object sender, CacheItemRemovedEventArgs e)
            {
                values.TryRemove(e.Key, out TaskCompletionSource<CacheValue> value);            
            }
         
            protected override Task<CacheValue> GetItemAsyncCore(string key, bool includeDependencies, CancellationToken cancellationToken)
            {
                if (values.TryGetValue(key, out var value))
                    return value.Task;
                else
                {
                    if (values.TryAdd(key, new TaskCompletionSource<CacheValue>()))
                        return Task.FromResult<CacheValue>(null);
                    else
                        return values[key].Task;
                }                        
            }
            
            protected async override Task SetItemAsyncCore(string key, CacheItem item, CancellationToken cancellationToken)
            {
                await base.SetItemAsyncCore(key, item, cancellationToken).ConfigureAwait(false);
                if (values.TryGetValue(key, out var value))
                    value.SetResult(await base.GetItemAsyncCore(key, true, cancellationToken).ConfigureAwait(false));
            }        
        }
    
  6. 4 Posted by jonyadamit on 29 Aug, 2017 03:23 PM

    jonyadamit's Avatar

    Scratch that. I think I found my solution.. Sorry for any inconvenience.
    Here is what I have come up with if anyone's interested (haven't tested this yet!):

      public class ConcurrentCachingBackendEnhancer : CachingBackendEnhancer
        {
            private ConcurrentDictionary<string, TaskCompletionSource<CacheValue>> promises = new ConcurrentDictionary<string, TaskCompletionSource<CacheValue>>();
    
            public ConcurrentCachingBackendEnhancer([Required] CachingBackend underlyingBackend) : base(underlyingBackend)
            {
            
            }
                 
            protected override Task<CacheValue> GetItemAsyncCore(string key, bool includeDependencies, CancellationToken cancellationToken)
            {
                if (promises.TryGetValue(key, out var value))
                    return value.Task;
                else
                    return base.GetItemAsyncCore(key, includeDependencies, cancellationToken);
            }
            
            protected async override Task SetItemAsyncCore(string key, CacheItem item, CancellationToken cancellationToken)
            {
                var promise = new TaskCompletionSource<CacheValue>();            
                if (promises.TryAdd(key, promise))
                {
                    await base.SetItemAsyncCore(key, item, cancellationToken).ConfigureAwait(false);
                    promise.SetResult(await base.GetItemAsyncCore(key, true, cancellationToken).ConfigureAwait(false));
                    promises.TryRemove(key, out promise);
                }
                
            }        
        }
    
  7. jonyadamit closed this discussion on 29 Aug, 2017 03:23 PM.

  8. jonyadamit re-opened this discussion on 26 Dec, 2017 09:42 AM

  9. 5 Posted by jonyadamit on 26 Dec, 2017 09:42 AM

    jonyadamit's Avatar

    Hi, the CachingBackendEnhancer doesn't allow enough control in order to implement concurrency with it, mainly because of the separation between the method that gets an item and the one that sets it (it creates rare race conditions, and implementing a fix with reference counting would be an overkill).
    Also please note that exceptions that occur inside async methods would be swallowed by the CachingBackendEnhancer.

    For now I have reverted back to my own CacheAttribute implementation.

    I understand that PostSharp 5.1 will provide support for a concurrent caching solution. Is it expected to come out soon? I noticed it isn't supported yet in the 5.1.1 preview.

    Thanks in advance, Jonathan.

  10. Support Staff 6 Posted by PostSharp Techn... on 27 Dec, 2017 03:42 PM

    PostSharp Technologies's Avatar

    Hi,

    we had to prioritize support for .NET Standard 2.0 and .NET Core 2.0, which is now the main feature planned for PostSharp 5.1. There's no exact plan yet for the release date. Because of importance of the support for these two platforms, it may also happen that the feature you request would be in PostSharp 5.2 instead of PostSharp 5.1.

    Best regards,
    -tony

  11. 7 Posted by jonyadamit on 27 Dec, 2017 04:35 PM

    jonyadamit's Avatar

    OK, thank you for your response.

  12. jonyadamit closed this discussion on 27 Dec, 2017 04:35 PM.

  13. jonyadamit re-opened this discussion on 17 Jul, 2018 10:44 AM

  14. 8 Posted by jonyadamit on 17 Jul, 2018 10:44 AM

    jonyadamit's Avatar

    You have indicated here that this issue/idea is completed, but far as I can tell - not quite.

    You've introduced locking mechanism, which does not mix well with async/await scenarios.
    What is needed is that a method will return a task, which will be completed with the actual result once the first invocation completes. I'm using a TaskCompletionSource for this purpose in my CacheAttribute. (see my OnInvokeAsync below).
    I might be missing something here though, so please correct me if I am mistaken.

    Here is an example of what I'm doing:

      public async override Task OnInvokeAsync(MethodInterceptionArgs args)
            {
                string key = GetCacheKey(args, keyParameters);
                object cachedValue = MemoryCache.Default[key];
    
                if (cachedValue == null)
                {
                    var state = new object();
                    var promise = promises.GetOrAdd(key, t => new TaskCompletionSource<bool>(state));
                    if (promise.Task.AsyncState == state)
                    {
                        try
                        {
                            var returnVal = await args.InvokeAsync(args.Arguments);
                            SetCache(args, key, returnVal);
    
                            if (promises.TryRemove(key, out var tcs))
                                tcs.SetResult(true);
                        }
                        catch (Exception ex)
                        {
                            promise.SetException(ex);
                            throw;
                        }
    
                    }
                    else
                    {
                        await promise.Task.ConfigureAwait(false);
                        var currentValue = MemoryCache.Default[key];
                        if (currentValue is CustomNull)
                            args.ReturnValue = null;
                        else
                            args.ReturnValue = currentValue;
                    }
    
    
    
                }
            }
    
  15. Support Staff 9 Posted by PostSharp Techn... on 17 Jul, 2018 12:39 PM

    PostSharp Technologies's Avatar

    Hello,

    the locking mechanism (ILockManager and it's implementation LocalLockManager) internally uses SemaphoreSlim.WaitAsync() for waiting, which does not block the calling thread and schedules the continuation after waiting is finished.

    You need to set CachingProfile.LockManager property to LocalLockManager as the default does not have any locking and allows calls to be completely concurrent.

    You can also implement your own (and possibly more efficient) implementation of ILockManager.

    Does this work for you?

    Best regards,
    Daniel

  16. 10 Posted by jonyadamit on 17 Jul, 2018 01:31 PM

    jonyadamit's Avatar

    Yes, I was hoping this was the kind of thing I was missing.
    I think this is worth mentioning in the help files.
    Thank you very much!

  17. Support Staff 11 Posted by PostSharp Techn... on 30 Jul, 2018 02:42 PM

    PostSharp Technologies's Avatar

    Hello,

    We are going to close this request as we believe it was solved. Please feel free to reopen the discussion if you need more help.

    Best regards,
    PostSharp Team

  18. PostSharp Technologies closed this discussion on 30 Jul, 2018 02:42 PM.

Comments are currently closed for this discussion. You can start a new one.

Keyboard shortcuts

Generic

? Show this help
ESC Blurs the current field

Comment Form

r Focus the comment reply box
^ + ↩ Submit the comment

You can use Command ⌘ instead of Control ^ on Mac