Skip to content

Commit be8e0f3

Browse files
Fix navigationUrlService and underlying models not being thread safe (#19689)
* Fix navigationUrlService and underlying models not being thread safe * Added migration to plan. --------- Co-authored-by: Andy Butland <[email protected]>
1 parent 0507d1a commit be8e0f3

File tree

8 files changed

+220
-5
lines changed

8 files changed

+220
-5
lines changed

src/Umbraco.Core/Collections/ConcurrentHashSet.cs

Lines changed: 177 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Umbraco.Cms.Core.Collections;
99
/// </summary>
1010
/// <typeparam name="T"></typeparam>
1111
[Serializable]
12-
public class ConcurrentHashSet<T> : ICollection<T>
12+
public class ConcurrentHashSet<T> : ICollection<T>, ISet<T>
1313
{
1414
private readonly HashSet<T> _innerSet = new();
1515
private readonly ReaderWriterLockSlim _instanceLocker = new(LockRecursionPolicy.NoRecursion);
@@ -274,4 +274,180 @@ private HashSet<T> GetThreadSafeClone()
274274

275275
return clone;
276276
}
277+
278+
public void ExceptWith(IEnumerable<T> other)
279+
{
280+
try
281+
{
282+
_instanceLocker.EnterWriteLock();
283+
_innerSet.ExceptWith(other);
284+
}
285+
finally
286+
{
287+
if (_instanceLocker.IsWriteLockHeld)
288+
{
289+
_instanceLocker.ExitWriteLock();
290+
}
291+
}
292+
}
293+
294+
public void IntersectWith(IEnumerable<T> other)
295+
{
296+
try
297+
{
298+
_instanceLocker.EnterWriteLock();
299+
_innerSet.IntersectWith(other);
300+
}
301+
finally
302+
{
303+
if (_instanceLocker.IsWriteLockHeld)
304+
{
305+
_instanceLocker.ExitWriteLock();
306+
}
307+
}
308+
}
309+
310+
public bool IsProperSubsetOf(IEnumerable<T> other)
311+
{
312+
try
313+
{
314+
_instanceLocker.EnterReadLock();
315+
return _innerSet.IsProperSubsetOf(other);
316+
}
317+
finally
318+
{
319+
if (_instanceLocker.IsReadLockHeld)
320+
{
321+
_instanceLocker.ExitReadLock();
322+
}
323+
}
324+
}
325+
326+
public bool IsProperSupersetOf(IEnumerable<T> other)
327+
{
328+
try
329+
{
330+
_instanceLocker.EnterReadLock();
331+
return _innerSet.IsProperSupersetOf(other);
332+
}
333+
finally
334+
{
335+
if (_instanceLocker.IsReadLockHeld)
336+
{
337+
_instanceLocker.ExitReadLock();
338+
}
339+
}
340+
}
341+
342+
public bool IsSubsetOf(IEnumerable<T> other)
343+
{
344+
try
345+
{
346+
_instanceLocker.EnterReadLock();
347+
return _innerSet.IsSubsetOf(other);
348+
}
349+
finally
350+
{
351+
if (_instanceLocker.IsReadLockHeld)
352+
{
353+
_instanceLocker.ExitReadLock();
354+
}
355+
}
356+
}
357+
358+
public bool IsSupersetOf(IEnumerable<T> other)
359+
{
360+
try
361+
{
362+
_instanceLocker.EnterReadLock();
363+
return _innerSet.IsSupersetOf(other);
364+
}
365+
finally
366+
{
367+
if (_instanceLocker.IsReadLockHeld)
368+
{
369+
_instanceLocker.ExitReadLock();
370+
}
371+
}
372+
}
373+
374+
public bool Overlaps(IEnumerable<T> other)
375+
{
376+
try
377+
{
378+
_instanceLocker.EnterReadLock();
379+
return _innerSet.Overlaps(other);
380+
}
381+
finally
382+
{
383+
if (_instanceLocker.IsReadLockHeld)
384+
{
385+
_instanceLocker.ExitReadLock();
386+
}
387+
}
388+
}
389+
390+
public bool SetEquals(IEnumerable<T> other)
391+
{
392+
try
393+
{
394+
_instanceLocker.EnterReadLock();
395+
return _innerSet.SetEquals(other);
396+
}
397+
finally
398+
{
399+
if (_instanceLocker.IsReadLockHeld)
400+
{
401+
_instanceLocker.ExitReadLock();
402+
}
403+
}
404+
}
405+
406+
public void SymmetricExceptWith(IEnumerable<T> other)
407+
{
408+
try
409+
{
410+
_instanceLocker.EnterWriteLock();
411+
_innerSet.IntersectWith(other);
412+
}
413+
finally
414+
{
415+
if (_instanceLocker.IsWriteLockHeld)
416+
{
417+
_instanceLocker.ExitWriteLock();
418+
}
419+
}
420+
}
421+
422+
public void UnionWith(IEnumerable<T> other)
423+
{
424+
try
425+
{
426+
_instanceLocker.EnterWriteLock();
427+
_innerSet.UnionWith(other);
428+
}
429+
finally
430+
{
431+
if (_instanceLocker.IsWriteLockHeld)
432+
{
433+
_instanceLocker.ExitWriteLock();
434+
}
435+
}
436+
}
437+
438+
bool ISet<T>.Add(T item)
439+
{
440+
try
441+
{
442+
_instanceLocker.EnterWriteLock();
443+
return _innerSet.Add(item);
444+
}
445+
finally
446+
{
447+
if (_instanceLocker.IsWriteLockHeld)
448+
{
449+
_instanceLocker.ExitWriteLock();
450+
}
451+
}
452+
}
277453
}

src/Umbraco.Core/Models/Navigation/NavigationNode.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
using System.Collections.Concurrent;
2+
using Umbraco.Cms.Core.Collections;
23

34
namespace Umbraco.Cms.Core.Models.Navigation;
45

56
public sealed class NavigationNode
67
{
7-
private HashSet<Guid> _children;
8+
private ConcurrentHashSet<Guid> _children;
89

910
public Guid Key { get; private set; }
1011

@@ -21,7 +22,7 @@ public NavigationNode(Guid key, Guid contentTypeKey, int sortOrder = 0)
2122
Key = key;
2223
ContentTypeKey = contentTypeKey;
2324
SortOrder = sortOrder;
24-
_children = new HashSet<Guid>();
25+
_children = new ConcurrentHashSet<Guid>();
2526
}
2627

2728
public void UpdateSortOrder(int newSortOrder) => SortOrder = newSortOrder;

src/Umbraco.Core/Persistence/Constants-Locks.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,10 @@ public static class Locks
8585
/// Long-running operations.
8686
/// </summary>
8787
public const int LongRunningOperations = -344;
88+
89+
/// <summary>
90+
/// All document URLs.
91+
/// </summary>
92+
public const int DocumentUrls = -345;
8893
}
8994
}

src/Umbraco.Core/Services/DocumentUrlService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ public async Task CreateOrUpdateUrlSegmentsAsync(IEnumerable<IContent> documents
369369

370370
if (toSave.Count > 0)
371371
{
372+
scope.WriteLock(Constants.Locks.DocumentUrls);
372373
_documentUrlRepository.Save(toSave);
373374
}
374375

src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,6 +1051,7 @@ private void CreateLockData()
10511051
_database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.WebhookRequest, Name = "WebhookRequest" });
10521052
_database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.WebhookLogs, Name = "WebhookLogs" });
10531053
_database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.LongRunningOperations, Name = "LongRunningOperations" });
1054+
_database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.DocumentUrls, Name = "DocumentUrls" });
10541055
}
10551056

10561057
private void CreateContentTypeData()

src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,5 +122,6 @@ protected virtual void DefinePlan()
122122

123123
// To 16.2.0
124124
To<V_16_2_0.AddLongRunningOperations>("{741C22CF-5FB8-4343-BF79-B97A58C2CCBA}");
125+
To<V_16_2_0.AddDocumentUrlLock>("{BE11D4D3-3A1F-4598-90D4-B548BD188C48}");
125126
}
126127
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using NPoco;
2+
using Umbraco.Cms.Core;
3+
using Umbraco.Cms.Infrastructure.Persistence;
4+
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
5+
using Umbraco.Extensions;
6+
7+
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_16_2_0;
8+
9+
[Obsolete("Remove in Umbraco 18.")]
10+
internal class AddDocumentUrlLock : MigrationBase
11+
{
12+
public AddDocumentUrlLock(IMigrationContext context)
13+
: base(context)
14+
{
15+
}
16+
17+
protected override void Migrate()
18+
{
19+
Sql<ISqlContext> sql = Database.SqlContext.Sql()
20+
.Select<LockDto>()
21+
.From<LockDto>()
22+
.Where<LockDto>(x => x.Id == Constants.Locks.DocumentUrls);
23+
24+
LockDto? existingLockDto = Database.FirstOrDefault<LockDto>(sql);
25+
if (existingLockDto is null)
26+
{
27+
Database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.DocumentUrls, Name = "DocumentUrls" });
28+
}
29+
}
30+
}

src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_2_0/AddLongRunningOperations.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ protected override void Migrate()
2626
.From<LockDto>()
2727
.Where<LockDto>(x => x.Id == Constants.Locks.LongRunningOperations);
2828

29-
LockDto? longRunningOperationsLock = Database.FirstOrDefault<LockDto>(sql);
30-
if (longRunningOperationsLock is null)
29+
LockDto? existingLockDto = Database.FirstOrDefault<LockDto>(sql);
30+
if (existingLockDto is null)
3131
{
3232
Database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.LongRunningOperations, Name = "LongRunningOperations" });
3333
}

0 commit comments

Comments
 (0)