mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2026-01-01 22:20:52 +00:00
Pull ConcurrentList<T> from ArchiBot
Previous implementation of ConcurrentSortedHashSet was prone to deadlocks and very suboptimal, on top of no guarantee to do what it claimed to.
This commit is contained in:
@@ -76,6 +76,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="3.0.0-preview7.19362.4" />
|
||||
<PackageReference Include="Microsoft.Win32.Registry" Version="4.6.0-preview7.19362.9" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
<PackageReference Include="Nito.AsyncEx.Coordination" Version="5.0.0" />
|
||||
<PackageReference Include="NLog" Version="4.6.6" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="4.8.4" />
|
||||
<PackageReference Include="protobuf-net" Version="3.0.0-alpha.43" />
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace ArchiSteamFarm {
|
||||
internal readonly ConcurrentHashSet<Game> CurrentGamesFarming = new ConcurrentHashSet<Game>();
|
||||
|
||||
[JsonProperty]
|
||||
internal readonly ConcurrentSortedHashSet<Game> GamesToFarm = new ConcurrentSortedHashSet<Game>();
|
||||
internal readonly ConcurrentList<Game> GamesToFarm = new ConcurrentList<Game>();
|
||||
|
||||
[JsonProperty]
|
||||
internal TimeSpan TimeRemaining =>
|
||||
@@ -100,7 +100,6 @@ namespace ArchiSteamFarm {
|
||||
EventSemaphore.Dispose();
|
||||
FarmingInitializationSemaphore.Dispose();
|
||||
FarmingResetSemaphore.Dispose();
|
||||
GamesToFarm.Dispose();
|
||||
|
||||
// Those are objects that might be null and the check should be in-place
|
||||
IdleFarmingTimer?.Dispose();
|
||||
@@ -1058,34 +1057,34 @@ namespace ArchiSteamFarm {
|
||||
|
||||
private async Task SortGamesToFarm() {
|
||||
// Put priority idling appIDs on top
|
||||
IOrderedEnumerable<Game> gamesToFarm = GamesToFarm.OrderByDescending(game => Bot.IsPriorityIdling(game.AppID));
|
||||
IOrderedEnumerable<Game> orderedGamesToFarm = GamesToFarm.OrderByDescending(game => Bot.IsPriorityIdling(game.AppID));
|
||||
|
||||
foreach (BotConfig.EFarmingOrder farmingOrder in Bot.BotConfig.FarmingOrders) {
|
||||
switch (farmingOrder) {
|
||||
case BotConfig.EFarmingOrder.Unordered:
|
||||
break;
|
||||
case BotConfig.EFarmingOrder.AppIDsAscending:
|
||||
gamesToFarm = gamesToFarm.ThenBy(game => game.AppID);
|
||||
orderedGamesToFarm = orderedGamesToFarm.ThenBy(game => game.AppID);
|
||||
|
||||
break;
|
||||
case BotConfig.EFarmingOrder.AppIDsDescending:
|
||||
gamesToFarm = gamesToFarm.ThenByDescending(game => game.AppID);
|
||||
orderedGamesToFarm = orderedGamesToFarm.ThenByDescending(game => game.AppID);
|
||||
|
||||
break;
|
||||
case BotConfig.EFarmingOrder.BadgeLevelsAscending:
|
||||
gamesToFarm = gamesToFarm.ThenBy(game => game.BadgeLevel);
|
||||
orderedGamesToFarm = orderedGamesToFarm.ThenBy(game => game.BadgeLevel);
|
||||
|
||||
break;
|
||||
case BotConfig.EFarmingOrder.BadgeLevelsDescending:
|
||||
gamesToFarm = gamesToFarm.ThenByDescending(game => game.BadgeLevel);
|
||||
orderedGamesToFarm = orderedGamesToFarm.ThenByDescending(game => game.BadgeLevel);
|
||||
|
||||
break;
|
||||
case BotConfig.EFarmingOrder.CardDropsAscending:
|
||||
gamesToFarm = gamesToFarm.ThenBy(game => game.CardsRemaining);
|
||||
orderedGamesToFarm = orderedGamesToFarm.ThenBy(game => game.CardsRemaining);
|
||||
|
||||
break;
|
||||
case BotConfig.EFarmingOrder.CardDropsDescending:
|
||||
gamesToFarm = gamesToFarm.ThenByDescending(game => game.CardsRemaining);
|
||||
orderedGamesToFarm = orderedGamesToFarm.ThenByDescending(game => game.CardsRemaining);
|
||||
|
||||
break;
|
||||
case BotConfig.EFarmingOrder.MarketableAscending:
|
||||
@@ -1095,11 +1094,11 @@ namespace ArchiSteamFarm {
|
||||
if ((marketableAppIDs != null) && (marketableAppIDs.Count > 0)) {
|
||||
switch (farmingOrder) {
|
||||
case BotConfig.EFarmingOrder.MarketableAscending:
|
||||
gamesToFarm = gamesToFarm.ThenBy(game => marketableAppIDs.Contains(game.AppID));
|
||||
orderedGamesToFarm = orderedGamesToFarm.ThenBy(game => marketableAppIDs.Contains(game.AppID));
|
||||
|
||||
break;
|
||||
case BotConfig.EFarmingOrder.MarketableDescending:
|
||||
gamesToFarm = gamesToFarm.ThenByDescending(game => marketableAppIDs.Contains(game.AppID));
|
||||
orderedGamesToFarm = orderedGamesToFarm.ThenByDescending(game => marketableAppIDs.Contains(game.AppID));
|
||||
|
||||
break;
|
||||
default:
|
||||
@@ -1111,23 +1110,23 @@ namespace ArchiSteamFarm {
|
||||
|
||||
break;
|
||||
case BotConfig.EFarmingOrder.HoursAscending:
|
||||
gamesToFarm = gamesToFarm.ThenBy(game => game.HoursPlayed);
|
||||
orderedGamesToFarm = orderedGamesToFarm.ThenBy(game => game.HoursPlayed);
|
||||
|
||||
break;
|
||||
case BotConfig.EFarmingOrder.HoursDescending:
|
||||
gamesToFarm = gamesToFarm.ThenByDescending(game => game.HoursPlayed);
|
||||
orderedGamesToFarm = orderedGamesToFarm.ThenByDescending(game => game.HoursPlayed);
|
||||
|
||||
break;
|
||||
case BotConfig.EFarmingOrder.NamesAscending:
|
||||
gamesToFarm = gamesToFarm.ThenBy(game => game.GameName);
|
||||
orderedGamesToFarm = orderedGamesToFarm.ThenBy(game => game.GameName);
|
||||
|
||||
break;
|
||||
case BotConfig.EFarmingOrder.NamesDescending:
|
||||
gamesToFarm = gamesToFarm.ThenByDescending(game => game.GameName);
|
||||
orderedGamesToFarm = orderedGamesToFarm.ThenByDescending(game => game.GameName);
|
||||
|
||||
break;
|
||||
case BotConfig.EFarmingOrder.Random:
|
||||
gamesToFarm = gamesToFarm.ThenBy(game => Utilities.RandomNext());
|
||||
orderedGamesToFarm = orderedGamesToFarm.ThenBy(game => Utilities.RandomNext());
|
||||
|
||||
break;
|
||||
case BotConfig.EFarmingOrder.RedeemDateTimesAscending:
|
||||
@@ -1157,11 +1156,11 @@ namespace ArchiSteamFarm {
|
||||
|
||||
switch (farmingOrder) {
|
||||
case BotConfig.EFarmingOrder.RedeemDateTimesAscending:
|
||||
gamesToFarm = gamesToFarm.ThenBy(game => redeemDates[game.AppID]);
|
||||
orderedGamesToFarm = orderedGamesToFarm.ThenBy(game => redeemDates[game.AppID]);
|
||||
|
||||
break;
|
||||
case BotConfig.EFarmingOrder.RedeemDateTimesDescending:
|
||||
gamesToFarm = gamesToFarm.ThenByDescending(game => redeemDates[game.AppID]);
|
||||
orderedGamesToFarm = orderedGamesToFarm.ThenByDescending(game => redeemDates[game.AppID]);
|
||||
|
||||
break;
|
||||
default:
|
||||
@@ -1178,8 +1177,11 @@ namespace ArchiSteamFarm {
|
||||
}
|
||||
}
|
||||
|
||||
// We must call ToList() here as we can't replace items while enumerating
|
||||
GamesToFarm.ReplaceWith(gamesToFarm.ToList());
|
||||
// We must call ToList() here as we can't do in-place replace
|
||||
List<Game> gamesToFarm = orderedGamesToFarm.ToList();
|
||||
|
||||
GamesToFarm.Clear();
|
||||
GamesToFarm.AddRange(gamesToFarm);
|
||||
}
|
||||
|
||||
internal sealed class Game : IEquatable<Game> {
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace ArchiSteamFarm.Collections {
|
||||
@@ -30,22 +29,20 @@ namespace ArchiSteamFarm.Collections {
|
||||
public T Current => Enumerator.Current;
|
||||
|
||||
private readonly IEnumerator<T> Enumerator;
|
||||
private readonly SemaphoreSlim Semaphore;
|
||||
private readonly IDisposable Lock;
|
||||
|
||||
object IEnumerator.Current => Current;
|
||||
|
||||
internal ConcurrentEnumerator([NotNull] IReadOnlyCollection<T> collection, [NotNull] SemaphoreSlim semaphore) {
|
||||
if ((collection == null) || (semaphore == null)) {
|
||||
throw new ArgumentNullException(nameof(collection) + " || " + nameof(semaphore));
|
||||
internal ConcurrentEnumerator([NotNull] IReadOnlyCollection<T> collection, [NotNull] IDisposable @lock) {
|
||||
if ((collection == null) || (@lock == null)) {
|
||||
throw new ArgumentNullException(nameof(collection) + " || " + nameof(@lock));
|
||||
}
|
||||
|
||||
Semaphore = semaphore;
|
||||
semaphore.Wait();
|
||||
|
||||
Lock = @lock;
|
||||
Enumerator = collection.GetEnumerator();
|
||||
}
|
||||
|
||||
public void Dispose() => Semaphore.Release();
|
||||
public void Dispose() => Lock.Dispose();
|
||||
public bool MoveNext() => Enumerator.MoveNext();
|
||||
public void Reset() => Enumerator.Reset();
|
||||
}
|
||||
|
||||
122
ArchiSteamFarm/Collections/ConcurrentList.cs
Normal file
122
ArchiSteamFarm/Collections/ConcurrentList.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2019 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed 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.
|
||||
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using JetBrains.Annotations;
|
||||
using Nito.AsyncEx;
|
||||
|
||||
namespace ArchiSteamFarm.Collections {
|
||||
internal sealed class ConcurrentList<T> : IList<T>, IReadOnlyList<T> {
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
internal int Count {
|
||||
get {
|
||||
using (Lock.ReaderLock()) {
|
||||
return BackingCollection.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<T> BackingCollection = new List<T>();
|
||||
private readonly AsyncReaderWriterLock Lock = new AsyncReaderWriterLock();
|
||||
|
||||
int ICollection<T>.Count => Count;
|
||||
int IReadOnlyCollection<T>.Count => Count;
|
||||
|
||||
public T this[int index] {
|
||||
get {
|
||||
using (Lock.ReaderLock()) {
|
||||
return BackingCollection[index];
|
||||
}
|
||||
}
|
||||
|
||||
set {
|
||||
using (Lock.WriterLock()) {
|
||||
BackingCollection[index] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(T item) {
|
||||
using (Lock.WriterLock()) {
|
||||
BackingCollection.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear() {
|
||||
using (Lock.WriterLock()) {
|
||||
BackingCollection.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Contains(T item) {
|
||||
using (Lock.ReaderLock()) {
|
||||
return BackingCollection.Contains(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void CopyTo(T[] array, int arrayIndex) {
|
||||
using (Lock.ReaderLock()) {
|
||||
BackingCollection.CopyTo(array, arrayIndex);
|
||||
}
|
||||
}
|
||||
|
||||
[NotNull]
|
||||
[SuppressMessage("ReSharper", "AnnotationRedundancyInHierarchy")]
|
||||
public IEnumerator<T> GetEnumerator() => new ConcurrentEnumerator<T>(BackingCollection, Lock.ReaderLock());
|
||||
|
||||
public int IndexOf(T item) {
|
||||
using (Lock.ReaderLock()) {
|
||||
return BackingCollection.IndexOf(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void Insert(int index, T item) {
|
||||
using (Lock.WriterLock()) {
|
||||
BackingCollection.Insert(index, item);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(T item) {
|
||||
using (Lock.WriterLock()) {
|
||||
return BackingCollection.Remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveAt(int index) {
|
||||
using (Lock.WriterLock()) {
|
||||
BackingCollection.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
[NotNull]
|
||||
[SuppressMessage("ReSharper", "AnnotationRedundancyInHierarchy")]
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
internal void AddRange([NotNull] IEnumerable<T> collection) {
|
||||
using (Lock.WriterLock()) {
|
||||
BackingCollection.AddRange(collection);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2019 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed 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.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace ArchiSteamFarm.Collections {
|
||||
internal sealed class ConcurrentSortedHashSet<T> : IDisposable, IReadOnlyCollection<T>, ISet<T> {
|
||||
public int Count {
|
||||
get {
|
||||
CollectionSemaphore.Wait();
|
||||
|
||||
try {
|
||||
return BackingCollection.Count;
|
||||
} finally {
|
||||
CollectionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
private readonly HashSet<T> BackingCollection = new HashSet<T>();
|
||||
private readonly SemaphoreSlim CollectionSemaphore = new SemaphoreSlim(1, 1);
|
||||
|
||||
public bool Add(T item) {
|
||||
CollectionSemaphore.Wait();
|
||||
|
||||
try {
|
||||
return BackingCollection.Add(item);
|
||||
} finally {
|
||||
CollectionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear() {
|
||||
CollectionSemaphore.Wait();
|
||||
|
||||
try {
|
||||
BackingCollection.Clear();
|
||||
} finally {
|
||||
CollectionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Contains(T item) {
|
||||
CollectionSemaphore.Wait();
|
||||
|
||||
try {
|
||||
return BackingCollection.Contains(item);
|
||||
} finally {
|
||||
CollectionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "AnnotationRedundancyInHierarchy")]
|
||||
public void CopyTo([NotNull] T[] array, int arrayIndex) {
|
||||
CollectionSemaphore.Wait();
|
||||
|
||||
try {
|
||||
BackingCollection.CopyTo(array, arrayIndex);
|
||||
} finally {
|
||||
CollectionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => CollectionSemaphore.Dispose();
|
||||
|
||||
public void ExceptWith(IEnumerable<T> other) {
|
||||
CollectionSemaphore.Wait();
|
||||
|
||||
try {
|
||||
BackingCollection.ExceptWith(other);
|
||||
} finally {
|
||||
CollectionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
[NotNull]
|
||||
[SuppressMessage("ReSharper", "AnnotationRedundancyInHierarchy")]
|
||||
public IEnumerator<T> GetEnumerator() => new ConcurrentEnumerator<T>(BackingCollection, CollectionSemaphore);
|
||||
|
||||
public void IntersectWith(IEnumerable<T> other) {
|
||||
CollectionSemaphore.Wait();
|
||||
|
||||
try {
|
||||
BackingCollection.IntersectWith(other);
|
||||
} finally {
|
||||
CollectionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsProperSubsetOf(IEnumerable<T> other) {
|
||||
CollectionSemaphore.Wait();
|
||||
|
||||
try {
|
||||
return BackingCollection.IsProperSubsetOf(other);
|
||||
} finally {
|
||||
CollectionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsProperSupersetOf(IEnumerable<T> other) {
|
||||
CollectionSemaphore.Wait();
|
||||
|
||||
try {
|
||||
return BackingCollection.IsProperSupersetOf(other);
|
||||
} finally {
|
||||
CollectionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSubsetOf(IEnumerable<T> other) {
|
||||
CollectionSemaphore.Wait();
|
||||
|
||||
try {
|
||||
return BackingCollection.IsSubsetOf(other);
|
||||
} finally {
|
||||
CollectionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSupersetOf(IEnumerable<T> other) {
|
||||
CollectionSemaphore.Wait();
|
||||
|
||||
try {
|
||||
return BackingCollection.IsSupersetOf(other);
|
||||
} finally {
|
||||
CollectionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Overlaps(IEnumerable<T> other) {
|
||||
CollectionSemaphore.Wait();
|
||||
|
||||
try {
|
||||
return BackingCollection.Overlaps(other);
|
||||
} finally {
|
||||
CollectionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(T item) {
|
||||
CollectionSemaphore.Wait();
|
||||
|
||||
try {
|
||||
return BackingCollection.Remove(item);
|
||||
} finally {
|
||||
CollectionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public bool SetEquals(IEnumerable<T> other) {
|
||||
CollectionSemaphore.Wait();
|
||||
|
||||
try {
|
||||
return BackingCollection.SetEquals(other);
|
||||
} finally {
|
||||
CollectionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void SymmetricExceptWith(IEnumerable<T> other) {
|
||||
CollectionSemaphore.Wait();
|
||||
|
||||
try {
|
||||
BackingCollection.SymmetricExceptWith(other);
|
||||
} finally {
|
||||
CollectionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void UnionWith(IEnumerable<T> other) {
|
||||
CollectionSemaphore.Wait();
|
||||
|
||||
try {
|
||||
BackingCollection.UnionWith(other);
|
||||
} finally {
|
||||
CollectionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "AnnotationConflictInHierarchy")]
|
||||
[SuppressMessage("ReSharper", "AssignNullToNotNullAttribute")]
|
||||
void ICollection<T>.Add([NotNull] T item) => Add(item);
|
||||
|
||||
[NotNull]
|
||||
[SuppressMessage("ReSharper", "AnnotationRedundancyInHierarchy")]
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
internal void ReplaceWith([NotNull] IEnumerable<T> other) {
|
||||
CollectionSemaphore.Wait();
|
||||
|
||||
try {
|
||||
BackingCollection.Clear();
|
||||
|
||||
foreach (T item in other) {
|
||||
BackingCollection.Add(item);
|
||||
}
|
||||
} finally {
|
||||
CollectionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user