/// <summary>Retrieves entity by type and primary key value and sets database lock on the underlying record. </summary> /// <typeparam name="TEntity">Entity type.</typeparam> /// <param name="session">Entity session.</param> /// <param name="primaryKey">The value of the primary key.</param> /// <param name="lockType">Lock type.</param> /// <returns>An entity instance.</returns> /// <remarks>For composite primary keys pass an instance of primary key /// created using the <see cref="EntitySessionExtensions.CreatePrimaryKey"/> extension method. /// </remarks> public static TEntity GetEntity <TEntity>(this IEntitySession session, object primaryKey, LockType lockType) where TEntity : class { if (lockType == LockType.None) { return(session.GetEntity <TEntity>(primaryKey)); //short path, no locks } Util.CheckParam(primaryKey, nameof(primaryKey)); session.LogMessage("-- Locking entity {0}/{1}", typeof(TEntity).Name, primaryKey); var entInfo = session.Context.App.Model.GetEntityInfo(typeof(TEntity), throwIfNotFound: true); var entSession = (EntitySession)session; EntityKey pk = entInfo.CreatePrimaryKeyInstance(primaryKey); var ent = entSession.SelectByPrimaryKey(entInfo, pk.Values, lockType); return((TEntity)ent); }
public static void ReleaseLocks(this IEntitySession session) { var entSession = (EntitySession)session; var currConn = entSession.CurrentConnection; if (currConn == null || currConn.DbTransaction == null) { return; } session.LogMessage("-- Releasing locks "); currConn.Abort(); if (currConn.Lifetime != ConnectionLifetime.Explicit) { currConn.Close(); entSession.CurrentConnection = null; } }
/// <summary>Retrieves entity by type and primary key value and sets database lock on the underlying record. </summary> /// <typeparam name="TEntity">Entity type.</typeparam> /// <param name="primaryKey">The value of the primary key.</param> /// <param name="lockType">Lock type.</param> /// <returns>An entity instance.</returns> /// <remarks>For composite primary keys pass an instance of primary key /// created using the <see cref="EntitySessionExtensions.CreatePrimaryKey"/> extension method. /// </remarks> public static TEntity GetEntity <TEntity>(this IEntitySession session, object primaryKey, LockType lockType) where TEntity : class { if (lockType == LockType.None) { return(session.GetEntity <TEntity>(primaryKey)); //short path, no locks } Util.CheckParam(primaryKey, nameof(primaryKey)); session.LogMessage("-- Locking entity {0}/{1}", typeof(TEntity).Name, primaryKey); var entInfo = session.Context.App.Model.GetEntityInfo(typeof(TEntity), throwIfNotFound: true); /* * var pkMembers = entInfo.PrimaryKey.KeyMembers; * Util.Check(pkMembers.Count == 1, "Cannot lock entity {0}: composite primary keys not supported.", entInfo.Name); * var pkMember = entInfo.PrimaryKey.KeyMembers[0].Member; * var prmEnt = Expression.Parameter(typeof(TEntity), "e"); * var pkRead = Expression.MakeMemberAccess(prmEnt, pkMember.ClrMemberInfo); * * // PK box - we could use Constant expr to hold PK value directly, but the result is that PK is embedded into translated SQL as literal. * // (that's how Linq translation works). We want a query with parameter, so that translated linq command is cached and reused. * // So we add extra Convert expression to trick LINQ translator. * var pkValueExpr = Expression.Convert(Expression.Constant(primaryKey, typeof(object)), pkMember.DataType); * var eq = Expression.Equal(pkRead, pkValueExpr); // Expression.Constant(primaryKey, pkMember.DataType)); * var filter = Expression.Lambda<Func<TEntity, bool>>(eq, prmEnt); */ var entSession = (EntitySession)session; EntityKey pk = entInfo.CreatePrimaryKeyInstance(primaryKey); var ent = entSession.SelectByPrimaryKey(entInfo, pk.Values, lockType); return((TEntity)ent); /* * var query = session.EntitySet<TEntity>(lockType).Where(filter); * // We use ToList() on entire query instead of First() because we have already filter on PK value, * // and we want to avoid adding any paging (skip/take) clauses to the SQL. * // We use FirstOrDefult on entire list, and check that we got something; if not, we throw clear message. * var ent = query.ToList().FirstOrDefault(); * Util.Check(ent != null, "Entity {0} with ID {1} does not exist, cannot set the lock.", entInfo.EntityType.Name, * primaryKey); * return ent; */ }
//For now implemented using dynamically built LINQ query; stored proc support to come later // TODO: save filter Func in EntityInfo and reuse it public static TEntity GetEntity <TEntity>(this IEntitySession session, object primaryKey, LockOptions options) where TEntity : class { if (options == LockOptions.None) { return(session.GetEntity <TEntity>(primaryKey)); //short path, no locks } session.LogMessage("-- Locking entity {0}/{1}", typeof(TEntity).Name, primaryKey); var entInfo = session.Context.App.Model.GetEntityInfo(typeof(TEntity), throwIfNotFound: true); var pkMembers = entInfo.PrimaryKey.KeyMembers; Util.Check(pkMembers.Count == 1, "Cannot lock entity {0}: composite primary keys not supported.", entInfo.Name); var pkMember = entInfo.PrimaryKey.KeyMembers[0].Member; var prmEnt = Expression.Parameter(typeof(TEntity), "e"); var pkRead = Expression.MakeMemberAccess(prmEnt, pkMember.ClrMemberInfo); var eq = Expression.Equal(pkRead, Expression.Constant(primaryKey, pkMember.DataType)); var filter = Expression.Lambda <Func <TEntity, bool> >(eq, prmEnt); var query = session.EntitySet <TEntity>(options).Where(filter); // We use ToList() on entire query instead of First() because we have already filter on PK value, // and we want to avoid adding any paging (skip/take) clauses to the SQL. // We use FirstOrDefult on entire list, and check that we got something; if not, we throw clear message. var ent = query.ToList().FirstOrDefault(); Util.Check(ent != null, "Entity {0} with ID {1} does not exist, cannot set the lock.", entInfo.EntityType.Name, primaryKey); return(ent); /* * //The following is just a sketch * if (checkLastModifiedId != null) { * Util.Check(entInfo.VersioningMember != null, "Entity {0} has no tracking/versioning column (last modified transaction id), cannot check data version.", entInfo.Name); * var lastTransId = EntityHelper.GetProperty<Guid>(ent, entInfo.VersioningMember.MemberName); * session.Context.ThrowIf(doNotUse_checkLastModifiedId.Value != lastTransId, ClientFaultCodes.ConcurrentUpdate, entInfo.VersioningMember.MemberName, "Entity {0} was modified by concurrent process.", entInfo.Name); * } * */ }
private void RunRandomReadWriteOp(Guid[] docIds, LockType readLock, LockType writeLock, int readWriteCount) { var rand = new Random(Thread.CurrentThread.ManagedThreadId); IEntitySession session = null; // Use context with its own buffered op log - all entries related to single load/update operation are buffered, // and then flushed together at the end of loop body; so they will appear together in the output file var ctx = new OperationContext(_app); ctx.Log = new BufferedLog(ctx.LogContext); for (int i = 0; i < readWriteCount; i++) { session = ctx.OpenSession(); var randomDocId = docIds[rand.Next(docIds.Length)]; IDoc iDoc; IDocDetail iDet; int randomOp = -1; try { Thread.Yield(); var randomValueName = "N" + rand.Next(5); randomOp = rand.Next(7); switch (randomOp) { case 0: case 1: //read and check total session.LogMessage("\r\n----------------- Load, check Doc total ---------------------"); iDoc = session.GetEntity <IDoc>(randomDocId, readLock); Thread.Yield(); //to let other thread mess it up var valuesSum = iDoc.Details.Sum(v => v.Value); if (valuesSum != iDoc.Total) { session.LogMessage("!!!! Sum error: Doc.Total: {0}, Sum(Value): {1}", iDoc.Total, valuesSum); Interlocked.Increment(ref _sumErrorCount); } Thread.Yield(); session.ReleaseLocks(); session.LogMessage("\r\n-------------- Completed Load/check total ---------------------"); break; case 2: case 3: case 4: //insert/update, we give it an edge over deletes, to have 2 upserts for 1 delete session.LogMessage("\r\n----------------- Update/insert DocDetail ---------------------"); iDoc = session.GetEntity <IDoc>(randomDocId, writeLock); Thread.Yield(); iDet = iDoc.Details.FirstOrDefault(iv => iv.Name == randomValueName); if (iDet == null) { iDet = session.NewEntity <IDocDetail>(); iDet.Doc = iDoc; iDet.Name = randomValueName; iDoc.Details.Add(iDet); //to correctly calculate total } iDet.Value = rand.Next(10); iDoc.Total = iDoc.Details.Sum(v => v.Value); Thread.Yield(); session.SaveChanges(); session.LogMessage("\r\n------Completed Update/insert doc detail ---------------------"); var entSession = (Vita.Entities.Runtime.EntitySession)session; if (entSession.CurrentConnection != null) { Debugger.Break(); //check that connection is closed and removed from session } break; case 6: //delete if exists session.LogMessage("\r\n----------------- Delete doc detail ---------------------"); //Note: deletes do not throw any errors - if record does not exist (had been just deleted), stored proc do not throw error iDoc = session.GetEntity <IDoc>(randomDocId, writeLock); Thread.Yield(); //allow others mess up iDet = iDoc.Details.FirstOrDefault(iv => iv.Name == randomValueName); if (iDet != null) { session.DeleteEntity(iDet); iDoc.Details.Remove(iDet); iDoc.Total = iDoc.Details.Sum(v => v.Value); session.SaveChanges(); // even if there's no changes, it will release lock } else { session.ReleaseLocks(); } session.LogMessage("\r\n----------------- Completed delete doc detail ---------------------"); break; }//switch } catch (Exception ex) { //most will be UniqueIndexViolation Debug.WriteLine(ex.ToLogString()); System.Threading.Interlocked.Increment(ref _updateErrorCount); session.Context.Log.AddEntry(new ErrorLogEntry(ctx.LogContext, ex)); var entSession = (Vita.Entities.Runtime.EntitySession)session; if (entSession.CurrentConnection != null) { entSession.CurrentConnection.Close(); } //session.Context.Log.Flush(); _app.Flush(); } finally { //_app.Flush(); } //session.Context.Log.Flush(); } //for i } //method
private void RunRandomReadWriteOp(Guid[] docIds, LockOptions readOptions, LockOptions writeOptions, int readWriteCount) { var rand = new Random(Thread.CurrentThread.ManagedThreadId); IEntitySession session = null; for (int i = 0; i < readWriteCount; i++) { var randomDocId = docIds[rand.Next(docIds.Length)]; var randomValueName = "N" + rand.Next(5); IDoc iDoc; IDocDetail iDet; int randomOp = -1; try { Thread.Yield(); session = _app.OpenSession(); randomOp = rand.Next(5); switch (randomOp) { case 0: case 1: //insert/update, we give it an edge over deletes, to have 2 upserts for 1 delete session.LogMessage("\r\n----------------- Update/insert value ---------------------"); iDoc = session.GetEntity <IDoc>(randomDocId, writeOptions); Thread.Yield(); iDet = iDoc.Details.FirstOrDefault(iv => iv.Name == randomValueName); if (iDet == null) { iDet = session.NewEntity <IDocDetail>(); iDet.Doc = iDoc; iDet.Name = randomValueName; iDoc.Details.Add(iDet); //to correctly calculate total } iDet.Value = rand.Next(10); iDoc.Total = iDoc.Details.Sum(v => v.Value); Thread.Yield(); session.SaveChanges(); var entSession = (Vita.Entities.Runtime.EntitySession)session; if (entSession.CurrentConnection != null) { Debugger.Break(); //check that connection is closed and removed from session } break; case 2: //delete if exists session.LogMessage("\r\n----------------- Delete value ---------------------"); //Note: deletes do not throw any errors - if record does not exist (had been just deleted), stored proc do not throw error iDoc = session.GetEntity <IDoc>(randomDocId, writeOptions); Thread.Yield(); //allow others mess up iDet = iDoc.Details.FirstOrDefault(iv => iv.Name == randomValueName); if (iDet != null) { session.DeleteEntity(iDet); iDoc.Details.Remove(iDet); iDoc.Total = iDoc.Details.Sum(v => v.Value); } Thread.Yield(); session.SaveChanges(); // even if there's no changes, it will release lock break; case 3: case 4: //read and check total session.LogMessage("\r\n----------------- Loading doc ---------------------"); iDoc = session.GetEntity <IDoc>(randomDocId, readOptions); Thread.Yield(); //to let other thread mess it up var valuesSum = iDoc.Details.Sum(v => v.Value); if (valuesSum != iDoc.Total) { session.LogMessage("!!!! Sum error: Doc.Total: {0}, Sum(Value): {1}", iDoc.Total, valuesSum); Interlocked.Increment(ref _sumErrorCount); } Thread.Yield(); session.ReleaseLocks(); break; }//switch WriteLog(session); } catch (Exception ex) { //most will be UniqueIndexViolation Debug.WriteLine(ex.ToLogString()); System.Threading.Interlocked.Increment(ref _updateErrorCount); if (session != null) { WriteLog(session); var log = session.Context.LocalLog.GetAllAsText(); Debug.WriteLine(log); var entSession = (Vita.Entities.Runtime.EntitySession)session; if (entSession.CurrentConnection != null) { entSession.CurrentConnection.Close(); } } } } //for i } //method