Browse Source

HPCC-17798 Improve ESP generalized cache

Implement a general cache interface and move the memcached code into
espcache.cpp; replace addMemCachedGlobal() and addMemCachedSeconds()
with setCacheTimeout(); add code for thread safe; add code to force
esp generalized cache.

Revise based on review: 1. Allow ESP to be started if environment.xml
does not have the ensureESPCache setting; 2. default the ensureESPCache
setting to false; 3. Remove extra methods from IEspCache; 4. rename the
ESP_CACHE_RETURN; 4. More small changes.

Signed-off-by: wangkx <kevin.wang@lexisnexis.com>
wangkx 8 years ago
parent
commit
75bce4ea36

+ 1 - 0
esp/bindings/SOAP/soaplib/CMakeLists.txt

@@ -28,6 +28,7 @@ set(SRCS
     ../../../platform/espcontext.cpp
     ../../../platform/espprotocol.cpp
     ../../../platform/espthread.cpp
+    ../../../platform/espcache.cpp
     ../../../platform/sechandler.cpp
     ../../../platform/txsummary.cpp
     ../../../protocols/http/mapinfo.cpp

+ 53 - 78
esp/bindings/http/platform/httpbinding.cpp

@@ -261,16 +261,6 @@ EspHttpBinding::EspHttpBinding(IPropertyTree* tree, const char *bindname, const
     if(m_challenge_realm.length() == 0)
         m_challenge_realm.append("ESP");
 
-#ifdef USE_LIBMEMCACHED
-    if(proc_cfg)
-    {
-        const char* initString = proc_cfg->queryProp("@memCachedOptionString");
-        if (!isEmptyString(initString))
-            memCachedInitString.set(initString);
-        else
-            memCachedInitString.set("--SERVER=127.0.0.1");//using local memcached server
-    }
-#endif
     if (!m_secmgr.get() || !daliClientActive())
     {
         if (!daliClientActive())
@@ -636,105 +626,92 @@ bool EspHttpBinding::basicAuth(IEspContext* ctx)
     return authorized;
 }
 
-#ifdef USE_LIBMEMCACHED
-void EspHttpBinding::ensureMemCachedClient()
-{
-    if ((memCachedMethods == 0) || memCachedClient)
-        return;
-
-    memCachedClient.setown(new ESPMemCached());
-    if (!memCachedClient->init(memCachedInitString.get()))
-        memCachedClient.clear();
-}
-
-int EspHttpBinding::queryMemCacheSeconds(const char *method)
+bool EspHttpBinding::queryCacheSeconds(const char *method, unsigned& cacheSeconds)
 {
     StringBuffer key(method);
-    int* value = memCachedSecondsMap.getValue(key.toUpperCase().str());
+    unsigned* value = cacheSecondsMap.getValue(key.toUpperCase().str());
     if (!value)
-        return -1;
-    return unsigned (*value);
+        return false;
+    cacheSeconds = *value;
+    return true;
 }
 
-bool EspHttpBinding::queryMemCacheGlobal(const char *method)
+bool EspHttpBinding::queryCacheGlobal(const char *method)
 {
     StringBuffer key(method);
-    bool* cacheGlobal = memCachedGlobalMap.getValue(key.toUpperCase().str());
+    bool* cacheGlobal = cacheGlobalMap.getValue(key.toUpperCase().str());
     return cacheGlobal && *cacheGlobal;
 }
 
-const char* EspHttpBinding::createMemCachedID(CHttpRequest* request, StringBuffer& memCachedID)
+const char* EspHttpBinding::createESPCacheID(CHttpRequest* request, StringBuffer& cacheID)
 {
-    memCachedID.clear();
-
-    const char* method = request->queryServiceMethod();
-    int cacheSeconds = queryMemCacheSeconds(method);
-    if (cacheSeconds < 0) //no memcache required for this method
-        return memCachedID.str();
-
+    StringBuffer idStr, msgType;
     if(request->isSoapMessage())
-        memCachedID.append(request->createUniqueRequestHash(queryMemCacheGlobal(method), "SOAP"));
+        msgType.set("SOAP");
     else if(request->isFormSubmission())
-        memCachedID.append(request->createUniqueRequestHash(queryMemCacheGlobal(method), "FORM"));
-    else
-        memCachedID.append(request->createUniqueRequestHash(queryMemCacheGlobal(method), ""));
-    return memCachedID.str();
-}
-
-bool EspHttpBinding::sendFromMemCached(CHttpRequest* request, CHttpResponse* response, const char* memCachedID)
-{
-    StringBuffer content, contentType, contentTypeCachedID;
-    contentTypeCachedID.set(memCachedID).append("t");
+        msgType.set("FORM");
 
+    if (!queryCacheGlobal(request->queryServiceMethod()))
     {
-        CriticalBlock block(memCachedCrit);
-
-        if (memCachedClient->exists("ESPResponse", memCachedID))
-            memCachedClient->get("ESPResponse", memCachedID, content);
-        if (memCachedClient->exists("ESPResponse", contentTypeCachedID.str()))
-            memCachedClient->get("ESPResponse", contentTypeCachedID.str(), contentType);
+        const char* userID = request->queryContext()->queryUserId();
+        if (!isEmptyString(userID))
+            idStr.append(userID).append("_");
     }
+    if (!msgType.isEmpty())
+        idStr.append(msgType.str()).append("_");
+    idStr.appendf("%s_%s_%s", request->queryServiceName(), request->queryServiceMethod(), request->queryAllParameterString());
+    cacheID.append(hashc((unsigned char *)idStr.str(), idStr.length(), 0));
+    return cacheID.str();
+}
+
+bool EspHttpBinding::sendFromESPCache(CHttpRequest* request, CHttpResponse* response, const char* cacheID)
+{
+    StringBuffer content, contentType;
+    if (!espCacheClient->readResponseCache(cacheID, content.clear(), contentType.clear()))
+        ESPLOG(LogMax, "Failed to read from ESP Cache for %s.", request->queryServiceMethod());
     if (content.isEmpty() || contentType.isEmpty())
         return false;
 
-    ESPLOG(LogMax, "Sending MemCached for %s.", request->queryServiceMethod());
+    ESPLOG(LogMax, "Sending from ESP Cache for %s.", request->queryServiceMethod());
     response->setContentType(contentType.str());
     response->setContent(content.str());
     response->send();
     return true;
 }
 
-void EspHttpBinding::addToMemCached(CHttpRequest* request, CHttpResponse* response, const char* memCachedID)
+void EspHttpBinding::addToESPCache(CHttpRequest* request, CHttpResponse* response, const char* cacheID)
 {
+    unsigned cacheSeconds = 0;
     const char* method = request->queryServiceMethod();
-    int cacheSeconds = queryMemCacheSeconds(method);
-    if (cacheSeconds < 0) //no memcache required for this method
+    if (!queryCacheSeconds(method, cacheSeconds)) //no cache required for this method
         return;
 
-    StringBuffer content, contentType, contentTypeID;
-    contentTypeID.set(memCachedID).append("t");
+    StringBuffer content, contentType;
     response->getContent(content);
     response->getContentType(contentType);
-    {
-        CriticalBlock block(memCachedCrit);
-        memCachedClient->set("ESPResponse", memCachedID, content.str(), (unsigned) cacheSeconds);
-        memCachedClient->set("ESPResponse", contentTypeID.str(), contentType.str(), (unsigned) cacheSeconds);
-    }
-    ESPLOG(LogMax, "AddTo MemCached for %s.", method);
+    if (espCacheClient->cacheResponse(cacheID, cacheSeconds, content.str(), contentType.str()))
+        ESPLOG(LogMax, "AddTo ESP Cache for %s.", method);
+    else
+        ESPLOG(LogMax, "Failed to add ESP Cache for %s.", method);
 }
-#endif
 
 void EspHttpBinding::handleHttpPost(CHttpRequest *request, CHttpResponse *response)
 {
-#ifdef USE_LIBMEMCACHED
-    ensureMemCachedClient();
-
-    StringBuffer memCachedID;
-    if (memCachedClient)
-        createMemCachedID(request, memCachedID);
-    if (!memCachedID.isEmpty() && sendFromMemCached(request, response, memCachedID.str()))
-        return;
-#endif
+    StringBuffer cacheID;
+    if (cacheMethods > 0)
+    {
+        unsigned cacheSeconds = 0;
+        const char* method = request->queryServiceMethod();
+        if (queryCacheSeconds(method, cacheSeconds)) //ESP cache is needed for this method
+        {
+            if (!espCacheClient)
+                espCacheClient.setown(createESPCache(espCacheInitString.get()));
+            if (espCacheClient)
+                createESPCacheID(request, cacheID);
+            if (!cacheID.isEmpty() && sendFromESPCache(request, response, cacheID.str()))
+                return;
+        }
+    }
 
     if(request->isSoapMessage()) 
     {
@@ -746,10 +723,8 @@ void EspHttpBinding::handleHttpPost(CHttpRequest *request, CHttpResponse *respon
     else
         onPost(request, response);
 
-#ifdef USE_LIBMEMCACHED
-    if (!memCachedID.isEmpty())
-        addToMemCached(request, response, memCachedID.str());
-#endif
+    if (!cacheID.isEmpty())
+        addToESPCache(request, response, cacheID.str());
 }
 
 int EspHttpBinding::onGet(CHttpRequest* request, CHttpResponse* response)

+ 20 - 28
esp/bindings/http/platform/httpbinding.hpp

@@ -19,6 +19,7 @@
 #define _HTTPBINDING_HPP__
 
 #include "http/platform/httptransport.ipp"
+#include "espcache.hpp"
 
 #include "bindutil.hpp"
 #include "seclib.hpp"
@@ -141,14 +142,18 @@ private:
 
     StringAttrMapping desc_map;
     StringAttrMapping help_map;
-#ifdef USE_LIBMEMCACHED
-    Owned<ESPMemCached> memCachedClient;
-    CriticalSection memCachedCrit;
-    StringAttr memCachedInitString;
-    unsigned memCachedMethods = 0;
-    MapStringTo<int> memCachedSecondsMap;
-    MapStringTo<bool> memCachedGlobalMap;
-#endif
+
+    Owned<IEspCache> espCacheClient;
+    StringAttr espCacheInitString;
+    unsigned cacheMethods = 0;
+    MapStringTo<unsigned> cacheSecondsMap;
+    MapStringTo<bool> cacheGlobalMap;
+
+    bool queryCacheSeconds(const char *method, unsigned& cacheSecond);
+    bool queryCacheGlobal(const char *method);
+    const char* createESPCacheID(CHttpRequest* request, StringBuffer& cacheID);
+    void addToESPCache(CHttpRequest* request, CHttpResponse* response, const char* cacheID);
+    bool sendFromESPCache(CHttpRequest* request, CHttpResponse* response, const char* cacheID);
 
     StringAttr              processName;
     StringAttr              domainName;
@@ -165,14 +170,6 @@ private:
     StringArray             domainAuthResourcesWildMatch;
 
     void getXMLMessageTag(IEspContext& ctx, bool isRequest, const char *method, StringBuffer& tag);
-#ifdef USE_LIBMEMCACHED
-    void ensureMemCachedClient();
-    int queryMemCacheSeconds(const char *method);
-    bool queryMemCacheGlobal(const char *method);
-    const char* createMemCachedID(CHttpRequest* request, StringBuffer& memCachedID);
-    void addToMemCached(CHttpRequest* request, CHttpResponse* response, const char* memCachedID);
-    bool sendFromMemCached(CHttpRequest* request, CHttpResponse* response, const char* memCachedID);
-#endif
 
 protected:
     MethodInfoArray m_methods;
@@ -227,20 +224,15 @@ public:
         StringBuffer key(method);
         help_map.setValue(key.toUpperCase().str(), help);
     }
-    void addMemCachedSeconds(const char *method, int cacheSeconds)
-    {
-#ifdef USE_LIBMEMCACHED
-        StringBuffer key(method);
-        memCachedSecondsMap.setValue(key.toUpperCase().str(), cacheSeconds);
-        memCachedMethods++;
-#endif
-    }
-    void addMemCachedGlobal(const char *method, bool cacheGlobal)
+    //The setCacheTimeout() is not thread safe because it is only called when ESP is
+    //starting and the WsWorkunits lib is loading.
+    void setCacheTimeout(const char *method, unsigned timeoutSeconds, bool global)
     {
-#ifdef USE_LIBMEMCACHED
         StringBuffer key(method);
-        memCachedGlobalMap.setValue(key.toUpperCase().str(), cacheGlobal);
-#endif
+        cacheSecondsMap.setValue(key.toUpperCase().str(), timeoutSeconds);
+        cacheMethods++;
+        if (global)
+            cacheGlobalMap.setValue(key.str(), global);
     }
 
     int onGetConfig(IEspContext &context, CHttpRequest* request, CHttpResponse* response);

+ 1 - 18
esp/bindings/http/platform/httptransport.cpp

@@ -456,9 +456,7 @@ void CHttpMessage::addParameter(const char* paramname, const char *value)
 
     m_queryparams->setProp(paramname, value);
     m_paramCount++;
-#ifdef USE_LIBMEMCACHED
     allParameterString.append("&").append(paramname).append("=").append(value);
-#endif
 }
 
 StringBuffer& CHttpMessage::getParameter(const char* paramname, StringBuffer& paramval)
@@ -1564,22 +1562,7 @@ StringBuffer& CHttpRequest::getPeer(StringBuffer& Peer)
     }
     return Peer;
 }
-#ifdef USE_LIBMEMCACHED
-unsigned CHttpRequest::createUniqueRequestHash(bool cacheGlobal, const char* msgType)
-{
-    StringBuffer idStr;
-    if (!cacheGlobal)
-    {
-        const char* userID = m_context->queryUserId();
-        if (!isEmptyString(userID))
-            idStr.append(userID).append("_");
-    }
-    if (!isEmptyString(msgType))
-        idStr.append(msgType).append("_");
-    idStr.appendf("%s_%s_%s", m_espServiceName.get(), m_espMethodName.get(), allParameterString.str());
-    return hashc((unsigned char *)idStr.str(), idStr.length(), 0);
-}
-#endif
+
 void CHttpRequest::getBasicAuthorization(StringBuffer& userid, StringBuffer& password,StringBuffer& realm)
 {
     StringBuffer authheader;

+ 1 - 224
esp/bindings/http/platform/httptransport.ipp

@@ -77,9 +77,7 @@ protected:
     Owned<IProperties> m_queryparams;
     MapStrToBuf  m_attachments;
     StringArray  m_headers;
-#ifdef USE_LIBMEMCACHED
     StringBuffer allParameterString;
-#endif
 
     Owned<IEspContext> m_context;
     IArrayOf<CEspCookie> m_cookies;
@@ -255,6 +253,7 @@ public:
         }
         return false;
     }
+    const char* queryAllParameterString() { return allParameterString.str(); }
 };
 
 
@@ -338,9 +337,6 @@ public:
     virtual int receive(IMultiException *me);
 
     void updateContext();
-#ifdef USE_LIBMEMCACHED
-    unsigned createUniqueRequestHash(bool cacheGlobal, const char* msgType);
-#endif
 
     virtual void setMaxRequestEntityLength(int len) {m_MaxRequestEntityLength = len;}
     virtual int getMaxRequestEntityLength() { return m_MaxRequestEntityLength; }
@@ -421,223 +417,4 @@ inline bool skipXslt(IEspContext &context)
     return (context.getResponseFormat()!=ESPSerializationANY);  //for now
 }
 
-#ifdef USE_LIBMEMCACHED
-#include <libmemcached/memcached.hpp>
-#include <libmemcached/util.h>
-
-class ESPMemCached : public CInterface
-{
-    memcached_st* connection = nullptr;
-    memcached_pool_st* pool = nullptr;
-    StringAttr options;
-    bool initialized = false;
-
-public :
-    ESPMemCached()
-    {
-#if (LIBMEMCACHED_VERSION_HEX < 0x01000010)
-        VStringBuffer msg("Memcached Plugin: libmemcached version '%s' incompatible with min version>=1.0.10", LIBMEMCACHED_VERSION_STRING);
-        ESPLOG(LogNormal, "%s", msg.str());
-#endif
-    }
-
-    ~ESPMemCached()
-    {
-        if (pool)
-        {
-            memcached_pool_release(pool, connection);
-            connection = nullptr;//For safety (from changing this destructor) as not implicit in either the above or below.
-            memcached_st *memc = memcached_pool_destroy(pool);
-            if (memc)
-                memcached_free(memc);
-        }
-        else if (connection)//This should never be needed but just in case.
-        {
-            memcached_free(connection);
-        }
-    };
-
-    bool init(const char * _options)
-    {
-        if (initialized)
-            return initialized;
-
-        options.set(_options);
-        pool = memcached_pool(_options, strlen(_options));
-        assertPool();
-
-        setPoolSettings();
-        connect();
-        if (connection)
-            initialized = checkServersUp();
-        return initialized;
-    }
-
-    void setPoolSettings()
-    {
-        assertPool();
-        const char * msg = "memcached_pool_behavior_set failed - ";
-        assertOnError(memcached_pool_behavior_set(pool, MEMCACHED_BEHAVIOR_KETAMA, 1), msg);//NOTE: alias of MEMCACHED_DISTRIBUTION_CONSISTENT_KETAMA amongst others.
-        memcached_pool_behavior_set(pool, MEMCACHED_BEHAVIOR_USE_UDP, 0);  // Note that this fails on early versions of libmemcached, so ignore result
-        assertOnError(memcached_pool_behavior_set(pool, MEMCACHED_BEHAVIOR_NO_BLOCK, 0), msg);
-        assertOnError(memcached_pool_behavior_set(pool, MEMCACHED_BEHAVIOR_CONNECT_TIMEOUT, 1000), msg);//units of ms.
-        assertOnError(memcached_pool_behavior_set(pool, MEMCACHED_BEHAVIOR_SND_TIMEOUT, 1000000), msg);//units of mu-s.
-        assertOnError(memcached_pool_behavior_set(pool, MEMCACHED_BEHAVIOR_RCV_TIMEOUT, 1000000), msg);//units of mu-s.
-        assertOnError(memcached_pool_behavior_set(pool, MEMCACHED_BEHAVIOR_BUFFER_REQUESTS, 0), msg);
-        assertOnError(memcached_pool_behavior_set(pool, MEMCACHED_BEHAVIOR_BINARY_PROTOCOL, 1), "memcached_pool_behavior_set failed - ");
-    }
-
-    void connect()
-    {
-        assertPool();
-        if (connection)
-#if (LIBMEMCACHED_VERSION_HEX<0x53000)
-            memcached_pool_push(pool, connection);
-        memcached_return_t rc;
-        connection = memcached_pool_pop(pool, (struct timespec *)0 , &rc);
-#else
-            memcached_pool_release(pool, connection);
-        memcached_return_t rc;
-        connection = memcached_pool_fetch(pool, (struct timespec *)0 , &rc);
-#endif
-        assertOnError(rc, "memcached_pool_pop failed - ");
-    }
-
-    bool checkServersUp()
-    {
-        memcached_return_t rc;
-        char* args = nullptr;
-        OwnedMalloc<memcached_stat_st> stats;
-        stats.setown(memcached_stat(connection, args, &rc));
-
-        unsigned int numberOfServers = memcached_server_count(connection);
-        if (numberOfServers < 1)
-        {
-            ESPLOG(LogMin,"Memcached: no server connected.");
-            return false;
-        }
-
-        unsigned int numberOfServersDown = 0;
-        for (unsigned i = 0; i < numberOfServers; ++i)
-        {
-            if (stats[i].pid == -1)//perhaps not the best test?
-            {
-                numberOfServersDown++;
-                VStringBuffer msg("Memcached: Failed connecting to entry %u\nwithin the server list: %s", i+1, options.str());
-                ESPLOG(LogMin, "%s", msg.str());
-            }
-        }
-        if (numberOfServersDown == numberOfServers)
-        {
-            ESPLOG(LogMin,"Memcached: Failed connecting to ALL servers. Check memcached on all servers and \"memcached -B ascii\" not used.");
-            return false;
-        }
-
-        //check memcached version homogeneity
-        for (unsigned i = 0; i < numberOfServers-1; ++i)
-        {
-            if (!streq(stats[i].version, stats[i+1].version))
-                DBGLOG("Memcached: Inhomogeneous versions of memcached across servers.");
-        }
-        return true;
-    };
-
-    bool exists(const char* partitionKey, const char* key)
-    {
-#if (LIBMEMCACHED_VERSION_HEX<0x53000)
-        throw makeStringException(0, "memcached_exist not supported in this version of libmemcached");
-#else
-        memcached_return_t rc;
-        size_t partitionKeyLength = strlen(partitionKey);
-        if (partitionKeyLength)
-            rc = memcached_exist_by_key(connection, partitionKey, partitionKeyLength, key, strlen(key));
-        else
-            rc = memcached_exist(connection, key, strlen(key));
-
-        if (rc == MEMCACHED_NOTFOUND)
-            return false;
-        else
-        {
-            assertOnError(rc, "'Exists' request failed - ");
-            return true;
-        }
-#endif
-    };
-
-    const char* get(const char* partitionKey, const char* key, StringBuffer& out)
-    {
-        uint32_t flag = 0;
-        size_t returnLength;
-        memcached_return_t rc;
-
-        OwnedMalloc<char> value;
-        size_t partitionKeyLength = strlen(partitionKey);
-        if (partitionKeyLength)
-            value.setown(memcached_get_by_key(connection, partitionKey, partitionKeyLength, key, strlen(key), &returnLength, &flag, &rc));
-        else
-            value.setown(memcached_get(connection, key, strlen(key), &returnLength, &flag, &rc));
-
-        if (value)
-            out.set(value);
-
-        StringBuffer keyMsg = "'Get' request failed - ";
-        assertOnError(rc, appendIfKeyNotFoundMsg(rc, key, keyMsg));
-        return out.str();
-    };
-
-    void set(const char* partitionKey, const char* key, const char* value, unsigned __int64 expireSec)
-    {
-        size_t partitionKeyLength = strlen(partitionKey);
-        const char * msg = "'Set' request failed - ";
-        if (partitionKeyLength)
-            assertOnError(memcached_set_by_key(connection, partitionKey, partitionKeyLength, key, strlen(key), value, strlen(value), (time_t)expireSec, 0), msg);
-        else
-            assertOnError(memcached_set(connection, key, strlen(key), value, strlen(value), (time_t)expireSec, 0), msg);
-    };
-
-    void deleteKey(const char* partitionKey, const char* key)
-    {
-        memcached_return_t rc;
-        size_t partitionKeyLength = strlen(partitionKey);
-        if (partitionKeyLength)
-            rc = memcached_delete_by_key(connection, partitionKey, partitionKeyLength, key, strlen(key), (time_t)0);
-        else
-            rc = memcached_delete(connection, key, strlen(key), (time_t)0);
-        assertOnError(rc, "'Delete' request failed - ");
-    };
-
-    void clear(unsigned when)
-    {
-        //NOTE: memcached_flush is the actual cache flush/clear/delete and not an io buffer flush.
-        assertOnError(memcached_flush(connection, (time_t)(when)), "'Clear' request failed - ");
-    };
-
-    void assertOnError(memcached_return_t rc, const char * _msg)
-    {
-        if (rc != MEMCACHED_SUCCESS)
-        {
-            VStringBuffer msg("Memcached: %s%s", _msg, memcached_strerror(connection, rc));
-            ESPLOG(LogNormal, "%s", msg.str());
-        }
-    };
-
-    const char * appendIfKeyNotFoundMsg(memcached_return_t rc, const char * key, StringBuffer & target) const
-    {
-        if (rc == MEMCACHED_NOTFOUND)
-            target.append("(key: '").append(key).append("') ");
-        return target.str();
-    };
-
-    void assertPool()
-    {
-        if (!pool)
-        {
-            StringBuffer msg = "Memcached: Failed to instantiate server pool with:";
-            msg.newline().append(options);
-            ESPLOG(LogNormal, "%s", msg.str());
-        }
-    }
-};
-#endif //USE_LIBMEMCACHED
-
 #endif

+ 304 - 0
esp/platform/espcache.cpp

@@ -0,0 +1,304 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2017 HPCC Systems®.
+
+    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.
+############################################################################## */
+
+#include "espcontext.hpp"
+#include "espcache.hpp"
+
+
+#ifdef USE_LIBMEMCACHED
+#include <libmemcached/memcached.hpp>
+#include <libmemcached/util.h>
+
+class ESPMemCached : implements IEspCache, public CInterface
+{
+    memcached_st* connection = nullptr;
+    memcached_pool_st* pool = nullptr;
+    StringAttr options;
+    bool initialized = false;
+    CriticalSection cacheCrit;
+
+    void setPoolSettings();
+    void connect();
+    bool checkServersUp();
+
+    void assertOnError(memcached_return_t rc, const char * _msg);
+    void assertPool();
+
+    virtual bool checkAndGet(const char* groupID, const char* cacheID, StringBuffer& out);
+
+public :
+    IMPLEMENT_IINTERFACE;
+    ESPMemCached();
+    ~ESPMemCached();
+
+    virtual bool cacheResponse(const char* cacheID, const unsigned cacheSeconds, const char* content, const char* contentType);
+    virtual bool readResponseCache(const char* cacheID, StringBuffer& content, StringBuffer& contentType);
+
+    bool init(const char * _options);
+    ESPCacheResult exists(const char* groupID, const char* cacheID);
+    ESPCacheResult get(const char* groupID, const char* cacheID, StringBuffer& out);
+    ESPCacheResult set(const char* groupID, const char* cacheID, const char* value, unsigned __int64 expireSec);
+    void remove(const char* groupID, const char* cacheID);
+    void flush(unsigned when);
+};
+
+ESPMemCached::ESPMemCached()
+{
+#if (LIBMEMCACHED_VERSION_HEX < 0x01000010)
+    VStringBuffer msg("ESPMemCached: libmemcached version '%s' incompatible with min version>=1.0.10", LIBMEMCACHED_VERSION_STRING);
+    ESPLOG(LogNormal, "%s", msg.str());
+#endif
+}
+
+ESPMemCached::~ESPMemCached()
+{
+    if (pool)
+    {
+        memcached_pool_release(pool, connection);
+        connection = nullptr;//For safety (from changing this destructor) as not implicit in either the above or below.
+        memcached_st *memc = memcached_pool_destroy(pool);
+        if (memc)
+            memcached_free(memc);
+    }
+    else if (connection)//This should never be needed but just in case.
+    {
+        memcached_free(connection);
+    }
+}
+
+bool ESPMemCached::init(const char * _options)
+{
+    CriticalBlock block(cacheCrit);
+
+    if (initialized)
+        return initialized;
+
+    if (!isEmptyString(_options))
+        options.set(_options);
+    else
+        options.set("--SERVER=127.0.0.1");
+    pool = memcached_pool(options.get(), options.length());
+    assertPool();
+
+    setPoolSettings();
+    connect();
+    if (connection)
+        initialized = checkServersUp();
+    return initialized;
+}
+
+bool ESPMemCached::cacheResponse(const char* cacheID, unsigned cacheSeconds, const char* content, const char* contentType)
+{
+    VStringBuffer contentTypeID("ContentType_%s", cacheID);
+
+    CriticalBlock block(cacheCrit);
+    ESPCacheResult ret = set("ESPResponse", cacheID, content, cacheSeconds);
+    if (ret != ESPCacheSuccess)
+        return false;
+    return set("ESPResponse", contentTypeID.str(), contentType, cacheSeconds) == ESPCacheSuccess;
+}
+
+bool ESPMemCached::readResponseCache(const char* cacheID, StringBuffer& content, StringBuffer& contentType)
+{
+    VStringBuffer contentTypeID("ContentType_%s", cacheID);
+
+    CriticalBlock block(cacheCrit);
+    if (!checkAndGet("ESPResponse", cacheID, content))
+        return false;
+    return checkAndGet("ESPResponse", contentTypeID.str(), contentType);
+}
+
+void ESPMemCached::setPoolSettings()
+{
+    assertPool();
+    const char * msg = "memcached_pool_behavior_set failed - ";
+    assertOnError(memcached_pool_behavior_set(pool, MEMCACHED_BEHAVIOR_KETAMA, 1), msg);//NOTE: alias of MEMCACHED_DISTRIBUTION_CONSISTENT_KETAMA amongst others.
+    memcached_pool_behavior_set(pool, MEMCACHED_BEHAVIOR_USE_UDP, 0);  // Note that this fails on early versions of libmemcached, so ignore result
+    assertOnError(memcached_pool_behavior_set(pool, MEMCACHED_BEHAVIOR_NO_BLOCK, 0), msg);
+    assertOnError(memcached_pool_behavior_set(pool, MEMCACHED_BEHAVIOR_CONNECT_TIMEOUT, 1000), msg);//units of ms.
+    assertOnError(memcached_pool_behavior_set(pool, MEMCACHED_BEHAVIOR_SND_TIMEOUT, 1000000), msg);//units of mu-s.
+    assertOnError(memcached_pool_behavior_set(pool, MEMCACHED_BEHAVIOR_RCV_TIMEOUT, 1000000), msg);//units of mu-s.
+    assertOnError(memcached_pool_behavior_set(pool, MEMCACHED_BEHAVIOR_BUFFER_REQUESTS, 0), msg);
+    assertOnError(memcached_pool_behavior_set(pool, MEMCACHED_BEHAVIOR_BINARY_PROTOCOL, 1), "memcached_pool_behavior_set failed - ");
+}
+
+void ESPMemCached::connect()
+{
+    assertPool();
+#if (LIBMEMCACHED_VERSION_HEX<0x53000)
+    if (connection)
+        memcached_pool_push(pool, connection);
+    memcached_return_t rc;
+    connection = memcached_pool_pop(pool, (struct timespec *)0 , &rc);
+#else
+    if (connection)
+        memcached_pool_release(pool, connection);
+    memcached_return_t rc;
+    connection = memcached_pool_fetch(pool, (struct timespec *)0 , &rc);
+#endif
+    assertOnError(rc, "memcached_pool_pop failed - ");
+}
+
+bool ESPMemCached::checkServersUp()
+{
+    memcached_return_t rc;
+    char* args = nullptr;
+    OwnedMalloc<memcached_stat_st> stats;
+    stats.setown(memcached_stat(connection, args, &rc));
+
+    unsigned int numberOfServers = memcached_server_count(connection);
+    if (numberOfServers < 1)
+    {
+        ESPLOG(LogMin,"ESPMemCached: no server connected.");
+        return false;
+    }
+
+    unsigned int numberOfServersDown = 0;
+    for (unsigned i = 0; i < numberOfServers; ++i)
+    {
+        if (stats[i].pid == -1)//perhaps not the best test?
+        {
+            numberOfServersDown++;
+            VStringBuffer msg("ESPMemCached: Failed connecting to entry %u\nwithin the server list: %s", i+1, options.str());
+            ESPLOG(LogMin, "%s", msg.str());
+        }
+    }
+    if (numberOfServersDown == numberOfServers)
+    {
+        ESPLOG(LogMin,"ESPMemCached: Failed connecting to ALL servers. Check memcached on all servers and \"memcached -B ascii\" not used.");
+        return false;
+    }
+
+    //check memcached version homogeneity
+    for (unsigned i = 0; i < numberOfServers-1; ++i)
+    {
+        if (!streq(stats[i].version, stats[i+1].version))
+            DBGLOG("ESPMemCached: Inhomogeneous versions of memcached across servers.");
+    }
+    return true;
+}
+
+ESPCacheResult ESPMemCached::exists(const char* groupID, const char* cacheID)
+{
+#if (LIBMEMCACHED_VERSION_HEX<0x53000)
+    throw makeStringException(0, "memcached_exist not supported in this version of libmemcached");
+#endif
+
+    memcached_return_t rc;
+    size_t groupIDLength = strlen(groupID);
+    if (groupIDLength)
+        rc = memcached_exist_by_key(connection, groupID, groupIDLength, cacheID, strlen(cacheID));
+    else
+        rc = memcached_exist(connection, cacheID, strlen(cacheID));
+
+    if (rc != MEMCACHED_NOTFOUND)
+        assertOnError(rc, "'Exists' request failed - ");
+    return rc == MEMCACHED_NOTFOUND ? ESPCacheNotFound : (rc == MEMCACHED_SUCCESS ? ESPCacheSuccess : ESPCacheError);
+}
+
+ESPCacheResult ESPMemCached::get(const char* groupID, const char* cacheID, StringBuffer& out)
+{
+    uint32_t flag = 0;
+    size_t returnLength;
+    memcached_return_t rc;
+
+    OwnedMalloc<char> value;
+    size_t groupIDLength = strlen(groupID);
+    if (groupIDLength)
+        value.setown(memcached_get_by_key(connection, groupID, groupIDLength, cacheID, strlen(cacheID), &returnLength, &flag, &rc));
+    else
+        value.setown(memcached_get(connection, cacheID, strlen(cacheID), &returnLength, &flag, &rc));
+
+    if (value)
+        out.set(value);
+
+    StringBuffer msg = "'Get' request failed - ";
+    if (rc == MEMCACHED_NOTFOUND)
+        msg.append("(cacheID: '").append(cacheID).append("') ");
+    assertOnError(rc, msg.str());
+    return rc == MEMCACHED_NOTFOUND ? ESPCacheNotFound : (rc == MEMCACHED_SUCCESS ? ESPCacheSuccess : ESPCacheError);
+}
+
+bool ESPMemCached::checkAndGet(const char* groupID, const char* cacheID, StringBuffer& item)
+{
+    ESPCacheResult ret = exists(groupID, cacheID);
+    if ((ret != ESPCacheSuccess) && (ret != ESPCacheNotFound))
+        return false;
+    if (ret == ESPCacheSuccess)
+        get(groupID, cacheID, item);
+    return true;
+}
+
+ESPCacheResult ESPMemCached::set(const char* groupID, const char* cacheID, const char* value, unsigned __int64 expireSec)
+{
+    memcached_return_t rc;
+    size_t groupIDLength = strlen(groupID);
+    if (groupIDLength)
+        rc = memcached_set_by_key(connection, groupID, groupIDLength, cacheID, strlen(cacheID), value, strlen(value), (time_t)expireSec, 0);
+    else
+        rc = memcached_set(connection, cacheID, strlen(cacheID), value, strlen(value), (time_t)expireSec, 0);
+    
+    assertOnError(rc, "'Set' request failed - ");
+    return rc == MEMCACHED_NOTFOUND ? ESPCacheNotFound : (rc == MEMCACHED_SUCCESS ? ESPCacheSuccess : ESPCacheError);
+}
+
+void ESPMemCached::remove(const char* groupID, const char* cacheID)
+{
+    memcached_return_t rc;
+    size_t groupIDLength = strlen(groupID);
+    if (groupIDLength)
+        rc = memcached_delete_by_key(connection, groupID, groupIDLength, cacheID, strlen(cacheID), (time_t)0);
+    else
+        rc = memcached_delete(connection, cacheID, strlen(cacheID), (time_t)0);
+    assertOnError(rc, "'Delete' request failed - ");
+}
+
+void ESPMemCached::flush(unsigned when)
+{
+    //NOTE: memcached_flush is the actual cache flush/clear/delete and not an io buffer flush.
+    assertOnError(memcached_flush(connection, (time_t)(when)), "'Clear' request failed - ");
+}
+
+void ESPMemCached::assertOnError(memcached_return_t rc, const char * _msg)
+{
+    if (rc != MEMCACHED_SUCCESS)
+    {
+        VStringBuffer msg("ESPMemCached: %s%s", _msg, memcached_strerror(connection, rc));
+        ESPLOG(LogNormal, "%s", msg.str());
+    }
+}
+
+void ESPMemCached::assertPool()
+{
+    if (!pool)
+    {
+        StringBuffer msg = "ESPMemCached: Failed to instantiate server pool with:";
+        msg.newline().append(options);
+        ESPLOG(LogNormal, "%s", msg.str());
+    }
+}
+#endif //USE_LIBMEMCACHED
+
+extern esp_http_decl IEspCache* createESPCache(const char* setting)
+{
+#ifdef USE_LIBMEMCACHED
+    Owned<ESPMemCached> espCache = new ESPMemCached();
+    if (espCache->init(setting))
+        return espCache.getClear();
+#endif
+    return nullptr;
+}

+ 43 - 0
esp/platform/espcache.hpp

@@ -0,0 +1,43 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2017 HPCC Systems®.
+
+    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.
+############################################################################## */
+
+#ifndef _ESPCACHE_IPP__
+#define _ESPCACHE_IPP__
+
+#include "esp.hpp"
+
+#ifdef ESPHTTP_EXPORTS
+    #define esp_http_decl DECL_EXPORT
+#else
+    #define esp_http_decl DECL_IMPORT
+#endif
+
+enum ESPCacheResult
+{
+    ESPCacheSuccess,
+    ESPCacheError,
+    ESPCacheNotFound
+};
+
+interface IEspCache : extends IInterface
+{
+    virtual bool cacheResponse(const char* cacheID, const unsigned cacheSeconds, const char* content, const char* contentType) = 0;
+    virtual bool readResponseCache(const char* cacheID, StringBuffer& content, StringBuffer& contentType) = 0;
+};
+
+extern esp_http_decl IEspCache* createESPCache(const char* setting);
+#endif

+ 18 - 0
esp/platform/espcfg.cpp

@@ -799,3 +799,21 @@ IEspPlugin* CEspConfig::getPlugin(const char* name)
     return NULL;
 }
 
+bool CEspConfig::checkESPCache()
+{
+    bool espCacheAvailable = false ;
+    list<binding_cfg*>::iterator iter = m_bindings.begin();
+    while (iter!=m_bindings.end())
+    {
+        binding_cfg& xcfg = **iter;
+        if (xcfg.bind->getCacheMethodCount() > 0)
+        {
+            Owned<IEspCache> espCache = createESPCache(m_cfg->queryProp("@espCacheInitString"));
+            espCacheAvailable = (espCache != nullptr);
+            break;
+        }
+        iter++;
+    }
+    return espCacheAvailable;
+}
+

+ 6 - 1
esp/platform/espcfg.ipp

@@ -41,6 +41,7 @@ using namespace std;
 //ESP
 #include "esp.hpp"
 #include "espplugin.hpp"
+#include "espcache.hpp"
 
 struct binding_cfg
 {
@@ -180,9 +181,13 @@ public:
         DBGLOG("loadServices");
         loadServices();
         loadProtocols();
-        loadBindings();      
+        loadBindings();
+
+        if (m_cfg->getPropBool("@ensureESPCache", false) && !checkESPCache())
+            throw MakeStringException(-1, "Failed in checking ESP cache service using %s", m_cfg->queryProp("@espCacheInitString"));
     }
 
+    bool checkESPCache();
     IEspPlugin* getPlugin(const char* name);
 
     void loadBuiltIns();

+ 1 - 0
esp/protocols/http/CMakeLists.txt

@@ -43,6 +43,7 @@ set(SRCS
     ../../platform/espcontext.cpp
     ../../platform/espprotocol.cpp
     ../../platform/espthread.cpp
+    ../../platform/espcache.cpp
     ../../platform/sechandler.cpp
     ../../platform/txsummary.cpp
     mapinfo.cpp

+ 1 - 0
esp/scm/esp.ecm

@@ -311,6 +311,7 @@ SCMinterface IEspRpcBinding(IInterface)
     void setContainer(IEspContainer *ic);
     void setXslProcessor(IInterface *xslp);
     IEspContainer* queryContainer();
+    unsigned getCacheMethodCount();
 };
 
 SCMinterface IEspUriMap(IInterface)

+ 9 - 9
esp/scm/ws_topology.ecm

@@ -633,25 +633,25 @@ ESPservice [auth_feature("DEFERRED"), noforms, version("1.26"), exceptions_inlin
     ESPuses ESPstruct TpServices;
     ESPuses ESPstruct TpTargetCluster;
 
-    ESPmethod [cache_seconds(180), cache_globel(1), resp_xsl_default("/esp/xslt/targetclusters.xslt")] TpTargetClusterQuery(TpTargetClusterQueryRequest, TpTargetClusterQueryResponse);
+    ESPmethod [cache_seconds(180), cache_global(1), resp_xsl_default("/esp/xslt/targetclusters.xslt")] TpTargetClusterQuery(TpTargetClusterQueryRequest, TpTargetClusterQueryResponse);
     ESPmethod [cache_seconds(180), cache_global(1), resp_xsl_default("/esp/xslt/topology.xslt")] TpClusterQuery(TpClusterQueryRequest, TpClusterQueryResponse);
     ESPmethod [cache_seconds(180), cache_global(1)] TpLogicalClusterQuery(TpLogicalClusterQueryRequest, TpLogicalClusterQueryResponse);
     ESPmethod [cache_seconds(180), cache_global(1)] TpGroupQuery(TpGroupQueryRequest, TpGroupQueryResponse);
-    ESPmethod [cache_seconds(180), cache_globel(1), resp_xsl_default("/esp/xslt/machines.xslt")] TpMachineQuery(TpMachineQueryRequest, TpMachineQueryResponse);
-    ESPmethod [cache_seconds(180), cache_globel(1), resp_xsl_default("/esp/xslt/cluster_info.xslt")] TpClusterInfo(TpClusterInfoRequest, TpClusterInfoResponse);
-    ESPmethod [cache_seconds(180), cache_globel(1), resp_xsl_default("/esp/xslt/thor_status.xslt")] TpThorStatus(TpThorStatusRequest, TpThorStatusResponse);
+    ESPmethod [cache_seconds(180), cache_global(1), resp_xsl_default("/esp/xslt/machines.xslt")] TpMachineQuery(TpMachineQueryRequest, TpMachineQueryResponse);
+    ESPmethod [cache_seconds(180), cache_global(1), resp_xsl_default("/esp/xslt/cluster_info.xslt")] TpClusterInfo(TpClusterInfoRequest, TpClusterInfoResponse);
+    ESPmethod [cache_seconds(180), cache_global(1), resp_xsl_default("/esp/xslt/thor_status.xslt")] TpThorStatus(TpThorStatusRequest, TpThorStatusResponse);
 
-    ESPmethod [cache_seconds(180), cache_globel(1), min_ver("1.26")] TpDropZoneQuery(TpDropZoneQueryRequest, TpDropZoneQueryResponse);
-    ESPmethod [cache_seconds(180), cache_globel(1), resp_xsl_default("/esp/xslt/services.xslt")] TpServiceQuery(TpServiceQueryRequest, TpServiceQueryResponse);
+    ESPmethod [cache_seconds(180), cache_global(1), min_ver("1.26")] TpDropZoneQuery(TpDropZoneQueryRequest, TpDropZoneQueryResponse);
+    ESPmethod [cache_seconds(180), cache_global(1), resp_xsl_default("/esp/xslt/services.xslt")] TpServiceQuery(TpServiceQueryRequest, TpServiceQueryResponse);
     ESPmethod TpSetMachineStatus(TpSetMachineStatusRequest, TpSetMachineStatusResponse);
     ESPmethod TpSwapNode(TpSwapNodeRequest, TpSwapNodeResponse);
     ESPmethod [cache_seconds(180)] TpXMLFile(TpXMLFileRequest, TpXMLFileResponse);
     ESPmethod [resp_xsl_default("/esp/xslt/tplog.xslt")] TpLogFile(TpLogFileRequest, TpLogFileResponse);
     ESPmethod [resp_xsl_default("/esp/xslt/tplogdisplay.xslt")] TpLogFileDisplay(TpLogFileRequest, TpLogFileResponse);
     ESPmethod TpGetComponentFile(TpGetComponentFileRequest, TpGetComponentFileResponse);
-    ESPmethod [cache_seconds(180), cache_globel(1)] TpGetServicePlugins(TpGetServicePluginsRequest, TpGetServicePluginsResponse);
-    ESPmethod [cache_seconds(180), cache_globel(1)] TpListTargetClusters(TpListTargetClustersRequest, TpListTargetClustersResponse);
-    ESPmethod [cache_seconds(180), cache_globel(1), min_ver(1.25)] TpMachineInfo(TpMachineInfoRequest, TpMachineInfoResponse);
+    ESPmethod [cache_seconds(180), cache_global(1)] TpGetServicePlugins(TpGetServicePluginsRequest, TpGetServicePluginsResponse);
+    ESPmethod [cache_seconds(180), cache_global(1)] TpListTargetClusters(TpListTargetClustersRequest, TpListTargetClustersResponse);
+    ESPmethod [cache_seconds(180), cache_global(1), min_ver(1.25)] TpMachineInfo(TpMachineInfoRequest, TpMachineInfoResponse);
 
     ESPmethod SystemLog(SystemLogRequest, SystemLogResponse);
 };

+ 1 - 0
esp/services/esdl_svc_engine/esdl_binding.hpp

@@ -375,6 +375,7 @@ public:
 
     bool usesESDLDefinition(const char * name, int version);
     bool usesESDLDefinition(const char * id);
+    virtual unsigned getCacheMethodCount(){return 0;}
 
 private:
     int onGetRoxieBuilder(CHttpRequest* request, CHttpResponse* response, const char *serv, const char *method);

+ 1 - 0
esp/services/ws_ecl/ws_ecl_service.hpp

@@ -149,6 +149,7 @@ public:
     StringBuffer &generateNamespace(IEspContext &context, CHttpRequest* request, const char *serv, const char *method, StringBuffer &ns);
 
     virtual int getQualifiedNames(IEspContext& ctx, MethodInfoArray & methods){return 0;}
+    virtual unsigned getCacheMethodCount(){return 0;}
 
     void getNavigationData(IEspContext &context, IPropertyTree & data);
     void getRootNavigationFolders(IEspContext &context, IPropertyTree & data);

+ 9 - 2
initfiles/componentfiles/configxml/esp.xsd.in

@@ -863,10 +863,17 @@
                     </xs:appinfo>
                 </xs:annotation>
             </xs:attribute>
-            <xs:attribute name="memCachedOptionString" type="xs:string" use="optional">
+            <xs:attribute name="ensureESPCache" type="xs:boolean" use="optional" default="false">
                 <xs:annotation>
                     <xs:appinfo>
-                        <tooltip>Option string used by ESP memcached client.</tooltip>
+                        <tooltip>If true, ESP will not be started if no ESP cache service is available.</tooltip>
+                    </xs:appinfo>
+                </xs:annotation>
+            </xs:attribute>
+            <xs:attribute name="espCacheInitString" type="xs:string" use="optional">
+                <xs:annotation>
+                    <xs:appinfo>
+                        <tooltip>String used for initializing ESP cache client.</tooltip>
                     </xs:appinfo>
                 </xs:annotation>
             </xs:attribute>

+ 8 - 5
tools/hidl/hidlcomp.cpp

@@ -5530,6 +5530,7 @@ void EspServInfo::write_esp_binding_ipp()
     outf("\tC%sSoapBinding(IPropertyTree* cfg, const char *bindname=NULL, const char *procname=NULL, http_soap_log_level level=hsl_none);\n", name_);
 
     outs("\tvirtual void init_strings();\n");
+    outs("\tvirtual unsigned getCacheMethodCount(){return m_cacheMethodCount;}\n");
 
     //method ==> processRequest
     outs("\tvirtual int processRequest(IRpcMessage* rpc_call, IRpcMessage* rpc_response);\n");
@@ -5622,6 +5623,7 @@ void EspServInfo::write_esp_binding_ipp()
 
     outs("private:\n");
     outs("\tMapStringTo<SecAccessFlags> m_accessmap;\n");
+    outs("\tunsigned m_cacheMethodCount = 0;\n");
 
     outs("};\n\n");
 }
@@ -5670,13 +5672,14 @@ void EspServInfo::write_esp_binding()
             StrBuffer tmp; 
             outf("\taddMethodHelp(\"%s\", \"%s\");\n", mthi->getName(), printfEncode(val.str(), tmp).str());
         }
+        int cacheGlobal = mthi->getMetaInt("cache_global", 0);
         int cacheSeconds = mthi->getMetaInt("cache_seconds", -1);
         if (cacheSeconds > -1) {
-            outf("\taddMemCachedSeconds(\"%s\", %d);\n", mthi->getName(), cacheSeconds);
-        }
-        int cacheGlobel = mthi->getMetaInt("cache_globel", 0);
-        if (cacheGlobel > 0) {
-            outf("\taddMemCachedGlobal(\"%s\", 1);\n", mthi->getName());
+            if (cacheGlobal > 0)
+                outf("\tsetCacheTimeout(\"%s\", %d, 1);\n", mthi->getName(), cacheSeconds);
+            else
+                outf("\tsetCacheTimeout(\"%s\", %d, 0);\n", mthi->getName(), cacheSeconds);
+            outs("\tm_cacheMethodCount++;\n");
         }
     }
     outs("}\n");