blob: d62bdd071da9d7af6290b2d540d352b172d01d53 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Apache.Ignite.EntityFramework.Impl
{
using System;
using System.Collections.Generic;
using System.Data.Entity.Core.Metadata.Edm;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using Apache.Ignite.Core;
using Apache.Ignite.Core.Binary;
using Apache.Ignite.Core.Cache;
using Apache.Ignite.Core.Cache.Configuration;
using Apache.Ignite.Core.Cache.Expiry;
using Apache.Ignite.Core.Common;
using Apache.Ignite.Core.Impl.Cache;
using Apache.Ignite.Core.Impl.Common;
using Apache.Ignite.Core.Log;
/// <summary>
/// Database query cache.
/// </summary>
internal class DbCache
{
/** Extension id. */
private const int ExtensionId = 1;
/** Invalidate sets extension operation. */
private const int OpInvalidateSets = 1;
/** Put data extension operation. */
private const int OpPutItem = 2;
/** Get data extension operation. */
private const int OpGetItem = 3;
/** Max number of cached expiry caches. */
private const int MaxExpiryCaches = 1000;
/** Main cache: stores SQL -> QueryResult mappings. */
private readonly ICache<string, object> _cache;
/** Entity set version cache. */
private readonly ICache<string, long> _metaCache;
/** Cached caches per (expiry_seconds * 10). */
private volatile Dictionary<long, ICache<string, object>> _expiryCaches =
new Dictionary<long, ICache<string, object>>();
/** Sync object. */
private readonly object _syncRoot = new object();
/// <summary>
/// Initializes a new instance of the <see cref="DbCache" /> class.
/// </summary>
/// <param name="ignite">The ignite.</param>
/// <param name="metaCacheConfiguration">The meta cache configuration.</param>
/// <param name="dataCacheConfiguration">The data cache configuration.</param>
[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods",
Justification = "Validation is present")]
public DbCache(IIgnite ignite, CacheConfiguration metaCacheConfiguration,
CacheConfiguration dataCacheConfiguration)
{
IgniteArgumentCheck.NotNull(ignite, "ignite");
IgniteArgumentCheck.NotNull(metaCacheConfiguration, "metaCacheConfiguration");
IgniteArgumentCheck.NotNull(dataCacheConfiguration, "metaCacheConfiguration");
IgniteArgumentCheck.Ensure(metaCacheConfiguration.Name != dataCacheConfiguration.Name,
"dataCacheConfiguration", "Meta and Data cache can't have the same name.");
_metaCache = ignite.GetOrCreateCache<string, long>(metaCacheConfiguration);
_cache = ignite.GetOrCreateCache<string, object>(dataCacheConfiguration);
var metaCfg = _metaCache.GetConfiguration();
if (metaCfg.AtomicityMode != CacheAtomicityMode.Transactional)
throw new IgniteException("EntityFramework meta cache should be Transactional.");
if (metaCfg.CacheMode == CacheMode.Partitioned && metaCfg.Backups < 1)
ignite.Logger.Warn("EntityFramework meta cache is partitioned and has no backups. " +
"This can lead to data loss and incorrect query results.");
}
/// <summary>
/// Gets the cache key to be used with GetItem and PutItem.
/// </summary>
public DbCacheKey GetCacheKey(string key, ICollection<EntitySetBase> dependentEntitySets, DbCachingMode mode)
{
if (mode == DbCachingMode.ReadWrite)
{
var versions = GetEntitySetVersions(dependentEntitySets);
return new DbCacheKey(key, dependentEntitySets, versions);
}
if (mode == DbCachingMode.ReadOnly)
return new DbCacheKey(key, null, null);
throw new ArgumentOutOfRangeException("mode");
}
/// <summary>
/// Gets the item from cache.
/// </summary>
public bool GetItem(DbCacheKey key, out object value)
{
var valueBytes = ((ICacheInternal) _cache).DoOutInOpExtension(ExtensionId, OpGetItem,
w => WriteKey(key, w, false), r => r.ReadObject<byte[]>());
if (valueBytes == null)
{
value = null;
return false;
}
using (var ms = new MemoryStream(valueBytes))
{
value = new BinaryFormatter().Deserialize(ms);
}
return true;
}
/// <summary>
/// Puts the item to cache.
/// </summary>
public void PutItem(DbCacheKey key, object value, TimeSpan absoluteExpiration)
{
using (var stream = new MemoryStream())
{
new BinaryFormatter().Serialize(stream, value);
var valueBytes = stream.ToArray();
var cache = GetCacheWithExpiry(absoluteExpiration);
((ICacheInternal)cache).DoOutInOpExtension<object>(ExtensionId, OpPutItem, w =>
{
WriteKey(key, w, true);
w.WriteByteArray(valueBytes);
}, null);
}
}
/// <summary>
/// Invalidates the sets.
/// </summary>
public void InvalidateSets(ICollection<EntitySetBase> entitySets)
{
Debug.Assert(entitySets != null && entitySets.Count > 0);
// Increase version for each dependent entity set and run a task to clean up old entries.
((ICacheInternal) _metaCache).DoOutInOpExtension<object>(ExtensionId, OpInvalidateSets, w =>
{
w.WriteString(_cache.Name);
w.WriteInt(entitySets.Count);
foreach (var set in entitySets)
w.WriteString(set.Name);
}, null);
}
/// <summary>
/// Gets the cache with expiry policy according to provided expiration date.
/// </summary>
/// <returns>Cache with expiry policy.</returns>
// ReSharper disable once UnusedParameter.Local
private ICache<string, object> GetCacheWithExpiry(TimeSpan absoluteExpiration)
{
if (absoluteExpiration == TimeSpan.MaxValue)
return _cache;
// Round up to 0.1 of a second so that we share expiry caches
var expirySeconds = GetSeconds(absoluteExpiration);
ICache<string, object> expiryCache;
if (_expiryCaches.TryGetValue(expirySeconds, out expiryCache))
return expiryCache;
lock (_syncRoot)
{
if (_expiryCaches.TryGetValue(expirySeconds, out expiryCache))
return expiryCache;
// Copy on write with size limit
_expiryCaches = _expiryCaches.Count > MaxExpiryCaches
? new Dictionary<long, ICache<string, object>>()
: new Dictionary<long, ICache<string, object>>(_expiryCaches);
expiryCache =
_cache.WithExpiryPolicy(GetExpiryPolicy(expirySeconds));
_expiryCaches[expirySeconds] = expiryCache;
return expiryCache;
}
}
/// <summary>
/// Gets the expiry policy.
/// </summary>
private static ExpiryPolicy GetExpiryPolicy(long absoluteSeconds)
{
var absolute = absoluteSeconds != long.MaxValue
? TimeSpan.FromSeconds((double)absoluteSeconds / 10)
: (TimeSpan?) null;
return new ExpiryPolicy(absolute, null, null);
}
/// <summary>
/// Gets the seconds.
/// </summary>
private static long GetSeconds(TimeSpan ts)
{
if (ts == TimeSpan.MaxValue)
return long.MaxValue;
var seconds = ts.TotalSeconds;
if (seconds < 0)
seconds = 0;
return (long) (seconds * 10);
}
/// <summary>
/// Gets the entity set versions.
/// </summary>
private IDictionary<string, long> GetEntitySetVersions(ICollection<EntitySetBase> sets)
{
// LINQ Select allocates less that a new List<> will do.
var versions = _metaCache.GetAll(sets.Select(x => x.Name)).ToDictionary(x => x.Key, x => x.Value);
// Some versions may be missing, fill up with 0.
foreach (var set in sets)
{
if (!versions.ContainsKey(set.Name))
versions[set.Name] = 0;
}
Debug.Assert(sets.Count == versions.Count);
return versions;
}
/// <summary>
/// Writes the key.
/// </summary>
private static void WriteKey(DbCacheKey key, IBinaryRawWriter writer, bool includeNames)
{
writer.WriteString(key.Key);
if (key.EntitySetVersions != null)
{
writer.WriteInt(key.EntitySetVersions.Count);
// Versions should be in the same order, so we can't iterate over the dictionary.
foreach (var entitySet in key.EntitySets)
{
writer.WriteLong(key.EntitySetVersions[entitySet.Name]);
if (includeNames)
writer.WriteString(entitySet.Name);
}
}
else
{
writer.WriteInt(-1);
}
}
}
}