LUMIERA.clone/src/lib/advice/index.hpp

595 lines
20 KiB
C++

/*
INDEX.hpp - data structure for organising advice solutions and matching
Copyright (C) Lumiera.org
2010, Hermann Vosseler <Ichthyostega@web.de>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/** @file index.hpp
** Implementation datastructure for use by the Advice system.
** To support the \em Advice collaboration, it is necessary to match advice requests with
** existing advice provisions. Each successful match creates an advice solution, resulting in
** the bound piece of advice (data) to become visible to all the advised entities having placed
** a matching advice request into the advice system.
**
** This header is intended to be incorporated as part of the advice system implementation (advice.cpp).
** It is \em not usable as an external interface. But it is written in a rather self-contained manner,
** in order to be testable in isolation. To this end, the actual PointOfAdvice entities being organised
** by this index datastructure remain abstract (defined as template parameter), and are only manipulated
** through the following functions:
** - \c hash_value(POA)
** - \c POA::getMatcher()
** - \c POA::getSolution()
** - \c POA::setSolution(solution*)
**
** \par implementation notes
** The advice binding index is implemented by two hashtables holding Binding::Matcher entries.
** Each entry associates a back-link to the corresponding POA (PointOfAdvice), which is assumed
** to be maintained outside the index. PointOfAdvice is an type-erased interface baseclass.
** Actually the advice system will have to deal with concrete advice::Request and advice::Provision
** objects, which are templated to a specific advice type; but this specifically typed context
** is kept on the interface level (advice.hpp) and the type information is stripped before
** calling into the actual implementation, so the index can be implemented generic.
**
** While both hashtables are organised by the binding pattern hash, the individual buckets are
** coded explicitly as ProvisionCluster and RequestCluster -- both based on a vector of entries.
** In case of the provisions, there is a stack-like order, inasmuch additions happen at the back
** and solutions are always searched starting from the end. Because of the basic structure of
** a binding match, solutions are possible \only between provision/request - clusters with the
** same hash value (which is based on the predicate symbols within the patterns to match). Thus,
** in case of changing an existing request or solution, the internal handling is different,
** depending on the new value to belong or don't belong to the same cluster (hash code).
** It's possible (for patterns including variables) that an entry leading to a solution with
** the old provision doesn't match a new provision (and vice versa); thus we'll have to traverse
** the contents of the whole cluster, find all old solutions, match against the new counterpart
** and treating those entries \em not matching with the new value as if they where completely
** newly added entries. In case we don't find any solution, the entries are supposed to be
** implemented such as to fall back to an default solution automatically (when receiving
** a \c NULL solution)
**
** @note as of 4/2010 this is an experimental setup and implemented just enough to work out
** the interfaces. Ichthyo expects this collaboration service to play a central role
** later at various places within proc-layer.
** @note for now, \em only the case of a completely constant (ground) pattern is implemented.
** Later we may consider to extend the binding patterns to allow variables. The mechanics
** of the index are designed right from start to support this case (and indeed the index
** could be much simpler if it wasn't to deal with this foreseeable additional complexity:
** When a pattern contains variables, then even within one bucket of the hashtable there
** might be non-matching entries. Each individual pair of (provision, request) has to be
** checked explicitly to determine a match. /////////////////////////TICKET #615
**
** @see advice.hpp
** @see binding.hpp
** @see advice-index-test.cpp
** @see advice-binding-pattern-test.cpp
** @see advice-basics-test.cpp
**
*/
#ifndef LIB_ADVICE_INDEX_H
#define LIB_ADVICE_INDEX_H
#include "lib/error.hpp"
#include "lib/advice/binding.hpp"
#include "lib/symbol.hpp"
#include "include/logging.h"
#include "lib/iter-adapter-stl.hpp"
#include "lib/util-foreach.hpp"
#include "lib/util.hpp"
#include <boost/operators.hpp>
#include <tr1/unordered_map>
#include <iostream>
#include <string>
namespace lib {
namespace advice {
namespace error = lumiera::error;
using std::tr1::placeholders::_1;
using std::tr1::unordered_map;
using lib::iter_stl::eachVal;
using lib::iter_stl::eachElm;
using util::for_each;
using util::contains;
using util::unConst;
using lib::Literal;
using std::string;
using std::vector;
using std::pair;
using std::ostream;
using std::cout;
using std::endl;
/**
* Index datastructure for organising advice solutions.
* Based on two hashtables for advice provisions and requests,
* the index allows to add, modify and remove entities of these
* two kinds. Each of these mutating operations immediately
* re-computes the advice solutions and publishes the results
* by invoking the \c setSolution() function on the
* corresponding PointOfAdvice entity.
*
* @note element \em identity is defined in terms of pointing
* to the same memory location of a POA (point of advice).
* Thus e.g. #hasProvision means this index holds an entry
* pointing to exactly this given data entity.
* @note the implementation of modifying a Request entry
* explicitly relies on that definition of equality.
* @note the diagnostic API is mainly intended for unit testing
* and \em not implemented with focus on performance.
*
* \par Exception safety
* Adding new registrations might throw error::Fatal or bad_alloc.
* The addition in this case has no effect and the index remains valid.
* The other mutating operations are NO_THROW, given that Binding::Matcher
* is a POD and std::vector fulfils the guarantee for POD content elements.
*/
template<class POA>
class Index
{
struct Entry
: pair<Binding::Matcher, POA*>
, boost::equality_comparable<Entry, POA,
boost::equality_comparable<Entry> >
{
explicit
Entry (POA& elm)
: pair<Binding::Matcher, POA*> (elm.getMatcher(), &elm)
{ }
// using default-copy, thus assuming copy is NO_THROW
friend bool
operator== (Entry const& a, Entry const& b)
{
return a.second == b.second;
}
friend bool
operator== (Entry const& a, POA const& p)
{
return a.second == &p;
}
friend ostream&
operator<< (ostream& os, Entry const& ent)
{
return os << "E-"<<hash_value(ent.first) << "--> " << ent.second ;
}
};
typedef vector<Entry> EntryList;
typedef typename EntryList::iterator EIter;
struct Cluster
{
EntryList elms_;
size_t
size() const
{
return elms_.size();
}
void
append (POA& elm)
{
REQUIRE (!contains (elm), "Duplicate entry");
try { elms_.push_back (Entry(elm)); }
catch(std::bad_alloc&)
{
throw error::Fatal("AdviceSystem failure due to exhausted memory");
}
}
void
overwrite (POA const& oldRef, POA& newElm)
{
EIter pos = std::find (elms_.begin(),elms_.end(), oldRef);
REQUIRE (pos!=elms_.end(), "Attempt to overwrite an entry which isn't there.");
REQUIRE_IF (&oldRef != &newElm, !contains (newElm), "Duplicate entry");
*pos = Entry(newElm);
REQUIRE_IF (&oldRef != &newElm, !contains (oldRef), "Duplicate entry");
}
void
remove (POA const& refElm)
{
EIter pos = std::find (elms_.begin(),elms_.end(), refElm);
if (pos!=elms_.end())
elms_.erase(pos);
ENSURE (!contains (refElm), "Duplicate entry");
}
bool
contains (POA const& refElm)
{
for (EIter i=elms_.begin(); i!=elms_.end(); ++i)
if (*i == refElm) return true;
return false;
}
void
dump() ///< debugging helper: Cluster contents --> STDOUT
{
cout << "elmList("<< elms_.size()<<")" << endl;
for (EIter i=elms_.begin(); i!=elms_.end(); ++i)
cout << "E...:"<< (*i) << endl;
}
RangeIter<EIter>
allElms ()
{
return eachElm (elms_);
}
};
struct ProvisionCluster
: Cluster
{
POA*
find_latest_solution (POA& requestElm)
{
typedef typename EntryList::reverse_iterator RIter;
Binding::Matcher pattern (requestElm.getMatcher());
for (RIter ii=this->elms_.rbegin();
ii!=this->elms_.rend();
++ii )
if (ii->first.matches (pattern))
return ii->second;
return NULL;
}
void
publish_latest_solution (POA& requestElm)
{
POA* solution = find_latest_solution (requestElm);
if (solution)
// found the most recent advice provision satisfying the (new) request
// thus publish this new advice solution into the request object
requestElm.setSolution (solution);
else
requestElm.setSolution ( NULL );
// report "no solution" which causes a default solution to be used
}
};
struct RequestCluster
: Cluster
{
void
publish_all_solutions (POA& provisionElm)
{
Binding::Matcher pattern (provisionElm.getMatcher());
for (EIter ii=this->elms_.begin();
ii!=this->elms_.end();
++ii )
if (pattern.matches (ii->first))
// the given (new) advice provision satisfies this request
// thus publish this new advice solution into the request object
ii->second->setSolution (&provisionElm);
}
void
retract_all_solutions (POA const& oldProv, ProvisionCluster& possibleReplacementSolutions)
{
Binding::Matcher pattern (oldProv.getMatcher());
for (EIter ii=this->elms_.begin();
ii!=this->elms_.end();
++ii )
if (pattern.matches (ii->first))
// the old advice provision (to be dropped) satisfied this request
// which thus needs to be treated anew (could cause quadratic complexity)
possibleReplacementSolutions.publish_latest_solution (*(ii->second));
}
void
rewrite_all_solutions (POA const& oldProv, POA& newProv, ProvisionCluster& possibleReplacementSolutions)
{
Binding::Matcher oldPattern (oldProv.getMatcher());
Binding::Matcher newPattern (newProv.getMatcher ());
for (EIter ii=this->elms_.begin();
ii!=this->elms_.end();
++ii )
if (newPattern.matches (ii->first))
ii->second->setSolution (&newProv);
else
if (oldPattern.matches (ii->first))
possibleReplacementSolutions.publish_latest_solution (*(ii->second));
}
};
/* ==== Index Tables ===== */
typedef unordered_map<HashVal, RequestCluster> RTable;
typedef unordered_map<HashVal, ProvisionCluster> PTable;
mutable RTable requestEntries_;
mutable PTable provisionEntries_;
public:
void
addRequest (POA& entry)
{
HashVal key (hash_value(entry));
requestEntries_[key].append (entry); // might throw
provisionEntries_[key].publish_latest_solution (entry);
}
/** @note explicitly relying on the implementation of \c ==
* which checks only the memory location of the Request.
* Thus we can use the already modified Request to find
* the old entry within the index pointing to this Request.
* @param oKey the binding hash value prior to modification
*/
void
modifyRequest (HashVal oKey, POA& entry)
{
HashVal nKey (hash_value(entry));
if (oKey != nKey)
{
requestEntries_[nKey].append (entry); // might throw
requestEntries_[oKey].remove (entry);
}
else
{ // rewrite Entry to include the new binding
requestEntries_[nKey].overwrite (entry, entry);
}
provisionEntries_[nKey].publish_latest_solution (entry);
}
void
removeRequest (POA const& refEntry)
{
HashVal oKey (hash_value(refEntry));
requestEntries_[oKey].remove (refEntry);
}
void
addProvision (POA& entry)
{
HashVal key (hash_value(entry));
provisionEntries_[key].append (entry); // might throw
requestEntries_[key].publish_all_solutions (entry);
}
void
modifyProvision (POA const& oldRef, POA& newEntry)
{
HashVal oKey (hash_value(oldRef));
HashVal nKey (hash_value(newEntry));
if (oKey != nKey)
{
provisionEntries_[nKey].append (newEntry); // might throw, in which case it has no effect
provisionEntries_[oKey].remove (oldRef);
requestEntries_[nKey].publish_all_solutions (newEntry);
requestEntries_[oKey].retract_all_solutions (oldRef, provisionEntries_[oKey]);
}
else
{
provisionEntries_[nKey].overwrite (oldRef, newEntry);
requestEntries_[nKey].rewrite_all_solutions (oldRef,newEntry, provisionEntries_[nKey]);
}
}
void
removeProvision (POA const& refEntry)
{
HashVal key (hash_value(refEntry));
provisionEntries_[key].remove (refEntry); // NO_THROW
requestEntries_[key].retract_all_solutions (refEntry, provisionEntries_[key]);
}
/** @warning calling this effectively detaches any existing advice information,
* but doesn't clean up storage of advice provisions incorporated
* within the advice system in general.
*/
void
clear ()
{
WARN (library, "Purging Advice Binding Index...");
requestEntries_.clear();
provisionEntries_.clear();
}
/* == diagnostics == */
/** validity self-check */
bool isValid() const;
size_t
size() const
{
return request_count() + provision_count();
}
size_t
request_count() const
{
return sumClusters (eachVal (requestEntries_));
}
size_t
provision_count() const
{
return sumClusters (eachVal (provisionEntries_));
}
bool
hasRequest (POA const& refEntry) const
{
return requestEntries_[hash_value(refEntry)].contains (refEntry);
}
bool
hasProvision (POA const& refEntry) const
{
return provisionEntries_[hash_value(refEntry)].contains (refEntry);
} // note: even just lookup might create a new (empty) cluster;
// thus the tables are defined as mutable
private:
/** internal: sum element count over all
* clusters in the given hashtable */
template<class IT>
static size_t
sumClusters (IT ii)
{
size_t sum=0;
for ( ; ii; ++ii )
sum += ii->size();
return sum;
}
void verify_Entry (Entry const&, HashVal) const;
void verify_Request (Entry const&, HashVal) const;
};
/* == Self-Verification == */
namespace { // self-check implementation helpers...
LUMIERA_ERROR_DEFINE(INDEX_CORRUPTED, "Advice-Index corrupted");
struct SelfCheckFailure
: error::Fatal
{
SelfCheckFailure (Literal failure)
: error::Fatal (string("Failed test: ")+failure
,LUMIERA_ERROR_INDEX_CORRUPTED)
{ }
};
}
/** Advice index self-verification: traverses the tables to check
* each entry is valid. Moreover, when a advice request has a stored solution
* which points back into the current advice provisions, this solution will be
* re-computed with the current data to prove it's still valid.
* @note expensive operation
*/
template<class POA>
bool
Index<POA>::isValid() const
{
typedef typename RTable::const_iterator RTIter;
typedef typename PTable::const_iterator PTIter;
try {
for (PTIter ii =provisionEntries_.begin();
ii != provisionEntries_.end(); ++ii)
{
HashVal hash (ii->first);
Cluster& clu = unConst(ii->second);
for_each (clu.allElms(), &Index::verify_Entry, this, _1, hash);
}
for (RTIter ii=requestEntries_.begin();
ii != requestEntries_.end(); ++ii)
{
HashVal hash (ii->first);
Cluster& clu = unConst(ii->second);
for_each (clu.allElms(), &Index::verify_Request, this, _1, hash);
}
return true;
}
catch(SelfCheckFailure& failure)
{
lumiera_error();
ERROR (library, "%s", failure.what());
}
return false;
}
#define VERIFY(_CHECK_, DESCRIPTION) \
if (!(_CHECK_)) \
throw SelfCheckFailure ((DESCRIPTION));
template<class POA>
void
Index<POA>::verify_Entry (Entry const& e, HashVal hash) const
{
VERIFY (hash == hash_value(e.first), "Wrong bucket, hash doesn't match bucket");
VERIFY (e.second, "Invalid Entry: back-link is NULL");
}
template<class POA>
void
Index<POA>::verify_Request (Entry const& e, HashVal hash) const
{
verify_Entry (e,hash);
POA& request = *(e.second);
const POA* solution (request.getSolution());
if (solution && hasProvision(*solution))
{
POA* currentSolution = provisionEntries_[hash].find_latest_solution (request);
VERIFY (e.first.matches (solution->getMatcher()),"stored advice solution not supported by binding match");
VERIFY (bool(currentSolution), "unable to reproduce stored solution with the current provisions")
VERIFY (solution == currentSolution, "stored advice solution isn't the topmost solution for this request")
}
}
#undef VERIFY
}} // namespace lib::advice
#endif