/* Web Polygraph http://www.web-polygraph.org/ * (C) 2003-2006 The Measurement Factory * Licensed under the Apache License, Version 2.0 */ #include "base/polygraph.h" #include #include "xstd/h/iostream.h" #include "xstd/h/iomanip.h" #include "xstd/BitMask.h" #include "xstd/Ssl.h" #include "base/ObjId.h" #include "base/RndPermut.h" #include "base/BStream.h" #include "base/AddrParsers.h" #include "base/polyLogCats.h" #include "base/polyLogTags.h" #include "runtime/AddrMap.h" #include "runtime/HostMap.h" #include "runtime/PubWorld.h" #include "runtime/HttpCookies.h" #include "runtime/StatPhase.h" #include "runtime/StatPhaseMgr.h" #include "runtime/LogComment.h" #include "runtime/ErrorMgr.h" #include "runtime/EphPortMgr.h" #include "runtime/ExpPortMgr.h" #include "runtime/PersistWorkSetMgr.h" #include "runtime/PopModel.h" #include "runtime/polyBcastChannels.h" #include "runtime/polyErrors.h" #include "runtime/StatPhaseMgr.h" #include "runtime/globals.h" #include "runtime/SslWrap.h" #include "runtime/SslWraps.h" #include "csm/ContentSel.h" #include "csm/ContentCfg.h" #include "csm/ContentMgr.h" #include "csm/oid2Url.h" #include "pgl/RobotSym.h" #include "dns/DnsMgr.h" #include "client/CltConnMgr.h" #include "client/CltOpts.h" #include "client/CltXact.h" #include "client/ForeignWorld.h" #include "client/IcpCltXact.h" #include "client/SessionMgr.h" #include "client/PrivWorlds.h" #include "client/PrivCache.h" #include "client/UserCred.h" #include "client/CltCfg.h" #include "client/Client.h" class IcpAgentCfg; struct SharedCfgItem { const RobotSym *sym; CltCfg *cfg; }; static Array SharedCfgs; XactFarm *Client::TheXacts = 0; ObjFarm Client::TheIcpXacts; CltSharedCfgs *Client::TheSharedCfgs = new CltSharedCfgs; Array Client::ThePortMgrs; Client::Client(): thePrivCache(0), theAuthOrigins(0), theCfg(0), theConnMgr(0), theDnsMgr(0), theSessionMgr(0), theIcpClient(0), theCcXactLvl(0), theExtraLaunchLvl(0), theCookiesKeepLimit(0), authProxy(false), isIdle(false) { theChannels.append(TheInfoChannel); theChannels.append(TheLogCfgChannel); theChannels.append(TheLogStateChannel); startListen(); } Client::~Client() { delete theConnMgr; delete theDnsMgr; delete theSessionMgr; delete thePrivCache; delete theAuthOrigins; } void Client::configure(const RobotSym *cfg, const NetAddr &aHost) { Assert(TheXacts); Assert(aHost.port() < 0); // remove later SockOpt opt; Agent::configure(cfg, aHost, opt); theCfg = TheSharedCfgs->getConfig(cfg); configurePrivWorlds(); int pcCap = 0; if (cfg->privCache(pcCap)) thePrivCache = new PrivCache(pcCap); theCfg->selectProxy(theProxyAddr); // sticky selection theConnMgr = new CltConnMgr; theConnMgr->configure(opt, cfg, theProxyAddr ? 1 : theCfg->viservLimit()); theConnMgr->portMgr(getPortMgr(true)); theConnMgr->idleTimeout(cfg->idlePconnTimeout()); const SslWrap *sslWrap = 0; if (theCfg->selectSslWrap(sslWrap)) { // sticky selection theSslCtx = sslWrap->makeClientCtx(theHost); theConnMgr->configureSsl(theSslCtx, sslWrap); } theDnsMgr = new DnsMgr(this); theDnsMgr->configure(cfg->dnsResolver()); if (theCfg->theBusyPeriod) { theSessionMgr = new SessionMgr(this); theSessionMgr->configure(theCfg); } if (theCfg->thePeerHttp && !theCfg->thePeerIcp) { cerr << cfg->loc() << "the HTTP peer is at " << theCfg->thePeerHttp << ", but where is the ICP peer?" << endl; exit(-3); } isCookieSender = theCfg->selectCookieSenderStatus(); if (isCookieSender) { theCookiesKeepLimit = theCfg->theCookiesKeepLimitSel ? (int)theCfg->theCookiesKeepLimitSel->trial() : 4; } TheXacts->limit(1024); // magic, no good way to estimate } void Client::icpClient(IcpClient *anIcpClient) { Assert(!theIcpClient); theIcpClient = anIcpClient; } void Client::start() { Assert(theConnMgr); Assert(theDnsMgr); Agent::start(); theDnsMgr->start(); theCfg->startWarmup(); // warmup plan created if needed if (theSessionMgr) theSessionMgr->start(); else becomeBusy(); } void Client::stop() { if (theSessionMgr) theSessionMgr->stop(); else if (!isIdle) becomeIdle(); theDnsMgr->stop(); Agent::stop(); } void Client::becomeBusy() { isIdle = false; selectHttpVersion(*theCfg); theMemberships.reset(); theCfg->selectCredentials(theCredentials); theCfg->findMemberships(theCredentials, theMemberships); Broadcast(TheSessionBegChannel, this); scheduleLaunch(TheClock); } void Client::continueSession() { Broadcast(TheSessionCntChannel, this); } void Client::becomeIdle() { isIdle = true; if (thePrivCache) thePrivCache->clear(); if (theAuthOrigins) theAuthOrigins->clear(); authProxy = false; while (theLaunchDebts.count()) launchCanceled(dequeSuspXact()); theExtraLaunchLvl = 0; theDnsMgr->clearCache(); theConnMgr->closeAllIdle(); Broadcast(TheSessionEndChannel, this); } void Client::describe(ostream &os) const { Agent::describe(os); if (theProxyAddr) os << " via " << theProxyAddr; } void Client::noteInfoEvent(BcastChannel *ch, InfoEvent ev) { Assert(ch == TheInfoChannel); if (ev == ieWssFreeze && !PrivWorld::Frozen()) { for (PrivWorldIterator p(this); !p.atEnd(); ++p) p.privWorld().freezeWss(); if (PrivWorld::Frozen()) { Comment(6) << "fyi: all private working sets are now frozen" << endc; ReportWss(5); } } } void Client::noteLogEvent(BcastChannel *ch, OLog &log) { if (ch == TheLogCfgChannel) { log << bege(lgCltCfg, lgcCltSide); // XXX: implement (CltCfgRec) log << ende; } else if (ch == TheLogStateChannel) { log << bege(lgCltState, lgcCltSide) << theSeqvId << thePrivWorlds << ende; } } void Client::noteXactDone(CltXact *x) { Assert(x); if (!Should(x->conn())) return; // XXX: fix me const ObjId oid(x->oid()); // XXX: request type may be different this time CltXact *retry = shouldRetry(x) ? genXact(oid, x) : 0; if (theSessionMgr) theSessionMgr->noteXactDone(x); if (!Should(x->conn())) return; // XXX: fix me if (isIdle) x->conn()->lastUse(true); // close all connections if we are idling theConnMgr->put(x->conn()); putXact(x); theCcXactLvl--; Assert(theCcXactLvl >= 0); // no xaction should restart when we are idle if (isIdle) { Should(!retry); return; } if (retry && tryLaunch(retry)) return; // push waiting xactions forward if (theLaunchDebts.count() && !theConnMgr->atHardConnLimit()) { resumeXact(); return; } if (theCcXactLvl == 0) loneXactFollowup(); } void Client::loneXactFollowup() { } CltXact *Client::fetch(const ObjId &oid, DistrPoint *dp) { Assert(oid); CltXact *x = genXact(oid, 0); x->cacheDistrPoint(dp); if (tryLaunch(x)) return x; return 0; } // must not be called directly but rather through tryLaunch bool Client::launch(CltXact *x) { Assert(x); Connection *conn = x->conn(); // non-zero if pipelining if (!conn) { Assert(!theConnMgr->atHardConnLimit()); const NetAddr &connAddr = x->nextHop(); Assert(connAddr); Assert(!connAddr.isDomainName()); const NetAddr destAddr = Oid2UrlHost(x->oid()); conn = theConnMgr->get(connAddr, destAddr); if (!conn && ReportError(errConnectEstb)) { Comment(1) << theHost << " failed to connect to " << connAddr << endc; } } if (conn) { theCcXactLvl++; x->exec(this, conn); return true; } launchFailed(x); return false; } // tries to generate and launch a transaction // returns true if the xaction will be launched [eventually] bool Client::tryLaunch() { if (theExtraLaunchLvl) { --theExtraLaunchLvl; return false; } return tryLaunch(genXact()); } // will launch the xaction (if possible) or postpone it (if not) // returns true if the xaction will be launched [eventually] bool Client::tryLaunch(CltXact *x) { Assert(x); if (isIdle) return launchCanceled(x); if (!x->nextHop()) { // should we ask peers for the best server? // XXX: remove askedPeer; the x->nextHop() should be enough? if (theCfg->thePeerIcp && !x->askedPeer()) { askPeer(theCfg->thePeerIcp, x); return true; } // next hop address should be known at this time // (but my not be resolved yet) if (!setNextHopAddr(x)) return false; // should we lookup the next hop address? if (theDnsMgr->needsLookup(x->nextHop())) { // async call unless fails immediately if (lookupAddr(x)) return true; launchFailed(x); return false; } } // check if we should postpone the xaction if (!x->conn() && theConnMgr->atHardConnLimit()) return suspendXact(x); return launch(x); } bool Client::suspendXact(CltXact *x) { if (theCfg->theWaitXactLmt < 0 || theLaunchDebts.count() < theCfg->theWaitXactLmt) { theLaunchDebts.append(x); Broadcast(TheWaitBegChannel, x); return true; } if (ReportError(errTooManyWaitXact)) { Comment(3) << "xactions active: " << theCcXactLvl << " waiting: " << theLaunchDebts.count() << " limit: " << theCfg->theWaitXactLmt << endc; } launchFailed(x); return false; } void Client::resumeXact() { launch(dequeSuspXact()); } CltXact *Client::dequeSuspXact() { CltXact *x = theLaunchDebts.pop(); Broadcast(TheWaitEndChannel, x); return x; } bool Client::launchCanceled(CltXact *x) { Assert(x); x->noteAbort(); putXact(x); return false; } bool Client::launchFailed(CltXact *x) { Assert(x); if (!isIdle) x->countFailure(); return launchCanceled(x); } void Client::putXact(CltXact *x) { // recycle x and xactions that caused it // stop if a xaction still has kids while (x && x->finished() && x->childCount() == 0) { CltXact *cause = x->cause(); if (cause) cause->noteChildGone(x); TheXacts->put(x); x = cause; } } CltXact *Client::genXact(const ObjId &oid, CltXact *cause) { CltXact *x = TheXacts->get(); x->oid(oid); if (cause) { cause->noteChildNew(x); x->cause(cause); } return x; } CltXact *Client::genXact() { ObjId oid; genOid(oid); return genXact(oid, 0); } void Client::genOid(ObjId &oid) { const int interest = theCfg->selectInterest(); if (interest == OidGenStat::intForeign) { selectForeignObj(oid); } else { selectViserv(oid); selectTarget(oid); selectObj(oid, interest); selectContType(oid); } selectReqType(oid); selectReqMethod(oid); if (theCfg->genUniqUrls) oid.world(UniqId::Create()); // changes every time } void Client::selectViserv(ObjId &oid) { const int viserv = theCfg->selectViserv(); HostCfg *host = TheHostMap->at(viserv); Assert(host); Assert(host->thePubWorld); Assert(host->theServerRep); oid.viserv(viserv); int limit = 0; if (doCookies(limit) && !host->theCookies) host->theCookies = new HttpCookies(limit); } void Client::selectTarget(ObjId &oid) { const NetAddr &visName = TheHostMap->at(oid.viserv())->theAddr; int niamIdx; // name in AddrMap index Assert(TheAddrMap->find(visName, niamIdx)); if (oid.type() < 0) selectAnyTarget(oid, niamIdx); else selectTypedTarget(oid, niamIdx); // sanity checks const HostCfg *host = TheHostMap->at(oid.target()); Assert(host); Assert(host->theContent); } void Client::selectObj(ObjId &oid, int interest) { static RndGen rng; OidGenStat &oidGenStat = TheStatPhaseMgr->oidGenStat(); const bool needPub = interest == OidGenStat::intPublic; const bool needRepeat = rng.event(theCfg->theRecurRatio* TheStatPhaseMgr->recurFactor().current()); oidGenStat.recordNeed(needRepeat, interest); PrivWorld &privWorld = thePrivWorlds[oid.viserv()]; PubWorld &pubWorld = *TheHostMap->at(oid.viserv())->thePubWorld; const bool privCanRep = privWorld.canRepeat(); const bool privCanProd = privWorld.canProduce(); const bool pubCanRep = pubWorld.canRepeat(); const bool pubCanProd = pubWorld.canProduce(); const bool canRep = privCanRep || pubCanRep; const bool canProd = privCanProd || pubCanProd; // the logic gives priority to repeatOid goal rather than to genPubOid if (canRep && (needRepeat || !canProd)) { if (pubCanRep && (needPub || !privCanRep)) { pubWorld.repeat(oid, theCfg->thePopModel); oidGenStat.recordGen(true, OidGenStat::intPublic); return; } Assert(privCanRep); privWorld.repeat(oid, theCfg->thePopModel); oidGenStat.recordGen(true, OidGenStat::intPrivate); return; } // new public if (pubCanProd && (needPub || !privCanProd)) { pubWorld.produce(oid, rng); oidGenStat.recordGen(false, OidGenStat::intPublic); return; } // new private object (last resort, never fails) privWorld.produce(oid, rng); oidGenStat.recordGen(false, OidGenStat::intPrivate); } void Client::selectContType(ObjId &oid) { Assert(oid.type() < 0); // we do not overwrite existing setting const HostCfg *hcfg = TheHostMap->at(oid.target()); Assert(hcfg); Assert(hcfg->theContent); const ContentCfg *ccfg = hcfg->theContent->getDir(oid); oid.type(ccfg->id()); } void Client::selectReqType(ObjId &oid) { static RndGen rng; const int reqType = rng.event(TheStatPhaseMgr->specialMsgFactor().current()) ? (int)theCfg->theReqTypeSel->trial() : rqtBasic; oid.ims200(reqType == rqtIms200); oid.ims304(reqType == rqtIms304); oid.reload(reqType == rqtReload); oid.rediredReq(false); } void Client::selectReqMethod(ObjId &oid) { static RndGen rng; const int reqMethod = rng.event(TheStatPhaseMgr->specialMsgFactor().current()) ? (int)theCfg->theReqMethodSel->trial() : rqmGet; oid.get(reqMethod == rqmGet); oid.post(reqMethod == rqmPost); oid.head(reqMethod == rqmHead); oid.put(reqMethod == rqmPut); } // find any target behind a visible name void Client::selectAnyTarget(ObjId &oid, int niamIdx) { const NetAddr &targetAddr = TheAddrMap->selectAddr(niamIdx); int targetIdx = -1; Assert(TheHostMap->find(targetAddr, targetIdx)); oid.target(targetIdx); } // find a target that has requested oid type void Client::selectTypedTarget(ObjId &oid, int niamIdx) { Assert(oid.type() > 0); for (AddrMapAddrIter i = TheAddrMap->addrIter(niamIdx); i; ++i) { int targetIdx = -1; Assert(TheHostMap->find(i.addr(), targetIdx)); const HostCfg *hcfg = TheHostMap->at(targetIdx); Assert(hcfg && hcfg->theContent); if (hcfg->theContent->hasContType(oid.type())) { oid.target(targetIdx); return; } } // we failed to find a target that has the right content if (ReportError(errUnreachContType)) { static const String strUndefined = "undefined"; const NetAddr &visName = TheHostMap->at(oid.viserv())->theAddr; const String kind = TheContentMgr.get(oid.type())->kind() ? TheContentMgr.get(oid.type())->kind() : strUndefined; Comment << "robot at " << host() << " cannot find content of '" << kind << "' kind on server(s) visible as " << visName << endc; } selectAnyTarget(oid, niamIdx); } void Client::selectForeignObj(ObjId &oid) { Assert(theCfg->foreignWorld()); ForeignWorld &foreignWorld = *theCfg->foreignWorld(); static RndGen rng; OidGenStat &oidGenStat = TheStatPhaseMgr->oidGenStat(); const bool needRepeat = rng.event(theCfg->theRecurRatio* TheStatPhaseMgr->recurFactor().current()); oidGenStat.recordNeed(needRepeat, OidGenStat::intForeign); const bool canRep = foreignWorld.canRepeat(); const bool canProd = foreignWorld.canProduce(); if (canRep && (needRepeat || !canProd)) { foreignWorld.repeat(oid, theCfg->thePopModel); oidGenStat.recordGen(true, OidGenStat::intForeign); return; } foreignWorld.produce(oid, rng); oidGenStat.recordGen(false, OidGenStat::intForeign); } bool Client::credsForOrigin(const ObjId &oid, UserCred &cred) const { if (!theAuthOrigins || !theAuthOrigins->isSet(oid.viserv())) return false; return genCredentials(cred); } bool Client::credsForProxy(const ObjId &, UserCred &cred) const { if (!authProxy) return false; return genCredentials(cred); } bool Client::genCredentials(UserCred &cred) const { cred = UserCred(theCredentials); static RndGen rng; if (cred.image() && rng.event(theCfg->theAuthError)) cred.invalidate(); return cred.image(); } bool Client::doCookies(int &limit) const { if (isCookieSender && theCookiesKeepLimit > 0) { limit = theCookiesKeepLimit; return true; } else { limit = 0; return false; } } bool Client::shouldRetry(const CltXact *x) const { // do not retry if we became idle while trying if (isIdle) return false; // count the number of consequitive retries int rcount = 0; while (x && x->needRetry()) { rcount++; x = x->cause(); } if (rcount > 10) { ReportError(errManyRetries); return false; } return rcount > 0; } void Client::noteOriginAuthReq(CltXact *cause) { if (!theAuthOrigins) theAuthOrigins = new BitMask(theCfg->viservLimit()); theAuthOrigins->beSet(cause->oid().viserv()); } void Client::noteProxyAuthReq(CltXact *cause) { authProxy = true; } void Client::noteRedirect(CltXact *cause, const ObjId &oid) { Assert(oid.rediredReq()); if (tryLaunch(genXact(oid, cause))) theExtraLaunchLvl++; } void Client::noteEmbedded(CltXact *parent, const ObjId &oid) { static RndGen rng; if (!rng.event(theCfg->theEmbedRecurRatio)) return; if (thePrivCache && thePrivCache->loadOid(oid)) return; // hit, no need to request oid CltXact *child = genXact(oid, parent); child->page(parent->page()); // simulate cached DNS responses for embedded objects if (!oid.foreignUrl() && oid.viserv() == parent->oid().viserv()) child->nextHopVar() = parent->nextHop(); // pipeline if possible if (child->nextHop() && child->nextHop() == parent->nextHop()) { if (CltXactMgr *mgr = parent->getPipeline()) child->pipeline(mgr); } if (tryLaunch(child)) theExtraLaunchLvl++; } void Client::askPeer(const NetAddr &addr, CltXact *x) { IcpCltXact *q = TheIcpXacts.get(); q->reason(x); q->exec(this, addr); } void Client::notePeerAsked(IcpCltXact *q) { Assert(q); CltXact *x = q->reason(this); x->usePeer(q->hit()); /* pass ICP stats here ... */ TheIcpXacts.put(q); tryLaunch(x); } bool Client::lookupAddr(CltXact *x) { return theDnsMgr->lookup(x->nextHop(), x); } void Client::noteAddrLookup(const NetAddr &addr, CltXact *x) { Assert(x); // successful lookup? if (addr) { x->nextHopVar() = addr; // update tryLaunch(x); } else { launchFailed(x); } } // select the address for the next hop connection bool Client::setNextHopAddr(CltXact *x) const { NetAddr &addr = x->nextHopVar(); if (x->usePeer() && theCfg->thePeerHttp) return addr = theCfg->thePeerHttp; if (theProxyAddr) return addr = theProxyAddr; if (x->oid().foreignUrl()) { const char *uri = x->oid().foreignUrl().cstr(); if (!SkipHostInUri(uri, uri+x->oid().foreignUrl().len(), addr)) { if (ReportError(errHostlessForeignUrl)) Comment << "foreign URL: " << uri << endc; return false; } return addr; } return addr = TheHostMap->at(x->oid().viserv())->theAddr; } PortMgr *Client::getPortMgr(bool bind2iface) { const NetAddr addr = bind2iface ? theHost : NetAddr(); // check if we already have a port mgr for host for (int i = 0; i < ThePortMgrs.count(); ++i) { if (ThePortMgrs[i]->addr() == addr) return ThePortMgrs[i]; } // create new port manager PortMgr *mgr = TheCltOpts.thePorts.set() ? (PortMgr*) new ExpPortMgr(addr, TheCltOpts.thePorts.lo(), TheCltOpts.thePorts.hi()) : (PortMgr*) new EphPortMgr(addr); ThePortMgrs.append(mgr); return mgr; } // configure private worlds, one for each [unique] origin void Client::configurePrivWorlds() { thePrivWorlds.stretch(theCfg->viservLimit()); thePrivWorlds.count(theCfg->viservLimit()); for (PrivWorldIterator p(this); !p.atEnd(); ++p) { if (!p.privWorld()) { p.privWorld() = PrivWorld(theId); PrivWorld::TheTotalCount++; } } } void Client::loadWorkingSet(IBStream &is) { static bool loadedWss = false; if (!loadedWss) { is >> PrivWorld::TheWss; PrivWorld::TheFrozenCount = 0; loadedWss = true; } Agent::loadWorkingSet(is); const int cfgCount = PrivWorldIterator(this).count(); const int storedCount = is.geti(); ThePersistWorkSetMgr.checkInput(); if (storedCount != cfgCount) { Comment << "warning: robot #" << theSeqvId << " used to talk to " << storedCount << " origin servers when working set was " << "stored but is configured to talk to " << cfgCount << " (possibly different) servers now" << endc; } // load private worlds, one at a time for (int i = 0; i < storedCount; ++i) { NetAddr visName; PrivWorld privWorld; is >> visName >> privWorld; ThePersistWorkSetMgr.checkInput(); int viserv = -1; if (TheHostMap->find(visName, viserv) && theCfg->hasViserv(viserv)) { if (!Should(viserv < thePrivWorlds.count())) { thePrivWorlds.stretch(viserv+1); thePrivWorlds.count(viserv+1); } if (!Should(thePrivWorlds[viserv])) PrivWorld::TheTotalCount++; thePrivWorlds.put(privWorld, viserv); // update frozen counter, assume default worlds were not frozen if (privWorld.wss() >= 0) PrivWorld::TheFrozenCount++; } else { Comment << "error: visible server " << visName << " in the " << "private working set (being loaded for robot " << host() << "from " << is.name() << ") is not in current robot's " << "origins configuration, skipping" << endc; } } } void Client::storeWorkingSet(OBStream &os) { static bool storedWss = false; if (!storedWss) { os << PrivWorld::TheWss; storedWss = true; } Agent::storeWorkingSet(os); PrivWorldIterator p(this); os << p.count(); for (; !p.atEnd(); ++p) os << p.addr() << p.privWorld(); } void Client::missWorkingSet() { // should we freeze all private worlds? } int Client::logCat() const { return lgcCltSide; } void Client::ReportWss(int commentLvl) { const BigSize meanFillSz = (*TheStatPhaseMgr && TheStatPhaseMgr->fillCnt()) ? TheStatPhaseMgr->fillSz()/TheStatPhaseMgr->fillCnt() : BigSize(0); ostream &os = Comment(commentLvl) << "fyi: min 'direct' objects in working set:" << endl; int pubFrozenCount = 0; int pubTotalCount = 0; const int pubWss = PubWorld::CurrentWss(pubFrozenCount, pubTotalCount); os << "\tglobal public: " << pubWss << " ("; if (meanFillSz > 0) os << "~" << (meanFillSz*pubWss) << " size, "; os << pubFrozenCount << '/' << pubTotalCount << '=' << Percent(pubFrozenCount, pubTotalCount) << "% frozen slices)" << endl; const int privFrozenCount = PrivWorld::TheFrozenCount; const int privTotalCount = PrivWorld::TheTotalCount; const int privWss = PrivWorld::TheWss; os << "\tlocal private: " << privWss << " ("; if (meanFillSz > 0) os << "~" << (meanFillSz*privWss) << " size, "; os << privFrozenCount << '/' << privTotalCount << '=' << Percent(privFrozenCount, privTotalCount) << "% frozen slices)" ; os << endc; } void Client::Farm(XactFarm *aFarm) { Assert(!TheXacts && aFarm); TheXacts = aFarm; } void Client::LogState(OLog &log) { log << bege(lgSrvRepState, lgcCltSide); // XXX: remove state logging? // OLogStorePtrs(log, ThePubWorlds); log << ende; }