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.

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