Skip to content

Commit

Permalink
Merge pull request #485 from MUnique/dev/persistence-cancellation-sup…
Browse files Browse the repository at this point in the history
…port
  • Loading branch information
sven-n authored Sep 3, 2024
2 parents 610d665 + b980873 commit 1d137d0
Show file tree
Hide file tree
Showing 29 changed files with 362 additions and 230 deletions.
26 changes: 14 additions & 12 deletions src/Persistence/DataSourceBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ namespace MUnique.OpenMU.Persistence;

using System.Collections;
using System.Diagnostics;
using System.Threading;
using Microsoft.Extensions.Logging;
using Nito.AsyncEx;

/// <summary>
/// Provider which provides the latest <see cref="Owner"/> and it's containing
/// Provider which provides the latest <see cref="Owner" /> and it's containing
/// child objects.
/// </summary>
/// <typeparam name="TOwner">The type of the owner.</typeparam>
/// <remarks>
/// Approach: One context for each composition root type. When child data is going to be edited, the whole type
/// should be loaded.
Expand All @@ -37,8 +39,8 @@ public abstract class DataSourceBase<TOwner> : IDataSource<TOwner>
/// <param name="persistenceContextProvider">The persistence context provider.</param>
protected DataSourceBase(ILogger<DataSourceBase<TOwner>> logger, IPersistenceContextProvider persistenceContextProvider)
{
_logger = logger;
ContextProvider = persistenceContextProvider;
this._logger = logger;
this.ContextProvider = persistenceContextProvider;
}

/// <summary>
Expand All @@ -58,33 +60,33 @@ protected DataSourceBase(ILogger<DataSourceBase<TOwner>> logger, IPersistenceCon
/// <inheritdoc />
public bool IsSupporting(Type type)
{
return TypeToEnumerables.ContainsKey(type);
return this.TypeToEnumerables.ContainsKey(type);
}

/// <inheritdoc />
async ValueTask<TOwner> IDataSource<TOwner>.GetOwnerAsync(Guid ownerId)
async ValueTask<TOwner> IDataSource<TOwner>.GetOwnerAsync(Guid ownerId, CancellationToken cancellationToken)
{
return (TOwner)(await this.GetOwnerAsync(ownerId).ConfigureAwait(false));
return (TOwner)(await this.GetOwnerAsync(ownerId, cancellationToken).ConfigureAwait(false));
}

/// <inheritdoc />
public async ValueTask<IContext> GetContextAsync()
public async ValueTask<IContext> GetContextAsync(CancellationToken cancellationToken)
{
return this._context ??= await this.CreateNewContextAsync();
return this._context ??= await this.CreateNewContextAsync().ConfigureAwait(false);
}

/// <inheritdoc />
public async ValueTask<object> GetOwnerAsync(Guid ownerId = default)
public async ValueTask<object> GetOwnerAsync(Guid ownerId = default, CancellationToken cancellationToken = default)
{
using var l = await _loadLock.LockAsync().ConfigureAwait(false);
using var l = await this._loadLock.LockAsync(cancellationToken).ConfigureAwait(false);

if (this._owner is { } owner
&& (ownerId == Guid.Empty || owner.GetId() == ownerId))
{
return owner;
}

var context = await this.GetContextAsync().ConfigureAwait(false);
var context = await this.GetContextAsync(cancellationToken).ConfigureAwait(false);
this._logger.LogDebug("Loading owner ...");
var stopwatch = new Stopwatch();
stopwatch.Start();
Expand All @@ -94,7 +96,7 @@ public async ValueTask<object> GetOwnerAsync(Guid ownerId = default)
}
else
{
owner = (await context.GetByIdAsync<TOwner>(ownerId).ConfigureAwait(false));
owner = (await context.GetByIdAsync<TOwner>(ownerId, cancellationToken).ConfigureAwait(false));
}

this._owner = owner;
Expand Down
52 changes: 35 additions & 17 deletions src/Persistence/EntityFramework/AccountRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework;
using MUnique.OpenMU.Persistence.EntityFramework.Json;
using MUnique.OpenMU.Persistence.EntityFramework.Model;
using System.Linq;
using System.Threading;

/// <summary>
/// Repository for accounts.
Expand All @@ -27,12 +28,14 @@ public AccountRepository(IContextAwareRepositoryProvider repositoryProvider, ILo
}

/// <inheritdoc />
public override async ValueTask<Account?> GetByIdAsync(Guid id)
public override async ValueTask<Account?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

(this.RepositoryProvider as ICacheAwareRepositoryProvider)?.EnsureCachesForCurrentGameConfiguration();

using var context = this.GetContext();
await context.Context.Database.OpenConnectionAsync().ConfigureAwait(false);
await context.Context.Database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
try
{
var accountEntry = context.Context.ChangeTracker.Entries<Account>().FirstOrDefault(a => a.Entity.Id == id);
Expand All @@ -45,7 +48,7 @@ public AccountRepository(IContextAwareRepositoryProvider repositoryProvider, ILo
}

var objectLoader = new AccountJsonObjectLoader();
account = await objectLoader.LoadObjectAsync<Account>(id, context.Context).ConfigureAwait(false);
account = await objectLoader.LoadObjectAsync<Account>(id, context.Context, cancellationToken).ConfigureAwait(false);
if (account != null && !(context.Context.Entry(account) is { } entry && entry.State != EntityState.Detached))
{
context.Context.Attach(account);
Expand All @@ -64,16 +67,23 @@ public AccountRepository(IContextAwareRepositoryProvider repositoryProvider, ILo
/// Gets the account by character name.
/// </summary>
/// <param name="characterName">The character name.</param>
/// <returns>The account Otherwise, null.</returns>
internal async ValueTask<DataModel.Entities.Account?> GetAccountByCharacterNameAsync(string characterName)
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// The account; otherwise, null.
/// </returns>
internal async ValueTask<DataModel.Entities.Account?> GetAccountByCharacterNameAsync(string characterName, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

using var context = this.GetContext();
var accountInfo = await context.Context.Set<Account>()
.AsNoTracking().FirstOrDefaultAsync(a => a.RawCharacters.Any(c => c.Name == characterName)).ConfigureAwait(false);
.AsNoTracking()
.FirstOrDefaultAsync(a => a.RawCharacters.Any(c => c.Name == characterName), cancellationToken)
.ConfigureAwait(false);

if (accountInfo != null)
{
return await this.GetByIdAsync(accountInfo.Id).ConfigureAwait(false);
return await this.GetByIdAsync(accountInfo.Id, cancellationToken).ConfigureAwait(false);
}

return null;
Expand All @@ -84,45 +94,53 @@ public AccountRepository(IContextAwareRepositoryProvider repositoryProvider, ILo
/// </summary>
/// <param name="loginName">The login name.</param>
/// <param name="password">The password.</param>
/// <returns>The account, if the password is correct. Otherwise, null.</returns>
internal async ValueTask<DataModel.Entities.Account?> GetAccountByLoginNameAsync(string loginName, string password)
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// The account, if the password is correct. Otherwise, null.
/// </returns>
internal async ValueTask<DataModel.Entities.Account?> GetAccountByLoginNameAsync(string loginName, string password, CancellationToken cancellationToken = default)
{
using var context = this.GetContext();
return await this.LoadAccountByLoginNameByJsonQueryAsync(loginName, password, context).ConfigureAwait(false);
return await this.LoadAccountByLoginNameByJsonQueryAsync(loginName, password, context, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Gets the account by login name.
/// </summary>
/// <param name="loginName">The login name.</param>
/// <returns>The account, if exist. Otherwise, null.</returns>
internal async ValueTask<DataModel.Entities.Account?> GetAccountByLoginNameAsync(string loginName)
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// The account, if exists. Otherwise, null.
/// </returns>
internal async ValueTask<DataModel.Entities.Account?> GetAccountByLoginNameAsync(string loginName, CancellationToken cancellationToken = default)
{
using var context = this.GetContext();

var accountInfo = await context.Context.Set<Account>()
.Select(a => new { a.Id, a.LoginName })
.AsNoTracking()
.FirstOrDefaultAsync(a => a.LoginName == loginName).ConfigureAwait(false);
.FirstOrDefaultAsync(a => a.LoginName == loginName, cancellationToken).ConfigureAwait(false);

if (accountInfo != null)
{
return await this.GetByIdAsync(accountInfo.Id).ConfigureAwait(false);
return await this.GetByIdAsync(accountInfo.Id, cancellationToken).ConfigureAwait(false);
}

return null;
}

private async ValueTask<Account?> LoadAccountByLoginNameByJsonQueryAsync(string loginName, string password, EntityFrameworkContextBase context)
private async ValueTask<Account?> LoadAccountByLoginNameByJsonQueryAsync(string loginName, string password, EntityFrameworkContextBase context, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

var accountInfo = await context.Context.Set<Account>()
.Select(a => new { a.Id, a.LoginName, a.PasswordHash })
.AsNoTracking()
.FirstOrDefaultAsync(a => a.LoginName == loginName).ConfigureAwait(false);
.FirstOrDefaultAsync(a => a.LoginName == loginName, cancellationToken).ConfigureAwait(false);

if (accountInfo != null && BCrypt.Verify(password, accountInfo.PasswordHash))
{
return await this.GetByIdAsync(accountInfo.Id).ConfigureAwait(false);
return await this.GetByIdAsync(accountInfo.Id, cancellationToken).ConfigureAwait(false);
}

return null;
Expand Down
19 changes: 10 additions & 9 deletions src/Persistence/EntityFramework/CachedRepository{T}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace MUnique.OpenMU.Persistence.EntityFramework;

using System.Collections;
using System.Threading;

/// <summary>
/// A repository which caches all of its data in memory.
Expand Down Expand Up @@ -35,13 +36,13 @@ public CachedRepository(IRepository<T> baseRepository)
protected IRepository<T> BaseRepository { get; }

/// <inheritdoc/>
async ValueTask<IEnumerable> IRepository.GetAllAsync()
async ValueTask<IEnumerable> IRepository.GetAllAsync(CancellationToken cancellationToken = default)
{
return await this.GetAllAsync().ConfigureAwait(false);
return await this.GetAllAsync(cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc/>
public async ValueTask<IEnumerable<T>> GetAllAsync()
public async ValueTask<IEnumerable<T>> GetAllAsync(CancellationToken cancellationToken = default)
{
if (this._allLoaded)
{
Expand All @@ -52,7 +53,7 @@ public async ValueTask<IEnumerable<T>> GetAllAsync()
{
while (this._loading)
{
await Task.Delay(10).ConfigureAwait(false);
await Task.Delay(10, cancellationToken).ConfigureAwait(false);
}

return this._cache.Values;
Expand All @@ -61,7 +62,7 @@ public async ValueTask<IEnumerable<T>> GetAllAsync()
this._loading = true;
try
{
IEnumerable<T> values = await this.BaseRepository.GetAllAsync().ConfigureAwait(false);
IEnumerable<T> values = await this.BaseRepository.GetAllAsync(cancellationToken).ConfigureAwait(false);
foreach (var obj in values)
{
if (!this._cache.ContainsKey(obj.Id))
Expand All @@ -81,17 +82,17 @@ public async ValueTask<IEnumerable<T>> GetAllAsync()
}

/// <inheritdoc/>
public async ValueTask<T?> GetByIdAsync(Guid id)
public async ValueTask<T?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
await this.GetAllAsync().ConfigureAwait(false);
await this.GetAllAsync(cancellationToken).ConfigureAwait(false);
this._cache.TryGetValue(id, out var result);
return result;
}

/// <inheritdoc/>
async ValueTask<object?> IRepository.GetByIdAsync(Guid id)
async ValueTask<object?> IRepository.GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
return await this.GetByIdAsync(id).ConfigureAwait(false);
return await this.GetByIdAsync(id, cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

using System.Threading;

namespace MUnique.OpenMU.Persistence.EntityFramework;

using Microsoft.EntityFrameworkCore;
Expand Down Expand Up @@ -29,18 +31,20 @@ public CachingGameConfigurationRepository(IContextAwareRepositoryProvider reposi
}

/// <inheritdoc />
public override async ValueTask<GameConfiguration?> GetByIdAsync(Guid id)
public override async ValueTask<GameConfiguration?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

if (this.RepositoryProvider.ContextStack.GetCurrentContext() is not EntityFrameworkContextBase currentContext)
{
throw new InvalidOperationException("There is no current context set.");
}

var database = currentContext.Context.Database;
await database.OpenConnectionAsync().ConfigureAwait(false);
await database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
try
{
return await this._objectLoader.LoadObjectAsync<GameConfiguration>(id, currentContext.Context).ConfigureAwait(false);
return await this._objectLoader.LoadObjectAsync<GameConfiguration>(id, currentContext.Context, cancellationToken).ConfigureAwait(false);
}
finally
{
Expand All @@ -49,18 +53,18 @@ public CachingGameConfigurationRepository(IContextAwareRepositoryProvider reposi
}

/// <inheritdoc />
public override async ValueTask<IEnumerable<GameConfiguration>> GetAllAsync()
public override async ValueTask<IEnumerable<GameConfiguration>> GetAllAsync(CancellationToken cancellationToken = default)
{
if (this.RepositoryProvider.ContextStack.GetCurrentContext() is not EntityFrameworkContextBase currentContext)
{
throw new InvalidOperationException("There is no current context set.");
}

var database = currentContext.Context.Database;
await database.OpenConnectionAsync().ConfigureAwait(false);
await database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
try
{
var configs = (await this._objectLoader.LoadAllObjectsAsync<GameConfiguration>(currentContext.Context).ConfigureAwait(false)).ToList();
var configs = (await this._objectLoader.LoadAllObjectsAsync<GameConfiguration>(currentContext.Context, cancellationToken).ConfigureAwait(false)).ToList();

var oldConfig = ((EntityDataContext)currentContext.Context).CurrentGameConfiguration;
try
Expand Down
18 changes: 10 additions & 8 deletions src/Persistence/EntityFramework/ConfigurationTypeRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

using MUnique.OpenMU.DataModel;

namespace MUnique.OpenMU.Persistence.EntityFramework;

using System.Collections;
using System.Collections.Concurrent;
using System.IO;
using System.Threading;
using Microsoft.Extensions.Logging;

using MUnique.OpenMU.DataModel;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.Persistence.EntityFramework.Json;
using MUnique.OpenMU.Persistence.EntityFramework.Model;
Expand Down Expand Up @@ -52,20 +52,22 @@ public ConfigurationTypeRepository(IContextAwareRepositoryProvider repositoryPro
/// Gets all objects by using the <see cref="_collectionSelector"/> to the current <see cref="GameConfiguration"/>.
/// </summary>
/// <returns>All objects of the repository.</returns>
public ValueTask<IEnumerable<T>> GetAllAsync()
public ValueTask<IEnumerable<T>> GetAllAsync(CancellationToken cancellationToken = default)
{
return ValueTask.FromResult<IEnumerable<T>>(this._collectionSelector(this.GetCurrentGameConfiguration()));
}

/// <inheritdoc/>
async ValueTask<IEnumerable> IRepository.GetAllAsync()
async ValueTask<IEnumerable> IRepository.GetAllAsync(CancellationToken cancellationToken = default)
{
return await this.GetAllAsync().ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
return await this.GetAllAsync(cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc />
public ValueTask<T?> GetByIdAsync(Guid id)
public ValueTask<T?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
this.EnsureCacheForCurrentConfiguration();

var dictionary = this._cache[this.GetCurrentGameConfiguration()];
Expand Down Expand Up @@ -103,9 +105,9 @@ public async ValueTask<bool> DeleteAsync(Guid id)
}

/// <inheritdoc />
async ValueTask<object?> IRepository.GetByIdAsync(Guid id)
async ValueTask<object?> IRepository.GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
return await this.GetByIdAsync(id).ConfigureAwait(false);
return await this.GetByIdAsync(id, cancellationToken).ConfigureAwait(false);
}

/// <summary>
Expand Down
Loading

0 comments on commit 1d137d0

Please sign in to comment.