Kompics Basics

This section of the tutorial will properly introduce the basic concepts in Kompics, namely components, ports, handlers, and channels.

The example application we will use is a simple Ping Pong application, with two components that send messages back and forth between them.

Components

A component in Kompics is a stateful object that can be scheduled and can access its own state without need for synchronisation. Typically, components encapsulate some kind of functionality, by providing certain services and possibly requiring others. Components can also have child components created within. These parent-*child* relations form a supervision hierarchy tree.

In programming language terms, a component is a class that extends ComponentDefinitionComponentDefinition. If a component needs any additional parameters upon creation, a constructor that takes an implementation of Init can be used and an instance passed on creation. Otherwise simply pass NONENone.

For this example we need three components, two to do the work, and a parent that handles setup:

Pinger

Java
public class Pinger extends ComponentDefinition {
}
Scala
class Pinger extends ComponentDefinition {
}

Ponger

Java
public class Ponger extends ComponentDefinition {
}
Scala
class Ponger extends ComponentDefinition {
}

Parent

Java
public class Parent extends ComponentDefinition {
  Component pinger = create(Pinger.class, Init.NONE);
  Component ponger = create(Ponger.class, Init.NONE);
}
Scala
class Parent extends ComponentDefinition {
  val pinger = create[Pinger];
  val ponger = create[Ponger];
}

Ports and Events

A port type in Kompics is a bit like a service interface. It defines what kind of events (you may think of messages, although in Kompics we typically reserve that term for events that are addressable via the network) may pass through the port and in which direction. Events may be declared in a port type in either indication ( positive ) or request ( negative ) direction. In a similar fashion a component either requires or provides a port (think of a port type instance).

Analogies

The closest analogy to the Kompics terminology in this respect might be electric charge carriers and electrodes in some kind of conductive medium. Think of the events as charge carriers (indications carry positive charge, and requests carry negative charge). Every port has both an anode and a cathode side. If a component requires port A then inside the component you have access to A’s positive (cathode) side where indications (positive charge carriers) come out of, and outside the component you have access to A negative (anode) side where requests (negative charge carriers) come out of. Conversely if a component provides A then inside the component the negative (anode) side spits out requests (negative charge carriers) and the outside is positive (cathode) and indications (positive charge carriers) come out this way. In both cases the charge that is not going out is the one that is going in.

An alternative analogy, that is a bit more limited but usually easier to keep in mind, is that of service providers and consumers. Consider a port A to be a service contract. A component that provides service A handles events that are specified as requests in A and sends out events that are specified as indications in A. Conversely a component that requires service A sends out events that are specified as requests in A and handles events that are specified as indications in A (thus are in a sense replies to its own requests).

In programming language terms an event is a class that is a subtype of KompicsEvent, which is only a marker interface with no required methods). A port type is a singleton that extends PortTypePort and registers its types with their direction during loading. Port instances fall in the two categeories:

  1. Those that implement Positive over the port type, which are the result of a call to requiresrequires, and
  2. those that implement Negative of the port type, which are the result of a call to providesprovides.

Internally ports are binary linked with both a positive and a negative side.

For this example we need two events and one port type:

Ping

Java
package jexamples.basics.pingpong;

import se.sics.kompics.KompicsEvent;

public class Ping implements KompicsEvent {}
Scala
object Ping extends KompicsEvent;

Pong

Java
package jexamples.basics.pingpong;

import se.sics.kompics.KompicsEvent;

public class Pong implements KompicsEvent {}
Scala
object Pong extends KompicsEvent;

PingPongPort

Java
package jexamples.basics.pingpong;

import se.sics.kompics.PortType;

public class PingPongPort extends PortType {
  {
    request(Ping.class);
    indication(Pong.class);
  }
}
Scala
object PingPongPort extends Port {
  request(Ping);
  indication(Pong);
}
Note

It is highly recommended to only write completely immutable events. Since Kompics will deliver the same event instance to all subscribed handlers in all connected components, writing mutable events can lead to some nasty and difficult to find bugs.

For encapsulating collections in a safe manner, the reader is referred Google’s excellent Guava library (which is already a dependency of Kompics core anyway) and its immutable collection types.

We also want to add the ports to the two components such that Pinger sends Ping\s and Ponger sends Pong\s, which is hopefully somewhat intuitive:

Pinger with Port

Java
public class Pinger extends ComponentDefinition {

  Positive<PingPongPort> ppp = requires(PingPongPort.class);
}
Scala
class Pinger extends ComponentDefinition {

  val ppp = requires(PingPongPort);
}

Ponger with Port

Java
public class Ponger extends ComponentDefinition {

  Negative<PingPongPort> ppp = provides(PingPongPort.class);
}
Scala
class Ponger extends ComponentDefinition {

  val ppp = provides(PingPongPort);
}

Event Handlers

In order for components to actually get scheduled and process events, a handler for a specific event type must be subscribed to a port that spits out that kind of event. If an event arrives at a component’s port and no handler is subscribed for its type, then the event is simply silently dropped.

In Java terms the most common way of working with handlers is to assign an anonymous class that extends Handler of the desired event type to either a class field or a block variable and then call subscribe with that variable and the target port.In Scala terms, a handler is a partial function f: Any => Unit, which is subscribed to a port instance p by calling p uponEvent f. Within f, the event types that are supposed to be handled can be pattern-matched for and then the appropriate code for each event invoked.

In our example we want the Pinger to send the first Ping when it received the Start event (which we saw in the helloworld example already), and then reply to every Pong event it receives with a new Ping. The Ponger simply waits for a Ping and replies with a Pong. We also log something so we can see it working on the console.

Pinger (final)

Java
package jexamples.basics.pingpong;

import se.sics.kompics.Kompics;
import se.sics.kompics.ComponentDefinition;
import se.sics.kompics.Positive;
import se.sics.kompics.Handler;
import se.sics.kompics.Start;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Pinger extends ComponentDefinition {

  Positive<PingPongPort> ppp = requires(PingPongPort.class);

  Handler<Start> startHandler =
      new Handler<Start>() {
        public void handle(Start event) {
          trigger(new Ping(), ppp);
        }
      };
  Handler<Pong> pongHandler =
      new Handler<Pong>() {
        public void handle(Pong event) {
          logger.info("Got Pong!");
          trigger(new Ping(), ppp);
        }
      };

  {
    subscribe(startHandler, control);
    subscribe(pongHandler, ppp);
  }
}
Scala
package sexamples.basics.pingpong;

import se.sics.kompics.sl._

class Pinger extends ComponentDefinition {

  val ppp = requires(PingPongPort);

  ctrl uponEvent {
    case _: Start => {
      trigger(Ping -> ppp);
    }
  }

  ppp uponEvent {
    case Pong => {
      log.info(s"Got Pong!");
      trigger(Ping -> ppp);
    }
  }
}

Ponger (final)

Java
package jexamples.basics.pingpong;

import se.sics.kompics.ComponentDefinition;
import se.sics.kompics.Negative;
import se.sics.kompics.Handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Ponger extends ComponentDefinition {

  Negative<PingPongPort> ppp = provides(PingPongPort.class);

  Handler<Ping> pingHandler =
      new Handler<Ping>() {
        public void handle(Ping event) {
          logger.info("Got Ping!");
          trigger(new Pong(), ppp);
        }
      };

  {
    subscribe(pingHandler, ppp);
  }
}
Scala
package sexamples.basics.pingpong;

import se.sics.kompics.sl._

class Ponger extends ComponentDefinition {

  val ppp = provides(PingPongPort);

  ppp uponEvent {
    case Ping => {
      log.info(s"Got Ping!");
      trigger(Pong -> ppp);
    }
  }
}

Channels

Since normal events in Kompics are not addressed, we need to tell the system somehow where a triggered event is supposed to go. The method for this is by connecting channels. A single channel connects exactly two ports of opposite polarity (or direction, if you prefer). You can connect both ports that are inside and outside of a component. The normal way is to connect the outside of a required port of a component A to the outside of a required port of another component B. In this setup B provides the port’s service to A, so to speak. The alternative setup is to connect the inside of required port of A to the outside of a required port of B (remember that insides and outsides are of opposite types, so this actually works). This setup is called chaining and it has both A and B share the service of whatever component connects to the outside of A’s port. Alternatively, A (or its parent) could pass on the outside of the port that A is connected to directly to B. This has the same effect, but is a bit more efficient (fewer method invocations while travelling along channel chains). However, it might also break abstraction in some cases. The decision of which method is appropriate under certain conditions is left up to the programmer.

In JavaScala channels are created with the connectconnect method. The directionality of the arrow (->) is from the component which provides the port, to the component which requires the port.

Parent (final)

Java
package jexamples.basics.pingpong;

import se.sics.kompics.Channel;
import se.sics.kompics.ComponentDefinition;
import se.sics.kompics.Component;
import se.sics.kompics.Init;

public class Parent extends ComponentDefinition {
  Component pinger = create(Pinger.class, Init.NONE);
  Component ponger = create(Ponger.class, Init.NONE);
  {
    connect(
        pinger.getNegative(PingPongPort.class),
        ponger.getPositive(PingPongPort.class),
        Channel.TWO_WAY);
  }
}
Scala
package sexamples.basics.pingpong;

import se.sics.kompics.sl._

class Parent extends ComponentDefinition {
  val pinger = create[Pinger];
  val ponger = create[Ponger];

  connect(PingPongPort)(ponger -> pinger);

}

Kompics Runtime

The runtime itself is responsible for starting and stopping the scheduler and initialising the component hierarchy. The entry point to start Kompics is Kompics.createAndStartKompics.createAndStart which comes in several variants for tuning and parameters. The most basic one simply takes the top-level component’s class instance.

For our example we want to start Kompics with the Parent top-level component and since it would ping-pong forever on its own, we also want to stop it again after waiting for some time, say ten seconds:

Main

Java
package jexamples.basics.pingpong;

import se.sics.kompics.Kompics;

public class Main {
  public static void main(String[] args) {
    Kompics.createAndStart(Parent.class);
    try {
      Thread.sleep(10_000);
      Kompics.shutdown();
    } catch (InterruptedException ex) {
      System.err.println(ex);
    }
  }
}
Scala
package sexamples.basics.pingpong

import se.sics.kompics.sl._

object Main {
  def main(args: Array[String]): Unit = {
    Kompics.createAndStart(classOf[Parent]);
    try {
      Thread.sleep(10000);
      Kompics.shutdown();
    } catch {
      case ex: InterruptedException => Console.err.println(ex)
    }
  }
}

Now finally compile with:

sbt compile

To run the project from within sbt, execute:

runMain jexamples.basics.pingpong.Main
runMain sexamples.basics.pingpong.Main

Component State

So far the Kompics components we used haven’t really used any state. To show a simple example we are going to introduce a counter variable in both Pinger and Pinger and print the sequence number of the current Ping or Pong to the console. To show that this works correctly even in multi-threaded execution we’ll also add a second thread to the Kompics runtime.

Wainting Main

Java
package jexamples.basics.pingpongstate;

import se.sics.kompics.Kompics;

public class Main {
  public static void main(String[] args) throws InterruptedException {
    Kompics.createAndStart(Parent.class, 2);
    Kompics.waitForTermination();
  }
}
Scala
package sexamples.basics.pingpongstate

import se.sics.kompics.sl._

object Main {
  def main(args: Array[String]): Unit = {
    Kompics.createAndStart(classOf[Parent], 2);
    Kompics.waitForTermination();
  }
}

Pinger with State

Java
package jexamples.basics.pingpongstate;

import se.sics.kompics.Kompics;
import se.sics.kompics.ComponentDefinition;
import se.sics.kompics.Positive;
import se.sics.kompics.Handler;
import se.sics.kompics.Start;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Pinger extends ComponentDefinition {

  Positive<PingPongPort> ppp = requires(PingPongPort.class);
  private long counter = 0;

  Handler<Start> startHandler =
      new Handler<Start>() {
        public void handle(Start event) {
          trigger(new Ping(), ppp);
        }
      };
  Handler<Pong> pongHandler =
      new Handler<Pong>() {
        public void handle(Pong event) {
          counter++;
          logger.info("Got Pong #{}!", counter);
          if (counter < 100) {
            trigger(new Ping(), ppp);
          } else {
            Kompics.asyncShutdown();
          }
        }
      };

  {
    subscribe(startHandler, control);
    subscribe(pongHandler, ppp);
  }
}
Scala
package sexamples.basics.pingpongstate

import se.sics.kompics.sl._

class Pinger extends ComponentDefinition {

  val ppp = requires(PingPongPort);

  private var counter: Long = 0L;

  ctrl uponEvent {
    case _: Start => {
      trigger(Ping -> ppp);
    }
  }

  ppp uponEvent {
    case Pong => {
      counter += 1L;
      log.info(s"Got Pong #${counter}!");
      if (counter < 100L) {
        trigger(Ping -> ppp);
      } else {
        Kompics.asyncShutdown();
      }
    }
  }
}

Ponger with State

Java
package jexamples.basics.pingpongstate;

import se.sics.kompics.ComponentDefinition;
import se.sics.kompics.Negative;
import se.sics.kompics.Handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Ponger extends ComponentDefinition {

  Negative<PingPongPort> ppp = provides(PingPongPort.class);

  private long counter = 0;

  Handler<Ping> pingHandler =
      new Handler<Ping>() {
        public void handle(Ping event) {
          counter++;
          logger.info("Got Ping #{}!", counter);
          trigger(new Pong(), ppp);
        }
      };

  {
    subscribe(pingHandler, ppp);
  }
}
Scala
package sexamples.basics.pingpongstate

import se.sics.kompics.sl._

class Ponger extends ComponentDefinition {

  val ppp = provides(PingPongPort);

  private var counter: Long = 0L;

  ppp uponEvent {
    case Ping => {
      counter += 1L;
      log.info(s"Got Ping #${counter}!");
      trigger(Pong -> ppp);
    }
  }
}

Compile and run from within sbt in the same way as before:

runMain jexamples.basics.pingpongstate.Main
runMain sexamples.basics.pingpongstate.Main
The source code for this page can be found here.