Skip to content

Commit 9c4dce5

Browse files
authored
Merge pull request #9543 from umbraco/v8/bugfix/sqlmaindom-server-terminated
SqlMainDomLock will stop listening if Sql Server connection terminates
2 parents 9cda7d1 + 3927a8d commit 9c4dce5

File tree

2 files changed

+76
-32
lines changed

2 files changed

+76
-32
lines changed

src/Umbraco.Core/Runtime/MainDom.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,16 @@ private bool Acquire()
144144

145145
_logger.Info<MainDom>("Acquiring.");
146146

147-
// Get the lock
148-
var acquired = _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).GetAwaiter().GetResult();
147+
// Get the lock
148+
var acquired = false;
149+
try
150+
{
151+
acquired = _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).GetAwaiter().GetResult();
152+
}
153+
catch (Exception ex)
154+
{
155+
_logger.Error<MainDom>(ex, "Error while acquiring");
156+
}
149157

150158
if (!acquired)
151159
{

src/Umbraco.Core/Runtime/SqlMainDomLock.cs

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System;
1+
using NPoco;
2+
using System;
23
using System.Data;
34
using System.Data.SqlClient;
45
using System.Diagnostics;
@@ -48,19 +49,23 @@ public async Task<bool> AcquireLockAsync(int millisecondsTimeout)
4849
}
4950

5051
if (!(_dbFactory.SqlContext.SqlSyntax is SqlServerSyntaxProvider sqlServerSyntaxProvider))
52+
{
5153
throw new NotSupportedException("SqlMainDomLock is only supported for Sql Server");
54+
}
5255

5356
_sqlServerSyntax = sqlServerSyntaxProvider;
5457

5558
_logger.Debug<SqlMainDomLock>("Acquiring lock...");
5659

5760
var tempId = Guid.NewGuid().ToString();
5861

59-
using var db = _dbFactory.CreateDatabase();
60-
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
62+
IUmbracoDatabase db = null;
6163

6264
try
6365
{
66+
db = _dbFactory.CreateDatabase();
67+
db.BeginTransaction(IsolationLevel.ReadCommitted);
68+
6469
try
6570
{
6671
// wait to get a write lock
@@ -101,7 +106,8 @@ public async Task<bool> AcquireLockAsync(int millisecondsTimeout)
101106
}
102107
finally
103108
{
104-
transaction.Complete();
109+
db?.CompleteTransaction();
110+
db?.Dispose();
105111
}
106112

107113

@@ -154,11 +160,11 @@ private void ListeningLoop()
154160
// new MainDom will just take over.
155161
if (_cancellationTokenSource.IsCancellationRequested)
156162
return;
157-
158-
using var db = _dbFactory.CreateDatabase();
159-
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
163+
IUmbracoDatabase db = null;
160164
try
161165
{
166+
db = _dbFactory.CreateDatabase();
167+
db.BeginTransaction(IsolationLevel.ReadCommitted);
162168
// get a read lock
163169
_sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom);
164170

@@ -182,7 +188,8 @@ private void ListeningLoop()
182188
}
183189
finally
184190
{
185-
transaction.Complete();
191+
db?.CompleteTransaction();
192+
db?.Dispose();
186193
}
187194
}
188195

@@ -201,34 +208,47 @@ private Task<bool> WaitForExistingAsync(string tempId, int millisecondsTimeout)
201208

202209
return Task.Run(() =>
203210
{
204-
using var db = _dbFactory.CreateDatabase();
205-
206-
var watch = new Stopwatch();
207-
watch.Start();
208-
while (true)
211+
try
209212
{
210-
// poll very often, we need to take over as fast as we can
211-
// local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO
212-
Thread.Sleep(1000);
213-
214-
var acquired = TryAcquire(db, tempId, updatedTempId);
215-
if (acquired.HasValue)
216-
return acquired.Value;
213+
using var db = _dbFactory.CreateDatabase();
217214

218-
if (watch.ElapsedMilliseconds >= millisecondsTimeout)
215+
var watch = new Stopwatch();
216+
watch.Start();
217+
while (true)
219218
{
220-
return AcquireWhenMaxWaitTimeElapsed(db);
219+
// poll very often, we need to take over as fast as we can
220+
// local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO
221+
Thread.Sleep(1000);
222+
223+
var acquired = TryAcquire(db, tempId, updatedTempId);
224+
if (acquired.HasValue)
225+
return acquired.Value;
226+
227+
if (watch.ElapsedMilliseconds >= millisecondsTimeout)
228+
{
229+
return AcquireWhenMaxWaitTimeElapsed(db);
230+
}
221231
}
222232
}
233+
catch (Exception ex)
234+
{
235+
_logger.Error<SqlMainDomLock>(ex, "An error occurred trying to acquire and waiting for existing SqlMainDomLock to shutdown");
236+
return false;
237+
}
238+
223239
}, _cancellationTokenSource.Token);
224240
}
225241

226242
private bool? TryAcquire(IUmbracoDatabase db, string tempId, string updatedTempId)
227243
{
228-
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
244+
// Creates a separate transaction to the DB instance so we aren't allocating tons of new DB instances for each transaction
245+
// since this is executed in a tight loop
246+
247+
ITransaction transaction = null;
229248

230249
try
231250
{
251+
transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
232252
// get a read lock
233253
_sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom);
234254

@@ -274,14 +294,18 @@ private Task<bool> WaitForExistingAsync(string tempId, int millisecondsTimeout)
274294
}
275295
finally
276296
{
277-
transaction.Complete();
297+
transaction?.Complete();
298+
transaction?.Dispose();
278299
}
279300

280301
return null; // continue
281302
}
282303

283304
private bool AcquireWhenMaxWaitTimeElapsed(IUmbracoDatabase db)
284305
{
306+
// Creates a separate transaction to the DB instance so we aren't allocating tons of new DB instances for each transaction
307+
// since this is executed in a tight loop
308+
285309
// if the timeout has elapsed, it either means that the other main dom is taking too long to shutdown,
286310
// or it could mean that the previous appdomain was terminated and didn't clear out the main dom SQL row
287311
// and it's just been left as an orphan row.
@@ -291,10 +315,12 @@ private bool AcquireWhenMaxWaitTimeElapsed(IUmbracoDatabase db)
291315

292316
_logger.Debug<SqlMainDomLock>("Timeout elapsed, assuming orphan row, acquiring MainDom.");
293317

294-
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
318+
ITransaction transaction = null;
295319

296320
try
297321
{
322+
transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
323+
298324
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
299325

300326
// so now we update the row with our appdomain id
@@ -317,7 +343,8 @@ private bool AcquireWhenMaxWaitTimeElapsed(IUmbracoDatabase db)
317343
}
318344
finally
319345
{
320-
transaction.Complete();
346+
transaction?.Complete();
347+
transaction?.Dispose();
321348
}
322349
}
323350

@@ -368,11 +395,12 @@ protected virtual void Dispose(bool disposing)
368395

369396
if (_dbFactory.Configured)
370397
{
371-
using var db = _dbFactory.CreateDatabase();
372-
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
373-
398+
IUmbracoDatabase db = null;
374399
try
375400
{
401+
db = _dbFactory.CreateDatabase();
402+
db.BeginTransaction(IsolationLevel.ReadCommitted);
403+
376404
// get a write lock
377405
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
378406

@@ -399,7 +427,15 @@ protected virtual void Dispose(bool disposing)
399427
}
400428
finally
401429
{
402-
transaction.Complete();
430+
try
431+
{
432+
db?.CompleteTransaction();
433+
db?.Dispose();
434+
}
435+
catch (Exception ex)
436+
{
437+
_logger.Error<SqlMainDomLock>(ex, "Unexpected error during dispose when completing transaction.");
438+
}
403439
}
404440
}
405441
}

0 commit comments

Comments
 (0)