Browse Source

Merge pull request #7355 from richardkchapman/cass-wu-part3

HPCC-12251 Create cassandra plugin for workunit storage

Reviewed-by: Gavin Halliday <ghalliday@hpccsystems.com>
Gavin Halliday 10 years ago
parent
commit
36a9d85054

+ 75 - 59
common/workunit/workunit.cpp

@@ -1417,6 +1417,10 @@ public:
             { return queryExtendedWU(c)->calculateHash(prevHash); }
     virtual void copyWorkUnit(IConstWorkUnit *cached, bool all)
             { queryExtendedWU(c)->copyWorkUnit(cached, all); }
+    virtual IPropertyTree *queryPTree() const
+            { return queryExtendedWU(c)->queryPTree(); }
+    virtual IPropertyTree *getUnpackedTree(bool includeProgress) const
+            { return queryExtendedWU(c)->getUnpackedTree(includeProgress); }
     virtual bool archiveWorkUnit(const char *base,bool del,bool deldll,bool deleteOwned)
             { return queryExtendedWU(c)->archiveWorkUnit(base,del,deldll,deleteOwned); }
     virtual void packWorkUnit(bool pack)
@@ -2147,6 +2151,7 @@ public:
         UniqueScopes us;
         if (secmgr /* && secmgr->authTypeRequired(RT_WORKUNIT_SCOPE) tbd */)
         {
+            // MORE - this will defeat any lazy-fetch mechanism in the incoming iterator
             scopes.setown(secmgr->createResourceList("wuscopes"));
             ForEach(*ptreeIter)
             {
@@ -2198,19 +2203,19 @@ CWorkUnitFactory::~CWorkUnitFactory()
 {
 }
 
-IWorkUnit* CWorkUnitFactory::createNamedWorkUnit(const char *wuid, const char *app, const char *user, ISecManager *secmgr, ISecUser *secuser)
+IWorkUnit* CWorkUnitFactory::createNamedWorkUnit(const char *wuid, const char *app, const char *scope, ISecManager *secmgr, ISecUser *secuser)
 {
-    checkWuScopeSecAccess(user, secmgr, secuser, SecAccess_Write, "Create", true, true);
+    checkWuScopeSecAccess(scope, secmgr, secuser, SecAccess_Write, "Create", true, true);
     Owned<CLocalWorkUnit> cw = _createWorkUnit(wuid, secmgr, secuser);
-    if (user)
-        cw->setWuScope(user);  // Note - this may check access rights and throw exception. Is that correct? We might prefer to only check access once, and this will check on the lock too...
+    if (scope)
+        cw->setWuScope(scope);  // Note - this may check access rights and throw exception. Is that correct? We might prefer to only check access once, and this will check on the lock too...
     IWorkUnit* ret = &cw->lockRemote(false);   // Note - this may throw exception if user does not have rights.
     ret->setDebugValue("CREATED_BY", app, true);
-    ret->setDebugValue("CREATED_FOR", user, true);
+    ret->setDebugValue("CREATED_FOR", scope, true);
     return ret;
 }
 
-IWorkUnit* CWorkUnitFactory::createWorkUnit(const char *app, const char *user, ISecManager *secmgr, ISecUser *secuser)
+IWorkUnit* CWorkUnitFactory::createWorkUnit(const char *app, const char *scope, ISecManager *secmgr, ISecUser *secuser)
 {
     StringBuffer wuid("W");
     char result[32];
@@ -2221,7 +2226,7 @@ IWorkUnit* CWorkUnitFactory::createWorkUnit(const char *app, const char *user, I
     wuid.append(result);
     if (workUnitTraceLevel > 1)
         PrintLog("createWorkUnit created %s", wuid.str());
-    IWorkUnit* ret = createNamedWorkUnit(wuid.str(), app, user, secmgr, secuser);
+    IWorkUnit* ret = createNamedWorkUnit(wuid.str(), app, scope, secmgr, secuser);
     if (workUnitTraceLevel > 1)
         PrintLog("createWorkUnit created %s", ret->queryWuid());
     addTimeStamp(ret, SSTglobal, NULL, StWhenCreated);
@@ -2534,18 +2539,6 @@ void CWorkUnitFactory::clearAborting(const char *wuid)
     }
 }
 
-unsigned CWorkUnitFactory::numWorkUnitsFiltered(WUSortField *filters,
-                                    const void *filterbuf,
-                                    ISecManager *secmgr,
-                                    ISecUser *secuser)
-{
-    if (!filters && !secuser && !secmgr)
-        return numWorkUnits();
-    unsigned total;
-    Owned<IConstWorkUnitIterator> iter =  getWorkUnitsSorted( NULL,filters,filterbuf,0,0x7fffffff,NULL,NULL,&total,secmgr,secuser);
-    return total;
-}
-
 static CriticalSection deleteDllLock;
 static Owned<IWorkQueueThread> deleteDllWorkQ;
 
@@ -2580,7 +2573,18 @@ public:
     {
         removeShutdownHook(*this);
     }
-
+    virtual unsigned validateRepository(bool fixErrors)
+    {
+        return 0;
+    }
+    virtual void deleteRepository(bool recreate)
+    {
+        UNIMPLEMENTED; // And will probably never be!
+    }
+    virtual void createRepository()
+    {
+        // Nothing to do
+    }
     virtual CLocalWorkUnit *_createWorkUnit(const char *wuid, ISecManager *secmgr, ISecUser *secuser)
     {
         StringBuffer wuRoot;
@@ -2955,6 +2959,10 @@ protected:
     SessionId session;
 };
 
+extern WORKUNIT_API IConstWorkUnitIterator *createConstWUIterator(IPropertyTreeIterator *iter, ISecManager *secmgr, ISecUser *secuser)
+{
+    return new CConstWUIterator(iter, secmgr, secuser);
+}
 static CriticalSection factoryCrit;
 static Owned<ILoadedDllEntry> workunitServerPlugin;  // NOTE - unload AFTER the factory is released!
 static Owned<IWorkUnitFactory> factory;
@@ -2985,9 +2993,11 @@ extern WORKUNIT_API IWorkUnitFactory * getWorkUnitFactory()
         CriticalBlock b(factoryCrit);
         if (!factory)   // NOTE - this "double test" paradigm is not guaranteed threadsafe on modern systems/compilers - I think in this instance that is harmless even in the (extremely) unlikely event that it resulted in the setown being called twice.
         {
+            const char *forceEnv = getenv("FORCE_DALI_WORKUNITS");
+            bool forceDali = forceEnv && !strieq(forceEnv, "off") && !strieq(forceEnv, "0");
             Owned<IRemoteConnection> conn = querySDS().connect("/Environment/Software/WorkUnitsServer", myProcessSession(), 0, SDS_LOCK_TIMEOUT);
             // MORE - arguably should be looking in the config section that corresponds to the dali we connected to. If you want to allow some dalis to be configured to use a WU server and others not.
-            if (conn)
+            if (conn && !forceDali)
             {
                 const IPropertyTree *ptree = conn->queryRoot();
                 const char *pluginName = ptree->queryProp("@plugin");
@@ -3022,6 +3032,19 @@ public:
         : baseFactory(_baseFactory), defaultSecMgr(_secMgr), defaultSecUser(_secUser)
     {
     }
+    virtual unsigned validateRepository(bool fix)
+    {
+        return baseFactory->validateRepository(fix);
+    }
+    virtual void deleteRepository(bool recreate)
+    {
+        return baseFactory->deleteRepository(recreate);
+    }
+    virtual void createRepository()
+    {
+        return baseFactory->createRepository();
+    }
+
     virtual IWorkUnit* createNamedWorkUnit(const char *wuid, const char *app, const char *user, ISecManager *secMgr, ISecUser *secUser)
     {
         if (!secMgr) secMgr = defaultSecMgr.get();
@@ -3137,15 +3160,6 @@ public:
         return baseFactory->numWorkUnits();
     }
 
-    virtual unsigned numWorkUnitsFiltered(WUSortField *filters,
-                                        const void *filterbuf,
-                                        ISecManager *secMgr, ISecUser *secUser)
-    {
-        if (!secMgr) secMgr = defaultSecMgr.get();
-        if (!secUser) secUser = defaultSecUser.get();
-        return baseFactory->numWorkUnitsFiltered(filters, filterbuf, secMgr, secUser);
-    }
-
     virtual bool isAborting(const char *wuid) const
     {
         return baseFactory->isAborting(wuid);
@@ -3286,7 +3300,7 @@ void CLocalWorkUnit::beforeDispose()
 
 void CLocalWorkUnit::cleanupAndDelete(bool deldll, bool deleteOwned, const StringArray *deleteExclusions)
 {
-    TIME_SECTION("WUDELETE cleanupAndDelete total");
+    MTIME_SECTION(queryActiveTimer(), "WUDELETE cleanupAndDelete total");
     // Delete any related things in SDS etc that might otherwise be forgotten
     if (p->getPropBool("@protected", false))
         throw MakeStringException(WUERR_WorkunitProtected, "%s: Workunit is protected",p->queryName());
@@ -5092,6 +5106,11 @@ static void copyTree(IPropertyTree * to, const IPropertyTree * from, const char
         to->setPropTree(xpath, match);
 }
 
+IPropertyTree *CLocalWorkUnit::queryPTree() const
+{
+    return p;
+}
+
 void CLocalWorkUnit::copyWorkUnit(IConstWorkUnit *cached, bool all)
 {
     CLocalWorkUnit *from = QUERYINTERFACE(cached, CLocalWorkUnit);
@@ -5222,6 +5241,9 @@ void CLocalWorkUnit::copyWorkUnit(IConstWorkUnit *cached, bool all)
     }
 
     p->setProp("@codeVersion", fromP->queryProp("@codeVersion"));
+    p->setProp("@buildVersion", fromP->queryProp("@buildVersion"));
+    p->setProp("@eclVersion", fromP->queryProp("@eclVersion"));
+    p->setProp("@hash", fromP->queryProp("@hash"));
     p->setPropBool("@cloneable", true);
     p->setPropBool("@isClone", true);
     resetWorkflow();  // the source Workflow section may have had some parts already executed...
@@ -5680,6 +5702,11 @@ void CLocalWorkUnit::setStatistic(StatisticCreatorType creatorType, const char *
     }
 }
 
+void CLocalWorkUnit::_loadStatistics() const
+{
+    statistics.load(p,"Statistics/*");
+}
+
 IConstWUStatisticIterator& CLocalWorkUnit::getStatistics(const IStatisticsFilter * filter) const
 {
     CriticalBlock block(crit);
@@ -8892,20 +8919,15 @@ void exportWorkUnitToXMLFileWithHiddenPasswords(IPropertyTree *p, const char *fi
 
 extern WORKUNIT_API StringBuffer &exportWorkUnitToXML(const IConstWorkUnit *wu, StringBuffer &str, bool unpack, bool includeProgress, bool hidePasswords)
 {
-    const CLocalWorkUnit *w = QUERYINTERFACE(wu, const CLocalWorkUnit);
-    if (!w)
-    {
-        const CLockedWorkUnit *wl = QUERYINTERFACE(wu, const CLockedWorkUnit);
-        if (wl)
-            w = wl->c;
-    }
-    if (w)
+    // MORE - queryPTree isn't really safe without holding CLocalWorkUnit::crit - really need to move these functions into CLocalWorkunit
+    const IExtendedWUInterface *ewu = queryExtendedWU(wu);
+    if (ewu)
     {
         Linked<IPropertyTree> p;
         if (unpack||includeProgress)
-            p.setown(w->getUnpackedTree(includeProgress));
+            p.setown(ewu->getUnpackedTree(includeProgress));
         else
-            p.set(w->p);
+            p.set(ewu->queryPTree());
         if (hidePasswords && p->hasProp("Variables/Variable[Format/@password]"))
             return exportWorkUnitToXMLWithHiddenPasswords(p, str);
         toXML(p, str, 0, XML_Format|XML_SortTags);
@@ -8915,33 +8937,22 @@ extern WORKUNIT_API StringBuffer &exportWorkUnitToXML(const IConstWorkUnit *wu,
     return str;
 }
 
-extern WORKUNIT_API IStringVal& exportWorkUnitToXML(const IConstWorkUnit *wu, IStringVal &str, bool unpack, bool includeProgress, bool hidePasswords)
-{
-    StringBuffer x;
-    str.set(exportWorkUnitToXML(wu,x,unpack, includeProgress, hidePasswords).str());
-    return str;
-}
-
 extern WORKUNIT_API void exportWorkUnitToXMLFile(const IConstWorkUnit *wu, const char * filename, unsigned extraXmlFlags, bool unpack, bool includeProgress, bool hidePasswords)
 {
-    const CLocalWorkUnit *w = QUERYINTERFACE(wu, const CLocalWorkUnit);
-    if (!w)
-    {
-        const CLockedWorkUnit *wl = QUERYINTERFACE(wu, const CLockedWorkUnit);
-        if (wl)
-            w = wl->c;
-    }
-    if (w)
+    const IExtendedWUInterface *ewu = queryExtendedWU(wu);
+    if (ewu)
     {
         Linked<IPropertyTree> p;
         if (unpack||includeProgress)
-            p.setown(w->getUnpackedTree(includeProgress));
+            p.setown(ewu->getUnpackedTree(includeProgress));
         else
-            p.set(w->p);
+            p.set(ewu->queryPTree());
         if (hidePasswords && p->hasProp("Variables/Variable[Format/@password]"))
             return exportWorkUnitToXMLFileWithHiddenPasswords(p, filename, extraXmlFlags);
         saveXML(filename, p, 0, XML_Format|XML_SortTags|extraXmlFlags);
     }
+    else
+        throw makeStringException(0, "Unrecognized workunit format");
 }
 
 
@@ -9807,11 +9818,16 @@ extern WORKUNIT_API IWorkflowScheduleConnection * getWorkflowScheduleConnection(
     return new CWorkflowScheduleConnection(wuid);
 }
 
-extern WORKUNIT_API IExtendedWUInterface * queryExtendedWU(IWorkUnit * wu)
+extern WORKUNIT_API IExtendedWUInterface * queryExtendedWU(IConstWorkUnit * wu)
 {
     return QUERYINTERFACE(wu, IExtendedWUInterface);
 }
 
+extern WORKUNIT_API const IExtendedWUInterface * queryExtendedWU(const IConstWorkUnit * wu)
+{
+    return QUERYINTERFACE(wu, const IExtendedWUInterface);
+}
+
 
 extern WORKUNIT_API void addExceptionToWorkunit(IWorkUnit * wu, ErrorSeverity severity, const char * source, unsigned code, const char * text, const char * filename, unsigned lineno, unsigned column)
 {

+ 12 - 5
common/workunit/workunit.hpp

@@ -1258,13 +1258,13 @@ typedef IIteratorOf<IPropertyTree> IConstQuerySetQueryIterator;
 
 interface IWorkUnitFactory : extends IInterface
 {
-    virtual IWorkUnit *createWorkUnit(const char *app, const char *user, ISecManager *secmgr = NULL, ISecUser *secuser = NULL) = 0;
+    virtual IWorkUnit *createWorkUnit(const char *app, const char *scope, ISecManager *secmgr = NULL, ISecUser *secuser = NULL) = 0;
     virtual bool deleteWorkUnit(const char *wuid, ISecManager *secmgr = NULL, ISecUser *secuser = NULL) = 0;
     virtual IConstWorkUnit * openWorkUnit(const char *wuid, bool lock, ISecManager *secmgr = NULL, ISecUser *secuser = NULL) = 0;
     virtual IConstWorkUnitIterator * getWorkUnitsByOwner(const char * owner, ISecManager *secmgr = NULL, ISecUser *secuser = NULL) = 0;
     virtual IWorkUnit * updateWorkUnit(const char * wuid, ISecManager *secmgr = NULL, ISecUser *secuser = NULL) = 0;
     virtual int setTracingLevel(int newlevel) = 0;
-    virtual IWorkUnit * createNamedWorkUnit(const char * wuid, const char * app, const char * user, ISecManager *secmgr = NULL, ISecUser *secuser = NULL) = 0;
+    virtual IWorkUnit * createNamedWorkUnit(const char * wuid, const char * app, const char * scope, ISecManager *secmgr = NULL, ISecUser *secuser = NULL) = 0;
     virtual IWorkUnit * getGlobalWorkUnit(ISecManager *secmgr = NULL, ISecUser *secuser = NULL) = 0;
     virtual IConstWorkUnitIterator * getWorkUnitsByState(WUState state, ISecManager *secmgr = NULL, ISecUser *secuser = NULL) = 0;
     virtual IConstWorkUnitIterator * getWorkUnitsByECL(const char * ecl, ISecManager *secmgr = NULL, ISecUser *secuser = NULL) = 0;
@@ -1274,12 +1274,15 @@ interface IWorkUnitFactory : extends IInterface
                                                         unsigned startoffset, unsigned maxnum, const char * queryowner, __int64 * cachehint, unsigned *total,
                                                         ISecManager *secmgr = NULL, ISecUser *secuser = NULL) = 0;
     virtual unsigned numWorkUnits() = 0;
-    virtual unsigned numWorkUnitsFiltered(WUSortField * filters, const void * filterbuf, ISecManager *secmgr = NULL, ISecUser *secuser = NULL) = 0;
     virtual void descheduleAllWorkUnits(ISecManager *secmgr = NULL, ISecUser *secuser = NULL) = 0;
     virtual IConstQuerySetQueryIterator * getQuerySetQueriesSorted(WUQuerySortField *sortorder, WUQuerySortField *filters, const void *filterbuf, unsigned startoffset, unsigned maxnum, __int64 *cachehint, unsigned *total, const MapStringTo<bool> *subset) = 0;
     virtual bool isAborting(const char *wuid) const = 0;
     virtual void clearAborting(const char *wuid) = 0;
     virtual WUState waitForWorkUnit(const char * wuid, unsigned timeout, bool compiled, bool returnOnWaitState) = 0;
+
+    virtual unsigned validateRepository(bool fixErrors) = 0;
+    virtual void deleteRepository(bool recreate) = 0;
+    virtual void createRepository() = 0;  // If not already there...
 };
 
 interface IWorkflowScheduleConnection : extends IInterface
@@ -1301,6 +1304,8 @@ interface IExtendedWUInterface
     virtual void copyWorkUnit(IConstWorkUnit *cached, bool all) = 0;
     virtual bool archiveWorkUnit(const char *base,bool del,bool ignoredllerrors,bool deleteOwned) = 0;
     virtual void packWorkUnit(bool pack=true) = 0;
+    virtual IPropertyTree *getUnpackedTree(bool includeProgress) const = 0;
+    virtual IPropertyTree *queryPTree() const = 0;
     
 };
 
@@ -1354,7 +1359,8 @@ extern WORKUNIT_API IStatisticGatherer * createGlobalStatisticGatherer(IWorkUnit
 extern WORKUNIT_API bool getWorkUnitCreateTime(const char *wuid,CDateTime &time); // based on WUID
 extern WORKUNIT_API bool restoreWorkUnit(const char *base,const char *wuid);
 extern WORKUNIT_API void clientShutdownWorkUnit();
-extern WORKUNIT_API IExtendedWUInterface * queryExtendedWU(IWorkUnit * wu);
+extern WORKUNIT_API IExtendedWUInterface * queryExtendedWU(IConstWorkUnit * wu);
+extern WORKUNIT_API const IExtendedWUInterface * queryExtendedWU(const IConstWorkUnit * wu);
 extern WORKUNIT_API unsigned getEnvironmentThorClusterNames(StringArray &thorNames, StringArray &groupNames, StringArray &targetNames, StringArray &queueNames);
 extern WORKUNIT_API unsigned getEnvironmentHThorClusterNames(StringArray &eclAgentNames, StringArray &groupNames, StringArray &targetNames);
 extern WORKUNIT_API StringBuffer &formatGraphTimerLabel(StringBuffer &str, const char *graphName, unsigned subGraphNum=0, unsigned __int64 subId=0);
@@ -1366,7 +1372,6 @@ extern WORKUNIT_API void setWorkUnitFactory(IWorkUnitFactory *_factory);
 extern WORKUNIT_API IWorkUnitFactory * getWorkUnitFactory();
 extern WORKUNIT_API IWorkUnitFactory * getWorkUnitFactory(ISecManager *secmgr, ISecUser *secuser);
 extern WORKUNIT_API ILocalWorkUnit* createLocalWorkUnit(const char *XML);
-extern WORKUNIT_API IStringVal& exportWorkUnitToXML(const IConstWorkUnit *wu, IStringVal &str, bool unpack, bool includeProgress, bool hidePasswords);
 extern WORKUNIT_API StringBuffer &exportWorkUnitToXML(const IConstWorkUnit *wu, StringBuffer &str, bool unpack, bool includeProgress, bool hidePasswords);
 extern WORKUNIT_API void exportWorkUnitToXMLFile(const IConstWorkUnit *wu, const char * filename, unsigned extraXmlFlags, bool unpack, bool includeProgress, bool hidePasswords);
 extern WORKUNIT_API void submitWorkUnit(const char *wuid, const char *username, const char *password);
@@ -1398,6 +1403,8 @@ extern WORKUNIT_API IPropertyTree * resolveDefinitionInArchive(IPropertyTree * a
 inline bool isLibrary(IConstWorkUnit * wu) { return wu->getApplicationValueInt("LibraryModule", "interfaceHash", 0) != 0; }
 extern WORKUNIT_API bool looksLikeAWuid(const char * wuid, const char firstChar);
 
+extern WORKUNIT_API IConstWorkUnitIterator *createConstWUIterator(IPropertyTreeIterator *iter, ISecManager *secmgr, ISecUser *secuser);
+
 enum WUQueryActivationOptions
 {
     DO_NOT_ACTIVATE = 0,

+ 3 - 2
common/workunit/workunit.ipp

@@ -294,6 +294,7 @@ public:
     virtual void subscribe(WUSubscribeOptions options);
     virtual unsigned calculateHash(unsigned prevHash);
     virtual void copyWorkUnit(IConstWorkUnit *cached, bool all);
+    virtual IPropertyTree *queryPTree() const;
     virtual unsigned queryFileUsage(const char *filename) const;
     virtual bool getCloneable() const;
     virtual IUserDescriptor * queryUserDescriptor() const;
@@ -559,6 +560,7 @@ protected:
     virtual void _unlockRemote() {};
     virtual void unsubscribe();
     virtual void _loadResults() const;
+    virtual void _loadStatistics() const;
 };
 
 interface ISDSManager; // MORE - can remove once dali split out
@@ -578,7 +580,7 @@ public:
     virtual IConstWorkUnit * openWorkUnit(const char * wuid, bool lock, ISecManager *secmgr, ISecUser *secuser);
     virtual IWorkUnit * updateWorkUnit(const char * wuid, ISecManager *secmgr, ISecUser *secuser);
     virtual int setTracingLevel(int newlevel);
-    virtual IWorkUnit * createNamedWorkUnit(const char * wuid, const char * app, const char * user, ISecManager *secmgr, ISecUser *secuser);
+    virtual IWorkUnit * createNamedWorkUnit(const char * wuid, const char * app, const char *scope, ISecManager *secmgr, ISecUser *secuser);
     virtual IWorkUnit * getGlobalWorkUnit(ISecManager *secmgr, ISecUser *secuser) = 0;
     virtual IConstWorkUnitIterator * getWorkUnitsByOwner(const char * owner, ISecManager *secmgr, ISecUser *secuser) = 0;
     virtual IConstWorkUnitIterator * getWorkUnitsByState(WUState state, ISecManager *secmgr = NULL, ISecUser *secuser = NULL) = 0;
@@ -596,7 +598,6 @@ public:
                                                 ISecManager *secmgr,
                                                 ISecUser *secuser) = 0;
     virtual unsigned numWorkUnits() = 0;
-    virtual unsigned numWorkUnitsFiltered(WUSortField *filters, const void *filterbuf, ISecManager *secmgr, ISecUser *secuser);
     virtual void descheduleAllWorkUnits(ISecManager *secmgr, ISecUser *secuser);
     virtual IConstQuerySetQueryIterator * getQuerySetQueriesSorted(WUQuerySortField *sortorder, WUQuerySortField *filters, const void *filterbuf, unsigned startoffset, unsigned maxnum, __int64 *cachehint, unsigned *total, const MapStringTo<bool> *subset);
     virtual bool isAborting(const char *wuid) const;

+ 0 - 11
dali/dfu/dfuwu.cpp

@@ -3107,17 +3107,6 @@ public:
         IPropertyTree *root = conn->queryRoot();
         return root->numChildren();
     }
-
-    virtual unsigned numWorkUnitsFiltered(DFUsortfield *filters,const void *filterbuf)
-    {
-        if (!filters)
-            return numWorkUnits();
-        unsigned total;
-        Owned<IConstDFUWorkUnitIterator> iter = getWorkUnitsSorted( NULL,filters,filterbuf,0,0x7fffffff,NULL,NULL,&total);
-        return total;
-    }
-
-
 };
 
 IDFUWorkUnitFactory * getDFUWorkUnitFactory()

+ 0 - 1
dali/dfu/dfuwu.hpp

@@ -450,7 +450,6 @@ interface IDFUWorkUnitFactory : extends IInterface
                                                         __int64 *cachehint,         // set to NULL if caching not required
                                                         unsigned *total) = 0;       // set to NULL if caching not required
     virtual unsigned numWorkUnits()=0;
-    virtual unsigned numWorkUnitsFiltered(DFUsortfield *filters,const void *filterbuf)=0;
     virtual __int64  subscribe(const char *xpath,void *iface) =0;       // internal use
 };
 

+ 6 - 0
ecl/eclagent/eclagent.cpp

@@ -1970,6 +1970,12 @@ void EclAgent::doProcess()
                 w->deleteTemporaries();
         }
 
+        if (globals->getPropBool("DUMPFINALWU", false))
+        {
+            StringBuffer xml;
+            exportWorkUnitToXML(wuRead, xml, true, false, true);
+            fprintf(stdout, "%s", xml.str());
+        }
         wuRead.clear(); // have a write lock still, but don't want to leave dangling unlocked wuRead after releasing write lock
                         // or else something can delete whilst still referenced (e.g. on complete signal)
         w.clear();

+ 1 - 1
ecl/hqlcpp/hqlecl.cpp

@@ -480,7 +480,7 @@ bool HqlDllGenerator::generateCode(HqlQueryContext & query)
 
 void HqlDllGenerator::addWorkUnitAsResource()
 {
-    SCMStringBuffer wuXML;
+    StringBuffer wuXML;
     exportWorkUnitToXML(wu, wuXML, false, false, false);
     code->addCompressResource("WORKUNIT", wuXML.length(), wuXML.str(), NULL, 1000);
 }

+ 2 - 0
ecl/wutest/CMakeLists.txt

@@ -36,6 +36,7 @@ include_directories (
          ./../../system/jlib 
          ./../../common/environment 
          ./../../common/workunit 
+         ./../../testing/unittests 
     )
 
 ADD_DEFINITIONS( -D_CONSOLE )
@@ -55,6 +56,7 @@ target_link_libraries ( wutest
          eclrtl 
          deftype
          workunit 
+         ${CPPUNIT_LIBRARIES}
     )
     
 if ( FORCE_WORKUNITS_TO_CASSANDRA )

+ 459 - 6
ecl/wutest/wutest.cpp

@@ -26,9 +26,17 @@
 
 #include "daclient.hpp"
 #include "dasds.hpp"
+#include "dautils.hpp"
 #include "danqs.hpp"
 #include "dalienv.hpp"
 
+#ifdef _USE_CPPUNIT
+#include <cppunit/extensions/TestFactoryRegistry.h>
+#include <cppunit/ui/text/TestRunner.h>
+#endif
+
+static unsigned testSize = 1000;
+
 void usage()
 {
     printf("Usage: WUTEST action [WUID=xxx] [OWNER=xxx]\n\n"
@@ -40,7 +48,8 @@ void usage()
            "   archive [TO=<directory>] [DEL=1] [KEEPFILERESULTS=1]\n"
            "   restore [FROM=<directory>]\n"
            "   pack\n"
-           "   unpack\n");
+           "   unpack\n"
+           "   validate [fix=1]\n");
 }
 
 bool dump(IConstWorkUnit &w, IProperties *globals)
@@ -48,7 +57,11 @@ bool dump(IConstWorkUnit &w, IProperties *globals)
     const char *action = globals->queryProp("#action");
     if (!action || stricmp(action, "list")==0)
     {
-        printf("%-30s %-20s %-10s\n", w.queryWuid(), w.queryJobName(), w.queryStateDesc());
+        Owned <IConstWUQuery> query = w.getQuery();
+        SCMStringBuffer queryText;
+        if (query)
+            query->getQueryText(queryText);
+        printf("%-20s %-10s %-10s %s\n", w.queryWuid(), w.queryJobName(), w.queryStateDesc(), queryText.str());
     }
     else if (stricmp(action, "results")==0)
     {
@@ -65,7 +78,7 @@ bool dump(IConstWorkUnit &w, IProperties *globals)
     }
     else if (stricmp(action, "dump")==0)
     {
-        SCMStringBuffer xml;
+        StringBuffer xml;
         exportWorkUnitToXML(&w, xml, true, false, true);
         printf("%s\n", xml.str());
     }
@@ -168,8 +181,9 @@ int main(int argc, const char *argv[])
     if (globals->getProp("CASSANDRASERVER", cassandraServer))
     {
         // Statically linking to cassandra plugin makes debugging easier (and means can debug simple cassandra workunit interactions without needing dali running)
-        Owned<IPTree> props = createPTreeFromXMLString("<WorkUnitsServer><Option name='server' value='.'/></WorkUnitsServer>");
+        Owned<IPTree> props = createPTreeFromXMLString("<WorkUnitsServer><Option name='server' value='.'/><Option name='randomWuidSuffix' value='4'/><Option name='traceLevel' value='0'/><Option name='keyspace' value='hpcc_test'></Option></WorkUnitsServer>");
         props->setProp("Option[@name='server']/@value", cassandraServer.str());
+        props->setPropInt("Option[@name='traceLevel']/@value", globals->getPropInt("tracelevel", 0));
         setWorkUnitFactory(createWorkUnitFactory(props));
     }
 #endif
@@ -200,9 +214,43 @@ int main(int argc, const char *argv[])
         }
         Owned<IWorkUnitFactory> factory = getWorkUnitFactory();
         const char *action = globals->queryProp("#action");
-        if (action && (stricmp(action, "testpaged")==0)) {
+        if (action && (stricmp(action, "testpaged")==0))
+        {
             testPagedWuList(factory);
         }
+#ifdef _USE_CPPUNIT
+        else if (action && (stricmp(action, "-selftest")==0))
+        {
+            testSize = globals->getPropInt("testSize", 100);
+            queryStderrLogMsgHandler()->setMessageFields(MSGFIELD_time | MSGFIELD_milliTime | MSGFIELD_prefix);
+            CppUnit::TextUi::TestRunner runner;
+            CppUnit::TestFactoryRegistry &registry = CppUnit::TestFactoryRegistry::getRegistry("WuTest");
+            runner.addTest( registry.makeTest() );
+            bool wasSucessful = runner.run( "", false );
+            return wasSucessful;
+        }
+#endif
+        else if (action && (stricmp(action, "validate")==0))
+        {
+            bool fix = globals->getPropBool("fix", false);
+            unsigned errors = factory->validateRepository(fix);
+            printf("%u errors %s\n", errors, (fix && errors) ? "fixed" : "found");
+        }
+        else if (action && (stricmp(action, "clear")==0))
+        {
+            if (globals->getPropBool("entire", false) && globals->getPropBool("repository", false))
+            {
+                factory->deleteRepository(false);
+                printf("Repository deleted\n");
+            }
+            else
+                printf("You need to specify entire=1 and repository=1 to delete entire repository\n");
+        }
+        else if (action && (stricmp(action, "initialize")==0))
+        {
+            factory->createRepository();
+            printf("Repository created\n");
+        }
         else if (action && (stricmp(action, "orphans")==0 || stricmp(action, "cleanup")==0))
         {
             factory->setTracingLevel(0);
@@ -323,7 +371,7 @@ int main(int argc, const char *argv[])
         }
         else if (globals->hasProp("WUID"))
         {
-            if (stricmp(globals->queryProp("#action"), "restore")==0)
+            if (action && stricmp(action, "restore")==0)
             {
                 StringBuffer from;
                 globals->getProp("FROM", from);
@@ -372,3 +420,408 @@ int main(int argc, const char *argv[])
     return 0;
 }
 
+#ifdef _USE_CPPUNIT
+#include "unittests.hpp"
+
+class WuTest : public CppUnit::TestFixture
+{
+    CPPUNIT_TEST_SUITE(WuTest);
+        CPPUNIT_TEST(testCreate);
+        CPPUNIT_TEST(testList);
+        CPPUNIT_TEST(testDelete);
+        CPPUNIT_TEST(testCopy);
+    CPPUNIT_TEST_SUITE_END();
+protected:
+    static StringArray wuids;
+    void testCreate()
+    {
+        Owned<IWorkUnitFactory> factory = getWorkUnitFactory();
+        unsigned before = factory->numWorkUnits();
+        unsigned start = msTick();
+        for (int i = 0; i < testSize; i++)
+        {
+            VStringBuffer userId("WuTestUser%d", i % 50);
+            VStringBuffer clusterName("WuTestCluster%d", i % 5);
+            VStringBuffer jobName("WuTest job %d", i % 3);
+            Owned<IWorkUnit>wu = factory->createWorkUnit("WuTest", NULL, NULL, NULL);
+            wu->setState(WUStateFailed);
+            wu->setUser(userId);
+            wu->setClusterName(clusterName);
+            if (i % 3)
+                wu->setJobName(jobName);
+            wuids.append(wu->queryWuid());
+        }
+        unsigned after = factory->numWorkUnits();
+        DBGLOG("%u workunits created in %d ms (%d total)", testSize, msTick()-start, after);
+        ASSERT(after-before==testSize);
+        ASSERT(wuids.length() == testSize);
+        ASSERT(factory->validateRepository(false)==0);
+    }
+
+    void testCopy()
+    {
+        Owned<IWorkUnitFactory> factory = getWorkUnitFactory();
+        Owned<IWorkUnit> createWu = factory->createWorkUnit("WuTest", NULL, NULL, NULL);
+        StringBuffer wuid(createWu->queryWuid());
+        Owned<ILocalWorkUnit> embeddedWU = createLocalWorkUnit(
+                // Note - generated by compiling the following ecl:
+                //
+                //   integer one := 1 : stored('one');
+                //   d := nofold(dataset([{1}], { integer v}));
+                //   ones := count(d(v=one)) : independent;
+                //   d(v = ones);
+                //
+                // then running
+                //
+                //   ./a.out DUMPFINALWU=1 | sed "s/\"/'/g" | sed s/^/\"/ | sed s/$/\"/
+                //
+                // then a little trimming to remove some of the statistics, sort the wfid's on the workflow, and add sequence=-1 on the variable.
+                "<W_LOCAL buildVersion='community_6.0.0-trunk0Debug[heads/cass-wu-part3-0-g10b954-dirty]'"
+                "         cloneable='1'"
+                "         clusterName=''"
+                "         codeVersion='158'"
+                "         eclVersion='6.0.0'"
+                "         hash='2796091347'"
+                "         state='completed'"
+                "         xmlns:xsi='http://www.w3.org/1999/XMLSchema-instance'>"
+                " <Debug>"
+                "  <debugquery>1</debugquery>"
+                "  <expandpersistinputdependencies>1</expandpersistinputdependencies>"
+                "  <savecpptempfiles>1</savecpptempfiles>"
+                "  <saveecltempfiles>1</saveecltempfiles>"
+                "  <spanmultiplecpp>0</spanmultiplecpp>"
+                "  <standaloneexe>1</standaloneexe>"
+                "  <targetclustertype>hthor</targetclustertype>"
+                " </Debug>"
+                " <Graphs>"
+                "  <Graph name='graph1' type='activities'>"
+                "   <xgmml>"
+                "    <graph wfid='2'>"
+                "     <node id='1'>"
+                "      <att>"
+                "       <graph>"
+                "        <att name='rootGraph' value='1'/>"
+                "        <edge id='2_0' source='2' target='3'/>"
+                "        <edge id='3_0' source='3' target='4'/>"
+                "        <edge id='4_0' source='4' target='5'/>"
+                "        <node id='2' label='Inline Row&#10;{1}'>"
+                "         <att name='definition' value='./sets.ecl(2,13)'/>"
+                "         <att name='_kind' value='148'/>"
+                "         <att name='ecl' value='ROW(TRANSFORM({ integer8 v },SELF.v := 1;));&#10;'/>"
+                "         <att name='recordSize' value='8'/>"
+                "         <att name='predictedCount' value='1'/>"
+                "        </node>"
+                "        <node id='3' label='Filter'>"
+                "         <att name='definition' value='./sets.ecl(3,15)'/>"
+                "         <att name='_kind' value='5'/>"
+                "         <att name='ecl' value='FILTER(v = STORED(&apos;one&apos;));&#10;'/>"
+                "         <att name='recordSize' value='8'/>"
+                "         <att name='predictedCount' value='0..?[disk]'/>"
+                "        </node>"
+                "        <node id='4' label='Count'>"
+                "         <att name='_kind' value='125'/>"
+                "         <att name='ecl' value='TABLE({ integer8 value := COUNT(group) });&#10;'/>"
+                "         <att name='recordSize' value='8'/>"
+                "         <att name='predictedCount' value='1'/>"
+                "        </node>"
+                "        <node id='5' label='Store&#10;Internal(&apos;wf2&apos;)'>"
+                "         <att name='_kind' value='22'/>"
+                "         <att name='ecl' value='extractresult(value, named(&apos;wf2&apos;));&#10;'/>"
+                "         <att name='recordSize' value='8'/>"
+                "        </node>"
+                "       </graph>"
+                "      </att>"
+                "     </node>"
+                "    </graph>"
+                "   </xgmml>"
+                "  </Graph>"
+                "  <Graph name='graph2' type='activities'>"
+                "   <xgmml>"
+                "    <graph wfid='3'>"
+                "     <node id='6'>"
+                "      <att>"
+                "       <graph>"
+                "        <att name='rootGraph' value='1'/>"
+                "        <edge id='7_0' source='7' target='8'/>"
+                "        <edge id='8_0' source='8' target='9'/>"
+                "        <node id='7' label='Inline Row&#10;{1}'>"
+                "         <att name='definition' value='./sets.ecl(2,13)'/>"
+                "         <att name='_kind' value='148'/>"
+                "         <att name='ecl' value='ROW(TRANSFORM({ integer8 v },SELF.v := 1;));&#10;'/>"
+                "         <att name='recordSize' value='8'/>"
+                "         <att name='predictedCount' value='1'/>"
+                "        </node>"
+                "        <node id='8' label='Filter'>"
+                "         <att name='definition' value='./sets.ecl(5,1)'/>"
+                "         <att name='_kind' value='5'/>"
+                "         <att name='ecl' value='FILTER(v = INTERNAL(&apos;wf2&apos;));&#10;'/>"
+                "         <att name='recordSize' value='8'/>"
+                "         <att name='predictedCount' value='0..?[disk]'/>"
+                "        </node>"
+                "        <node id='9' label='Output&#10;Result #1'>"
+                "         <att name='definition' value='./sets.ecl(1,1)'/>"
+                "         <att name='name' value='sets'/>"
+                "         <att name='definition' value='./sets.ecl(5,1)'/>"
+                "         <att name='_kind' value='16'/>"
+                "         <att name='ecl' value='OUTPUT(..., workunit);&#10;'/>"
+                "         <att name='recordSize' value='8'/>"
+                "        </node>"
+                "       </graph>"
+                "      </att>"
+                "     </node>"
+                "    </graph>"
+                "   </xgmml>"
+                "  </Graph>"
+                " </Graphs>"
+                " <Query fetchEntire='1'>"
+                "  <Associated>"
+                "   <File desc='a.out.cpp'"
+                "         filename='/Users/rchapman/HPCC-Platform/ossd/a.out.cpp'"
+                "         ip='192.168.2.203'"
+                "         type='cpp'/>"
+                "  </Associated>"
+                " </Query>"
+                " <Results>"
+                "  <Result isScalar='0'"
+                "          name='Result 1'"
+                "          recordSizeEntry='mf1'"
+                "          rowLimit='-1'"
+                "          sequence='0'"
+                "          status='calculated'>"
+                "   <rowCount>1</rowCount>"
+                "   <SchemaRaw xsi:type='SOAP-ENC:base64'>"
+                "    dgABCAEAGBAAAAB7IGludGVnZXI4IHYgfTsK   </SchemaRaw>"
+                "   <totalRowCount>1</totalRowCount>"
+                "   <Value xsi:type='SOAP-ENC:base64'>"
+                "    AQAAAAAAAAA=   </Value>"
+                "  </Result>"
+                " </Results>"
+                " <Statistics>"
+                "  <Statistic c='eclcc'"
+                "             count='1'"
+                "             creator='eclcc'"
+                "             kind='TimeElapsed'"
+                "             s='compile'"
+                "             scope='compile:parseTime'"
+                "             ts='1431603789722535'"
+                "             unit='ns'"
+                "             value='805622'/>"
+                "  <Statistic c='unknown'"
+                "             count='1'"
+                "             creator='unknownRichards-iMac.local'"
+                "             kind='WhenQueryStarted'"
+                "             s='global'"
+                "             scope='workunit'"
+                "             ts='1431603790007020'"
+                "             unit='ts'"
+                "             value='1431603790007001'/>"
+                "  <Statistic c='unknown'"
+                "             count='1'"
+                "             creator='unknownRichards-iMac.local'"
+                "             desc='Graph graph1'"
+                "             kind='TimeElapsed'"
+                "             s='graph'"
+                "             scope='graph1'"
+                "             ts='1431603790007912'"
+                "             unit='ns'"
+                "             value='0'/>"
+                " </Statistics>"
+                " <Temporaries>"
+                "  <Variable name='wf2' status='calculated'>"
+                "   <rowCount>1</rowCount>"
+                "   <totalRowCount>1</totalRowCount>"
+                "   <Value xsi:type='SOAP-ENC:base64'>"
+                "    AQAAAAAAAAA=   </Value>"
+                "  </Variable>"
+                " </Temporaries>"
+                " <Tracing>"
+                "  <EclAgentBuild>community_6.0.0-trunk0Debug[heads/cass-wu-part3-0-g10b954-dirty]</EclAgentBuild>"
+                " </Tracing>"
+                " <Variables>"
+                "  <Variable name='one' sequence='-1' status='calculated'>"
+                "   <rowCount>1</rowCount>"
+                "   <SchemaRaw xsi:type='SOAP-ENC:base64'>"
+                "    b25lAAEIAQAYAAAAAA==   </SchemaRaw>"
+                "   <totalRowCount>1</totalRowCount>"
+                "   <Value xsi:type='SOAP-ENC:base64'>"
+                "    AQAAAAAAAAA=   </Value>"
+                "  </Variable>"
+                " </Variables>"
+                " <Workflow>"
+                "  <Item mode='normal'"
+                "        state='done'"
+                "        type='normal'"
+                "        wfid='1'/>"
+                "  <Item mode='normal'"
+                "        state='done'"
+                "        type='normal'"
+                "        wfid='2'>"
+                "   <Dependency wfid='1'/>"
+                "  </Item>"
+                "  <Item mode='normal'"
+                "        state='done'"
+                "        type='normal'"
+                "        wfid='3'>"
+                "   <Dependency wfid='2'/>"
+                "   <Schedule/>"
+                "  </Item>"
+                " </Workflow>"
+                "</W_LOCAL>"
+                );
+        StringBuffer xml1, xml2, xml3;
+        exportWorkUnitToXML(embeddedWU, xml1, false, false, false);
+        queryExtendedWU(createWu)->copyWorkUnit(embeddedWU, true);
+        createWu->setState(WUStateCompleted);
+        exportWorkUnitToXML(createWu, xml2, false, false, false);
+        createWu->commit();
+        createWu.clear();
+
+        // Now try to re-read
+        Owned<IConstWorkUnit> wu = factory->openWorkUnit(wuid, false);
+        ASSERT(streq(wu->queryWuid(), wuid));
+        ASSERT(streq(wu->queryJobName(), embeddedWU->queryJobName()));
+        exportWorkUnitToXML(wu, xml3, false, false, false);
+        // Check that writing to/reading from the server leaves unmodified
+        // This is complicated by the fact that the order is not preserved for statistics
+        sortStatistics(xml2);
+        sortStatistics(xml3);
+
+        DBGLOG("Comparing xml2 and xml3");
+        checkStringsMatch(xml2, xml3);
+
+        // Check that copy preservers everything it should (and resets what it should)
+        // We can't directly compare xml1 with xml2 - not everything is copied
+        Owned<IPropertyTree> p2 = createPTreeFromXMLString(xml2);
+        p2->removeProp("Statistics/Statistic[@kind='WhenCreated']");
+        p2->removeProp("Debug/created_by");
+        p2->removeProp("@isClone");
+        p2->removeProp("@wuidVersion");
+        ASSERT(streq(p2->queryProp("Variables/Variable[@name='one']/@status"), "undefined"));
+        p2->setProp("Variables/Variable[@name='one']/@status", "calculated");
+        p2->renameProp("/", "W_LOCAL");
+        Owned<IPropertyTree> p1 = createPTreeFromXMLString(xml1);
+        // Checking that temporaries and tracing were not copied
+        p1->removeProp("Temporaries");
+        p1->removeProp("Tracing");
+        // Checking that variables were reset by the copy
+        p1->removeProp("Variables/Variable[@name='one']/rowCount");
+        p1->removeProp("Variables/Variable[@name='one']/totalRowCount");
+        p1->removeProp("Variables/Variable[@name='one']/Value");
+        // Checking that workflow was reset by the copy
+        p1->setProp("Workflow/Item[@wfid='1']/@state", "null");
+        p1->setProp("Workflow/Item[@wfid='2']/@state", "null");
+        p1->setProp("Workflow/Item[@wfid='3']/@state", "reqd");
+        toXML(p1, xml1.clear(), 0, XML_Format|XML_SortTags);
+        toXML(p2, xml2.clear(), 0, XML_Format|XML_SortTags);
+        DBGLOG("Comparing xml1 and xml2");
+        checkStringsMatch(xml1, xml2);
+        wu.clear();
+        factory->deleteWorkUnit(wuid);
+    }
+
+    void sortStatistics(StringBuffer &xml)
+    {
+        Owned<IPropertyTree> p = createPTreeFromXMLString(xml);
+        Owned<IPropertyTreeIterator> stats = p->getElements("Statistics/Statistic");
+        StringArray unknownAttributes;
+        IArrayOf<IPropertyTree> sorted;
+        sortElements(stats, "@ts", NULL, NULL, unknownAttributes, sorted);
+        p->removeProp("Statistics");
+        if (sorted.length())
+        {
+            Owned<IPTree> parent = createPTree("Statistics");
+            ForEachItemIn(idx, sorted)
+            {
+                parent->addPropTree("Statistic", LINK(&sorted.item(idx)));
+            }
+            p->addPropTree("Statistics", parent.getClear());
+        }
+        toXML(p, xml.clear(), 0, XML_Format|XML_SortTags);
+    }
+    void checkStringsMatch(const char *s1, const char *s2)
+    {
+        if (!streq(s1, s2))
+        {
+            int i;
+            for (i = 0; s1[i] && s2[i]==s1[i]; i++)
+                ;
+            DBGLOG("Strings differ:\n%s\n%s\n", s1+i, s2+i);
+        }
+        ASSERT(streq(s1, s2));
+    }
+
+    void testDelete()
+    {
+        ASSERT(wuids.length() == testSize);
+        Owned<IWorkUnitFactory> factory = getWorkUnitFactory();
+        unsigned before = factory->numWorkUnits();
+        unsigned start = msTick();
+        for (int i = 0; i < testSize; i++)
+        {
+            factory->deleteWorkUnit(wuids.item(i));
+        }
+        unsigned after = factory->numWorkUnits();
+        DBGLOG("%u workunits deleted in %d ms (%d remain)", testSize, msTick()-start, after);
+        ASSERT(before-after==testSize);
+        ASSERT(factory->validateRepository(false)==0);
+    }
+
+    void testList()
+    {
+        Owned<IWorkUnitFactory> factory = getWorkUnitFactory();
+        unsigned before = factory->numWorkUnits();
+        unsigned start = msTick();
+        unsigned numIterated = 0;
+        Owned<IConstWorkUnitIterator> wus = factory->getWorkUnitsByOwner(NULL, NULL, NULL);
+        ForEach(*wus)
+        {
+            IConstWorkUnitInfo &wu = wus->query();
+            numIterated++;
+        }
+        DBGLOG("%d workunits listed in %d ms", numIterated, msTick()-start);
+        ASSERT(numIterated == before);
+        // Now by owner
+        wus.setown(factory->getWorkUnitsByOwner("WuTestUser0", NULL, NULL));
+        start = msTick();
+        numIterated = 0;
+        ForEach(*wus)
+        {
+            IConstWorkUnitInfo &wu = wus->query();
+            ASSERT(streq(wu.queryUser(), "WuTestUser0"));
+            numIterated++;
+        }
+        DBGLOG("%d workunits listed in %d ms", numIterated, msTick()-start);
+        ASSERT(numIterated == (testSize+49)/50);
+
+        // And by non-existent owner...
+        wus.setown(factory->getWorkUnitsByOwner("NoSuchWuTestUser0", NULL, NULL));
+        start = msTick();
+        numIterated = 0;
+        ForEach(*wus)
+        {
+            numIterated++;
+        }
+        DBGLOG("%d workunits listed in %d ms", numIterated, msTick()-start);
+        ASSERT(numIterated == 0);
+
+        // And by cluster
+        wus.setown(factory->getWorkUnitsByCluster("WuTestCluster0", NULL, NULL));
+        start = msTick();
+        numIterated = 0;
+        ForEach(*wus)
+        {
+            IConstWorkUnitInfo &wu = wus->query();
+            ASSERT(streq(wu.queryClusterName(), "WuTestCluster0"));
+            numIterated++;
+        }
+        DBGLOG("%d workunits listed in %d ms", numIterated, msTick()-start);
+        ASSERT(numIterated == (testSize+4)/5);
+
+    }
+};
+StringArray WuTest::wuids;
+
+CPPUNIT_TEST_SUITE_REGISTRATION( WuTest );
+CPPUNIT_TEST_SUITE_NAMED_REGISTRATION( WuTest, "WuTest" );
+
+#endif

+ 1 - 1
esp/services/ws_workunits/ws_workunitsHelpers.cpp

@@ -2130,7 +2130,7 @@ void WsWuInfo::getWorkunitXml(const char* plainText, MemoryBuffer& buf)
     else
         header = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><?xml-stylesheet href=\"../esp/xslt/xmlformatter.xsl\" type=\"text/xsl\"?>";
 
-    SCMStringBuffer xml;
+    StringBuffer xml;
     exportWorkUnitToXML(cw, xml, true, false, true);
 
     buf.append(strlen(header), header);

File diff suppressed because it is too large
+ 709 - 182
plugins/cassandra/cassandraembed.cpp