UNTIL NOW WE HAVE EXPLAINED how Petri-nets can be used to describe the behavior of interfaces, we have described which case we will use and which conflicts we will investigate. We will now explain which solution we propose to solve the problem of conflicting concurrency strategies. In general, we will do this by inserting an adaptor between the different communicating components. This adaptor will mediate the differences between the different interfaces. To do so, the adaptor will make use of three different modules: a liveness module, an enforce-action module and a concurrency module. Every module will be responsible for a certain functionality, which will meet certain requirements.
In this chapter we will first explain which assumptions we make on the components, then we will explain which requirements we place on the adaptor and last we will describe which modules we propose.
BELOW WE INTRODUCE A number of important assumptions that form the basis of our research.
The first assumption we make is that the concurrency behavior of all involved components is documented by means of a Petri-net description. These Petri-net descriptions follow the guidelines presented in section 3.8 and are used by the adaptor to keep track of the underlying component behavior. For every component involved, a marking resides within the adaptor. Whenever a message comes in from a component, the associated Petri-net will execute the transition corresponding to . As such, the adaptor always has a correct representation of the state of the underlying components.
A second assumption is that the concurrency adaptor, which is placed between conflicting components, needs to work at runtime. Working at runtime means that the adaptor will choose certain synchronization actions to modify the behavior of the synchronization overall. However, before the adaptor can verify whether this overall synchronization behavior is correct, the adaptor should be able to obtain some kind of feedback. However, because the adaptor itself has no explicit supervisor, it needs some point of reference that can be used to verify its own behavior. Such a point of reference should allow the adaptor to verify a number of requirements of the mediated concurrency behavior, hence our point of reference should strongly correlate with the concurrency behavior. Therefore we will assume that the core functionalities of the communicating components is compatible, while the concurrency behavior of the different components might be incompatible. In our case we will separate these as follows:
Fourthly, we assume that no concurrency strategy of any of the components within the conflict locks out certain core functionality, because this would make it effectively impossible to mediate the conflict.
GIVEN THE ABOVE ASSUMPTIONS we will now define the requirements of our concurrency adaptor. The three requirements we present have been introduced after doing preliminary experiments (discussed in chapter 11). Initially we tried to create an adaptor between different components simply requiring that no-conflict should arise. This however, resulted often a) in adaptors that either behaved in such a way that no communication with the server component occurred (by feeding always a lockFalse back to the originator), or b) in adaptors that mediated the conflict in such a way that race conditions were allowed.
Therefore we have introduced three requirements: the no-conflict requirement, the no-races requirement and the liveness requirement. Below we will explain these three requirements in more detail. Together, if satisfied, they lead to an adaptor that will mediate concurrency conflicts in an appropriate way. However, the three requirements we will present are not exhaustive. For instance other requirements with respect to timing, or requirements with respect to dead-locks might need to be added in other domains.
If we assume that the adaptor contains Petri-nets for all participating components and that the marking of every Petri-net represents the current state of the underlying component, then we can intuitively declare a certain situation to be a conflict whenever one logic transition can be executed within one Petri-net but not in the other.
Formally, two interfaces and are, given two markings and , in conflict when a logic transition exists that is enabled in only one of both interfaces. We will use to denote a conflict between two Petri-net markings:
When at the moment a message arrives then we can be sure that the adaptor is not working correctly. We will define this to be the no-conflict requirement
The above definition is a 'good-enough' definition but not a 'cover-all' definition. The definition states that the adaptor is not working correctly if the precondition doesn't hold, but it does not ensure that every incorrect working adaptor is detected by the precondition. In practice, this requirement is difficult to check because there should be no possible execution trace leading to a conflict. This means, that if we want to verify this requirement, we must be sure that we have investigated all possible traces. We will come back to this issue later on.
The no-conflict requirement immediately results from the fact that we want to solve a conflict between interfaces. Nevertheless, it does not guarantee that enabled actions do not interfere with each other. To guarantee this we need to make sure that actions that are in the same critical section (which we will formally explain in section 10.2) are executed atomically. Therefore we need another requirement:
The no-races requirement, if satisfied, guarantees that no unwanted behavior as a result from race conditions will occur. However, often other requirements such as no-deadlocks or fairness could be also in place. Here we assume that, whenever appropriate the no-races requirement can be extended to include these extra requirements.
The problem with the above requirement is that it is difficult to define a critical section. Neither within the Petri-nets, nor within the components, there is a uniform notion of 'a critical section'. Therefore we define a critical section as a set changes that cannot be interrupted without bringing the corresponding Petri-nets in conflict. In section 10.2 (page ) we will elaborate further on this and explain more intuitively why this is a good definition.
With the above no-conflict and the no-races requirements, we do not avoid adaptors that do not work. Adaptors that simply avoid any synchronization operation by always feeding a LockFalse back to the requester will match both previous requirements. However, such an adaptor is clearly not doing what is expected. Therefore we introduce the concept of liveness, or how well a component can proceed with its core functionality. The problem with the notion of liveness is that it can be either defined formally using Petri-nets, or informally based on some reward from the underlying component. Below we will present two possible definitions of liveness.
The first definition defines formally liveness on the Petri-nets involved. If the Petri-net is alive we assume that the underlying component is alive as well. Liveness on a Petri-net is defined as the number of transitions that still have an option to be enabled in the future. If under a marking a transition exists that can no longer be enabled, the Petri-net is not alive. However, this formal definition is very strong because not even all Petri-nets, received from the underlying components, will be alive when they start. For instance, a Petri-net with an Init transition might, after executing this transition, not be alive anymore, because this Init transition will never be enabled. This indicates that the formal definition of liveness might be a bit too strong.
The second definition introduces rewards. Here we assume that the underlying component knows exactly what it wants to obtain in the future and indicates this by sending rewards to the adaptor. A component developer could specify this kind of information as checkpoints within the source code or as favorite transitions within the offered Petri-net. In both cases, the liveness requirement specifies that as many as possible positive future branches need to be executed, or in other words need to obtain as much reward as possible from the underlying client component. How the rewards are defined will be discussed in detail in section 9.2 (page ) and will later on be used as the rewards within a reinforcement learning algorithm (section 9). This definition of liveness is similar to the description given in [Lyn96].
THE PROBLEM OF CONCURRENCY STRATEGY conflicts, embodied in our case, the availability of formal interface descriptions and the functional requirements of a solution now allow us to investigate which techniques are suitable to solve the problem. In general two techniques are possible: formal deduction of an adaptor and learning. We will investigate for every requirement the applicability of each technique. A pure application of one of both techniques will be impossible, as we will explain below, therefore in section 7.4 we will explain how we create a hybrid, modularized adaptor.
Given the formal Petri-net description of all involved components and assuming that we have a full formal definition of the requirements we might expect that it is possible to automatically deduce an adaptor, which satisfies all requirements and honors all restrictions. Such a formal technique would, given the Petri-nets as input, find some algorithm that could be placed in between the different Petri-nets such that all requirements hold. If we assume that itself is a Petri-net that directly links the two involved Petri-nets together, then we might not be able to verify the liveness requirement because the formal liveness property of Petri-nets is not yet known to be decidable or not (see section 3.10 on page ). From this observation, it becomes clear that it is unlikely to create automatically.
In this section we will investigate whether a pure learning-algorithm approach (as pictured in figure 7.2) might be suitable. Learning algorithms are applicable in situations where formal techniques fail to offer solutions. A learning algorithm is typically a search algorithm that uses a number of heuristics (implicitly or explicitly represented) to find solutions. However, learning algorithms still remain probabilistic processes and proving that a probabilistic process will only result in adaptors that satisfy all requirements might be very difficult. Especially if the requirements state -behavior (= -behavior), such as the no-races or no-conflicts requirements.
The enforce-action module is placed between the server component and the remaining modules of the adaptor (see figure 7.3). Its main goal is to prepare the server for incoming messages. It does this by inserting appropriate synchronization messages in the message stream. For instance, when an setPosition arrives, and the server has not yet been locked, this module will lock it and send through the setPosition message.
This way, the adaptor fulfills the no conflict requirement, because it can always enforce requested actions upon the server component. If the client component expects a certain transition to be enabled (for instance setPosition), the adaptor should be able to enable this transition at the server component. Given the Petri-net description of an interface it is not too difficult to deduce formally what should be done to enable a certain action, or to reach a certain state. In chapter 8 we will explain how we do this. The responsibilities of the enforce-action module are:
A second important part of the concurrency adaptor are the liveness-modules. For every client-component there will be one liveness module. Every module is responsible for learning at runtime how to keep a client component alive, thereby honoring the constraints offered by the Petri-net. How the rewards are defined and assigned at runtime will be described in detail in section 9.2. The working of the entire liveness module is described in chapter 9. The responsibilities of the liveness module are:
The concurrency module is placed between the two other modules. However, the problem of a good concurrency strategy is even more prominent than before, because any possible action chosen by the client will always be able to execute at the server (because of the enforce-action module), resulting in data and action races. Solving this problem is the responsibility of the concurrency module.
The concurrency module will be discussed in chapter 10. It receives from the client all the actions that should be forwarded to the server, from the liveness modules it receives the necessary information to be able to recognize critical sections. The concurrency module will use this information and make the different connected client components work together in a way that avoids colliding critical sections. The responsibilities of the concurrency module are:
THE PRESENTED MODULES make our initial requirements much more accessible. Two of the 'no-'-requirements (no-conflicts, no-races) are satisfied by known-to-work solutions and the liveness-requirement is satisfied by means of a learning algorithm. Cascading these three modules as pictured in figure 7.3 will result in a concurrency adaptor. In this section we will argument that this construction results in an adaptor that honors all the previously stated requirements.
The no-conflict requirement declares that at no point in time the possibility of an immediate conflict should exist, that is a transition that is enabled in one Petri-net but not in the other. Formulated in terms of messages, that there is no message that can be sent but not received. The enforce-action module guarantees that it will always be able to execute any incoming message on the server-component. The two other modules present, the concurrency module and the liveness module, literally pass through any message. So any, possible request from any client component will always be executed. Hence no message exists that can be posted but not received, thus satisfying the no-conflict requirement.
The concurrency module its main responsibility is recognizing and interleaving critical sections. So, if we know that no component but the concurrency module can contact the server-component, we know that there are no races. To argument this, let us assume the inverse, that the whole setup allows for two colliding critical sections. The only place where this collision could happen is at the concurrency module, because this is the only place where both critical sections will be present at the same time. The liveness modules work separately, hence cannot result in an incorrect interweaving of messages. The action enforcer module comes after the concurrency module and simply does not interleave any message, every incoming message is simply passed through. Thus normally all critical sections should already be serialized. Therefore, if two colliding critical sections can occur, the fault will lie in the concurrency module. Thus, if the concurrency module works correctly, then the interconnection of the three modules will also be correct.
The liveness requirement states that all components should stay alive. This will be guaranteed by the interaction of the concurrency module and the liveness module. Every liveness module is responsible for keeping one component alive and because the concurrency module is, among other things, responsible for being fair with respect to liveness, all components will be alive.
IN THIS CHAPTER we have stated the assumptions we make. Firstly we assume that the core functionality of the conflicting components is compatible, secondly we assume that all components offer a Petri-net description of their concurrency behavior and that the concurrency behavior does not hide any core functionality. Thirdly we assume that a clear separation of functionalities exists with respect to the role of clients and server interfaces, and with respect to the role of every message involved in an interface. Messages that take part in the core functionality of a component are called logic messages. Messages that take part in the synchronization behavior are called synchronization messages.
After presenting the assumptions made, we have stated the requirements of a concurrency adaptor. These are the no-conflict requirement, the no-races requirement and the liveness requirement. With these requirements in mind we explained that neither a pure formal deductive, nor a pure learning approach will work to find a solution that will satisfy all requirements. Therefore we modularized the adaptor into three modules. The first module is an enforce-action module, which will solve the no-conflict requirement. The second module is a concurrency module, that satisfies the no-races requirement. And the third module is a liveness module that satisfies the liveness requirement. Finally we have argued that, given the assumptions and the responsibilities of the different modules, the interconnection of these three modules will result in an adaptor that satisfies all three requirements. In the following chapters we will describe every module in detail.