Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,9 @@ protected override string GetThumbUrl(string thumbFileName, string storeLocation
/// </summary>
/// <param name="thumbFileName">Thumb file name</param>
/// <param name="binary">Picture binary</param>
protected override Task SaveThumb(string thumbFileName, byte[] binary)
protected override async Task SaveThumb(string thumbFileName, byte[] binary)
{
CheckBucketExists().Wait();
await CheckBucketExists();

using (Stream stream = new MemoryStream(binary))
{
Expand All @@ -197,11 +197,10 @@ protected override Task SaveThumb(string thumbFileName, byte[] binary)
Key = thumbFileName,
StorageClass = S3StorageClass.Standard
};
_s3Client.PutObjectAsync(putObjectRequest).Wait();
await _s3Client.PutObjectAsync(putObjectRequest);
}

_s3Client.MakeObjectPublicAsync(_bucketName, thumbFileName, true).Wait();
return Task.CompletedTask;
await _s3Client.MakeObjectPublicAsync(_bucketName, thumbFileName, true);
}

/// <summary>
Expand Down
19 changes: 8 additions & 11 deletions src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,25 +110,21 @@ public virtual async Task<T> SetAsync<T>(string key, Func<Task<T>> acquire, int
}
}

public virtual Task RemoveAsync(string key, bool publisher = true)
public virtual async Task RemoveAsync(string key, bool publisher = true)
{
_cache.Remove(key);

if (publisher)
_mediator.Publish(new EntityCacheEvent(key, CacheEvent.RemoveKey));

return Task.CompletedTask;
await _mediator.Publish(new EntityCacheEvent(key, CacheEvent.RemoveKey));
}

public virtual Task RemoveByPrefix(string prefix, bool publisher = true)
public virtual async Task RemoveByPrefix(string prefix, bool publisher = true)
{
var entriesToRemove = CacheEntries.Where(x => x.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
foreach (var cacheEntries in entriesToRemove) _cache.Remove(cacheEntries.Key);

if (publisher)
_mediator.Publish(new EntityCacheEvent(prefix, CacheEvent.RemovePrefix));

return Task.CompletedTask;
await _mediator.Publish(new EntityCacheEvent(prefix, CacheEvent.RemovePrefix));
}

public virtual Task Clear(bool publisher = true)
Expand All @@ -137,10 +133,11 @@ public virtual Task Clear(bool publisher = true)
foreach (var cacheEntry in CacheEntries.Keys.ToList())
_cache.Remove(cacheEntry);

//cancel
//cancel, but do not dispose: a writer that already read this source is still handing it to
//MemoryCache, which registers an eviction callback on it while storing the entry, and that
//registration throws ObjectDisposedException on a disposed source. Cancelling releases the
//registrations, and the source has no timer and no WaitHandle, so it is simply collected
_resetCacheToken.Cancel();
//dispose
_resetCacheToken.Dispose();

_resetCacheToken = new CancellationTokenSource();

Expand Down
29 changes: 9 additions & 20 deletions src/Core/Grand.Infrastructure/StartupBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,34 +231,30 @@ private static void RegisterConfigurations(IServiceCollection services, IConfigu
/// </summary>
/// <param name="services">Collection of service descriptors</param>
/// <param name="configuration">Configuration root of the application</param>
public static void ConfigureServices(IServiceCollection services, IConfiguration configuration)
public static void ConfigureServices(IServiceCollection services, IConfiguration configuration,
IWebHostEnvironment hostingEnvironment)
{
services.AddFeatureManagement();

//find startup configurations provided by other assemblies
var typeSearcher = new TypeSearcher();
services.AddSingleton<ITypeSearcher>(typeSearcher);

var provider = services.BuildServiceProvider();
var hostingEnvironment = provider.GetRequiredService<IWebHostEnvironment>();

//register application
var mvcBuilder = RegisterApplication(services, configuration, hostingEnvironment, typeSearcher);

//register extensions
//register extensions
RegisterExtensions(mvcBuilder, configuration, hostingEnvironment);

var startupConfigurations = typeSearcher.ClassesOfType<IStartupApplication>();

//Register startup
var instancesBefore = startupConfigurations
//instantiate once - the same instances serve both configuration passes
var startupInstances = typeSearcher.ClassesOfType<IStartupApplication>()
.Where(PluginExtensions.OnlyInstalledPlugins)
.Select(startup => (IStartupApplication)Activator.CreateInstance(startup))
.Where(startup => startup!.BeforeConfigure)
.OrderBy(startup => startup.Priority);
.OrderBy(startup => startup!.Priority)
.ToList();

//configure services
foreach (var instance in instancesBefore)
foreach (var instance in startupInstances.Where(startup => startup.BeforeConfigure))
instance.ConfigureServices(services, configuration);

//register mapper configurations
Expand All @@ -273,15 +269,8 @@ public static void ConfigureServices(IServiceCollection services, IConfiguration
//add mediator
AddMediator(services, typeSearcher);

//Register startup
var instancesAfter = startupConfigurations
.Where(PluginExtensions.OnlyInstalledPlugins)
.Select(startup => (IStartupApplication)Activator.CreateInstance(startup))
.Where(startup => !startup!.BeforeConfigure)
.OrderBy(startup => startup.Priority);

//configure services
foreach (var instance in instancesAfter)
foreach (var instance in startupInstances.Where(startup => !startup.BeforeConfigure))
instance.ConfigureServices(services, configuration);

//Execute startup interface
Expand Down
14 changes: 9 additions & 5 deletions src/Modules/Grand.Module.Migration/Startup/StartupApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ public void Configure(WebApplication application, IWebHostEnvironment webHostEnv
if (!DataSettingsManager.DatabaseIsInstalled())
return;
var featureManager = application.Services.GetRequiredService<IFeatureManager>();
if (featureManager.IsEnabledAsync("Grand.Module.Migration").Result)
{
var migrationProcess = application.Services.GetRequiredService<IMigrationProcess>();
migrationProcess.RunMigrationProcess();
}
//Configure is synchronous, so the startup path has to block here
if (!featureManager.IsEnabledAsync("Grand.Module.Migration").GetAwaiter().GetResult())
return;

//IMigrationProcess is scoped - resolving it from the root provider fails scope validation
//and otherwise roots the repository graph for the lifetime of the process
using var scope = application.Services.CreateScope();
var migrationProcess = scope.ServiceProvider.GetRequiredService<IMigrationProcess>();
migrationProcess.RunMigrationProcess();
}

public int Priority => 100;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Grand.Infrastructure.Caching;
using Grand.Infrastructure.Configuration;
using Grand.Infrastructure.Events;
using Grand.Mediator;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -139,4 +140,66 @@ public async Task SetAsync_Key_Exist_ShouldSetCacheEntry()
Assert.IsNotNull(cacheResult);
Assert.AreEqual(cacheEntry, cacheResult);
}

[TestMethod]
public async Task RemoveAsync_AwaitsTheNotification()
{
_mediatorMock
.Setup(x => x.Publish(It.IsAny<EntityCacheEvent>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException("handler failed"));

//a fire-and-forget Publish would swallow this
await Assert.ThrowsAsync<InvalidOperationException>(() => _service.RemoveAsync("key"));
}

[TestMethod]
public async Task RemoveByPrefix_AwaitsTheNotification()
{
_mediatorMock
.Setup(x => x.Publish(It.IsAny<EntityCacheEvent>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException("handler failed"));

await Assert.ThrowsAsync<InvalidOperationException>(() => _service.RemoveByPrefix("key"));
}

/// <summary>
/// Guards against disposing the reset token in <see cref="MemoryCacheBase.Clear" />.
/// </summary>
/// <remarks>
/// A writer reads the token source, then MemoryCache registers an eviction callback on it while
/// storing the entry. Disposing the previous source in Clear makes that registration throw
/// ObjectDisposedException, and swapping the field first only narrows the window rather than
/// closing it. The assertion only fires on an exception actually raised by the race, so this
/// cannot fail spuriously - it can only miss.
/// </remarks>
[TestMethod]
[Timeout(60000)]
[DoNotParallelize]
public void Clear_WhileEntriesAreBeingWritten_DoesNotThrow()
{
Exception captured = null;
var stopWriting = false;

var writer = Task.Run(async () =>
{
var i = 0;
while (!stopWriting)
try
{
await _service.SetAsync($"race-{i++}", () => Task.FromResult("value"));
}
catch (Exception ex)
{
captured ??= ex;
return;
}
Comment thread
KrzysztofPajak marked this conversation as resolved.
Dismissed
});

for (var i = 0; i < 5000 && captured == null; i++) _service.Clear(false).GetAwaiter().GetResult();

stopWriting = true;
writer.Wait(TimeSpan.FromSeconds(5));

Assert.IsNull(captured, $"Clear raced a concurrent write: {captured}");
}
}
2 changes: 1 addition & 1 deletion src/Web/Grand.Web.Admin/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
});

//add services
StartupBase.ConfigureServices(builder.Services, builder.Configuration);
StartupBase.ConfigureServices(builder.Services, builder.Configuration, builder.Environment);

builder.ConfigureApplicationSettings();

Expand Down
2 changes: 1 addition & 1 deletion src/Web/Grand.Web.Store/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
});

//add services
StartupBase.ConfigureServices(builder.Services, builder.Configuration);
StartupBase.ConfigureServices(builder.Services, builder.Configuration, builder.Environment);

builder.ConfigureApplicationSettings();

Expand Down
2 changes: 1 addition & 1 deletion src/Web/Grand.Web.Vendor/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
});

//add services
StartupBase.ConfigureServices(builder.Services, builder.Configuration);
StartupBase.ConfigureServices(builder.Services, builder.Configuration, builder.Environment);

builder.ConfigureApplicationSettings();

Expand Down
2 changes: 1 addition & 1 deletion src/Web/Grand.Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
});

//add services
StartupBase.ConfigureServices(builder.Services, builder.Configuration);
StartupBase.ConfigureServices(builder.Services, builder.Configuration, builder.Environment);

builder.ConfigureApplicationSettings();

Expand Down
Loading