Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementing Custom UpdateTimestampsCache #2192

Closed
gokhanabatay opened this issue Jul 29, 2019 · 1 comment
Closed

Implementing Custom UpdateTimestampsCache #2192

gokhanabatay opened this issue Jul 29, 2019 · 1 comment

Comments

@gokhanabatay
Copy link
Contributor

gokhanabatay commented Jul 29, 2019

Hi,
Please give us a way to implement our custom UpdateTimestampsCache or consider early release of #2129 and #2115. There is a real performance issue.

I made an workaround...
I know that its not a good idea working with readonly fields.

Also I really do not understand why we lock "[MethodImpl(MethodImplOptions.Synchronized)]" retrieve operations with "IsUpdate" and "AreUpToDate" methods.
Below implementation has no lock and no concurrency issue, every second level select operation checks "UpdateTimestamp" from redis at the same time.
I think we need a lock just for "writing cache provider" and somehow key based could be awesome.

Java Implementation no lock

public static class SessionFactoryExtension
{
    public static void FixUpdateTimestampsCacheLockIssue(this ISessionFactory sessionFactory)
    {
        var settings = typeof(SessionFactoryImpl).GetField("settings", BindingFlags.Instance | BindingFlags.NonPublic)
            .GetValue(sessionFactory) as Settings;

        var properties = typeof(SessionFactoryImpl).GetField("properties", BindingFlags.Instance | BindingFlags.NonPublic)
            .GetValue(sessionFactory) as IDictionary<string, string>;

        var entityPersisters = typeof(SessionFactoryImpl).GetField("entityPersisters", BindingFlags.Instance | BindingFlags.NonPublic)
            .GetValue(sessionFactory) as IDictionary<string, IEntityPersister>;

        var cacheableSpaces = entityPersisters.Where(x => x.Value.HasCache).SelectMany(x => x.Value.PropertySpaces).ToHashSet();

        var updateTimestampsCache = new CustomUpdateTimestampsCache(settings, properties, cacheableSpaces);

        sessionFactory.GetType().GetField("updateTimestampsCache", BindingFlags.Instance|BindingFlags.NonPublic)
            .SetValue(sessionFactory, updateTimestampsCache);
        var queryCache = ((SessionFactoryImpl)sessionFactory).QueryCache;
        queryCache.GetType().GetField("_updateTimestampsCache", BindingFlags.Instance | BindingFlags.NonPublic)
                .SetValue(queryCache, updateTimestampsCache);
    }
}

public class CustomUpdateTimestampsCache : NHibernate.Cache.UpdateTimestampsCache
{
    protected readonly ISet<string> CacheableSpaces;
    protected readonly NHibernate.Cache.CacheBase UpdateTimestamps;

    public CustomUpdateTimestampsCache(Settings settings, IDictionary<string, string> props, ISet<string> cacheableSpaces) : base(settings, props)
    {
        CacheableSpaces = cacheableSpaces;
        UpdateTimestamps = typeof(NHibernate.Cache.UpdateTimestampsCache).GetField("_updateTimestamps", BindingFlags.Instance|BindingFlags.NonPublic)
            .GetValue(this as NHibernate.Cache.UpdateTimestampsCache) as NHibernate.Cache.CacheBase;
    }

    #region Invalidate

    public override void Invalidate(IReadOnlyCollection<string> spaces)
    {
        spaces = spaces.Where(x => CacheableSpaces.Contains(x)).ToList();

        if (spaces.Count > 0)
        {
            base.Invalidate(spaces);
        }
    }

    public override void PreInvalidate(IReadOnlyCollection<string> spaces)
    {
        spaces = spaces.Where(x => CacheableSpaces.Contains(x)).ToList();

        if (spaces.Count > 0)
        {
            base.PreInvalidate(spaces);
        }
    }

    public override Task InvalidateAsync(IReadOnlyCollection<string> spaces, CancellationToken cancellationToken)
    {
        if (cancellationToken.IsCancellationRequested)
        {
            return Task.FromCanceled<object>(cancellationToken);
        }

        spaces = spaces.Where(x => CacheableSpaces.Contains(x)).ToList();

        if (spaces.Count == 0)
        {
            return Task.CompletedTask;
        }

        return base.InvalidateAsync(spaces, cancellationToken);
    }

    public override Task PreInvalidateAsync(IReadOnlyCollection<string> spaces, CancellationToken cancellationToken)
    {
        if (cancellationToken.IsCancellationRequested)
        {
            return Task.FromCanceled<object>(cancellationToken);
        }

        spaces = spaces.Where(x => CacheableSpaces.Contains(x)).ToList();

        if (spaces.Count == 0)
        {
            return Task.CompletedTask;
        }

        return base.PreInvalidateAsync(spaces, cancellationToken);
    }

    #endregion

    #region IsUpToDate

    public override bool IsUpToDate(ISet<string> spaces, long timestamp)
    {
        if (spaces.Count == 0)
            return true;
        else
        {
            var lastUpdates = UpdateTimestamps.GetMany(spaces.ToArray<object>());
            return lastUpdates.All(lastUpdate => !IsOutdated(lastUpdate as long?, timestamp));
        }
    }

    public override bool[] AreUpToDate(ISet<string>[] spaces, long[] timestamps)
    {
        if (spaces.Sum(x => x.Count) == 0)
            return Array.Empty<bool>();
        else
        {
            var allSpaces = new HashSet<string>();
            foreach (var sp in spaces)
            {
                allSpaces.UnionWith(sp);
            }

            if (allSpaces.Count == 0)
                return ArrayHelper.Fill(true, spaces.Length);

            var keys = allSpaces.ToArray<object>();

            var index = 0;
            var lastUpdatesBySpace =
                UpdateTimestamps
                    .GetMany(keys)
                    .ToDictionary(u => keys[index++], u => u as long?);

            var results = new bool[spaces.Length];
            for (var i = 0; i < spaces.Length; i++)
            {
                var timestamp = timestamps[i];
                results[i] = spaces[i].All(space => !IsOutdated(lastUpdatesBySpace[space], timestamp));
            }

            return results;
        }
    }

    public override async Task<bool> IsUpToDateAsync(ISet<string> spaces, long timestamp, CancellationToken cancellationToken)
    {
        if (spaces.Count == 0)
            return true;
        else
            return await base.IsUpToDateAsync(spaces, timestamp, cancellationToken);
    }

    public override async Task<bool[]> AreUpToDateAsync(ISet<string>[] spaces, long[] timestamps, CancellationToken cancellationToken)
    {
        if (spaces?.Sum(x => x.Count) == 0)
            return Array.Empty<bool>();
        else
            return await base.AreUpToDateAsync(spaces, timestamps, cancellationToken);
    }

    #endregion

    private static bool IsOutdated(long? lastUpdate, long timestamp)
    {
        if (!lastUpdate.HasValue)
        {
            //the last update timestamp was lost from the cache
            //(or there were no updates since startup!)

            //NOTE: commented out, since users found the "safe" behavior
            //      counter-intuitive when testing, and we couldn't deal
            //      with all the forum posts :-(
            //updateTimestamps.put( space, new Long( updateTimestamps.nextTimestamp() ) )
            //result = false; // safer

            //OR: put a timestamp there, to avoid subsequent expensive
            //    lookups to a distributed cache - this is no good, since
            //    it is non-threadsafe (could hammer effect of an actual
            //    invalidation), and because this is not the way our
            //    preferred distributed caches work (they work by
            //    replication)
            //updateTimestamps.put( space, new Long(Long.MIN_VALUE) )
        }
        else
        {
            if (lastUpdate >= timestamp)
            {
                return true;
            }
        }

        return false;
    }

}
@gokhanabatay
Copy link
Contributor Author

gokhanabatay commented Jun 2, 2021

#2744 #2742 merge of pull request, there will be no need custom UpdateTimestampsCache.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants