'How to use SignalR to notify app user`s that the system in deployment process right now

Is there a way to notify system`s users on real-time that the system is in deployment process(publish to production)?The purpose is to prevent them from starting to do atomic operations? the system is an ASP.NET-based system and it already has SignalR Dlls, but I do not exactly know how to get to the "source" in the application from which I know that the system is deploying right now.



Solution 1:[1]

This is highly dependent on your deployment process, but I achieved something similar in the following way:

I created a method in one of my controllers called AnnounceUpdate:

[HttpPost("announce-update")]
public async Task<IActionResult> AnnounceUpdate([FromQuery] int secondsUntilUpdate, string updateToken)
{
    await _tenantService.AnnounceUpdate(secondsUntilUpdate, updateToken);
    return Ok();
}

The controller method takes in the amount of seconds till the update, as well as a secret token to ensure not just anyone can call this endpoint.

The idea is that we will call this controller just before we deploy, to announce the pending deployment. I make my deployments using Azure Dev Ops, and so I was able to create a release task that automatically runs the following PowerShell code to call my endpoint:

$domain = $env:LOCALURL;
$updateToken = $env:UPDATETOKEN;
$minutesTillUpdate = 5;

$secondsUntilUpdate = $minutesTillUpdate * 60;
$len = $secondsUntilUpdate / 10;

#notify users every 10 seconds about update
for($num =1; $num -le $len; $num++)
{
    $url = "$domain/api/v1/Tenant/announce-update?secondsUntilUpdate=$secondsUntilUpdate&updateToken=$updateToken";

    $r = Invoke-WebRequest $url -Method Post -UseBasicParsing;
    $minsLeft = [math]::Floor($secondsUntilUpdate/60);
    $secsLeft = $secondsUntilUpdate - $minsLeft * 60;

    $timeLeft;
    if($minsLeft -eq 0){
        $timeLeft = "$secsLeft seconds";
    }else{
        if($secsLeft -eq 0){
            $timeLeft = "$minsLeft minute(s)";
        }else{
            $timeLeft = "$minsLeft minute(s) $secsLeft seconds";
        }
    };

    $code = $r.StatusCode;

    Write-Output "";
    Write-Output "Notified users $num/$len times.";
    Write-Output "Response: $code.";
    Write-Output "$timeLeft remaining."
    Write-Output "_________________________________"

    Start-Sleep -Seconds 10;

    $secondsUntilUpdate = $secondsUntilUpdate - 10;
}

Write-Output "Allowing users to log out.";
Write-Output "";

Start-Sleep -Seconds 1;

Write-Output "Users notfied! Proceeding with update.";

As you can see, on the script I have set that the time till the update is 5 minutes. I then call my AnnounceUpdate endpoint every 10 seconds for the duration of the 5 minutes. I have done this because if I announce an update that will occur in 5 minutes, and then 2 minutes later someone connects, they will not see the update message. On the client side I set a variable called updatePending to true when the client receives the update notification, so that they do not keep on getting a message every 10 seconds. Only clients that have not yet seen the update message will get it.

In the tenant service I then have this code:

public async Task AnnounceUpdate(int secondsUntilUpdate, string updateToken)
{
    if (updateToken != _apiSettings.UpdateToken) throw new ApiException("Invalid update token");
    await _realTimeHubWrapper.AnnouncePendingUpdate(secondsUntilUpdate);
}

I simply check if the token is valid and then conitnue to call my HUB Wrapper.
The hub wrapper is an implementation of signalR's hub context, which allows to invoke signalR methods from within our code. More info can be read here

In the HUB wrapper, I have the following method:

public Task AnnouncePendingUpdate(int secondsUntilUpdate) =>
_hubContext.Clients.All.SendAsync("UpdatePending", secondsUntilUpdate);

On the client side I have set up this handler:

  // When an update is on the way, clients will be notified every 10 seconds.
  private listenForUpdateAnnouncements() {
    this.hubConnection.on(
      'PendingUpdate', (secondsUntilUpdate: number) => {
        if (!this.updatePending) {
          const updateTime = currentTimeString(true, secondsUntilUpdate);
          const msToUpdate = secondsUntilUpdate * 1000;

          const message =
            secondsUntilUpdate < 60
              ? `The LMS will update in ${secondsUntilUpdate} seconds.
        \n\nPlease save your work and close this page to avoid any loss of data.`
              : `The LMS is ready for an update.
        \n\nThe update will start at ${updateTime}.
        \n\nPlease save your work and close this page to avoid any loss of data.`;

          this.toastService.showWarning(message, msToUpdate);

          this.updatePending = true;

          setTimeout(() => {
            this.authService.logout(true, null, true);
            this.stopConnection();
          }, msToUpdate);
        }
      }
    );
  }

I show a toast message to the client, notifying them of the update. I then set a timeout (using the value of secondsUntilUpdate) which will log the user out and stop the connection. This was specifically for my use case. You can do whatever you want at this point

To sum it up, the logical flow is:
PowerShell Script -> Controller -> Service -> Hub Wrapper -> Client


The main take away is that somehow we need to still trigger the call to the endpoint to announce the update. I am lucky enough to be able to have it run automatically during my release process. If you are manually publishing and copying the published code, perhaps you can just run the PowerShell script manually, and then deploy when it's done?

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1