Last active
March 5, 2023 19:21
-
-
Save davidfowl/d16c352c19b89acc2a20fe4c1061cad9 to your computer and use it in GitHub Desktop.
ModuleLoader: This handles concurrent requests adding modules and duplicates
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Concurrent; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Reflection; | |
using System.Runtime.Loader; | |
using System.Threading; | |
using System.Threading.Tasks; | |
using Microsoft.AspNetCore.Mvc.ApplicationParts; | |
using Microsoft.AspNetCore.Mvc.Infrastructure; | |
using Microsoft.Extensions.Primitives; | |
namespace LazyControllers | |
{ | |
/// <summary> | |
/// A custom application part that supports loading assemblies dynamically. AddModuleAsync should complete | |
/// when the module has been successfully loaded by routing and MVC. This allows requests to queue up waiting | |
/// for a module to be loaded. | |
/// | |
/// There's no way to precise way to know when MVC has updated routing's knowledge about new controller routes so we use | |
/// a call to IChangeToken.RegisterChangeCallback as an approximation. | |
/// </summary> | |
public class ModuleLoader : ApplicationPart, IApplicationPartTypeProvider, IActionDescriptorChangeProvider | |
{ | |
private CancellationTokenSource _cts = new CancellationTokenSource(); | |
private readonly ConcurrentDictionary<string, ModuleEntry> _cache = new ConcurrentDictionary<string, ModuleEntry>(); | |
// This list represents the list of unloaded modules that mvc observed (the ones it enumerated). | |
// We can use this to determine which modules to mark completed once MVC finishes loading them. | |
// It doesn't need a lock because MVC applies updates sequentially. | |
private readonly List<ModuleEntry> _observed = new List<ModuleEntry>(); | |
public ModuleLoader(ApplicationPartManager applicationPartManager) | |
{ | |
applicationPartManager.ApplicationParts.Add(this); | |
} | |
public Task AddModuleAsync(string moduleName) | |
{ | |
while (true) | |
{ | |
// Try to get the current module loading entry | |
if (_cache.TryGetValue(moduleName, out var entry)) | |
{ | |
// Return the task from the first request | |
return entry.Task; | |
} | |
// Make a new task that all other requests will wait on if they try to | |
// execute while loading | |
var newEntry = new ModuleEntry(moduleName); | |
if (!_cache.TryAdd(moduleName, newEntry)) | |
{ | |
// We failed to add the entry, that means another thread won the race | |
// start over and try to get the cached entry | |
continue; | |
} | |
// Tell MVC we have a new module | |
Interlocked.Exchange(ref _cts, new CancellationTokenSource())?.Cancel(); | |
return newEntry.Task; | |
} | |
} | |
private static Assembly GetAssembly(string moduleName) | |
=> AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(Environment.CurrentDirectory, "..", moduleName, @"bin\Debug\netcoreapp3.1", moduleName + ".dll")); | |
public IChangeToken GetChangeToken() | |
{ | |
return new ModuleChangeToken(this); | |
} | |
public IEnumerable<TypeInfo> Types | |
{ | |
get | |
{ | |
_observed.Clear(); | |
foreach (var (key, entry) in _cache) | |
{ | |
var assembly = entry.Assembly; | |
// Add the list of entries that MVC read on this update round | |
// we only care about modules that haven't loaded so we can mark them for completion | |
if (!entry.Task.IsCompletedSuccessfully) | |
{ | |
_observed.Add(entry); | |
} | |
foreach (var item in assembly.GetTypes()) | |
{ | |
yield return item.GetTypeInfo(); | |
} | |
} | |
} | |
} | |
public override string Name => "Dynamic Module Loader Part"; | |
internal void UpdateModules() | |
{ | |
// MVC as loaded these entries and told routing the change has applied | |
foreach (var entry in _observed) | |
{ | |
entry.SetLoadingComplete(); | |
} | |
} | |
/// <summary> | |
/// A change token implementation that triggers module loader updates on call to RegisterChangeCallback. | |
/// </summary> | |
private class ModuleChangeToken : IChangeToken | |
{ | |
private readonly ModuleLoader _moduleLoader; | |
private readonly CancellationToken _token; | |
public ModuleChangeToken(ModuleLoader moduleLoader) | |
{ | |
_moduleLoader = moduleLoader; | |
_token = moduleLoader._cts.Token; | |
} | |
public bool ActiveChangeCallbacks => true; | |
public bool HasChanged => _token.IsCancellationRequested; | |
public IDisposable RegisterChangeCallback(Action<object> callback, object state) | |
{ | |
// Notify the module loader | |
_moduleLoader.UpdateModules(); | |
return _token.UnsafeRegister(callback, state); | |
} | |
} | |
private class ModuleEntry | |
{ | |
private Assembly _assembly; | |
private readonly TaskCompletionSource<object> _tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); | |
public string ModuleName { get; } | |
public Task Task => _tcs.Task; | |
public Assembly Assembly | |
{ | |
get | |
{ | |
object obj = this; | |
return LazyInitializer.EnsureInitialized(ref _assembly, ref obj, () => GetAssembly(ModuleName)); | |
} | |
} | |
public ModuleEntry(string moduleName) | |
{ | |
ModuleName = moduleName; | |
} | |
public void SetLoadingComplete() => _tcs.TrySetResult(null); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment