D·ASYNC syntax mapping

The idea behind D·ASYNC technology is to use C# syntax, OOP paradigms, and design patterns as an abstraction layer to describe a distributed application. Here are some basic mappings:

  1. The basics. And resiliency.
/* This is your 'service' or 'workflow'. */
public class BaristaSimulationWorkflow
{
  /* This is a 'routine' of a workflow. */
  public virtual async Task Run()
  {
    /* This will call a sub-routine and save the sate of the current one. */
    var order = await TakeOrder();
    
    /* If the process terminates abruptly here, after restart the routine continue at exact point without executing previous steps. Any async method is compiled into a state machine, so it's possible to save and restore its state and context. */
    
    var cup = await MakeCoffee(order);
    
    /* Essentially this is an Actor Model of a scalable distributed system. A routine maps to an actor, because an async method compiles into a state machine (which has its state), and a routine can call sub-routines - same as an actor can invoke other actors, where async-await is the perfect candidate for a Message-Oriented design. */
        
    await Serve(cup);
  }
  
  /* This is a 'sub-routine' of a workflow. */
  protected virtual async Task<Order> TakeOrder();
  
  protected virtual async Task<Cup> MakeCoffee(Order order);
  
  protected virtual async Task Serve(Cup cup);
}
  1. Inter-service communication, dependency injection, and transactionality.
/* Declaration of the interface of another service that might be deployed in a different environment. */
public interface IPaymentTerminal
{
  Task Pay(Order order, CreditCard card);
}

public class BaristaWorker
{
  private IPaymentTerminal _paymentTerminal;

  /* Another service/workflow can be consumed by injecting as a dependency. All calls to that service will be routed to that particular deployment using its communication mechanism. All replies will be routed back to this service. This is where Dependency Injection meets Service Discovery and Service Mesh. */
  public BaristaWorker(IPaymentTerminal paymentTerminal)
  {
    _paymentTerminal = paymentTerminal;
  }
  
  protected virtual async Task<Order> TakeOrder()
  {
    Order order = ...;
    CreditCard card = ...;
    /* Simple call to another service may ensure transactionality between two. That complexity is hidden to help you focus on the business logic. */
    await _paymentTerminal.Pay(order, card);
    /* And again, state is saved here for resiliency. */
  }
}
  1. Scalability: Factory pattern and resource provisioning.
public interface IBaristaWorker : IDisposable
{
  Task PerformDuties();
}

public interface IBaristaWorkerFactory
{
  Task<IBaristaWorker> Create();
}

public class CoffeeShopManager
{
  private IBaristaWorkerFactory _factory;
  
  public CoffeeShopManager(IBaristaWorkerFactory factory)
  {
    _factory = factory;
  }
  
  public virtual async Task OnCustomerLineTooLong()
  {
    /* Create an instance of a workflow, where 'under the hood' it can provision necessary cloud resources first. That is hidden behind the factory abstraction, what allows to focus on the business logic and put the infrastructure aside. */
    using (var baristaWorker = await _factory.Create())
    {
      // This can be routed to a different cloud resource
      // or deployment what enables dynamic scalability.
      await baristaWorker.PerformDuties();
      /* Calling IDisposable.Dispose() will de-provision allocated resources. */
    }
  }
}
  1. Scalability: Parallel execution.
public class CoffeeMachine
{
  public virtual async Task PourCoffeeAndMilk(Cup cup)
  {
    /* You can execute multiple routines in parallel to 'horizontally scale out' the application. */
    Task coffeeTask = PourCoffee(cup);
    Task milkTask = PourMilk(cup);
    
    /* Then just await all of them, as you would normally do with TPL. */
    await Task.WhenAll(coffeeTask, milkTask);
    
    /* And that will be translated into such series of steps:
    1. Save state of current routine;
    2. Schedule PourCoffee
    3. Schedule PourMilk
    4. PourCoffee signals 'WhenAll' on completion
    5. PourMilk signals 'WhenAll' on completion
    6. 'WhenAll' resumes current routine from saved state. */
  }
}
  1. Statefulness and instances.
/* This service has no private fields - it is stateless. */
public class CoffeeMachine
{
}

/* This service has one or more private fields - it is stateful. */
public class BaristaWorker
{
  private string _fullName;
}

/* Even though this service has a private field, it is stateless, because the field represents an injected dependency - something that can be re-constructed and does not need to be persisted in a storage. */
public class BaristaWorker
{
  private IPaymentTerminal _paymenTerminal;

  public BaristaWorker(IPaymentTerminal paymentTerminal)
  {
    _paymentTerminal = paymentTerminal;
  }
}
 
/* Most likely this factory service is a singleton, however it creates an instance of a service, which can be a multiton for example. */
public interface IBaristaWorkerFactory
{
  Task<IBaristaWorker> Summon(string fullName);
}
  1. Integration with other TPL functions.
public class BaristaWorker
{
  protected virtual async Task<Order> TakeOrder()
  {
    var order = new Order();
    
    order.DrinkName = Console.ReadLine();
    
    /* Normally, 'Yield' instructs runtime to re-schedule continuation of an async method, thus gives opportunity for other work items on the thread pool to execute. Similarly, DASYNC Execution Engine will save the state of the routine and will schedule its continuation, possibly on a different node. */
   
    await Task.Yield();
    
    /* I.e. if the process terminates abruptly here (after the Yield), the method will be re-tried from exact point without calling Console.ReadLine again. */
    
    order.PersonName = Console.ReadLine();
    
    /* No need to call 'Yield' here, because this is the end of the routine, which result will be committed upon completion of the last step. */
    
    return order;
  }

  public async Task ServeCustomers()
  {
    while (!TimeToGoHome)
    {
      if (!AnyNewCustomer)
      {
        /* The delay is translated by the DASYNC Execution Engine to saving state of the routine and resuming after given amount of time. This can be useful when you do polling for example, but don't want the method to be volatile (lose its execution context) and/or to allocate compute and memory resources. */
        await Task.Delay(20_000);
      }
      else
      {
        ...
      }
    }
  }
}

 

As you can see from examples above, the code does not specify whether your application is running in a single process or distributed across multiple nodes. That gives you flexibility of choosing what cloud/distributed platform your application is running on without a need to be locked to any particular technology, and without necessity to re-write the code if you want to switch. In addition, it also helps to adapt existing monoliths applications to the microservice ecosystem faster – aka simplified “lift-and-shift” that works exactly as it would have been architectured for the cloud in first place.

At the moment of writing this article, few concepts are not fully fleshed out, and can be changed in the future. Other extra concepts are yet to come.

 


Read Next

10 benefits of D·ASYNC

Leave a comment