/**
* Copyright 2011-2012 Universite Joseph Fourier, LIG, ADELE team
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.imag.adele.apam.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import fr.imag.adele.apam.ApamResolver;
import fr.imag.adele.apam.CST;
import fr.imag.adele.apam.Component;
import fr.imag.adele.apam.Composite;
import fr.imag.adele.apam.Instance;
import fr.imag.adele.apam.Link;
import fr.imag.adele.apam.RelationDefinition;
import fr.imag.adele.apam.Resolved;
import fr.imag.adele.apam.apform.Apform2Apam;
import fr.imag.adele.apam.declarations.ComponentKind;
import fr.imag.adele.apam.declarations.references.components.ComponentReference;
import fr.imag.adele.apam.declarations.references.resources.ResourceReference;
/**
* This is the base class that is used to represent the pending requests that need to
* be resolved.
*
* There are two cases :
*
* 1) Blocked requests, in which a thread is waiting for resolution to happen
* 2) Dynamic requests, in which resolution is performed asynchronously
*
* Most of the code is common for both cases, however blocked request are short-lived
* objects that exist only for the duration of the unsatisfied condition, while dynamic
* request are long-standing objects that must be asynchronously updated.
*
* TODO perhaps we should have different classes to represent these two kinds of requests
* and move the common code into an abstract superclass.
*
*/
public class PendingRequest extends Apform2Apam.PendingThread {
/**
* The source of the relation
*/
protected final Component source;
/**
* The relation to resolve
*/
protected final RelationDefinition relation;
/**
* The composite in which context the resolution will be performed
*/
protected final Composite context;
/**
* The resolver
*/
protected final ApamResolver resolver;
/**
* The result of the resolution
*/
private Resolved<?> resolution;
/**
* Whether this request has been disposed, this happen for instance when the
* source component is removed
*/
private boolean isDisposed = false;
/**
* Whether the thread that created this request is blocked waiting for resolution
*/
private boolean isBlocked = false;
/**
* The stack of the blocked requests
*/
private List<StackTraceElement> stack;
/**
* Builds a new pending request reification
*/
public PendingRequest(ApamResolver resolver, Component source, RelationDefinition relDef) {
super(source.toString());
this.resolver = resolver;
this.source = source;
this.context = (source instanceof Instance) ? ((Instance) source).getComposite() : CompositeImpl.getRootAllComposites();
this.relation = relDef;
this.resolution = null;
}
@Override
public String getCondition() {
return "resolution of "+relation.toString();
}
/**
* The context in which the resolution is requested
*/
public Composite getContext() {
return context;
}
/**
* The source of the relation
*/
public Component getSource() {
return source;
}
/**
* The relation that needs resolution
*/
public RelationDefinition getRelation() {
return relation;
}
/**
* Pending requests may be generated for exactly the same source and target in different threads, so
* we force reference equality.
*
* TODO For dynamic request it makes sense to have semantic equality based on the source and the relation,
* one more reason to consider splitting this class
*/
@Override
public final boolean equals(Object object) {
return this == object;
}
@Override
public final int hashCode() {
return super.hashCode();
}
/**
* The result of the last resolution of this request
*/
public synchronized Resolved<?> getResolution() {
return resolution;
}
/**
* Whether this dynamic request is already being resolved by the another thread
*/
private boolean isResolving = false;
/**
* Resolves the request when context changes may have satisfied the request.
*/
public void resolve() {
/*
* If a thread is blocked waiting for resolution of this request, we simply notify it to let it retry
* resolution if needed.
*
* TODO There is an important invariant that a blocked request is owned by a single thread. This is no clear
* in this class, as it is also used for dynamic requests that are "pending" to be recalculated, but that are
* not blocking a thread. We should consider separating the two different classes to represent the different
* concepts.
*/
synchronized (this) {
if (this.isBlocked) {
this.notifyAll();
return;
}
}
/*
* Otherwise, this is dynamic request an we resolve it in the context of the current thread. If the request is
* already being resolved by another thread, we wait to avoid concurrent invocations of the resolver.
*
*
* NOTE is the responsibility of the caller to invoke this method in the appropriate thread to implement the
* intended dynamic update policy. Notice that this method may block or throw exceptions (as a side effect of
* resolution) that may impact the calling thread.
*/
synchronized (this) {
try {
while (this.isResolving && ! this.isDisposed) {
this.wait();
}
} catch (InterruptedException e) {
}
this.isResolving = true;
}
/*
* IMPORTANT NOTE Notice that we must invoke the resolution outside any synchronized block to avoid
* potential deadlocks
*/
Resolved<?> result = null;
try {
result= resolver.resolveLink(source, relation);
}
catch (Exception resolutionException) {
/*
* TODO currently if the relation is marked as dynamic/eager and fail exception, we simply ignore
* all exceptions if they are thrown in asynchronous resolutions. We need to better specify the
* expected behavior.
*/
}
/*
* register the result of the resolution
*/
synchronized (this) {
this.resolution = result;
this.isResolving = false;
this.notifyAll();
}
}
/**
* Block the current thread until a component satisfying the request is available.
*
*/
public void block() {
/*
* If it is a retry of this blocked request, we do not block again.
*
* IMPORTANT This can only happen in case of a unsuccessful retry. In that case we simply
* return and the failure handler will simply ignore the result.
*
* The stack should look like this :
*
* +--------------------------+
* + this.block() + <- Unsuccessful retry
* +--------------------------+
* + FailureManager.resolve() +
* +--------------------------+
* + Resolver.resolveLink() +
* +--------------------------+
* + this.retry() +
* +--------------------------+
* + this.block() + <- Blocked request
* +--------------------------+
* + FailureManager.resolve() +
* +--------------------------+
*
*
* Control will go back to the previous invocation of this method in the stack, that will
* block again until a new event signal the need for another retry.
*
*/
if (this.isRetry()) {
return;
}
/*
* Mark this request as blocked
*/
synchronized (this) {
isBlocked = true;
stack = getCurrentStack();
}
/*
* wait for some event that signals a change in the environment that may unblock this resolution,
* and try to resolve again
*/
while (!isResolved()) {
synchronized (this) {
try {
this.wait();
} catch (InterruptedException e) {
}
}
retry();
}
/*
* Mark the request as resolved
*/
synchronized (this) {
isBlocked = false;
stack = null;
}
}
/**
* Whether this request was resolved by the last resolution retry
*/
private synchronized boolean isResolved() {
return resolution != null || isDisposed;
}
/**
* The stack trace for blocked requests
*/
public List<StackTraceElement> getStack() {
return stack;
}
public synchronized void dispose() {
isDisposed = true;
this.notifyAll();
}
/**
* Decides whether the specified component could potentially resolve this request.
*
* This is used as a hint to avoid unnecessarily retrying a resolution that is not concerned with an
* event.
*
* TODO Currently we avoid forcing a resolution in cases where instantiating an implementation could
* satisfy the pending request, we should better specify the expected behavior.
*/
public boolean isSatisfiedBy(Component candidate) {
/*
* Check if the candidate kind matches the target kind of the relation.
*
* TODO Consider the special case for instantiable implementations that can satisfy an instance
* relation
*/
boolean matchKind = relation.getTargetKind().equals(candidate.getKind());
if (!matchKind) {
return false;
}
/*
* Check if the candidate matches the target of the relation
*/
boolean matchTarget = false;
if (relation.getTarget() instanceof ComponentReference<?>) {
Component target = CST.componentBroker.getComponent(relation.getTarget().getName());
matchTarget = (target != null) && (target.isAncestorOf(candidate) || target.equals(candidate));
}
if (relation.getTarget() instanceof ResourceReference) {
matchTarget = candidate.getProvidedResources().contains(relation.getTarget());
}
if (!matchTarget) {
return false;
}
/*
* Check visibility, including potential promotions
*/
if (source instanceof Instance) {
boolean promotion = false;
for (RelationDefinition compoDep : ((Instance) source).getComposite().getCompType().getLocalRelations()) {
if (relation.matchRelation((Instance) source, compoDep)) {
promotion = true;
}
}
if (!promotion && !source.canSee(candidate))
return false;
} else if (!source.canSee(candidate)) {
return false;
}
/*
* Special validations for target instances
*/
if (relation.getTargetKind().equals(ComponentKind.INSTANCE)) {
boolean valid = ((candidate instanceof Instance) && ((Instance) candidate).isSharable());
if (!valid) {
return false;
}
}
/*
* If this request has blocked the creating thread we should retry the
* resolution to unblock it.
*
*/
synchronized (this) {
if (this.isBlocked) {
return true;
}
}
/*
* Otherwise it is an asynchronous dynamic request and we verify if this request has not been
* already resolved (possibly in another thread) to avoid invoking the resolver unnecessarily.
*/
Set<Link> resolutions = ((ComponentImpl) source).getExistingLinks(relation.getName());
/*
* For single-valued relations we just verify there is some resolution
*/
if (!relation.isMultiple()) {
return resolutions.isEmpty();
}
/*
* For multi-valued relations we check if the candidate has already been added
*/
for (Link resolution : resolutions) {
if (resolution.getDestination().equals(candidate)) {
return false;
}
}
return true;
}
/**
* The request that is blocked in the current thread.
*
* Notice that several request may be be blocked in the same thread, nested in the stack, so this
* variable only references the one at the top of the stack.
*/
private static ThreadLocal<PendingRequest> blockedRequest = new ThreadLocal<PendingRequest>();
/**
* Whether the current thread is performing a reevaluation for this request.
*
* NOTE notice that in this case we use semantic equality between requests, as the resolver is not
* aware that is retrying a previously blocked request it will create a new request.
*
* TODO this should be better handled by the failure resolution manager
*/
private boolean isRetry() {
PendingRequest current = blockedRequest.get();
return current != null &&
current.relation.getName().equals(this.relation.getName()) &&
current.source.equals(this.source) &&
current.context.equals(this.context);
}
/**
* Invoke the resolver again for this request.
*
* IMPORTANT Notice that this method is only used for blocked requests, and can only be invoked in the
* context of the blocked thread owning this request.
*
* TODO There is an important invariant that a blocked request is owned by a single thread. This is no clear
* in this class, as it is also used for dynamic requests that are "pending" to be recalculated, but that are
* not blocking a thread. We should consider separating the two different classes to represent the different
* concepts.
*
* Notice that we catch all exceptions that the resolution may be thrown as a side effect, and we simply
* consider that the reevaluation did not succeed.
*
* Notice also that resolution is performed without synchronization on this request object, and all context
* is kept confined to the stack or thread local fields. This is because resolution may block as a side
* effect and event processing concerning this request must be able to proceed, simply signaling that a new
* retry must be tempted, if the current one is unsuccessful.
*/
private void retry() {
PendingRequest previous = beginResolution();
Resolved<?> resolverResult = null;
try {
resolverResult = resolver.resolveLink(source, relation);
}
catch (Exception ignoredFailure) {
}
finally {
endResolution(previous,resolverResult);
}
}
private synchronized PendingRequest beginResolution() {
PendingRequest previous = blockedRequest.get();
blockedRequest.set(this);
resolution = null;
return previous;
}
private synchronized void endResolution(PendingRequest previous, Resolved<?> resolverResult) {
blockedRequest.set(previous);
resolution = resolverResult;
}
/**
* The stack of the request executing in the context of the current thread.
*
*/
private static List<StackTraceElement> getCurrentStack() {
List<StackTraceElement> stack = new ArrayList<StackTraceElement>(Arrays.asList(new Throwable().getStackTrace()));
/*
* Remove all internal APAM implementation frameworks from the top of the stack, to increase
* the readability of the stack trace
*/
Iterator<StackTraceElement> frames = stack.iterator();
while (frames.hasNext()) {
if (frames.next().getClassName().startsWith(PendingRequest.class.getPackage().getName())) {
frames.remove();
continue;
}
break;
}
return stack;
}
}