New Year’s Eve

This example illustrates a simple usage of resource management, balancing resources between two servers based on client traffic. The example is inspired by mobile phone users on New Year’s Eve, who change their behavior from alternating between making phone calls and sending SMS to flooding the SMS server with messages during the “midnight window” on New Year’s Eve.

1 Modeling the Servers

We consider two simple services:

  • Telephone Service: This service allows clients to place calls with a given duration
  • SMS Service: This service allows clients to send SMS

Each service is defined by an ABS interface.

interface TelephoneService  {
  Unit call(Int calltime);
}

interface SMSService {
  Unit sendSMS();
}

The two services are implemented by servers. For this example we consider very simple servers, implemented by ABS classes. The telephone server offers a method call which takes the remaining time of the call as parameter. The remaining time of a call decrements for every time interval, as expressed by the ABS statement await duration(1,1). The SMS server offers a method sendSMS. Each server has a counter to keep track of the number of client requests they have received.

class TelephoneServer implements TelephoneService  {
  Int callcount = 0;
  Unit call(Int calltime){
    // println("[Time: "+toString(timeValue(now()))+"] Placing a call");
    while (calltime > 0) {
      calltime = calltime - 1;
      await duration(1, 1);
    }
    callcount = callcount + 1;
  }
}

class SMSServer implements SMSService  {
  Int smscount = 0;
  Unit sendSMS() {
    // println("[Time: "+toString(timeValue(now()))+"] Sending an sms");
    smscount = smscount + 1;
  }
}

By uncommenting the print-statements, you will be able to see the activity of the servers on the console.

2 Modeling a Client

We model a client Handset which interacts with the telephone and SMS services. The handset makes requests to the two servers. The normal behavior of the handset is to alternate between sending an SMS and making a call at each time interval. When it makes a call, it waits for the call to end before proceeding. (In our example, the duration of the call is 1 time interval.) This gives us the following scenario:

label_telephone1

However, every year at at midnight on New Year’s Eve, the behavior changes. For a certain period, the handsets stop making calls and flood the server with SMS messages. Therefore, we model the handset with a spike. In our example, the spike starts at time 50 and ends at time 70.  This gives us the following scenario:

ClientNYE

This handset’s spike occurs periodically, every 100 time intervals. During a spike, the handset asynchronously sends 10 SMS requests. To evaluate whether the current time is in a spike period, the expression timeValue(now()) returns the current time as a Rat, which we compare modulo the spikeperiod to the interval starting at 50 and ending at 70.

class Handset (Int spikeperiod, TelephoneServer ts, SMSServer smss) {
  Bool call = False;
  Int current = 0;
  Bool spike = False;
  
  Unit run(){
    while(True) {
      current = timeValue(now()) % spikeperiod;
      if (!spike && current > 50 && current < 70) {
        spike = True;
        println("\n[Time: "+toString(timeValue(now()))+"] Handset is entering spike");
      }
      if (spike && current >= 70) {
        spike = False;
        println("\n[Time: "+toString(timeValue(now()))+"] Handset is leaving spike");
      }
      if (spike) { // Spike behavior
        Int i = 0;
        while (i < 10) {
          smss!sendSMS();
          i = i + 1;
        }
      } else { // Normal behavior
        if (call) { await ts!call(1); } else { smss!sendSMS(); }
        call = ~ call;
      }
      await duration(1,1);
    }
  }
}

3 Running the Simulator

To run this model with one Handset, we set up the main block of the ABS model as follows:

{ // Main block:
  println("[Time: "+toString(timeValue(now()))+"] Starting servers");
  SMSService sms = new SMSServer();
  TelephoneService tel = new TelephoneServer();
  println("[Time: "+toString(timeValue(now()))+"] Starting handset");
  new Handset(100,tel,sms);
}

We can now compile and run the model in the Erlang simulator. We can run the model until time 500 by giving the simulator a parameter -l500. The simulation results the following output on the console:

?> ./gen/erl/run -l500
Start m_Example
[Time: 0] Starting servers
[Time: 0] Starting handset

[Time: 51] Handset is entering spike

[Time: 70] Handset is leaving spike

[Time: 151] Handset is entering spike

[Time: 170] Handset is leaving spike

[Time: 251] Handset is entering spike

[Time: 270] Handset is leaving spike

[Time: 351] Handset is entering spike

[Time: 370] Handset is leaving spike

[Time: 451] Handset is entering spike

[Time: 470] Handset is leaving spike

4 Deploying the Servers

Let us now make this model resource-sensitive by deploying the servers on (virtual) machines.

We first extend the servers with a very simple cost model. We add a cost of 1 for each time interval during a call and for each sendSMS invocation. The costs are added as annotations to the two server classes (which are otherwise unchanged):

class TelephoneServer implements TelephoneService {
  Int callcount = 0;
  Unit call(Int calltime){
    // println("[Time: "+toString(timeValue(now()))+"] Placing a call");
    while (calltime > 0) {
      [Cost: 1] calltime = calltime - 1;
      await duration(1, 1);
    }
    callcount = callcount + 1;
  }
}

class SMSServer implements SMSService {
  Int smscount = 0;
  Unit sendSMS() {
    // println("[Time: "+toString(timeValue(now()))+"] Sending an sms");
    [Cost: 1] smscount = smscount + 1;
  }
}

We then define two deployment components in the main block, one for the telephone server and the other for the SMS server.This results in the following model:

label_telephone2

We let the two deployment components have the same processing speed, 90. To create some more traffic, let us consider a model with 20 handsets.

{ // Main block:
    
  DC smscomp = new DeploymentComponent("smscomp", map[Pair(Speed, 90)]);
  DC telcomp = new DeploymentComponent("telcomp", map[Pair(Speed, 90)]);
  println("[Time: "+toString(timeValue(now()))+"] Starting servers");
  [DC: smscomp] SMSService sms = new SMSServer();
  [DC: telcomp] TelephoneService tel = new TelephoneServer();

  println("[Time: "+toString(timeValue(now()))+"] Starting handsets");
  new Handset(100,tel,sms);
  new Handset(100,tel,sms);
  new Handset(100,tel,sms);
  new Handset(100,tel,sms);
  new Handset(100,tel,sms);
  new Handset(100,tel,sms);
  new Handset(100,tel,sms);
  new Handset(100,tel,sms);
  new Handset(100,tel,sms);
  new Handset(100,tel,sms);
  await duration(1,1); // Slightly out of phase
  new Handset(100,tel,sms);
  new Handset(100,tel,sms);
  new Handset(100,tel,sms);
  new Handset(100,tel,sms);
  new Handset(100,tel,sms);
  new Handset(100,tel,sms);
  new Handset(100,tel,sms);
  new Handset(100,tel,sms);
  new Handset(100,tel,sms);
  new Handset(100,tel,sms);
}

If we compile and run the resource-sensitive model in the Erlang simulator, the console shows us when the handsets enter and leave the spike periods. In order to see the effect of the spike periods on the resource-aware model, we look at the deployment view. The two images below show the processing capacity and load of the two machines after slightly more 500 time intervals:

load_telcompload_smscompWe see that the load on the telephone server (“telcomp”) drops to 0 in the spike periods, and that the load on the SMS server (“smscomp”) goes to 100%. Obersve that there is a backlog of sendSMS requests on the SMS server: although the spike period ends at time interval 70, the server resumes normal load slightly before time 100.

5 Load-Balancing of Resources

We now consider how the model can be made resource-aware and enable the two machines hosting the telephone and SMS servers to exchange resources. The idea we pursue here is to define a simple resource manager (“Balancer”) for each server which monitors load and requests resources from the other machine if necessary. Each Balancer has an interface which allows the partner Balancer to be specified and resources to be requested:

interface Balancer  {
  Unit requestResources(DC comp);
  Unit setPartner(Balancer p);
}

The class itself takes as parameters a name (for friendly console-messages) and a minimum value for the locally available resources. The class has an active process defined by its run() method, which monitors the local load. Note that thisDC() is a reference to the deployment component on which an object is deployed and the method call load(Speed, 1) returns the percentwise usage of processing speed in the previous time interval. If the load is above 90%, the Balancer requests resources from its partner. If a Balancer receives a request for resources, it will transfer 1/3 of its available processing speed to its partner, unless this would reduce its own capacity below the minimum.

class Balancer(String name, Rat minimum) implements Balancer {
  Balancer partner = null;
  Rat ld = 100;

  Unit run() {
    await partner != null;
    while (True) {
      await duration(1, 1);
      ld = await thisDC()!load(Speed, 1); 
      if (ld > 90) { // Threshold1
        println("[Time: "+toString(timeValue(now()))+"] Balancer "+name+
                " has load "+toString(ld)+"% and needs more resources!");
        await partner!requestResources(thisDC());
      }
    }
  }

  Unit requestResources(DC comp) {
    InfRat total = await thisDC()!total(Speed);
    Rat ld = await thisDC()!load(Speed, 1);
    Rat requested = finvalue(total) / 3; // we know total will not be InfRat
    if (ld < 50 && (finvalue(total)-requested>minimum)) { // Threshold2
      thisDC()!transfer(comp, requested, Speed); }
  }

  Unit setPartner(Balancer p) { partner = p; }
}

Here, there are two thresholds which regulate the behavior of the local Balancer objects:

  • Threshold 1: Local load has reached a critical limit (here: ld>90). In this case, the Balancer will request resources from the partner Balancer.
  • Threshold 2: Available processing speed has reached a critical limit (here represented by ld < 50 && (finvalue(total)-requested>minimum). In this case, the Balancer will not transfer resources to the partner Balancer.

We add the Balancers to the main block of the model and set the partner Balancer:

{ // Main block:
    
  DC smscomp = new DeploymentComponent("smscomp", map[Pair(Speed, 90)]);
  DC telcomp = new DeploymentComponent("telcomp", map[Pair(Speed, 90)]);
  println("[Time: "+toString(timeValue(now()))+"] Starting servers");
  [DC: smscomp] SMSService sms = new SMSServer();
  [DC: telcomp] TelephoneService tel = new TelephoneServer();

  println("[Time: "+toString(timeValue(now()))+"] Starting balancers");
  Rat minimum = 15;
  [DC: smscomp] Balancer smsb = new Balancer("smsb",minimum);
  [DC: telcomp] Balancer telb = new Balancer("telb",minimum);
  await smsb!setPartner(telb);
  await telb!setPartner(smsb);

  println("[Time: "+toString(timeValue(now()))+"] Starting handsets");
  // Handsets go here, as before
}

Our model now looks like this:

label_telephone3

Let us now compile and run the resource-aware model in the Erlang simulator and compare its behavior to the resource-aware model above:

tel-balanced

sms-balanced