Skip to content

Deferred methods example

This example shows how the deferred methods work. The idea is to model a Manager who will receive the workload from clients and forward it along all the available Workers published in a bus called workersBus. If there are no available workers, the manager will perform the task.

Interface

The fibonacci package contains the fibonacci.stl file, which holds two interface classes: Manager and Worker. Manager inherits from Worker for allowing him to behave as a worker as well.

package fibonacci;

// Example class that does some work. In this case, we
// illustrate it by computing the fibonacci sequence.
class Worker
{
  // Returns the Fibonacci number of the element "n" of the Fibonacci sequence.
  //
  // @param n index of the Fibonacci sequence.
  fn computeFibonacci(n: u32) -> u64;
}


// A special kind of worker that forwards the
// incoming work to other workers.
class Manager : extends Worker
{
  // Where to look for the workers.
  var workersBus : string [static];
}

Implementation

To mark methods as deferred, we need to tell the Sen code generator that we don't want to (or can't) provide a result straight away. To do so, we specify our code generation settings:

The codegen_settings.json file is passed to the Sen code generator via the CODEGEN_SETTINGS option in add_sen_package. It is a JSON object keyed by class name under classes, each listing the methods to change. Here fibonacci.Worker lists computeFibonacci under deferredMethods.

{
  "classes": {
    "fibonacci.Worker": {
      "deferredMethods": [
        "computeFibonacci"
      ]
    }
  }
}

This will force us to implement a method that, instead of returning a value, receives an std::promise that we can set whenever we want. In the case of the regular worker, we do the following:

class FibonacciWorker: public WorkerBase
{
public:
  SEN_NOCOPY_NOMOVE(FibonacciWorker)

public:
  using WorkerBase::WorkerBase;
  ~FibonacciWorker() override = default;

public:
  void computeFibonacciImpl(uint32_t n, std::promise<uint64_t>&& promise) override
  {
    std::this_thread::sleep_for(std::chrono::seconds(5));  // simulate heavy workload
    promise.set_value(::fibonacci::computeFibonacci(n));
  }
};

In the case of the manager, our implementation is a bit different:

  /// Forwards the call to a random worker, or does the work if none is found.
  void computeFibonacciImpl(uint32_t n, std::promise<uint64_t>&& promise) override
  {
    if (workers_->list.getObjects().empty())
    {
      std::cout << "No workers available, manager is performing the task" << std::endl;
      std::this_thread::sleep_for(std::chrono::seconds(5));  // simulate heavy workload
      promise.set_value(::fibonacci::computeFibonacci(n));
    }
    else
    {
      auto& selectedWorker = selectRandomWorker();
      std::cout << "Sending task to " << selectedWorker.asObject().getName() << std::endl;
      selectedWorker.computeFibonacci(
        n, {this, [p = std::move(promise)](const auto& result) mutable { p.set_value(result.getValue()); }});
    }
  }

Here we have a list of workers. If we don't find any worker we perform the job on our own. Otherwise, we select a random worker from the list and call its computeFibonacci function. When we get the result, we set the value of the promise we made to the user. Note that we need to std::move the promise into the callback so that we can use it from within that context.

In Sen, each Component runs in an execution thread. If the Workers are defined in the same component they will share the same thread and won't be able to do work in parallel. In this example, each Worker lives in a different component. Same thing with the Manager. Each implementation of computeFibonacciImpl sleeps for five seconds before computing, to make this threading behavior visible. The computeFibonacci function itself has no side effects.

NOTE: Since Manager extends Worker, a query for workers on the workersBus would match the manager itself. The manager's interest excludes it by name, so it never appears in its own worker list.

Configuration file
# $schema: ../base/schema.json
include:
  - ../base/term.yaml

build:
  - name: myComponent
    freqHz: 30
    imports: [fibonacci]
    group: 3
    objects:
      - class: fibonacci.FibonacciManager
        workersBus: my.tutorial
        name: fibManager
        bus: my.tutorial
  - name: myComponentA
    freqHz: 30
    imports: [fibonacci]
    group: 3
    objects:
      - class: fibonacci.FibonacciWorker
        name: fibWorkerA
        bus: my.tutorial
  - name: myComponentB
    freqHz: 30
    imports: [fibonacci]
    group: 3
    objects:
      - class: fibonacci.FibonacciWorker
        name: fibWorkerB
        bus: my.tutorial
  - name: myComponentC
    freqHz: 30
    imports: [fibonacci]
    group: 3
    objects:
      - class: fibonacci.FibonacciWorker
        name: fibWorkerC
        bus: my.tutorial
  - name: myComponentD
    freqHz: 30
    imports: [fibonacci]
    group: 3
    objects:
      - class: fibonacci.FibonacciWorker
        name: fibWorkerD
        bus: my.tutorial
  - name: myComponentE
    freqHz: 30
    imports: [fibonacci]
    group: 3
    objects:
      - class: fibonacci.FibonacciWorker
        name: fibWorkerE
        bus: my.tutorial

How to run it

sen run config/12_fibonacci/1_fibonacci.yaml

This will open a shell and tell Sen to instantiate the implementations in the my.tutorial bus.

You ask the Manager for computing n Fibonacci number by:

my.tutorial.fibManager.computeFibonacci 8

Note that the potential of this example is to ask the Manager for as many petitions as desired, while crosschecking that the workload is constantly being distributed and simultaneously performed by the different workers without delaying the workload distribution.

You can also ask one of the workers directly. The configuration publishes five of them, fibWorkerA through fibWorkerE:

my.tutorial.fibWorkerA.computeFibonacci 8

Testing package internals with TEST_TARGET

By default, Sen packages compile with hidden symbol visibility, so unit tests cannot link directly against the package and call internal functions. The TEST_TARGET option creates an additional STATIC library with all symbols exposed, which you can link your test executables against.

The fibonacci package uses this to expose and test the pure computeFibonacci algorithm independently of the runtime. In the package's CMakeLists.txt:

add_sen_package(
  TARGET fibonacci
  TEST_TARGET fibonacci_test_lib # static library with all symbols, used by unit tests
  MAINTAINER "Sen Core Team (enrique.parodi@airbus.com)"
  VERSION "0.0.1"
  DESCRIPTION "Illustrates the usage of deferred functions"
  SOURCES src/fibonacci.cpp
  STL_FILES stl/fibonacci.stl
  CODEGEN_SETTINGS src/codegen_settings.json
  SCHEMA_PATH ${CMAKE_CURRENT_BINARY_DIR}
)

add_sen_unit_test_suite(
  fibonacci_test
  test/fibonacci_algorithm_test.cpp
  LINK_DEPS
  fibonacci_test_lib
)

set_target_properties(fibonacci_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")

TEST_TARGET names the static library, fibonacci_test_lib. The unit test suite is a separate target, registered with add_sen_unit_test_suite and linked against that static library rather than against the shared package.

Any modifications to the target (e.g. target_include_directories) must be applied to fibonacci_obj, the intermediate OBJECT target, not to fibonacci or fibonacci_test_lib directly (see the CMake integration page of the manual for details).