HomeForumSourceResearchGuide
<< back to guide home

In this section we describe how to perform runtime adaptation, with a complete example. We use dynamic loading to configure a working system and we then use runtime adaptation to change the components used in that system. Dana is designed to support immediate runtime adaptation: when an adaptation procedure finishes, the system is guaranteed to be in the new adapted state.

The adaptation API

Adaptation can be most easily performed via the composition.Adapt interface. You can see the source code behind Dana's adaptation protocol by looking at the corresponding component implementing this interface.

interface Adapt {
    bool adaptObject(Component ofComponent, Object object, IDC newImplementation, char typeName[])
    bool adaptRequiredInterface(IDC ofComponent, char interfaceName[], IDC toComponent, opt char toIntfName[])
    }

The adaptRequiredInterface() function is used to adapt required interfaces. It re-wires a component's required interface from its current connection to a given component to instead point to the provided interface of a different component. All object instances sourced via that required interface are immediately adapted to the new implementation from the new provided interface. This is the most common kind of adaptation.

The adaptObject() function is used to adapt one specific object to a different implementation. This function should generally only be used with dynamically created objects (i.e. objects created with the notation new MyObject() from c)

Adaptation example

Here we provide a complete example of dynamically building a system and adapting it, using the above adaptation API.

Create a new project directory, with a resources directory inside it.

Inside the resources folder add the following two files:

Core.dn
interface Core{
    void call()
    }
Counter.dn
interface Counter{
    transfer int number
    int getNext()
    }

And in the base directory of the project, add the following four files:

Core.dn
component provides Core requires io.Output out, data.IntUtil iu, Counter counter {
    void Core:call()
        {
        out.println("next number: $(iu.intToString(counter.getNext()))")
        }
    }
CounterA.dn
component provides Counter{
    
    int Counter:getNext()
        {
        number ++
        return number
        }
    }
CounterB.dn
component provides Counter{
    
    int Counter:getNext()
        {
        number += 2
        return number
        }
    }
Main.dn
uses Core

component provides App requires Loader loader, io.Output out,
                time.Timer timer, composition.Adapt adapt {
    
    int App:main(AppParam param[])
        {
        //load the components that we'll be using
        IDC myComponent = loader.load("Core.o")
        
        IDC variantA = loader.load("CounterA.o")
        IDC variantB = loader.load("CounterB.o")
        
        //bind our required interface to its initial configuration
        myComponent.wire("Counter", variantA, "Counter")
        
        Core myObject = new Core() from myComponent
        
        //test out adaptation...
        bool alpha = true
        while(true)
            {
            timer.sleep(1000)
            
            if (alpha)
                adapt.adaptRequiredInterface(myComponent, "Counter", variantA)
                else
                adapt.adaptRequiredInterface(myComponent, "Counter", variantB)
            
            alpha = !alpha
            
            myObject.call()
            }
        
        return 1
        }
    }

Here we have one implementation of "Core" and two variants of "Counter", which we switch between every second. When you run this example you will see that state is properly transferred between the different instances of Counter during adaptation.

To run this system, compile everything using dnc . and then use the command:

dana Main.o

State transfer across adaptation

It is sometimes useful to transfer state between different implementations of an adapted object. This is done by declaring transfer fields in the appropriate interface (see the Counter interface in the above example).

When an object is instantiated during an adaptation procedure, its regular constructor is not used (as it is not possible to provide the parameters). If an object needs to perform some logic when it is instantiated during an adaptation, the AdaptEvents interface should be provided as described next.

Receiving detailed adaptation notifications

A component can be made aware of runtime adaptation events that are happening to it by implementing the interface AdaptEvents as a secondary interface:

component provides MyObject(AdaptEvents) {
    void AdaptEvents:active()
        {
        }
    
    void AdaptEvents:inactive()
        {
        }
    }

The active function is called when the given component is replacing an instance of another implementation. The inactive function is called when the given component is being replaced. Note that inactive is always called on the outgoing implementation before active is called on the new implementation.

Summary

This tutorial has described how to perform runtime software adaptation in Dana and how to write components that can be adapted, including transferable state. The above examples of adaptation are entirely static, however; Dana's adaptation capabilities really become powerful when the set of components being used are not hard-coded but rather are dynamically discovered and dynamically dropped into and out of a system.

Programs that control adaptation in a more dynamic and generalised way are called meta-systems. Dana has several of these available in its standard library, with the PAL system being the most advanced. PAL provides a fully automated meta-system for dynamically composing software from discovered components following Dana's packaging system, and can be connected to a machine learning algorithm to discover which composition(s) perform best in different deployment conditions.