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 ComponentDefinition
ComponentDefinition
. 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 NONE
None
.
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).
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 PortType
Port
and registers its types with their direction during loading. Port instances fall in the two categeories:
- Those that implement
Positive
over the port type, which are the result of a call torequires
requires
, and - those that implement
Negative
of the port type, which are the result of a call toprovides
provides
.
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); }
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 connect
connect
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.createAndStart
Kompics.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