-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
generalize CacheInvalidator to ServerBrodcast to support methodName /…
… Argument
- Loading branch information
1 parent
15116a8
commit 1903c0d
Showing
9 changed files
with
255 additions
and
162 deletions.
There are no files selected for viewing
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
68 changes: 68 additions & 0 deletions
68
Signum.Engine.Extensions/Cache/Broadcast/PostgresBroadcast.cs
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
using Npgsql; | ||
using System.Diagnostics; | ||
|
||
namespace Signum.Engine.Cache; | ||
|
||
public class PostgresBroadcast : IServerBroadcast | ||
{ | ||
public event Action<string, string>? 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)}()"; | ||
} | ||
} |
108 changes: 108 additions & 0 deletions
108
Signum.Engine.Extensions/Cache/Broadcast/SimpleHttpBroadcast.cs
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string, string>? 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. |
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
Oops, something went wrong.
1903c0d
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CacheLogic.ServerBrodcast
In yesterday's post we saw how Alerts modules uses SignalR so that the server can communicate with the clients in real-time... but what if we have more than one server?
Traditionally the solution of Signum Framework to communicate Cache Invalidations was SqlDependency but it has some problems:
Alternatively Signum had a
ICacheInvalidator
that allows server-to-server cache invalidation using aSendInvalidation
method and aReceiveInvalidation
event.The only implementation of this interface till now was
PostgresCacheInvalidation
, using the nativeNOTIFY
functionality.Three changes
This change does three things:
Generalize
ICacheInvalidator
toIServerBroadcast
that has a genericSend
method andReceive
event, each with two arguments:methodName
andargument
. This way the infrastructure can be used for sending other types of notifications, not only cache invalidation.PostgresCacheInvalidation
has been renamed toPostgresBroadcast
.Implement
AzureServiceBusBroadcast : IServerBroadcast
that uses Azure Service Bus pub/sub functionality (topics and subscriptions) to implement this interface. This is the recommended solution if you have a farm of servers, but it cost about 8€/monthImplement
SimpleHttpBroadcast : IServerBroadcast
this is a simpler implementation where each server know the URLs of all the other servers and just makes an HTTP request. It automatically ignores his own requests. This is the recommended solution when you just have 2 or 3 servers, like using Azure Slot Deployment.How to use it
This is how you receive messages form peer servers: https://github.com/signumsoftware/framework/blob/master/Signum.React.Extensions/Alerts/AlertsServer.cs#L36
This is how you sent messages: https://github.com/signumsoftware/framework/blob/master/Signum.React.Extensions/Alerts/AlertsServer.cs#L44
Useful for Azure Slot Deployments
One use case where this is useful is in Azure Slot Deployments where you have two servers to ensure 0 downtime deployments.
In this case, I typically chose one server to run the scheduled tasks / background process, (let's say the green one), but this server is not always in the front side, and when is not, it needs to refresh the caches / notify the front-end server.
1903c0d
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1903c0d
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent design, iterative/constant/great improvements, as always
Bravo! 🎉