User Tools

Site Tools


runtime-adaptation

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.Adapter interface. You can see the source code behind Dana's adaptation protocol by looking at the corresponding component implementing this interface.

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

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 only be used with dynamically created objects (i.e. using new MyObject() from c :< MyObject)

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 the usual resources folder.

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
 uses Counter
 
 component provides App requires Loader loader, io.Output out,
                                 time.Timer timer, composition.Adapter adapter {
 
	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
		dana.rewire(myComponent :> Counter, variantA :< Counter)
 
		Core myObject = new Core() from myComponent
 
		//test out adaptation...
		bool alpha = true
		while(true)
			{
			timer.sleep(1000)
 
			if (alpha)
				adapter.adaptRequiredInterface(myComponent, "Counter", variantA)
				else
				adapter.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 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 and esher systems being the most advanced. PAL provides a fully automated meta-system for dynamically composing software from discovered components following Dana's packaging system, while esher provides an interactive way of dynamically composing software using a command-line interface.

runtime-adaptation.txt · Last modified: 2017/08/18 11:10 by barryfp

Page Tools