Skip to content

Add monitor based sync only locker for ReadWrite cache #2944

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

Merged
merged 11 commits into from
Feb 14, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/AsyncGenerator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@
- conversion: Ignore
anyBaseTypeRule: IsTestCase
executionPhase: PostProviders
- conversion: Ignore
name: SyncOnlyCacheFixture
ignoreDocuments:
- filePathEndsWith: Linq/MathTests.cs
- filePathEndsWith: Linq/ExpressionSessionLeakTest.cs
Expand Down
180 changes: 180 additions & 0 deletions src/NHibernate.Test/CacheTest/SyncOnlyCacheFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.Threading;
using NHibernate.Cache;
using NHibernate.Cache.Access;
using NHibernate.Cfg;
using NUnit.Framework;
using Environment = NHibernate.Cfg.Environment;

namespace NHibernate.Test.CacheTest
{
[TestFixture]
public class SyncOnlyCacheFixture : TestCase
{
protected override void Configure(Configuration cfg)
{
base.Configure(cfg);
cfg.SetProperty(Environment.CacheReadWriteLockFactory, "sync");
}

[Test]
public void TestSimpleCache()
{
DoTestCache(new HashtableCacheProvider());
}

private CacheKey CreateCacheKey(string text)
{
return new CacheKey(text, NHibernateUtil.String, "Foo", null, null);
}

public void DoTestCache(ICacheProvider cacheProvider)
{
var cache = (CacheBase) cacheProvider.BuildCache(typeof(String).FullName, new Dictionary<string, string>());

long longBefore = Timestamper.Next();

Thread.Sleep(15);

long before = Timestamper.Next();

Thread.Sleep(15);

ICacheConcurrencyStrategy ccs = CreateCache(cache);

// cache something
CacheKey fooKey = CreateCacheKey("foo");

Assert.IsTrue(ccs.Put(fooKey, "foo", before, null, null, false));

Thread.Sleep(15);

long after = Timestamper.Next();

Assert.IsNull(ccs.Get(fooKey, longBefore));
Assert.AreEqual("foo", ccs.Get(fooKey, after));
Assert.IsFalse(ccs.Put(fooKey, "foo", before, null, null, false));

// update it;

ISoftLock fooLock = ccs.Lock(fooKey, null);

Assert.IsNull(ccs.Get(fooKey, after));
Assert.IsNull(ccs.Get(fooKey, longBefore));
Assert.IsFalse(ccs.Put(fooKey, "foo", before, null, null, false));

Thread.Sleep(15);

long whileLocked = Timestamper.Next();

Assert.IsFalse(ccs.Put(fooKey, "foo", whileLocked, null, null, false));

Thread.Sleep(15);

ccs.Release(fooKey, fooLock);

Assert.IsNull(ccs.Get(fooKey, after));
Assert.IsNull(ccs.Get(fooKey, longBefore));
Assert.IsFalse(ccs.Put(fooKey, "bar", whileLocked, null, null, false));
Assert.IsFalse(ccs.Put(fooKey, "bar", after, null, null, false));

Thread.Sleep(15);

long longAfter = Timestamper.Next();

Assert.IsTrue(ccs.Put(fooKey, "baz", longAfter, null, null, false));
Assert.IsNull(ccs.Get(fooKey, after));
Assert.IsNull(ccs.Get(fooKey, whileLocked));

Thread.Sleep(15);

long longLongAfter = Timestamper.Next();

Assert.AreEqual("baz", ccs.Get(fooKey, longLongAfter));

// update it again, with multiple locks

ISoftLock fooLock1 = ccs.Lock(fooKey, null);
ISoftLock fooLock2 = ccs.Lock(fooKey, null);

Assert.IsNull(ccs.Get(fooKey, longLongAfter));

Thread.Sleep(15);

whileLocked = Timestamper.Next();

Assert.IsFalse(ccs.Put(fooKey, "foo", whileLocked, null, null, false));

Thread.Sleep(15);

ccs.Release(fooKey, fooLock2);

Thread.Sleep(15);

long betweenReleases = Timestamper.Next();

Assert.IsFalse(ccs.Put(fooKey, "bar", betweenReleases, null, null, false));
Assert.IsNull(ccs.Get(fooKey, betweenReleases));

Thread.Sleep(15);

ccs.Release(fooKey, fooLock1);

Assert.IsFalse(ccs.Put(fooKey, "bar", whileLocked, null, null, false));

Thread.Sleep(15);

longAfter = Timestamper.Next();

Assert.IsTrue(ccs.Put(fooKey, "baz", longAfter, null, null, false));
Assert.IsNull(ccs.Get(fooKey, whileLocked));

Thread.Sleep(15);

longLongAfter = Timestamper.Next();

Assert.AreEqual("baz", ccs.Get(fooKey, longLongAfter));
}

private ICacheConcurrencyStrategy CreateCache(CacheBase cache, string strategy = CacheFactory.ReadWrite)
{
return CacheFactory.CreateCache(strategy, cache, Sfi.Settings);
}

private void DoTestMinValueTimestampOnStrategy(CacheBase cache, ICacheConcurrencyStrategy strategy)
{
CacheKey key = CreateCacheKey("key");
strategy.Cache = cache;
strategy.Put(key, "value", long.MinValue, 0, null, false);

Assert.IsNull(strategy.Get(key, long.MinValue), "{0} strategy fails the test", strategy.GetType());
Assert.IsNull(strategy.Get(key, long.MaxValue), "{0} strategy fails the test", strategy.GetType());
}

[Test]
public void MinValueTimestamp()
{
var cache = new HashtableCacheProvider().BuildCache("region", new Dictionary<string, string>());

DoTestMinValueTimestampOnStrategy(cache, CreateCache(cache));
DoTestMinValueTimestampOnStrategy(cache, CreateCache(cache, CacheFactory.NonstrictReadWrite));
DoTestMinValueTimestampOnStrategy(cache, CreateCache(cache, CacheFactory.ReadOnly));
}

[Test]
public void AsyncOperationsThrow()
{
var cache = new HashtableCacheProvider().BuildCache("region", new Dictionary<string, string>());
var strategy = CreateCache(cache);
CacheKey key = CreateCacheKey("key");
var stamp = Timestamper.Next();
Assert.ThrowsAsync<InvalidOperationException>(
() =>
strategy.PutAsync(key, "value", stamp, 0, null, false, default(CancellationToken)));
Assert.ThrowsAsync<InvalidOperationException>(() => strategy.GetAsync(key, stamp, default(CancellationToken)));
}

protected override string[] Mappings => Array.Empty<string>();
}
}
19 changes: 17 additions & 2 deletions src/NHibernate/Cache/CacheFactory.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using NHibernate.Cfg;
using NHibernate.Util;

namespace NHibernate.Cache
{
Expand Down Expand Up @@ -43,7 +44,7 @@ public static ICacheConcurrencyStrategy CreateCache(

var cache = BuildCacheBase(name, settings, properties);

var ccs = CreateCache(usage, cache);
var ccs = CreateCache(usage, cache, settings);

if (mutable && usage == ReadOnly)
log.Warn("read-only cache configured for mutable: {0}", name);
Expand All @@ -57,7 +58,21 @@ public static ICacheConcurrencyStrategy CreateCache(
/// <param name="usage">The name of the strategy that <see cref="ICacheProvider"/> should use for the class.</param>
/// <param name="cache">The <see cref="CacheBase"/> used for this strategy.</param>
/// <returns>An <see cref="ICacheConcurrencyStrategy"/> to use for this object in the <see cref="ICache"/>.</returns>
// TODO: Since v5.4
//[Obsolete("Please use overload with a CacheBase and Settings parameters.")]
public static ICacheConcurrencyStrategy CreateCache(string usage, CacheBase cache)
{
return CreateCache(usage, cache, null);
}

/// <summary>
/// Creates an <see cref="ICacheConcurrencyStrategy"/> from the parameters.
/// </summary>
/// <param name="usage">The name of the strategy that <see cref="ICacheProvider"/> should use for the class.</param>
/// <param name="cache">The <see cref="CacheBase"/> used for this strategy.</param>
/// <param name="settings">NHibernate settings</param>
/// <returns>An <see cref="ICacheConcurrencyStrategy"/> to use for this object in the <see cref="ICache"/>.</returns>
public static ICacheConcurrencyStrategy CreateCache(string usage, CacheBase cache, Settings settings)
{
if (log.IsDebugEnabled())
log.Debug("cache for: {0} usage strategy: {1}", cache.RegionName, usage);
Expand All @@ -69,7 +84,7 @@ public static ICacheConcurrencyStrategy CreateCache(string usage, CacheBase cach
ccs = new ReadOnlyCache();
break;
case ReadWrite:
ccs = new ReadWriteCache();
ccs = new ReadWriteCache(settings == null ? new AsyncReaderWriterLock() : settings.CacheReadWriteReadWriteLockFactory.Create());
break;
case NonstrictReadWrite:
ccs = new NonstrictReadWriteCache();
Expand Down
35 changes: 35 additions & 0 deletions src/NHibernate/Cache/ICacheLock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Threading.Tasks;
using NHibernate.Util;

namespace NHibernate.Cache
{
public interface ICacheLock : IDisposable
{
IDisposable ReadLock();
IDisposable WriteLock();
Task<IDisposable> ReadLockAsync();
Task<IDisposable> WriteLockAsync();
}

public interface ICacheReadWriteLockFactory
{
ICacheLock Create();
}

class AsyncCacheReadWriteLockFactory : ICacheReadWriteLockFactory
{
public ICacheLock Create()
{
return new AsyncReaderWriterLock();
}
}

class SyncCacheReadWriteLockFactory : ICacheReadWriteLockFactory
{
public ICacheLock Create()
{
return new SyncCacheLock();
}
}
}
12 changes: 11 additions & 1 deletion src/NHibernate/Cache/ReadWriteCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,17 @@ public interface ILockable

private CacheBase _cache;
private int _nextLockId;
private readonly AsyncReaderWriterLock _asyncReaderWriterLock = new AsyncReaderWriterLock();
private readonly ICacheLock _asyncReaderWriterLock;

public ReadWriteCache()
{
_asyncReaderWriterLock = new AsyncReaderWriterLock();
}

public ReadWriteCache(ICacheLock locker)
{
_asyncReaderWriterLock = locker;
}

/// <summary>
/// Gets the cache region name.
Expand Down
49 changes: 49 additions & 0 deletions src/NHibernate/Cache/SyncCacheLock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace NHibernate.Cache
{
class SyncCacheLock : ICacheLock
{
class MonitorLock : IDisposable
{
private readonly object _lockObj;

public MonitorLock(object lockObj)
{
Monitor.Enter(lockObj);
_lockObj = lockObj;
}

public void Dispose()
{
Monitor.Exit(_lockObj);
}
}

public void Dispose()
{
}

public IDisposable ReadLock()
{
return new MonitorLock(this);
}

public IDisposable WriteLock()
{
return new MonitorLock(this);
}

public Task<IDisposable> ReadLockAsync()
{
throw new InvalidOperationException("This locker supports only sync operations.");
}

public Task<IDisposable> WriteLockAsync()
{
throw new InvalidOperationException("This locker supports only sync operations.");
}
}
}
16 changes: 13 additions & 3 deletions src/NHibernate/Cache/UpdateTimestampsCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public partial class UpdateTimestampsCache
{
private static readonly INHibernateLogger log = NHibernateLogger.For(typeof(UpdateTimestampsCache));
private readonly CacheBase _updateTimestamps;
private readonly AsyncReaderWriterLock _asyncReaderWriterLock = new AsyncReaderWriterLock();
private readonly ICacheLock _asyncReaderWriterLock;

public virtual void Clear()
{
Expand All @@ -40,11 +40,21 @@ public UpdateTimestampsCache(Settings settings, IDictionary<string, string> prop
/// <summary>
/// Build the update timestamps cache.
/// </summary>x
/// <param name="cache">The <see cref="ICache" /> to use.</param>
public UpdateTimestampsCache(CacheBase cache)
/// <param name="cache">The <see cref="CacheBase" /> to use.</param>
public UpdateTimestampsCache(CacheBase cache) : this(cache, new AsyncReaderWriterLock())
{
}

/// <summary>
/// Build the update timestamps cache.
/// </summary>
/// <param name="cache">The <see cref="CacheBase" /> to use.</param>
/// <param name="locker">Locker to use.</param>
public UpdateTimestampsCache(CacheBase cache, ICacheLock locker)
{
log.Info("starting update timestamps cache at region: {0}", cache.RegionName);
_updateTimestamps = cache;
_asyncReaderWriterLock = locker;
}

//Since v5.1
Expand Down
1 change: 1 addition & 0 deletions src/NHibernate/Cfg/Environment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ public static string Version
public const string CacheProvider = "cache.provider_class";
public const string UseQueryCache = "cache.use_query_cache";
public const string QueryCacheFactory = "cache.query_cache_factory";
public const string CacheReadWriteLockFactory = "cache.read_write_lock_factory";
public const string UseSecondLevelCache = "cache.use_second_level_cache";
public const string CacheRegionPrefix = "cache.region_prefix";
public const string UseMinimalPuts = "cache.use_minimal_puts";
Expand Down
1 change: 1 addition & 0 deletions src/NHibernate/Cfg/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ public Settings()
public ConnectionReleaseMode ConnectionReleaseMode { get; internal set; }

public ICacheProvider CacheProvider { get; internal set; }
public ICacheReadWriteLockFactory CacheReadWriteReadWriteLockFactory { get; internal set; }

public IQueryCacheFactory QueryCacheFactory { get; internal set; }

Expand Down
Loading