From 1903c0df005bec80dac4a7fca5b20b257420c591 Mon Sep 17 00:00:00 2001 From: Olmo del Corral Cano Date: Wed, 15 Dec 2021 17:28:17 +0100 Subject: [PATCH] generalize CacheInvalidator to ServerBrodcast to support methodName / Argument --- .../AzureServiceBusBroadcast.cs} | 38 +++--- .../Cache/Broadcast/PostgresBroadcast.cs | 68 +++++++++++ .../Cache/Broadcast/SimpleHttpBroadcast.cs | 108 ++++++++++++++++++ Signum.Engine.Extensions/Cache/CacheLogic.cs | 36 ++++-- .../Cache/PostgresCacheInvalidation.cs | 46 -------- .../Cache/SimpleHttpCacheInvalidator.cs | 82 ------------- Signum.React.Extensions/Cache/CacheClient.tsx | 2 + .../Cache/CacheController.cs | 18 +-- .../Cache/CacheStatisticsPage.tsx | 19 +++ 9 files changed, 255 insertions(+), 162 deletions(-) rename Signum.Engine.Extensions/Cache/{AzureServiceBusCacheInvalidator.cs => Broadcast/AzureServiceBusBroadcast.cs} (83%) create mode 100644 Signum.Engine.Extensions/Cache/Broadcast/PostgresBroadcast.cs create mode 100644 Signum.Engine.Extensions/Cache/Broadcast/SimpleHttpBroadcast.cs delete mode 100644 Signum.Engine.Extensions/Cache/PostgresCacheInvalidation.cs delete mode 100644 Signum.Engine.Extensions/Cache/SimpleHttpCacheInvalidator.cs diff --git a/Signum.Engine.Extensions/Cache/AzureServiceBusCacheInvalidator.cs b/Signum.Engine.Extensions/Cache/Broadcast/AzureServiceBusBroadcast.cs similarity index 83% rename from Signum.Engine.Extensions/Cache/AzureServiceBusCacheInvalidator.cs rename to Signum.Engine.Extensions/Cache/Broadcast/AzureServiceBusBroadcast.cs index 4797395302..8f4087b293 100644 --- a/Signum.Engine.Extensions/Cache/AzureServiceBusCacheInvalidator.cs +++ b/Signum.Engine.Extensions/Cache/Broadcast/AzureServiceBusBroadcast.cs @@ -10,10 +10,10 @@ namespace Signum.Engine.Cache; //Never Tested, works only in theory //https://github.com/briandunnington/SynchronizedCache/blob/master/SynchronizedCache.cs //https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/servicebus/Azure.Messaging.ServiceBus/MigrationGuide.md -public class AzureServiceBusCacheInvalidator : ICacheMultiServerInvalidator, IAsyncDisposable +public class AzureServiceBusBroadcast : IServerBroadcast, IAsyncDisposable { - public event Action? ReceiveInvalidation; - + public event Action? Receive; + ServiceBusAdministrationClient adminClient; ServiceBusClient client; @@ -25,7 +25,7 @@ public class AzureServiceBusCacheInvalidator : ICacheMultiServerInvalidator, IAs public DateTime StartTime; - public AzureServiceBusCacheInvalidator(string namespaceConnectionString, string topicName = "cache-invalidation") + public AzureServiceBusBroadcast(string namespaceConnectionString, string topicName = "cache-invalidation") { this.TopicName = topicName; this.SubscriptionName = Environment.MachineName + "-" + Schema.Current.ApplicationName; @@ -33,18 +33,18 @@ public AzureServiceBusCacheInvalidator(string namespaceConnectionString, string adminClient = new ServiceBusAdministrationClient(namespaceConnectionString); client = new ServiceBusClient(namespaceConnectionString); sender = client.CreateSender(this.TopicName); - } - - - - public void SendInvalidation(string cleanName) + } + + + public void Send(string methodName, string argument) { sender.SendMessageAsync(new ServiceBusMessage(BinaryData.FromObjectAsJson(new AzureInvalidationMessage { CreationDate = DateTime.UtcNow, OriginMachineName = Environment.MachineName, OriginApplicationName = Schema.Current.ApplicationName, - CleanName = cleanName, + MethodName = methodName, + Argument = argument, }, EntityJsonContext.FullJsonSerializerOptions))).Wait(); } @@ -79,7 +79,7 @@ async Task StartMessageListener() private Task Processor_ProcessErrorAsync(ProcessErrorEventArgs arg) { - arg.Exception.LogException(ex => ex.ControllerName = nameof(AzureServiceBusCacheInvalidator)); + arg.Exception.LogException(ex => ex.ControllerName = nameof(AzureServiceBusBroadcast)); return Task.CompletedTask; } @@ -96,19 +96,25 @@ private async Task Processor_ProcessMessageAsync(ProcessMessageEventArgs arg) message.OriginApplicationName == Schema.Current.ApplicationName) return; - ReceiveInvalidation?.Invoke(message.CleanName); + Receive?.Invoke(message.MethodName, message.Argument); await arg.CompleteMessageAsync(arg.Message); }catch (Exception ex) { - ex.LogException(ex => ex.ControllerName = nameof(AzureServiceBusCacheInvalidator)); + ex.LogException(ex => ex.ControllerName = nameof(AzureServiceBusBroadcast)); } } public async ValueTask DisposeAsync() { await this.client.DisposeAsync(); - } + } + + public override string ToString() + { + return $"{nameof(AzureServiceBusBroadcast)}(TopicName = {TopicName}, SubscriptionName = {SubscriptionName})"; + } + } #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. @@ -117,6 +123,8 @@ public class AzureInvalidationMessage public DateTime CreationDate; public string OriginMachineName; public string OriginApplicationName; - public string CleanName; + public string MethodName; + + public string Argument { get; internal set; } } #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. diff --git a/Signum.Engine.Extensions/Cache/Broadcast/PostgresBroadcast.cs b/Signum.Engine.Extensions/Cache/Broadcast/PostgresBroadcast.cs new file mode 100644 index 0000000000..62c2faf991 --- /dev/null +++ b/Signum.Engine.Extensions/Cache/Broadcast/PostgresBroadcast.cs @@ -0,0 +1,68 @@ +using Npgsql; +using System.Diagnostics; + +namespace Signum.Engine.Cache; + +public class PostgresBroadcast : IServerBroadcast +{ + public event Action? Receive; + + public void Send(string methodName, string argument) + { + Executor.ExecuteNonQuery($"NOTIFY table_changed, '{methodName}/{Process.GetCurrentProcess().Id}/{argument}'"); + } + + public void Start() + { + Task.Run(() => + { + try + { + var conn = (NpgsqlConnection)Connector.Current.CreateConnection(); + conn.Open(); + conn.Notification += (o, e) => + { + try + { + var methodName = e.Payload.Before('/'); + var after = e.Payload.After("/"); + + var pid = int.Parse(after.Before("/")); + var arguments = after.After("/"); + + if (Process.GetCurrentProcess().Id != pid) + Receive?.Invoke(methodName, arguments); + } + catch (Exception ex) + { + ex.LogException(a => a.ControllerName = nameof(PostgresBroadcast)); + } + }; + + using (var cmd = new NpgsqlCommand("LISTEN table_changed", conn)) + { + cmd.ExecuteNonQuery(); + } + + while (true) + { + conn.Wait(); // Thread will block here + } + } + catch (Exception e) + { + e.LogException(a => + { + a.ControllerName = nameof(PostgresBroadcast); + a.ActionName = "Fatal"; + }); + } + }); + } + + + public override string ToString() + { + return $"{nameof(PostgresBroadcast)}()"; + } +} diff --git a/Signum.Engine.Extensions/Cache/Broadcast/SimpleHttpBroadcast.cs b/Signum.Engine.Extensions/Cache/Broadcast/SimpleHttpBroadcast.cs new file mode 100644 index 0000000000..e9c327f398 --- /dev/null +++ b/Signum.Engine.Extensions/Cache/Broadcast/SimpleHttpBroadcast.cs @@ -0,0 +1,108 @@ + +using Azure.Messaging.ServiceBus; +using Azure.Messaging.ServiceBus.Administration; +using Npgsql; +using Signum.Engine.Json; +using Signum.Entities.Cache; +using Signum.Services; +using System.Diagnostics; +using System.Net.Http; +using System.Net.Http.Json; + +namespace Signum.Engine.Cache; + + +public class SimpleHttpBroadcast : IServerBroadcast +{ + + HttpClient client = new HttpClient(); + readonly string bordcastSecretHash; + readonly string[] broadcastUrls; + + public SimpleHttpBroadcast(string broadcastSecret, string broadcastUrls) + { + this.bordcastSecretHash = Convert.ToBase64String(Security.EncodePassword(broadcastSecret)); + this.broadcastUrls = broadcastUrls + .SplitNoEmpty(new char[] { ';', ',' } /*In theory ; and , are valid in a URL, but since we talk only domain names or IPs...*/) + .Select(a => a.Trim()) + .Where(a => a.HasText()) + .ToArray(); + } + + public event Action? Receive; + + public void Start() + { + } + + //Called from Controller + public void InvalidateTable(InvalidateTableRequest request) + { + if (this.bordcastSecretHash != request.SecretHash) + throw new InvalidOperationException("invalidationSecret does not match"); + + if (request.OriginMachineName == Environment.MachineName || + request.OriginApplicationName == Schema.Current.ApplicationName) + return; + + Receive?.Invoke(request.MethodName, request.Argument); + } + + public void Send(string methodName, string argument) + { + var request = new InvalidateTableRequest + { + MethodName = methodName, + Argument = argument, + SecretHash = this.bordcastSecretHash, + OriginMachineName = Environment.MachineName, + OriginApplicationName = Schema.Current.ApplicationName, + }; + + foreach (var url in broadcastUrls) + { + string? errorBody = null; + try + { + var fullUrl = url.TrimEnd('/') + "/api/cache/invalidateTable"; + + var json = JsonContent.Create(request, options: EntityJsonContext.FullJsonSerializerOptions /*SignumServer.JsonSerializerOptions*/); + + var response = client.PostAsync(fullUrl, json).Result; + + if (!response.IsSuccessStatusCode) + { + errorBody = response.Content.ReadAsStringAsync().Result; + } + + } + + catch (Exception e) + { + e.LogException(a => + { + a.ControllerName = nameof(SimpleHttpBroadcast); + a.Data.Text = errorBody; + }); + } + } + } + + public override string ToString() + { + return $"{nameof(SimpleHttpBroadcast)}(Urls={broadcastUrls.ToString(", ")})"; + } + + +} + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. +public class InvalidateTableRequest +{ + public string OriginMachineName; + public string OriginApplicationName; + public string SecretHash; + public string Argument; + public string MethodName; +} +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. diff --git a/Signum.Engine.Extensions/Cache/CacheLogic.cs b/Signum.Engine.Extensions/Cache/CacheLogic.cs index ec1105bd0b..1d66918910 100644 --- a/Signum.Engine.Extensions/Cache/CacheLogic.cs +++ b/Signum.Engine.Extensions/Cache/CacheLogic.cs @@ -16,16 +16,16 @@ namespace Signum.Engine.Cache; -public interface ICacheMultiServerInvalidator +public interface IServerBroadcast { void Start(); - void SendInvalidation(string cleanName); - event Action? ReceiveInvalidation; + void Send(string methodName, string argument); + event Action? Receive; } public static class CacheLogic { - public static ICacheMultiServerInvalidator? CacheInvalidator { get; private set; } + public static IServerBroadcast? ServerBroadcast { get; private set; } public static bool WithSqlDependency { get; internal set; } @@ -50,7 +50,7 @@ public static void AssertStarted(SchemaBuilder sb) /// Change Server Authentication mode and enable SA: http://msdn.microsoft.com/en-us/library/ms188670.aspx /// Change Database ownership to sa: ALTER AUTHORIZATION ON DATABASE::yourDatabase TO sa /// - public static void Start(SchemaBuilder sb, bool? withSqlDependency = null, ICacheMultiServerInvalidator? cacheInvalidator = null) + public static void Start(SchemaBuilder sb, bool? withSqlDependency = null, IServerBroadcast? serverBroadcast = null) { if (sb.NotDefined(MethodInfo.GetCurrentMethod())) { @@ -63,14 +63,14 @@ public static void Start(SchemaBuilder sb, bool? withSqlDependency = null, ICach WithSqlDependency = withSqlDependency ?? Connector.Current.SupportsSqlDependency; - if (cacheInvalidator != null && WithSqlDependency) + if (serverBroadcast != null && WithSqlDependency) throw new InvalidOperationException("cacheInvalidator is only necessary if SqlDependency is not enabled"); - CacheInvalidator = cacheInvalidator; - if(CacheInvalidator != null) + ServerBroadcast = serverBroadcast; + if(ServerBroadcast != null) { - CacheInvalidator!.ReceiveInvalidation += CacheInvalidator_ReceiveInvalidation; - sb.Schema.BeforeDatabaseAccess += () => CacheInvalidator!.Start(); + ServerBroadcast!.Receive += ServerBroadcast_Receive; + sb.Schema.BeforeDatabaseAccess += () => ServerBroadcast!.Start(); } sb.Schema.SchemaCompleted += () => Schema_SchemaCompleted(sb); @@ -115,7 +115,17 @@ static void Schema_SchemaCompleted(SchemaBuilder sb) } } - static void CacheInvalidator_ReceiveInvalidation(string cleanName) + static void ServerBroadcast_Receive(string methodName, string argument) + { + BroadcastReceivers.TryGetC(methodName)?.Invoke(argument); + } + + public static Dictionary> BroadcastReceivers = new Dictionary> + { + { InvalidateTable, ServerBroadcast_InvalidateTable} + }; + + static void ServerBroadcast_InvalidateTable(string cleanName) { Type type = TypeEntity.TryGetType(cleanName)!; @@ -707,6 +717,8 @@ This may be because SchemaCompleted is not yet called and you are accesing some } } + const string InvalidateTable = "InvalidateTable"; + internal static void NotifyInvalidateAllConnectedTypes(Type type) { var connected = inverseDependencies.IndirectlyRelatedTo(type, includeInitialNode: true); @@ -717,7 +729,7 @@ internal static void NotifyInvalidateAllConnectedTypes(Type type) if (controller != null) controller.NotifyInvalidated(); - CacheInvalidator?.SendInvalidation(TypeLogic.GetCleanName(stype)); + ServerBroadcast?.Send(InvalidateTable, TypeLogic.GetCleanName(stype)); } } diff --git a/Signum.Engine.Extensions/Cache/PostgresCacheInvalidation.cs b/Signum.Engine.Extensions/Cache/PostgresCacheInvalidation.cs deleted file mode 100644 index 2417cc85de..0000000000 --- a/Signum.Engine.Extensions/Cache/PostgresCacheInvalidation.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Npgsql; -using System.Diagnostics; - -namespace Signum.Engine.Cache; - -public class PostgresCacheInvalidation : ICacheMultiServerInvalidator -{ - public event Action? ReceiveInvalidation; - - public void SendInvalidation(string cleanName) - { - - Executor.ExecuteNonQuery($"NOTIFY table_changed, '{cleanName}/{Process.GetCurrentProcess().Id}'"); - } - - public void Start() - { - Task.Run(() => - { - try - { - var conn = (NpgsqlConnection)Connector.Current.CreateConnection(); - conn.Open(); - conn.Notification += (o, e) => - { - if (Process.GetCurrentProcess().Id != int.Parse(e.Payload.After("/"))) - ReceiveInvalidation?.Invoke(e.Payload.Before("/")); - }; - - using (var cmd = new NpgsqlCommand("LISTEN table_changed", conn)) - { - cmd.ExecuteNonQuery(); - } - - while (true) - { - conn.Wait(); // Thread will block here - } - } - catch (Exception e) - { - e.LogException(); - } - }); - } -} diff --git a/Signum.Engine.Extensions/Cache/SimpleHttpCacheInvalidator.cs b/Signum.Engine.Extensions/Cache/SimpleHttpCacheInvalidator.cs deleted file mode 100644 index 4196b60e70..0000000000 --- a/Signum.Engine.Extensions/Cache/SimpleHttpCacheInvalidator.cs +++ /dev/null @@ -1,82 +0,0 @@ - -using Azure.Messaging.ServiceBus; -using Azure.Messaging.ServiceBus.Administration; -using Npgsql; -using Signum.Engine.Json; -using Signum.Entities.Cache; -using Signum.Services; -using System.Diagnostics; -using System.Net.Http; -using System.Net.Http.Json; - -namespace Signum.Engine.Cache; - - -public class SimpleHttpCacheInvalidator : ICacheMultiServerInvalidator { - - HttpClient client = new HttpClient(); - readonly string invalidationSecretHash; - readonly string[] invalidationUrls; - - public SimpleHttpCacheInvalidator(string invalidationSecret, string[] invalidationUrls) - { - this.invalidationSecretHash = Convert.ToBase64String(Security.EncodePassword(invalidationSecret)); - this.invalidationUrls = invalidationUrls; - } - - public event Action? ReceiveInvalidation; - - public void Start() - { - } - - //Called from Controller - public void InvalidateTable(InvalidateTableRequest request) - { - if (this.invalidationSecretHash != request.InvalidationSecretHash) - throw new InvalidOperationException("invalidationSecret does not match"); - - if (request.OriginMachineName == Environment.MachineName || - request.OriginApplicationName == Schema.Current.ApplicationName) - return; - - ReceiveInvalidation?.Invoke(request.CleanName); - } - - public void SendInvalidation(string cleanName) - { - var request = new InvalidateTableRequest - { - CleanName = cleanName, - InvalidationSecretHash = this.invalidationSecretHash, - OriginMachineName = Environment.MachineName, - OriginApplicationName = Schema.Current.ApplicationName, - }; - - foreach (var url in invalidationUrls) - { - try - { - var fullUrl = url.TrimEnd('/') + "/api/cache/invalidateTable"; - - var json = JsonContent.Create(request, options: EntityJsonContext.FullJsonSerializerOptions /*SignumServer.JsonSerializerOptions*/); - - var response = client.PostAsync(fullUrl, json).Result.EnsureSuccessStatusCode(); - } - catch(Exception e) - { - e.LogException(a => a.ControllerName = nameof(SimpleHttpCacheInvalidator)); - } - } - } -} - -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. -public class InvalidateTableRequest -{ - public string OriginMachineName; - public string OriginApplicationName; - public string CleanName; - public string InvalidationSecretHash; -} -#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. diff --git a/Signum.React.Extensions/Cache/CacheClient.tsx b/Signum.React.Extensions/Cache/CacheClient.tsx index dd742c407b..d568d7a3b5 100644 --- a/Signum.React.Extensions/Cache/CacheClient.tsx +++ b/Signum.React.Extensions/Cache/CacheClient.tsx @@ -39,6 +39,8 @@ export module API { export interface CacheState { isEnabled: boolean; + serverBroadcast: string | undefined; + sqlDependency: boolean; tables: CacheTableStats[]; lazies: ResetLazyStats[]; } diff --git a/Signum.React.Extensions/Cache/CacheController.cs b/Signum.React.Extensions/Cache/CacheController.cs index 225d766ffa..22e3e3be34 100644 --- a/Signum.React.Extensions/Cache/CacheController.cs +++ b/Signum.React.Extensions/Cache/CacheController.cs @@ -20,9 +20,11 @@ public CacheStateTS View() return new CacheStateTS { - isEnabled = !CacheLogic.GloballyDisabled, - tables = tables, - lazies = lazies + IsEnabled = !CacheLogic.GloballyDisabled, + ServerBroadcast = CacheLogic.ServerBroadcast?.ToString(), + SqlDependency = CacheLogic.WithSqlDependency, + Tables = tables, + Lazies = lazies }; } @@ -58,7 +60,7 @@ public void Clear() [HttpPost("api/cache/invalidateTable"), SignumAllowAnonymous] public void InvalidateTable([FromBody]InvalidateTableRequest req) { - if (CacheLogic.CacheInvalidator is not SimpleHttpCacheInvalidator sci) + if (CacheLogic.ServerBroadcast is not SimpleHttpBroadcast sci) throw new InvalidOperationException("CacheInvalidator is not a SimpleHttpCacheInvalidator"); sci.InvalidateTable(req); @@ -85,9 +87,11 @@ public ResetLazyStatsTS(ResetLazyStats rls) public class CacheStateTS { - public bool isEnabled; - public List tables; - public List lazies; + public bool IsEnabled; + public bool SqlDependency; + public string? ServerBroadcast; + public List Tables; + public List Lazies; } public class CacheTableTS diff --git a/Signum.React.Extensions/Cache/CacheStatisticsPage.tsx b/Signum.React.Extensions/Cache/CacheStatisticsPage.tsx index c865af257c..75f3598a42 100644 --- a/Signum.React.Extensions/Cache/CacheStatisticsPage.tsx +++ b/Signum.React.Extensions/Cache/CacheStatisticsPage.tsx @@ -4,6 +4,8 @@ import { RouteComponentProps } from 'react-router-dom' import { Tab, Tabs } from 'react-bootstrap' import { API, CacheTableStats, ResetLazyStats, CacheState } from './CacheClient' import { useAPI, useAPIWithReload } from '@framework/Hooks' +import { SearchControl } from '../../Signum.React/Scripts/Search' +import { ExceptionEntity } from '../../Signum.React/Scripts/Signum.Entities.Basics' export default function CacheStatisticsPage(p: RouteComponentProps<{}>) { @@ -37,6 +39,12 @@ export default function CacheStatisticsPage(p: RouteComponentProps<{}>) { {state.isEnabled == false && } {} +
+ Server Broadcast: {state.serverBroadcast} +
+ SqlDependency: {state.sqlDependency.toString()} +
+
{state.tables && @@ -47,6 +55,17 @@ export default function CacheStatisticsPage(p: RouteComponentProps<{}>) { {renderLazies(state)} } + + {state.serverBroadcast && + + a.entity.controllerName), value: state.serverBroadcast.before("(") } + ] + }} /> + + } );