Skip to content

About

Provides a set of services for building a cancellation engine and starting event loops within an application

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Disclaimer: The software provided in this repository was developed without the use of generative AI. Generative AI may only be used to verify grammatical correctness and syntax.

Part of the proposed interfaces were implemented between 2020 and 2024 while building an integration with Kubernetes. This project extends their usage to demonstrate a more general approach to managing and integrating them into a software system.

Message-Loop

  1. Overview
  2. Managing Long-Running Operations
  3. Managing Messages
  4. Created With
  5. NuGet

Overview

The application demonstrates an http-based protocol that allows background operations to be managed and non-blocking custom-frequency message loops to be configured. The Web.Common.dll assembly provides two controllers, each of which works with a single service assigned to it.

In this demo, both services from the Common.dll are registered in a singleton scope. The demo contains a custom topology that is generated from Node.exe by varying environmental variables within a PowerShell script. The executables run on different ports and, the protocol binds them together.

The purpose of demo is to show a convenient and minimal web interface for retrieving a complex structure from background jobs across multiple multithreaded services.

To start the demo, execute the following script from the project's root folder.

.\StartAll.ps1

hierarchy

Fig. 1 - Hierarchy of modules.

Custom Topology On Ports

To build a custom topology, it is necessary to ensure that IConfigurationBuilder overrides the NodeOptions section of appsettings.json with environmental variables. The ChildNodes property represents an array of target hosts on which the subsequent background operations will be created. Additionally, the relevant model, contains a Break property that can be used if a request loops indefinitely.

topology

Fig. 2 - Example of a topology which is generated by the StartAll.ps1 script. Each arrow line points to a child node.

To start a simulation it is necessary to send a POST request to the /api/v1/node/onNode route. The response returns a unique identifier that can be used by the long-run controller to access the resulting object containing the actual graph of background operations.

The following JSON graph provides an example of the background operations results generated from the topology shown above. After sending a GET request to the /api/v1/longrun/{tokenId} route with the unique identifier, the resulting graph is as follows:

JSON Graph
{
  "status": 5,
  "exception": null,
  "data": {
    "result": "Operation completed on localhost:5000",
    "childResults": [
      {
        "status": 5,
        "exception": null,
        "data": {
          "result": "Operation completed on localhost:5001",
          "childResults": [
            {
              "status": 5,
              "exception": null,
              "data": {
                "result": "Operation completed on localhost:5002",
                "childResults": [
                  {
                    "status": 5,
                    "exception": null,
                    "data": {
                      "result": "Operation completed on localhost:5003",
                      "childResults": []
                    }
                  }
                ]
              }
            },
            {
              "status": 5,
              "exception": null,
              "data": {
                "result": "Operation completed on localhost:5003",
                "childResults": []
              }
            }
          ]
        }
      },
      {
        "status": 5,
        "exception": null,
        "data": {
          "result": "Operation completed on localhost:5002",
          "childResults": [
            {
              "status": 5,
              "exception": null,
              "data": {
                "result": "Operation completed on localhost:5003",
                "childResults": []
              }
            }
          ]
        }
      },
      {
        "status": 5,
        "exception": null,
        "data": {
          "result": "Operation completed on localhost:5003",
          "childResults": []
        }
      }
    ]
  }
}

Fig. 3 - Example of the output produced by a background operation created within the onNode action. The status field shows the TaskStatus.RanToCompletion value, and the data field contains the actual graph shown in Fig. 2.

It is also possible to format the response using an extension method. Calling BuildDataTree(string, IEnumerable<LongRunResult>) on the resulting constant within the onNode long-running handler produces the resulting graph as a single string:

Output generated by calling the BuildDataTree extension
{
  "status": 5,
  "exception": null,
  "data": "Operation completed on localhost:5000-->Operation completed on localhost:5001-->Operation completed on localhost:5002-->Operation completed on localhost:5003\r\n"
          "Operation completed on localhost:5000-->Operation completed on localhost:5001-->Operation completed on localhost:5003\r\n"
          "Operation completed on localhost:5000-->Operation completed on localhost:5002-->Operation completed on localhost:5003\r\n"
          "Operation completed on localhost:5000-->Operation completed on localhost:5003\r\n"
}

Fig. 4 - Example of the output produced by returning the result from the onNode action, using the BuildDataTree extension. The output has been formatted to improve readability.

Message Loop

Sometimes, an advanced workflow may require messaging between operations or propagating signals to another process. To avoid introducing external libraries or additional processes to consume messages, the system provides an interface containing a wrapper around a set of Channel<T> instances, which can be used to build a message loop within the application.

Since IMessageService<TMessage> has a singleton scope, its usage is not limited to the Web API-to-loop relationship; it also supports loop-to-loop communication. In this demo, there is an example of several custom messages used to manage the workflow: Message.OK to break out of the loop, Message.Cancel to trigger external cancellation, and Message.Fail to signal to the loop that an error occurred.

The exact implementation can be found in MessageLoopService.cs.

Managing Long-Running Operations

There are several related components that allow the subsystem to be built. The ILongRunService<T> binds a task with a unique identifier, maintaining two maps of running and completed operations. Additionally, the service captures a context containing ambient data, some properties of which can be initialized at the filter level or to accessed within a subsequent asynchronous workflow. Through the TryGetResult method, the service extracts a resulting token containing the current TaskStatus, Exception, and if the task has completed, the final result.

To manage long-running operations using a WEB API, the system provides the LongRunController. Its main purpose is to retrieve a LongRunResult by sending a GET HTTP request with a unique identifier. If a background workflow is configured to monitor the CancellationToken, it is possible to cancel a task via the Abort call, by sending a PATCH HTTP request providing the same unique identifier.

Polling

To retrieve tokens containing data to from background operations across different processes, it is possible to use Poll<T>(ILongRunApi<T>, LongRunToken, CancellationToken, int). The extension targets the LongRunController, which is mapped to the ILongRunApi<T> by URL. It evaluates the TaskStatus enumeration to determine whether a background operation is still in progress, and if a cancellation has been requested, sends an Abort request using the unique identifier from the token.

private readonly ILongRunApi<LongRunItem> _api;
private readonly ILongRunService<LongRunItem> _service;
private readonly IExternalApi _external;
...
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult Demo() 
{
    var token = _service.PutTask(async (cncl) => 
    {
        var externalToken = await _external.RunOperation();
        var result = await _api.Poll(externalToken, cncl);
        return result;
    });
    return Ok(token);
}

Fig. 5 - Generating a token with a GET HTTP request. The background operation obtains another token from the external api, and the ILongRunApi<T> starts polling the result.

Managing Messages

To enable messaging, the system provides the IMessageService<TMessage> interface, which assembles a collection of channels and binds them to a string constant. By default, when a channel is added to the service,SendMessage unicasts an object, which is then written to the channel. When the Add method is executed multiple times with the same constant, the service continues to add channels, enabling multicast messaging.

The actions within the MessageController<TEnum> consider only a unicast and broadcast methods of message transfer. The generic argument allows the routing to be dynamically extended, making custom messages possible. To use the controller, it is necessary to register two components from the Web.Common.dll assembly: MessageControllerConvention, and MessageControllerProvider.

Unicast/Multicast

To unicast a message to the MessageController<TEnum>, one possible approach is to generate an http-client from the IMessageApi and call the Unicast method.

private readonly IMessageApi _api;
...
var response = await _api.Unicast(nameof(TEnum), key, TEnum.Message.ToString());

Fig. 6 - Example of sending a TEnum.Message to a channel.

In case IMessageService<TMessage> is used outside web interfaces, the same behavior can be achieved by calling SendMessage.

Broadcast

The following statement iterates over the entire collection of keys, broadcasting the TEnum.Message to all channels.

private readonly IMessageApi _api;
...
var response = await _api.Broadcast(nameof(TEnum), TEnum.Message.ToString());

Fig. 7 - Broadcasting a Message to all channels.

Created With

.NET 10.0, netstandard2.0

ASP.NET Core

Refit

PowerShell 7

Nuget

MessageLoop.Common

MessageLoop.Web.Common

About

Provides a set of services for building a cancellation engine and starting event loops within an application

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages