Below is a picture of some profiling with Azure Application Insights. It comes from the execution of that simple request:
public HttpResponseMessage Login(LoginRequest loginRequest)
{
var t = new DependencyTracker();
using (var context = t.TrackDependency(() => new SocialDbContext(), "NewContext"))
{
var user = t.TrackDependency(() => context.Users.AsNoTracking().SingleOrDefault(x => x.Email == loginRequest.Id || x.Name == loginRequest.Id), "GetUser");
if (user == null) return t.TrackDependency(() => Request.CreateResponse(HttpStatusCode.BadRequest, ApiError.WrongCredentials), "WrongCredentialsUser");
var sid = t.TrackDependency(() => context.IdMappers.AsNoTracking().SingleOrDefault(x => x.Provider == "custom" && x.UserId == user.Id)?.Sid, "GettingSid");
var account = t.TrackDependency(() => context.Accounts.AsNoTracking().SingleOrDefault(a => a.Id == sid), "GettingAccount");
if (account == null) return t.TrackDependency(() => Request.CreateResponse(HttpStatusCode.BadRequest, ApiError.WrongCredentials), "WrongCredentialsAccount");
var incoming = t.TrackDependency(() => Hash(loginRequest.Password, account.Salt), "Hash");
var isLoginOk = t.TrackDependency(() => SlowEquals(incoming, account.SaltedAndHashedPassword), "SlowEquals");
if (isLoginOk && account.TempPasswordExpiry != null)
{
if (DateTimeOffset.Now.CompareTo(account.TempPasswordExpiry.Value) > 0)
return t.TrackDependency(() => Request.CreateResponse(HttpStatusCode.BadRequest, ApiError.WrongCredentials), "WrongCredentialsPassword");
return t.TrackDependency(() => Request.CreateResponse(HttpStatusCode.BadRequest, ApiError.TemporaryPassword), "WrongCredentialsExpiredTempPassword");
}
return isLoginOk
? t.TrackDependency(() => SendToken(user.Id, sid), "SendToken")
: t.TrackDependency(() => Request.CreateResponse(HttpStatusCode.Unauthorized, ApiError.WrongCredentials), "WrongCredentialsPassword");
}
}
There are 3 SingleOrDefault calls. In the profiler results I see these 3 calls.
The first one takes 3325ms (3017ms waiting for the connection)
The second one takes 2356ms (2349ms waiting for the connection)
Then the last one takes 0.28ms
What are these incredibly long wait times for the connection?
What causes them and how to get ride of it?
UPDATE
I may be wrong but I think I know what is happening in a mixture of lazy loading and lock on Metdata from that line as explained in this documentation.
Account will need sid but it locked the metadata. So sid waits. Then sid needs user but it locked the metadata. So user waits. Then user gets executed then sid then account.
Although I would have thought that user == null and .Sid would have forced the execution.
Is there a way to turn lazy loading to test what I'm saying?
UPDATE 2
I just tried with this.Configuration.LazyLoadingEnabled = false; in my context ctor but the problem remains :-(
Note that I can only reproduce the issue every 5 min or so. In between it seems there is some caching as the queries are immediately executed.
UPDATE 3
I replaced the code with one query instead of three:
public HttpResponseMessage Login(LoginRequest loginRequest)
{
var t = new DependencyTracker();
using (var context = t.TrackDependency(() => new SocialDbContext(), "NewContext"))
{
var query = from a in context.Users
join b in context.IdMappers on a.Id equals b.UserId
join c in context.Accounts on b.Sid equals c.Id
where b.Provider == "custom" && (a.Email == loginRequest.Id || a.Name == loginRequest.Id)
select new { Sid = c.Id, c.Salt, c.SaltedAndHashedPassword, c.TempPasswordExpiry, UserId = a.Id };
var account = t.TrackDependency(() => query.SingleOrDefault(), "GettingAccount");
if (account == null) return t.TrackDependency(() => Request.CreateResponse(HttpStatusCode.BadRequest, ApiError.WrongCredentials), "WrongCredentialsAccount");
var incoming = t.TrackDependency(() => Hash(loginRequest.Password, account.Salt), "Hash");
var isLoginOk = t.TrackDependency(() => SlowEquals(incoming, account.SaltedAndHashedPassword), "SlowEquals");
if (isLoginOk && account.TempPasswordExpiry != null)
{
if (DateTimeOffset.Now.CompareTo(account.TempPasswordExpiry.Value) > 0)
return t.TrackDependency(() => Request.CreateResponse(HttpStatusCode.BadRequest, ApiError.WrongCredentials), "WrongCredentialsPassword");
return t.TrackDependency(() => Request.CreateResponse(HttpStatusCode.BadRequest, ApiError.TemporaryPassword), "WrongCredentialsExpiredTempPassword");
}
return isLoginOk
? t.TrackDependency(() => SendToken(account.UserId, account.Sid), "SendToken")
: t.TrackDependency(() => Request.CreateResponse(HttpStatusCode.Unauthorized, ApiError.WrongCredentials), "WrongCredentialsPassword");
}
}
But the issue remains even though now there is only one wait.
But again, it only happens every 5 minutes.
Note: I can't reproduce it locally, it only happens on Azure.
UPDATE 4
The issue remains even when invoking a stored procedure through the context. So I guess it's not related to Query Plan.
What I don't understand is that the issue remains as well even if I call the stored procedure with an SqlCommand hence whithout Entity Framework...
UPDATE 5
This is not an Entity framework issue. The problem is waiting for the sql connection. Whether using EF or using SqlCommand the same slow wait time occurs every 5 minutes. I'll close that issue and open a new one with the appropriate question.
This is not an Entity framework issue. The problem is waiting for the sql connection. Whether using EF or using SqlCommand the same slow wait time occurs every 5 minutes.
Related
I'm seen System.Net.Sockets.SocketException: Connection reset by peer error doing the call to retrieve groups using TransitiveMemberOf method from Microsoft.Graph.dll.
public async Task<List<AdGroup>> GetAllGroupsByUserId(string userId, CancellationToken token)
{
_client = GraphConnection.GetGraphClient(_config);
var memberships = new List<AdGroup>();
try
{
IUserTransitiveMemberOfCollectionWithReferencesRequest? next = null;
do
{
IUserTransitiveMemberOfCollectionWithReferencesPage data;
if (next == null)
{
var id = Guid.NewGuid().ToString();
var options = new List<Option>()
{
new QueryOption("$top", "999"),
new HeaderOption("client-request-id", id)
};
data = await _client.Users[userId].TransitiveMemberOf.Request(options).Select("id,displayName").GetAsync(token);
}
else
{
data = await next.GetAsync(token);
}
next = data.NextPageRequest;
if (data.CurrentPage.Count == 0)
break;
var foundGroups = data.CurrentPage
.Select(i =>
{
if (i is Group group)
return new AdGroup()
{
Id = group.Id,
Name = string.IsNullOrWhiteSpace(group.DisplayName) ? group.AdditionalData?["displayName"].ToString() ?? "" : group.DisplayName
};
if (i is DirectoryRole role)
return new AdGroup()
{
Id = role.Id,
Name = string.IsNullOrWhiteSpace(role.DisplayName) ? role.AdditionalData?["displayName"].ToString() ?? "" : role.DisplayName
};
return null;
})
.Where(g => g != null && Constants.CONTAINS_LIST.Any(c => g.Name.Contains(c, StringComparison.InvariantCultureIgnoreCase)));
if (foundGroups != null)
memberships.AddRange(foundGroups);
} while (next != null);
}
catch (Exception e)
{
_logger.LogError(e, "GetAllGroupsByUserId error");
}
return memberships;
}
I tried to run the code from Azure VM ( I have Linux OS there) as well as from my local machine (Windows OS). Now I'm checking the load of VM because the code is running in threads and thinking about what can be wrong.
I've also created a ticken in Azure Portal and I have the following answer from the Microsoft:
This error is not coming from Microsoft Graph rather from your clients
socket (otherwise meaning a network error)
For these errors, you must start with your networking team to
investigate. There could be a number of networking reasons i.e. your
Firewall, Proxy, Loadbalancer, network bandwidth throttling.
That answer didn't help me a lot, but I'm trying to investigate futher.
I will appreciate any help, thanks
In case if someone face the similar issue, I posted the information which helped me to resolve it:
Increase the number of cores on VM from 4 to 16
Decrease the number of tasks from 200 to 100 which were doing the call to MS Graph and some logic
Initialize a new MS Graph client on each iteration inside of the method GetAllGroupsByUserId
After upgrading my project from ef core 2.2 to ef core 3.1 almost all of my entity framework LINQ queries are broken
Here is an example I have a problem with:
System.AggregateException: 'One or more errors occurred. (The LINQ expression 'DbSet
.Where(b => b.IsDeleted == __displayIsDeleted_0)
.Where(b => __properties_1
.Any(p => p.GetValue((object)b) != null && p.GetValue((object)b).ToString().IndexOf(
value: __8__locals1_query_2,
comparisonType: InvariantCultureIgnoreCase) >= 0))' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.)'
public static IQueryable<T> WhereDynamic<T>(
this IQueryable<T> sourceList, string query)
{
if (string.IsNullOrEmpty(query))
{
return sourceList;
}
try
{
var properties = typeof(T).GetProperties()
.Where(x => x.CanRead && x.CanWrite && !x.GetGetMethod().IsVirtual);
//Expression
sourceList = sourceList.Where(c =>
properties.Any(p => p.GetValue(c) != null && p.GetValue(c).ToString()
/* .Contains(query, StringComparison.InvariantCultureIgnoreCase)*/
.IndexOf(query, StringComparison.InvariantCultureIgnoreCase) >= 0 ));
}
catch (Exception e)
{
Console.WriteLine(e);
}
return sourceList;
}
If possible, tell me what changes I should make to this code
EF Core 2 executed code on client side when it was not possible to translate C# to SQL. With this option being disabled in EF Core 3 by default, you could achieve same behaviour with code below
sourceList = sourceList
.ToList()
.Where(c =>
properties.Any(p => p.GetValue(c) != null && p.GetValue(c).ToString()
.IndexOf(query, StringComparison.InvariantCultureIgnoreCase) >= 0 ))
.AsQueryable();
More about client evaluation in this article
I have the following Scenario:
I have 2 web api functions, which delete / insert data into a SQL Server database. The data Access is handled via .net entityframework v6. The insert / delete methods were only called from a local running c# program. I am using HttpClient class to call the web api methods. The web methods works as follows, when I call insert all existing records will be deleted and the new ones will be inserted, so there is no real update process.
Here are my 2 functions:
[HttpDelete()]
public async Task<int> DeleteStartlist(int eventid, int run, int heat, string category)
{
_data.dbsStartlistEntries.RemoveRange(_data.dbsStartlistEntries.Where(s => s.Event.Id == eventid && s.RoundOrder == run && s.HeatOrder == heat && s.Category == category));
return await _data.SaveChangesAsync();
}
[HttpPost()]
public async Task<int> UpdateStartlists(int eventid, List<StartlistEntry> en)
{
try
{
if (en.Count == 0)
return 0;
var xdel = await DeleteStartlist(eventid, en[0].RoundOrder, en[0].HeatOrder, en[0].Category);
var ev = await _data.dbsEvents.FindAsync(eventid);
if (ev != null)
{
en.ForEach(e => e.Event = ev);
_data.dbsStartlistEntries.AddRange(en);
}
return await _data.SaveChangesAsync();
}
catch (System.Exception ex)
{
return 1;
}
}
But now I have the following Problem. For example when I call the Update Method 10 times in a row without waiting between the function calls I receive following exception:
Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. Refresh ObjectStateManager entries.
For me this sounds like a concurrency Problem, but I do not really know how to solve it.
So here is my question, is there a way to let the api calls wait for each other server side, or are they always running concurrent or is there a way to lock the database?
I am investigating slow performance over the network of my application which uses EntityFramework version 5. One option I tried was to retrieve only the Id field, instead of the entire object. However, examining in Wireshark, I see that all objects are transferred anyway. In other words, the following two code blocks cause exactly the same network activity. Would anyone know how I can cause the db to only return Ids in the first query?
List<long> test = dbContext.UserActivities
.Where(ua => ua.Activity.Id == activityId)
.Select(ua => ua.User)
.ToList()
.Select(u => u.Id)
.ToList();
List<long> test = dbContext.UserActivities
.Where(ua => ua.Activity.Id == activityId)
.Select(ua => ua.User)
.ToList();
.ToList() materializes the object. Essentially, executing the query. Anything after is LINQ to Objects.
try something like this:
List<long> test = dbContext.UserActivities
.Where(ua => ua.Activity.Id == activityId)
.Select(ua => ua.User.Id).ToList();
List<long> test = dbContext.UserActivities
.Where(ua => ua.Activity.Id == activityId)
.Select(ua => ua.User.Id).ToList();
I have a EF code first context which represents a queue of jobs which a processing application can retrieve and run. These processing applications can be running on different machines but pointing at the same database.
The context provides a method that returns a QueueItem if there is any work to do, or null, called CollectQueueItem.
To ensure no two applications can pick up the same job, the collection takes place in a transaction with an ISOLATION LEVEL of REPEATABLE READ. This means that if there are two attempts to pick up the same job at the same time, one will be chosen as the deadlock victim and be rolled back. We can handle this by catching the DbUpdateException and return null.
Here is the code for the CollectQueueItem method:
public QueueItem CollectQueueItem()
{
using (var transaction = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.RepeatableRead }))
{
try
{
var queueItem = this.QueueItems.FirstOrDefault(qi => !qi.IsLocked);
if (queueItem != null)
{
queueItem.DateCollected = DateTime.UtcNow;
queueItem.IsLocked = true;
this.SaveChanges();
transaction.Complete();
return queueItem;
}
}
catch (DbUpdateException) //we might have been the deadlock victim. No matter.
{ }
return null;
}
}
I ran a test in LinqPad to check that this is working as expected. Here is the test below:
var ids = Enumerable.Range(0, 8).AsParallel().SelectMany(i =>
Enumerable.Range(0, 100).Select(j => {
using (var context = new QueueContext())
{
var queueItem = context.CollectQueueItem();
return queueItem == null ? -1 : queueItem.OperationId;
}
})
);
var sw = Stopwatch.StartNew();
var results = ids.GroupBy(i => i).ToDictionary(g => g.Key, g => g.Count());
sw.Stop();
Console.WriteLine("Elapsed time: {0}", sw.Elapsed);
Console.WriteLine("Deadlocked: {0}", results.Where(r => r.Key == -1).Select(r => r.Value).SingleOrDefault());
Console.WriteLine("Duplicates: {0}", results.Count(r => r.Key > -1 && r.Value > 1));
//IsolationLevel = IsolationLevel.RepeatableRead:
//Elapsed time: 00:00:26.9198440
//Deadlocked: 634
//Duplicates: 0
//IsolationLevel = IsolationLevel.ReadUncommitted:
//Elapsed time: 00:00:00.8457558
//Deadlocked: 0
//Duplicates: 234
I ran the test a few times. Without the REPEATABLE READ isolation level, the same job is retrieved by different theads (seen in the 234 duplicates). With REPEATABLE READ, jobs are only retrieved once but performance suffers and there are 634 deadlocked transactions.
My question is: is there a way to get this behaviour in EF without the risk of deadlocks or conflicts? I know in real life there will be less contention as the processors won't be continually hitting the database, but nonetheless, is there a way to do this safely without having to handle the DbUpdateException? Can I get performance closer to that of the version without the REPEATABLE READ isolation level? Or are Deadlocks not that bad in fact and I can safely ignore the exception and let the processor retry after a few millis and accept that the performance will be OK if the not all the transactions are happening at the same time?
Thanks in advance!
Id recommend a different approach.
a) sp_getapplock
Use an SQL SP that provides an Application lock feature
So you can have unique app behaviour, which might involve read from the DB or what ever else activity you need to control. It also lets you use EF in a normal way.
OR
b) Optimistic concurrency
http://msdn.microsoft.com/en-us/data/jj592904
//Object Property:
public byte[] RowVersion { get; set; }
//Object Configuration:
Property(p => p.RowVersion).IsRowVersion().IsConcurrencyToken();
a logical extension to the APP lock or used just by itself is the rowversion concurrency field on DB. Allow the dirty read. BUT when someone goes to update the record As collected, it fails if someone beat them to it. Out of the box EF optimistic locking.
You can delete "collected" job records later easily.
This might be better approach unless you expect high levels of concurrency.
As suggested by Phil, I used optimistic concurrency to ensure the job could not be processed more than once. I realised that rather than having to add a dedicated rowversion column I could use the IsLocked bit column as the ConcurrencyToken. Semantically, if this value has changed since we retrieved the row, the update should fail since only one processor should ever be able to lock it. I used the fluent API as below to configure this, although I could also have used the ConcurrencyCheck data annotation.
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<QueueItem>()
.Property(p => p.IsLocked)
.IsConcurrencyToken();
}
I was then able to simple the CollectQueueItem method, losing the TransactionScope entirely and catching the more DbUpdateConcurrencyException.
public OperationQueueItem CollectQueueItem()
{
try
{
var queueItem = this.QueueItems.FirstOrDefault(qi => !qi.IsLocked);
if (queueItem != null)
{
queueItem.DateCollected = DateTime.UtcNow;
queueItem.IsLocked = true;
this.SaveChanges();
return queueItem;
}
}
catch (DbUpdateConcurrencyException) //someone else grabbed the job.
{ }
return null;
}
I reran the tests, you can see it's a great compromise. No duplicates, nearly 100x faster than with REPEATABLE READ, and no DEADLOCKS so the DBAs won't be on my case. Awesome!
//Optimistic Concurrency:
//Elapsed time: 00:00:00.5065586
//Deadlocked: 624
//Duplicates: 0