Files
ArchiSteamFarm/ArchiSteamFarm/GlobalDatabase.cs

138 lines
4.0 KiB
C#
Raw Normal View History

2016-03-07 02:39:55 +01:00
/*
_ _ _ ____ _ _____
/ \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
/ _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
/ ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
/_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
2017-01-02 20:05:21 +01:00
Copyright 2015-2017 Łukasz "JustArchi" Domeradzki
2016-03-07 02:39:55 +01:00
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.Generic;
2016-03-07 02:39:55 +01:00
using System.IO;
using System.Threading;
using Newtonsoft.Json;
2016-03-07 02:39:55 +01:00
namespace ArchiSteamFarm {
2016-08-19 05:25:08 +02:00
internal sealed class GlobalDatabase : IDisposable {
private static readonly JsonSerializerSettings CustomSerializerSettings = new JsonSerializerSettings {
Converters = new List<JsonConverter>(2) {
new IPAddressConverter(),
new IPEndPointConverter()
}
};
2016-12-07 14:05:19 +01:00
[JsonProperty(Required = Required.DisallowNull)]
internal readonly Guid Guid = Guid.NewGuid();
[JsonProperty(Required = Required.DisallowNull)]
internal readonly InMemoryServerListProvider ServerListProvider = new InMemoryServerListProvider();
private readonly object FileLock = new object();
2016-06-27 18:48:25 +02:00
internal uint CellID {
get { return _CellID; }
2016-06-27 18:48:25 +02:00
set {
if ((value == 0) || (_CellID == value)) {
2016-06-27 18:48:25 +02:00
return;
}
_CellID = value;
Save();
2016-06-27 18:48:25 +02:00
}
}
[JsonProperty(Required = Required.DisallowNull)]
private uint _CellID;
2016-06-27 18:48:25 +02:00
private string FilePath;
// This constructor is used when creating new database
private GlobalDatabase(string filePath) : this() {
if (string.IsNullOrEmpty(filePath)) {
throw new ArgumentNullException(nameof(filePath));
}
FilePath = filePath;
Save();
}
// This constructor is used only by deserializer
private GlobalDatabase() {
ServerListProvider.ServerListUpdated += OnServerListUpdated;
}
Fix async/await with ConcurrentHashSet Now this is a nice bug that was found accidentally by ArchiBoT... ReaderWriterLockSlim() is very decent solution, but it's thread-based, and we're using our ConcurrentHashSet in mixed async/sync context. This means that if we use something like: foreach (var item in concHashSet) { await AnythingAsync().ConfigureAwait(false); } It's totally possible that we'll request read lock as thread 1, and release the read lock as thread 2, which will lead to RWLock exception => System.Threading.SynchronizationLockException: The read lock is being released without being held. Fortunately it looks like we didn't have any scenario like this in ASF, as this was possible only when we async/await while enumerating over ConcurrentHashSet, so that specific bug didn't affect ASF codebase (yet). Still, I must fix this as current implementation is not thread-safe, so our HashSet is in fact not concurrent in the first place. I analyzed possible solutions and there are basically 3: either using ConcurrentDictionary and wrapping around it, replacing lock with SemaphoreSlim, or using third-party AsyncReaderWriterLock from StephenCleary. SemaphoreSlim entirely kills the concept of multiple readers one writer, and could affect performance negatively, moreover - it doesn't support upgreadable lock scenario we have with ReplaceIfNeededWith(). Concurrent dictionary would be nice if I didn't have that awful memory hit from storing mandatory pointless value, plus I don't really like concept of wrapping around conc dictionary if I can simply use it right away and drop my conc hashset entirely. AsyncReaderWriterLock seem to be really well written, and works on Mono + should be compatible with .NET core in the future, so we should go for it as it's the best bet both performance-wise and memory-wise. This brings another package dependency and changes a bit backend of ConcurrentHashSet
2017-02-07 20:14:51 +01:00
public void Dispose() => ServerListProvider.ServerListUpdated -= OnServerListUpdated;
2016-06-27 18:48:25 +02:00
internal static GlobalDatabase Load(string filePath) {
if (string.IsNullOrEmpty(filePath)) {
ASF.ArchiLogger.LogNullError(nameof(filePath));
2016-06-27 18:48:25 +02:00
return null;
}
if (!File.Exists(filePath)) {
return new GlobalDatabase(filePath);
}
GlobalDatabase globalDatabase;
try {
globalDatabase = JsonConvert.DeserializeObject<GlobalDatabase>(File.ReadAllText(filePath), CustomSerializerSettings);
2016-06-27 18:48:25 +02:00
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
2016-06-27 18:48:25 +02:00
return null;
}
if (globalDatabase == null) {
ASF.ArchiLogger.LogNullError(nameof(globalDatabase));
2016-06-27 18:48:25 +02:00
return null;
}
globalDatabase.FilePath = filePath;
return globalDatabase;
}
2016-08-19 05:25:08 +02:00
private void OnServerListUpdated(object sender, EventArgs e) => Save();
2016-07-08 09:48:15 +02:00
private void Save() {
string json = JsonConvert.SerializeObject(this, CustomSerializerSettings);
2016-06-27 18:48:25 +02:00
if (string.IsNullOrEmpty(json)) {
ASF.ArchiLogger.LogNullError(nameof(json));
2016-06-27 18:48:25 +02:00
return;
}
2017-03-15 08:33:38 +01:00
// This call verifies if JSON is alright
// We don't wrap it in try catch as it should always be the case
2017-03-15 08:34:21 +01:00
// And if it's not, we want to know about it (in a crash) and correct it in future version
2017-03-15 08:33:38 +01:00
JsonConvert.DeserializeObject<GlobalDatabase>(json);
lock (FileLock) {
for (byte i = 0; i < 5; i++) {
2016-06-27 18:48:25 +02:00
try {
File.WriteAllText(FilePath, json);
break;
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
2016-06-27 18:48:25 +02:00
}
Thread.Sleep(1000);
}
2016-06-27 18:48:25 +02:00
}
}
}
}