6

Concurrency-Safe Execution Using Ballerina - DZone Microservices

 2 years ago
source link: https://dzone.com/articles/concurrency-safe-execution-ballerina-isolation
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Concurrency-Safe Execution Using Ballerina Isolation

The concept of isolation in Ballerina simplifies development by ensuring the safety of shared resources during concurrent execution.

Join the DZone community and get the full member experience.

Join For Free

The Ballerina language establishes a concurrent friendly approach to programming through light-weighted threads called strands. This is achieved by providing support for both preemptive and cooperative multitasking. When executing a concurrent program in a multi-threaded environment, the safe usage of shared resources is pivotal. This is obtained through a language concept called isolation. In this article, we will take an in-depth look at the concurrent safety support of Ballerina and see how HTTP services can be implemented to provide timely and accurate responses using isolation.

Race Condition in Concurrent Programming

In order to maintain the dynamic nature of a service, the following two aspects are considered during its implementation. 

  • Performance - Ensure the liveness through immediate and continuous responses
  • Safety - Ensure responding with correct results and service not reaching a bad state

The performance of a service can be elevated through concurrent programming, as it allows independent operations to be executed concurrently using multiple threads. But the challenge in designing such a program is to avoid the state of shared resources getting modified simultaneously and producing erroneous results. An appropriate solution for this condition needs to detect the unwanted data races in a concurrent program without significant performance impact. Ballerina rectifies the race conditions in the program by constructs like lock statements and isolated qualifiers along with the immutability support.

Let’s take a look at some sample Ballerina code that shows us the usage of lock statements to safely access mutable resources.

Lock Statements

A lock statement can be used in a program to safely access the mutable states from multiple strands that run on separate threads. The code block that is enclosed within a lock statement acts as an atomic block that prevents concurrent execution of the code block from different strands. 

string[] menu = ["pizza", "hot dog", "fries"];

function modifyMenu(int index, string dish) {
   lock {
       menu[index] = dish;
   }
}

The above sample shows a function that modifies a module-level variable menu. As the scope of menu is module level, it can be accessed and modified concurrently if multiple strands execute the function modifyMenu. Having a lock statement where we modify the variable menu can solve this problem.

The usage of locks in a program is implementation-dependent. Users can have a naive implementation that contains a single global recursive lock. Alternatively, users can have an efficient implementation by having fine-grained locks using compile-time lock inference. 

As using lock statements in the correct manner is an added responsibility for the user, we require more controlled constructs when implementing a service to ensure the execution of methods in a service does not create a data race.

Let’s look at an example HTTP service that can result in a data race.

Sample HTTP Service With Possible Race Condition

import ballerina/http;

string[] menu = ["pizza", "hot dog", "fries"];

service / on new http:Listener(8080) {
   resource function post menu(@http:Payload string dish, int index) {
       menu[index] = dish;
   }
}

The above code will start a service with an HTTP listener on port 8080 that contains an HTTP resource at the path /menu. As the resource method mutates the module-level variable menu, a data race is possible if concurrent requests are made to that method. Therefore, it is beneficial to identify the safety of strands executing a resource method on separate threads while developing the service. 

The concept of isolation is introduced to improve this situation.

Isolated Functions

An isolated function is a function that complies with the following constraints. 

  • It can only access mutable states through its arguments
  • It can only call isolated functions 
  • It can access immutable states without any restrictions

Execution of isolated functions can ensure concurrency safety if the arguments passed to the functions are safe. 

final string dish = "burger";

isolated function modifyMenu(string[] menu, int index) {
   menu[index] = dish;
}

In the above sample, the isolated function modifyMenu can access the module-level variable dish without restrictions as it is immutable. But, there is still a possibility of a race condition as the user can pass a mutable common state as an argument. For example, if a mutable module-level variable is passed as the argument menu, then we can’t ensure the concurrency safety of the isolated function modifyMenu.

Isolated Functions Complementing Readonly Data

We can confirm a complete concurrency safe execution of an isolated function if all the arguments of that function are readonly.  In Ballerina, it is guaranteed that a value can never be mutated if its declared type is a subtype of readonly. This restricts the isolated function from mutating its arguments. 

type Menu string[] & readonly;

final Menu menu = ["pizza", "hot dog", "fries"];

isolated function getLastDish() returns string {
   return menu[menu.length() - 1];
}

In the above example, the type of variable menu is string[] & readonly. This indicates that the value of menu is deeply immutable. In other words, we are not allowed to modify the elements of the string array menu.  And as it is declared final, we are not allowed to assign a new value to the variable either. Therefore, an isolated function can access final variables with readonly type without using any lock statements.  

Isolated Expressions and Locks 

Allowing only immutable states and mutable arguments in an isolated function can be too restrictive. The concept of Isolated root allows access to mutable states inside an isolated function. 

A value is an isolated root if a mutable state is reachable from that value and cannot be reached from outside except through itself. 

Diagram depicting isolated root with three mutable states.

An expression is identified as an isolated expression if it follows the rules that guarantee its value to be an isolated root. The language specifies a set of conditions to be fulfilled by an expression in order to consider it as an isolated expression depending on the expression kind.

For example, 

  • If an expression is a subtype of readonly, then it is always isolated.
  • A list constructor expression [E1, E2] is isolated if its sub-expressions E1 and E2 are isolated.

isolated variables and isolated objects are considered isolation roots. It is guaranteed that any mutable state which is freely reachable from an isolated object or an isolated variable is accessed only through a lock statement. This ensures there will be no data race when accessing that mutable state. 

Isolated Variables

An isolated variable is a non-public, module-level variable that is initialized with an isolated expression. A lock statement must fulfill the following rules when accessing an isolated variable.

  • It must be accessing only one isolated variable
  • It can call only isolated functions
  • The value transfers must be done using isolated expressions
isolated string[] menu = ["pizza", "hot dog", "fries"];

isolated function modifyMenu(int index, string dish) {
   lock {
       menu[index] = dish;
   }
}

isolated function getLastDish() returns string {
   lock {
       return menu[menu.length() - 1];
   }
}

In the above code, the isolated variable menu is initialized with an isolated expression. And, regardless of its mutable state, it is accessed in the isolated functions modifyMenu and getLastDish within lock statements.

Service Concurrency Through Isolated Methods

Our initial goal was to achieve concurrency safety when executing resource/remote methods of services in order to improve their performance. This can be solved by using isolated methods in service objects. 

isolated string[] menu = ["pizza", "hot dog", "fries"];

class MenuService {
   int servings = 0;
   isolated function modifyMenu(int index, string dish) {
       lock {
           menu[index] = dish;
       }
   }

   isolated function getLastDish() returns string {
       lock {
           return menu[menu.length() - 1];
       }
   }

   function addServing() {
       self.servings += 1;
   }
}

An isolated method is an object method that behaves similarly to an isolated function and treats the self as a parameter. In the above sample, we have isolated methods modifyMenu and getLastDish in the object MenuService. When a listener makes calls to these isolated methods, we can ensure concurrency safety, if both the object itself and the passed parameters are safe. But in this case, we have a mutable field servings in the object MenuService and it can be mutated during the concurrent calls to the methods. Therefore, we need more control over the service object to achieve complete concurrency safety. 

Isolated Objects

The behavior of an isolated object is similar to a module with isolated variables.

Diagram comparing isolated object to module with isolated variables.

In order to provide concurrency safety to the object, the mutable fields in an isolated object must meet the following constraints. 

  • They must be private. Therefore it can be accessed only by using self
  • They must be initialized with an isolated expression
  • They can only be accessed within a lock statement
  • The lock statement that mutates the fields must follow the same rules for self as for an isolated variable
  • The field is mutable unless it is final and has type that is subtype of readonly
isolated string[] menu = ["pizza", "hot dog", "fries"];

isolated class MenuService {
   private int servings = 0;

   isolated function modifyMenu(int index, string dish) {
       lock {
           menu[index] = dish;
       }
   }

   isolated function getLastDish() returns string {
       lock {
           return menu[menu.length() - 1];
       }
   } 

   isolated function addServing() {
       lock {
           self.servings += 1;
       }
   }
}

Consider the above code. We have a private mutable field servings in the isolated object MenuService that is accessed only inside a lock statement.

The language infers the objects without mutable fields as inherently isolated. Though, the user needs to use lock statements in appropriate places, such as accessing self with mutable state and accessing mutable module-level variables.

Sample HTTP Service That Supports Concurrency Safe Execution

Combining all these features, we can assure the safe execution of a concurrent service, if the listener handles calls to the service methods where:

  • The parameters of the method are immutable or isolated
  • The method is an isolated method
  • The service object that contains the method is isolated
isolated string[] menu = ["pizza", "hot dog", "fries"];
 
isolated service / on new http:Listener(8080) {
   private int servings = 0; 

   isolated resource function post menu(@http:Payload string dish, int index) {
       lock {
           menu[index] = dish;
       }
   }

   isolated resource function get lastDish() returns string {
       lock {
           return menu[menu.length() - 1];
       }
   }

   isolated resource function post serving() {
       lock {
           self.servings += 1;
       }
   }
}

The above HTTP service provides complete support for concurrency safety by associating with isolation and lock statements.

Summary

In this article, we explored the language constructs of Ballerina such as lock statements, immutable states, and isolation, and how we can implement a concurrent service that assures the safety of its shared resources. We successfully developed a concurrency-safe HTTP service by combining these features.

For more information on this functionality, refer to Ballerina examples.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK