Updated Branches: refs/heads/master 2e8ae19cd -> 69bd54736
TS-1370: Restore original stale-wile-revalidate code for posterity Project: http://git-wip-us.apache.org/repos/asf/trafficserver/repo Commit: http://git-wip-us.apache.org/repos/asf/trafficserver/commit/69bd5473 Tree: http://git-wip-us.apache.org/repos/asf/trafficserver/tree/69bd5473 Diff: http://git-wip-us.apache.org/repos/asf/trafficserver/diff/69bd5473 Branch: refs/heads/master Commit: 69bd547366b24c23ad616c39d896002c0b9c773c Parents: 2e8ae19 Author: Phil Sorber <[email protected]> Authored: Sat Jul 21 17:22:29 2012 -0700 Committer: James Peach <[email protected]> Committed: Sat Jul 21 17:23:07 2012 -0700 ---------------------------------------------------------------------- CHANGES | 3 + .../experimental/stale_while_revalidate/swr.cpp | 1229 +++++++++++++++ 2 files changed, 1232 insertions(+), 0 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/trafficserver/blob/69bd5473/CHANGES ---------------------------------------------------------------------- diff --git a/CHANGES b/CHANGES index 061fa45..da628d0 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,8 @@ -*- coding: utf-8 -*- Changes with Apache Traffic Server 3.3.0 + *) [TS-1370] Restore original stale-wile-revalidate code for posterity + Author: Phil Sorber + *) [TS-1340] Improve IPv6 port example in records.comfig Author: Jan-Frode Myklebust http://git-wip-us.apache.org/repos/asf/trafficserver/blob/69bd5473/plugins/experimental/stale_while_revalidate/swr.cpp ---------------------------------------------------------------------- diff --git a/plugins/experimental/stale_while_revalidate/swr.cpp b/plugins/experimental/stale_while_revalidate/swr.cpp index e69de29..0f571e5 100644 --- a/plugins/experimental/stale_while_revalidate/swr.cpp +++ b/plugins/experimental/stale_while_revalidate/swr.cpp @@ -0,0 +1,1229 @@ +/** @file + + A brief file description + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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. + */ + +/** + * swr.cpp + * + * If the cache data has expired but falls within the stale while revalidate window, + * serve the cached data and make an async request for new data. + * +*/ + +#include <stdio.h> +#include <set> +#include <string> +#include <sstream> +#include <iostream> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <netdb.h> +#include "InkAPI.h" +#include <string.h> +#include <stdlib.h> +#include <limits.h> + +#define DEBUG 0 + +static const char SWR_LOG_TAG[] = "http_swr_plugin"; + +/// Field in Cache control header which defines the SWR window. +static const char HTTP_VALUE_STALE_WHILE_REVALIDATE[] = "stale-while-revalidate"; + +/// Field in Cache control header which defines the time to wait for SWR response +static const char HTTP_VALUE_TIME_TO_WAIT[] = "time-to-wait"; + +/// Field in Cache control header for background-fetch +static const char HTTP_VALUE_BACKGROUND_FETCH[] = "background-fetch"; + +/// This header is added when a SWR request is made. +/// This is used by the plugin to distinguish between a regular request and a SWR request +static const char SWR_FETCH_HEADER[] = "X-TS-SWR: 1\r\n\r\n"; + +/// Can be set with stale_while_revalidate_window config param. +/// This is overridden by the server's Cache control header. +static time_t STALE_WHILE_REVALIDATE_WINDOW = 0; + +/// Can be set with stale_while_revalidate_window config param. +/// This is overridden by the server's Cache control header. +static long STALE_WHILE_REVALIDATE_WINDOW_INFINITE = -1; + +/// In milli seconds. Can be set with time_to_wait config param. +/// Controls the time to wait for asynchronous request to complete before returning stale data. +static unsigned int TIME_TO_WAIT = 0; + +/// Can be set with max_age config param. +/// This is overridden by the server's Cache control header. +/// This is needed because some origin servers do not advertise either max-age or mime-field-expires +static time_t MAX_AGE = 0; + +static + std::set < + std::string > + swr_sites_requested; +static INKMutex swr_mutex = INKMutexCreate(); + +static const char *SWR_WARNING_HEADER = "110 \"Response is stale\""; + +typedef struct +{ + + INKVIO writeVIO; + INKVIO readVIO; + INKIOBuffer reqBuff; + INKIOBufferReader reqReader; + INKIOBuffer respBuff; + INKIOBufferReader respReader; + INKIOBuffer dumpBuff; + int iDumpLen; + +} FetchData; + +struct request_header_values_t +{ + bool swr_can_run; + bool only_if_cached; + bool background_fetch; + request_header_values_t(); +}; + +request_header_values_t::request_header_values_t() +: +swr_can_run(true), only_if_cached(false), background_fetch(false) +{ + +} + +struct response_header_values_t +{ + time_t mime_field_expires; + long stale_while_revalidate_window; + time_t max_age; + time_t date; + time_t time_to_wait; + bool must_revalidate; + + response_header_values_t(); + void init(); + time_t get_expiration(); + time_t get_max_stale_time(); + time_t get_time_to_wait(); + long get_swr_window(); + +}; + +response_header_values_t::response_header_values_t() +: +mime_field_expires(0), +stale_while_revalidate_window(STALE_WHILE_REVALIDATE_WINDOW), max_age(MAX_AGE), date(0), time_to_wait(TIME_TO_WAIT), +must_revalidate(false) +{ + +} + +void +response_header_values_t::init() +{ + mime_field_expires = 0; + stale_while_revalidate_window = STALE_WHILE_REVALIDATE_WINDOW; + max_age = MAX_AGE; + date = 0; + time_to_wait = TIME_TO_WAIT; +} + +/** +* Get expiration time for the page from cache_control header +* expiration time = date + max_age +* @param[out] time_t : expiration time for the URL +*/ +time_t +response_header_values_t::get_expiration() +{ + if (max_age) + return date + max_age; + else if(mime_field_expires) + return mime_field_expires; + + // If max_age and mime_field_expires are 0, expire NOW! + return date; +} + +/** +* Get time until which the page is considered valid. +* max_stale_time = exp_time + stale_white_revalidate_window +* @param[out] time_t : time till which the page can be served even if it is past expiration time +*/ +time_t +response_header_values_t::get_max_stale_time() +{ + time_t expiration_time = get_expiration(); + if (expiration_time == 0) { + return 0; + } + return (expiration_time + stale_while_revalidate_window); +} + +time_t +response_header_values_t::get_time_to_wait() +{ + return time_to_wait; +} + +long +response_header_values_t::get_swr_window() +{ + return stale_while_revalidate_window; +} + +/** +* This will be called only in debug mode +* Dumps out response from origin server +*/ +static void +dump_response(INKCont contp) +{ + + FetchData *pData = (FetchData *) INKContDataGet(contp); + INKIOBufferReader reader = INKIOBufferReaderAlloc(pData->dumpBuff); + + int iReadTotal = 0; + const int BUFF_SIZE = (pData->iDumpLen + 1); + char dump[BUFF_SIZE]; + char *dumpPtr = &dump[0]; + memset((void *) dumpPtr, 0, BUFF_SIZE); + + int iAvail = INKIOBufferReaderAvail(reader); + + if (iAvail <= 0) { + INKDebug(SWR_LOG_TAG, "[swr] nothing to read... "); + return; + } + + + INKIOBufferBlock startBlock = INKIOBufferReaderStart(reader); + + while (iAvail > 0 && startBlock != INK_ERROR_PTR) { + const char *startPtr = INKIOBufferBlockReadStart(startBlock, reader, &iAvail); + + if (startPtr == INK_ERROR_PTR || iAvail == INK_ERROR) { + INKError("[swr] dump: could not get block read starting point \n"); + break; + } + + iReadTotal += iAvail; + memcpy((void *) dumpPtr, (void *) startPtr, iAvail); + dumpPtr += iAvail; + INKIOBufferReaderConsume(reader, iAvail); + startBlock = INKIOBufferBlockNext(startBlock); + iAvail = INKIOBufferReaderAvail(reader); + + iReadTotal += iAvail; + if (iReadTotal > pData->iDumpLen) { + INKError + ("[swr] dump: read was bigger than expected, aborting. total resp len: %d wanted to read: %d \n", + pData->iDumpLen, iReadTotal); + break; + } + } + + dumpPtr = &dump[0]; + if (dumpPtr != NULL && strlen(dumpPtr) > 0) { + INKDebug(SWR_LOG_TAG, "[swr] dump: successful copy: %s \n", dumpPtr); + } + + INKIOBufferReaderFree(reader); + return; +} + +/** +* Read data from VIO +* Reenable if there is more data to be read +*/ +static void +read_response(INKCont contp) +{ + FetchData *pData = (FetchData *) INKContDataGet(contp); + if (pData == INK_ERROR_PTR) { + INKError("[swr] ERROR could not get data from contp to write fetch"); + return; + } + + int iTodo = INKVIONTodoGet(pData->readVIO); + if (iTodo > 0) { + int iAvail = INKIOBufferReaderAvail(pData->respReader); + if (iAvail == INK_ERROR) { + INKError("[swr] could not get avail bytes from read vio, returning"); + INKVIOReenable(pData->readVIO); + return; + } + + if (iTodo > iAvail) { + iTodo = iAvail; + } + + INKDebug(SWR_LOG_TAG, "[swr] going to read in: %d \n", iTodo); + + if (iTodo > 0) { + INKIOBufferCopy(pData->dumpBuff, pData->respReader, iTodo, 0); + + // just move pointer in reader, don't actually get data + if (INKIOBufferReaderConsume(pData->respReader, iTodo) == INK_ERROR) { + INKDebug(SWR_LOG_TAG, "[swr] could not tell resp reader to consume, returning"); + INKVIOReenable(pData->readVIO); + return; + } + pData->iDumpLen += iTodo; + INKDebug(SWR_LOG_TAG, "[swr] bytes to be dumped: %d \n", pData->iDumpLen); + } + + iTodo = INKVIONTodoGet(pData->readVIO); + + if (iTodo > 0) { + // still have some left to read + INKVIOReenable(pData->readVIO); + INKDebug(SWR_LOG_TAG, "[swr] more data to read... reenable read vio \n"); + } + } + + return; +} + +/** +* Write request to server +*/ +static void +write_fetch_request(INKCont contp) +{ + FetchData *pData = (FetchData *) INKContDataGet(contp); + + if (pData == INK_ERROR_PTR) { + INKError("[swr] ERROR could not get data from contp to write fetch"); + return; + } + + int iTodo = INKVIONTodoGet(pData->writeVIO); + INKError("[swr] write todo ret: %d", iTodo); + + int iWriteVIONBytes = INKVIONBytesGet(pData->writeVIO); + INKDebug(SWR_LOG_TAG, "[swr] writeVIO NBytes ret: %d", iWriteVIONBytes); + + + if (INKVIOReenable(pData->writeVIO) == INK_ERROR) { + INKError("[swr] could not re-enable write vio"); + } + + return; +} + +static int +fetch_handler(INKCont contp, INKEvent event, void *edata) +{ + + switch (event) { + case INK_EVENT_VCONN_WRITE_READY: + { + INKDebug(SWR_LOG_TAG, "[swr] FETCH_HANDLER::INK_EVENT_VCONN_WRITE_READY calling write_fetch_request"); + write_fetch_request(contp); + INKDebug(SWR_LOG_TAG, "[swr] FETCH_HANDLER::INK_EVENT_VCONN_WRITE_READY write_fetch_request done"); + break; + } + + case INK_EVENT_VCONN_WRITE_COMPLETE: + { + INKDebug(SWR_LOG_TAG, "[swr] FETCH_HANDLER::INK_EVENT_VCONN_WRITE_COMPLETE"); + break; + } + + case INK_EVENT_VCONN_READ_READY: + { + // - there is new data in the read buffer; when we're done reading, re-enable VIO + INKDebug(SWR_LOG_TAG, "[swr] FETCH_HANDLER::EVENT_VCONN_READ_READY calling read_response"); + read_response(contp); + INKDebug(SWR_LOG_TAG, "[swr] FETCH_HANDLER::EVENT_VCONN_READ_READY read_response done"); + break; + } + + case INK_EVENT_VCONN_READ_COMPLETE: + { + // - the VIO has read all the bytes specified by INKVConnRead, vconn can be re-used or tossed + INKDebug(SWR_LOG_TAG, "[swr] FETCH_HANDLER::EVENT_VCONN_READ_COMPLETE"); + break; + } + + case INK_EVENT_VCONN_EOS: + { + // - occurs when read goes past end of byte stream b/c # bytes specified in VConnRead was bigger + INKDebug(SWR_LOG_TAG, "[swr] FETCH_HANDLER::EVENT_VCONN_EOS"); +#if DEBUG + dump_response(contp); +#endif + + FetchData *pData = (FetchData *) INKContDataGet(contp); + INKIOBufferDestroy(pData->reqBuff); + INKIOBufferDestroy(pData->respBuff); + INKIOBufferDestroy(pData->dumpBuff); + INKVConn fetchConn = INKVIOVConnGet(pData->writeVIO); + INKVConnShutdown(fetchConn, 1, 1); + INKVConnClose(fetchConn); + delete pData; + INKContDestroy(contp); + + break; + } + case INK_EVENT_ERROR: + { + INKDebug(SWR_LOG_TAG, "[swr] FETCH_HANDLER::EVENT_ERROR"); + break; + } + + default: + { + INKDebug(SWR_LOG_TAG, "[swr] FETCH_HANDLER::DEFAULT"); + break; + } + } + + return 0; +} + +/** +* get the request URL +*/ +static char * +getURLFromReqHeader(INKHttpTxn & txnp) +{ + // MAKE SURE YOU FREE THE RETURNED STRING! + + INKMBuffer req_bufp; + INKMLoc hdr_loc; + char *url_str; + int url_length; + + if (!INKHttpTxnClientReqGet(txnp, &req_bufp, &hdr_loc)) { + INKError("[swr] getURLFromReqHeader : couldn't retrieve client response header\n"); + return NULL; + } + + INKMLoc url_loc = INKHttpHdrUrlGet(req_bufp, hdr_loc); + if (!url_loc) { + INKError("[swr] getURLFromReqHeader : couldn't retrieve request url\n"); + INKHandleMLocRelease(req_bufp, INK_NULL_MLOC, hdr_loc); + return NULL; + } + + url_str = INKUrlStringGet(req_bufp, url_loc, &url_length); + INKHandleMLocRelease(req_bufp, hdr_loc, url_loc); + INKHandleMLocRelease(req_bufp, INK_NULL_MLOC, hdr_loc); + return url_str; +} + +/** +* Check if the req is a Stale while revalidate reques +* @param[out] bool true if the request is a stale while revalidate request. false otherwise +*/ +static bool +isSWR(INKHttpTxn & txnp) +{ + INKMBuffer req_bufp; + INKMLoc req_loc; + bool bRet = false; + + INKHttpTxnClientReqGet(txnp, &req_bufp, &req_loc); + INKMLoc swr_loc = NULL; + swr_loc = INKMimeHdrFieldFind(req_bufp, req_loc, "X-TS-SWR", strlen("X-TS-SWR")); + + if (swr_loc != INK_ERROR_PTR && swr_loc != NULL) { + bRet = true; + INKDebug(SWR_LOG_TAG, "[swr] Request is Stale while revalidate"); + } else { + INKDebug(SWR_LOG_TAG, "[swr] Request NOT Stale while revalidate"); + } + + INKHandleMLocRelease(req_bufp, req_loc, swr_loc); + INKHandleMLocRelease(req_bufp, INK_NULL_MLOC, req_loc); + + return bRet; +} + + +/** + +*/ +static void +deleteFromHeader(INKMBuffer & req_bufp, INKMLoc & req_loc, const char *header, const char *field, int field_len) +{ + INKDebug(SWR_LOG_TAG, "[swr] deleteFromHeader trying to remove from %s : %s ", header, field); + INKMLoc header_loc, dup_header_loc; + const char *value; + + header_loc = INKMimeHdrFieldFind(req_bufp, req_loc, header, -1); + while (header_loc != INK_ERROR_PTR && header_loc != 0) { + int nvalues = INKMimeFieldValuesCount(req_bufp, header_loc); + for (int i = 0; i < nvalues; i++) { + int value_len; + value = INKMimeFieldValueGet(req_bufp, header_loc, i, &value_len); + if (value_len == field_len && strncasecmp(value, field, field_len) == 0) { + INKDebug(SWR_LOG_TAG, "[swr] deleteFromHeader : %s, %s ", header, field); + INKMimeHdrFieldValueDelete(req_bufp, req_loc, header_loc, i); + } + + INKHandleStringRelease(req_bufp, header_loc, value); + } + + // Get next header + dup_header_loc = INKMimeHdrFieldNextDup(req_bufp, req_loc, header_loc); + INKHandleMLocRelease(req_bufp, req_loc, header_loc); + header_loc = dup_header_loc; + } + + INKHandleMLocRelease(req_bufp, INK_NULL_MLOC, req_loc); +} + +/** +* Send request to self +* Add special SWR_FETCH_HEADER so that this can be differntiated from other requests +*/ +static bool +sendRequestToSelf(INKHttpTxn &txnp, response_header_values_t &my_state) +{ + INKDebug(SWR_LOG_TAG, "[swr] sendRequestToSelf called"); + INKVConn fetchOnDemandVC; + bool ret = true; + + unsigned int client_ip = INKHttpTxnClientIPGet(txnp); + if (INKHttpConnect(htonl(client_ip), 9999, &fetchOnDemandVC) != INK_ERROR) { + // writer needs: request to write, + INKCont fetchCont = INKContCreate(fetch_handler, INKMutexCreate()); + FetchData *pData = new FetchData(); + if (pData) { + INKContDataSet(fetchCont, pData); + + // Create req and resp buffers for background fetch + pData->iDumpLen = 0; + pData->reqBuff = INKIOBufferCreate(); + pData->reqReader = INKIOBufferReaderAlloc(pData->reqBuff); + + pData->respBuff = INKIOBufferCreate(); + pData->respReader = INKIOBufferReaderAlloc(pData->respBuff); + pData->dumpBuff = INKIOBufferCreate(); + + // get the original request with headers and copy to the background fetch request + INKMBuffer req_bufp; + INKMLoc req_loc; + INKIOBuffer reqBuff; + INKIOBufferReader reqReader; + int block_avail; + + reqBuff = INKIOBufferCreate(); + reqReader = INKIOBufferReaderAlloc(reqBuff); + INKHttpTxnClientReqGet(txnp, &req_bufp, &req_loc); + + // Get pristine URL + INKMLoc pristine_url_loc; + + if (INKHttpTxnPristineUrlGet(txnp, &req_bufp, &pristine_url_loc) != INK_ERROR) { + // Set pristine URL in request + INKDebug(SWR_LOG_TAG, "[swr] setting pristine URL in request"); + INKHttpHdrUrlSet(req_bufp, req_loc, pristine_url_loc); + } + // write original header to background fetch request + if (INKHttpHdrPrint(req_bufp, req_loc, reqBuff) == INK_ERROR) { + INKDebug(SWR_LOG_TAG, "[swr] INKHttpHdrPrint failed"); + ret = false; + } else { + INKDebug(SWR_LOG_TAG, "[swr] INKHttpHdrPrint succeeded"); + if (INKIOBufferReaderAvail(reqReader)) { + INKIOBufferBlock block = INKIOBufferReaderStart(reqReader); + while (1) { + const char *block_start; + block_start = INKIOBufferBlockReadStart(block, reqReader, &block_avail); + if ((block = INKIOBufferBlockNext(block)) != NULL) { + INKIOBufferWrite(pData->reqBuff, block_start, block_avail); + } else { + // need to truncate the very last newline character as we need to add + // a couple of more headers. (the last newline is treated as terminator) + if (block_start[block_avail - 1] == '\n' && block_start[block_avail - 2] == '\r') + INKIOBufferWrite(pData->reqBuff, block_start, block_avail - 2); + else + INKIOBufferWrite(pData->reqBuff, block_start, block_avail - 1); + break; + } + } + + // add a If-Modified-Since header so traffic server will update the cache instead of replacing the entry + INKMLoc ims = INKMimeHdrFieldCreate(req_bufp, req_loc); + INKMimeFieldNameSet(req_bufp, ims, INK_MIME_FIELD_IF_MODIFIED_SINCE, INK_MIME_LEN_IF_MODIFIED_SINCE); + INKMimeHdrFieldValueDateSet(req_bufp, req_loc, ims, my_state.date); + + if (INKIOBufferWrite(pData->reqBuff, SWR_FETCH_HEADER, strlen(SWR_FETCH_HEADER)) == INK_ERROR) { + ret = false; + INKDebug(SWR_LOG_TAG, "[swr] could not write req to buffer"); + } +#if 1 + block = INKIOBufferReaderStart(pData->reqReader); + const char *block_start = INKIOBufferBlockReadStart(block, pData->reqReader, + &block_avail); + int size = INKIOBufferBlockReadAvail(block, pData->reqReader); + INKDebug(SWR_LOG_TAG, "[swr] request string: %.*s", size, block_start); +#endif + } + } + + pData->writeVIO = INKVConnWrite(fetchOnDemandVC, + fetchCont, pData->reqReader, INKIOBufferReaderAvail(pData->reqReader)); + pData->readVIO = INKVConnRead(fetchOnDemandVC, fetchCont, pData->respBuff, INT_MAX); + + // Release stuff + INKIOBufferReaderFree(reqReader); + INKIOBufferDestroy(reqBuff); + INKHandleMLocRelease(req_bufp, INK_NULL_MLOC, req_loc); + } else { + ret = false; + INKDebug(SWR_LOG_TAG, "[swr] problem creating continuation"); + INKContDestroy(fetchCont); + INKVConnShutdown(fetchOnDemandVC, 0, 0); + } + } else { + ret = false; + INKError("[swr] problem doing http connect"); + } + INKDebug(SWR_LOG_TAG, "[swr] sendRequestToSelf ends"); + + return ret; +} + +/** +* Set cache lookup status to whatever is passed in +*/ +static void +setCacheStatus(INKHttpTxn & txnp, int lookupStatus) +{ + // Set cache status to FRESH + INKDebug(SWR_LOG_TAG, "[swr] setCacheStatusFresh : setting cache hit status to %d", lookupStatus); + INKHttpTxnCacheLookupStatusSet(txnp, lookupStatus); +} + +/** +* Add warning header to indicate that response is stale +*/ +static bool +addSWRWarningHeader(INKHttpTxn & txnp) +{ + INKMBuffer bufp = NULL; + INKMLoc hdr_loc = NULL; + INKMLoc field_loc = NULL; + bool new_field = false; + if (!INKHttpTxnClientRespGet(txnp, &bufp, &hdr_loc)) { + INKHandleMLocRelease(bufp, INK_NULL_MLOC, hdr_loc); + INKDebug(SWR_LOG_TAG, "addSWRWarningHeader : Could not get server response"); + return false; + } + INKDebug(SWR_LOG_TAG, "addSWRWarningHeader : trying to add header"); + + // look for the field first + if ((field_loc = INKMimeHdrFieldFind(bufp, hdr_loc, INK_MIME_FIELD_WARNING, INK_MIME_LEN_WARNING)) == NULL) { + // "set" or "append", need to create the field first + field_loc = INKMimeHdrFieldCreate(bufp, hdr_loc); + INKMimeHdrFieldNameSet(bufp, hdr_loc, field_loc, INK_MIME_FIELD_WARNING, INK_MIME_LEN_WARNING); + new_field = true; + } + // append the value at the end + INKMimeHdrFieldValueStringInsert(bufp, hdr_loc, field_loc, -1, SWR_WARNING_HEADER, strlen(SWR_WARNING_HEADER)); + + if (new_field) { + // append the new field + INKMimeHdrFieldAppend(bufp, hdr_loc, field_loc); + } + + INKHandleMLocRelease(bufp, hdr_loc, field_loc); + INKHandleMLocRelease(bufp, INK_NULL_MLOC, hdr_loc); + INKDebug(SWR_LOG_TAG, "addSWRWarningHeader : done"); + return true; +} + +/** +* looks for no-cache directive from the client +*/ +static void +parseRequestHeaders(INKHttpTxn & txnp, request_header_values_t & my_state) +{ + INKDebug(SWR_LOG_TAG, "[swr] parseRequestHeaders called"); + INKMBuffer req_bufp; + INKMLoc req_loc; + const char *value; + + if (!INKHttpTxnClientReqGet(txnp, &req_bufp, &req_loc)) { + INKError("[swr] parseRequestHeaders : couldn't retrieve client request header."); + return; + } + + INKMLoc cache_control_loc, dup_cache_control_loc; + cache_control_loc = INKMimeHdrFieldFind(req_bufp, req_loc, "Cache-Control", -1); + while (cache_control_loc != INK_ERROR_PTR && cache_control_loc != 0) { + int nvalues = INKMimeFieldValuesCount(req_bufp, cache_control_loc); + for (int i = 0; i < nvalues; i++) { + int value_len; + value = INKMimeFieldValueGet(req_bufp, cache_control_loc, i, &value_len); + if (value_len == INK_HTTP_LEN_NO_CACHE && strncasecmp(value, INK_HTTP_VALUE_NO_CACHE, INK_HTTP_LEN_NO_CACHE) == 0) { + INKDebug(SWR_LOG_TAG, "[swr] parseRequestHeader : set swr_can_run to false"); + my_state.swr_can_run = false; + } else if (value_len == INK_HTTP_LEN_ONLY_IF_CACHED && + strncasecmp(value, INK_HTTP_VALUE_ONLY_IF_CACHED, INK_HTTP_LEN_ONLY_IF_CACHED) == 0) { + INKDebug(SWR_LOG_TAG, "[swr] parseRequestHeader : set only_if_cached to true"); + my_state.only_if_cached = true; + } else if (value_len == (int) strlen(HTTP_VALUE_BACKGROUND_FETCH) && + strncasecmp(value, HTTP_VALUE_BACKGROUND_FETCH, strlen(HTTP_VALUE_BACKGROUND_FETCH)) == 0) { + INKDebug(SWR_LOG_TAG, "[swr] parseRequestHeader : set background_fetch to true"); + my_state.background_fetch = true; + } + + INKHandleStringRelease(req_bufp, cache_control_loc, value); + } + + // Get next Cache-Control header + dup_cache_control_loc = INKMimeHdrFieldNextDup(req_bufp, req_loc, cache_control_loc); + INKHandleMLocRelease(req_bufp, req_loc, cache_control_loc); + cache_control_loc = dup_cache_control_loc; + } + + INKHandleMLocRelease(req_bufp, INK_NULL_MLOC, req_loc); + INKDebug(SWR_LOG_TAG, "[swr] parseRequestHeaders ends"); +} + +/** +* looks for max-age, stale-while-revalidate, time-to-wait, must-revalidate +*/ +static void +parseResponseHeaders(INKHttpTxn & txnp, response_header_values_t & my_state) +{ + INKMBuffer resp_bufp; + INKMLoc resp_loc; + const char *value; + + if (!INKHttpTxnCachedRespGet(txnp, &resp_bufp, &resp_loc)) { + INKError("[swr] parseResponseHeaders : couldn't retrieve server response header."); + return; + } + + INKMLoc date_loc; + date_loc = INKMimeHdrFieldFind(resp_bufp, resp_loc, INK_MIME_FIELD_DATE, INK_MIME_LEN_DATE); + if (date_loc != INK_ERROR_PTR && date_loc != 0) { + INKMimeHdrFieldValueDateGet(resp_bufp, resp_loc, date_loc, &my_state.date); + INKHandleMLocRelease(resp_bufp, resp_loc, date_loc); + } + + INKMLoc cache_control_loc, dup_cache_control_loc; + cache_control_loc = + INKMimeHdrFieldFind(resp_bufp, resp_loc, INK_MIME_FIELD_CACHE_CONTROL, INK_MIME_LEN_CACHE_CONTROL); + while (cache_control_loc != INK_ERROR_PTR && cache_control_loc != 0) { + int nvalues = INKMimeFieldValuesCount(resp_bufp, cache_control_loc); + for (int i = 0; i < nvalues; i++) { + int value_len; + value = INKMimeFieldValueGet(resp_bufp, cache_control_loc, i, &value_len); + if (value_len >= INK_HTTP_LEN_MAX_AGE + 2) { // +2 - one for =, atleast another for a number + const char *ptr; + if (ptr = strcasestr(value, INK_HTTP_VALUE_MAX_AGE)) { + ptr += INK_HTTP_LEN_MAX_AGE; + if (*ptr == '=') { + ptr++; + my_state.max_age = atol(ptr); + } + } + } + + if (value_len >= (int) strlen(HTTP_VALUE_STALE_WHILE_REVALIDATE) + 2) { // +2 - one for =, atleast another for a number + const char *ptr; + if (ptr = strcasestr(value, HTTP_VALUE_STALE_WHILE_REVALIDATE)) { + ptr += strlen(HTTP_VALUE_STALE_WHILE_REVALIDATE); + if (*ptr == '=') { + ptr++; + my_state.stale_while_revalidate_window = atol(ptr); + } + } + } + + if (value_len >= (int) strlen(HTTP_VALUE_TIME_TO_WAIT) + 2) { // +2 - one for =, atleast another for a number + const char *ptr; + if (ptr = strcasestr(value, HTTP_VALUE_TIME_TO_WAIT)) { + ptr += strlen(HTTP_VALUE_TIME_TO_WAIT); + if (*ptr == '=') { + ptr++; + my_state.time_to_wait = atol(ptr); + } + } + } + + if (value_len == INK_HTTP_LEN_MUST_REVALIDATE && + strncasecmp(value, INK_HTTP_VALUE_MUST_REVALIDATE, INK_HTTP_LEN_MUST_REVALIDATE) == 0) { + my_state.must_revalidate = true; + } + + if (value_len == INK_HTTP_LEN_PROXY_REVALIDATE && + strncasecmp(value, INK_HTTP_VALUE_PROXY_REVALIDATE, INK_HTTP_LEN_PROXY_REVALIDATE) == 0) { + my_state.must_revalidate = true; + } + + INKHandleStringRelease(resp_bufp, cache_control_loc, value); + } + + // Get next Cache-Control header + dup_cache_control_loc = INKMimeHdrFieldNextDup(resp_bufp, resp_loc, cache_control_loc); + INKHandleMLocRelease(resp_bufp, resp_loc, cache_control_loc); + cache_control_loc = dup_cache_control_loc; + } + + INKMLoc mime_field_expires_loc; + mime_field_expires_loc = INKMimeHdrFieldFind(resp_bufp, resp_loc, INK_MIME_FIELD_EXPIRES, INK_MIME_LEN_EXPIRES); + if (mime_field_expires_loc != INK_ERROR_PTR && mime_field_expires_loc != 0) { + INKMimeHdrFieldValueDateGet(resp_bufp, resp_loc, mime_field_expires_loc, &my_state.mime_field_expires); + INKHandleMLocRelease(resp_bufp, resp_loc, mime_field_expires_loc); + } + + INKHandleMLocRelease(resp_bufp, INK_NULL_MLOC, resp_loc); + INKDebug(SWR_LOG_TAG, + "[swr] parseResponseHeaders : mime_field_expires=%ld, stale_while_revalidate_window=%ld, max_age=%ld, date=%ld, time_to_wait=%ld", + my_state.mime_field_expires, my_state.stale_while_revalidate_window, my_state.max_age, my_state.date, + my_state.time_to_wait); +} + +/** +* This function gets called only if (curr_time > exp_time) +* Logic +* if ((curr_time<=max stale time)) && (req is not a SWR req)) +* { +* if(no one else has made async req for URL) +* { +* make async req to self for the URL; +* } +* return stale data from cache; +* } +* else +* { +* nothing special needs to be done +* } +* @param[out] int. 0 : Do not serve stale data, as SWR was off or time is past max_stale_time +* @param[out] int. 1 : Serve stale data as SWR was done +*/ +static int +doStaleWhileRevalidate(INKCont & contp, void *edata, response_header_values_t & my_state) +{ + INKHttpTxn txnp = (INKHttpTxn) edata; + INKDebug(SWR_LOG_TAG, "[swr] doStaleWhileRevalidate : Started"); + + long swr_window = my_state.get_swr_window(); + if (swr_window == 0) { + INKDebug(SWR_LOG_TAG, "[swr] doStaleWhileRevalidate : turned OFF"); + return 0; + } + int retval = 0; + time_t curr_time = INKhrtime() / 1000000000; + time_t max_stale_time = my_state.get_max_stale_time(); + + time_t diff = max_stale_time - curr_time; + INKDebug(SWR_LOG_TAG, + "[swr] doStaleWhileRevalidate : curr_time=%ld, max_stale_time=%ld, diff=%ld", + curr_time, max_stale_time, diff); + if (diff > 0 || swr_window == STALE_WHILE_REVALIDATE_WINDOW_INFINITE) { + if (!isSWR(txnp)) { + // If no one else has made the request or is making the request, + // Send request to self after adding special header + + char *url = getURLFromReqHeader(txnp); + if (url == NULL) { + INKError("[swr] doStaleWhileRevalidate : url is NULL"); + return 0; + } + // retval to indicate that stale data must be served + retval = 1; + bool useTimeToWait = true; + INKDebug(SWR_LOG_TAG, "[swr] doStaleWhileRevalidate : url=%s", url); + if (INKMutexLock(swr_mutex) == INK_SUCCESS) { + // Check again after getting the mutex lock as someone else might have requested the URL in the interim + if (swr_sites_requested.find(url) == swr_sites_requested.end()) { + INKDebug(SWR_LOG_TAG, "[swr] doStaleWhileRevalidate : sending req to self. inserting URL=%s", url); + + // insert URL into sites requested and unlock mutex + swr_sites_requested.insert(url); + INKMutexUnlock(swr_mutex); + + if (!sendRequestToSelf(txnp, my_state)) { + // If sending request to self failed, do not use time for wait + // remove URL from sites requested + useTimeToWait = false; + if (INKMutexLock(swr_mutex) == INK_SUCCESS) { + INKDebug(SWR_LOG_TAG, "[swr] doStaleWhileRevalidate : removing url=%s", url); + swr_sites_requested.erase(url); + INKMutexUnlock(swr_mutex); + } + } + } else { + INKDebug(SWR_LOG_TAG, "[swr] doStaleWhileRevalidate : some one else is requesting URL=%s", url); + INKMutexUnlock(swr_mutex); + useTimeToWait = false; + } + } else { + useTimeToWait = false; + } + INKfree((void *) url); + + // wait for time-to-wait milli secs and then set cache lookup status to fresh. + // this will give the async request some time to complete. + //FIXME + //Commenting out time to wait code as this cannot be implemented easily now + /* + if (useTimeToWait) { + time_t time_to_wait = my_state.get_time_to_wait(); + INKDebug(SWR_LOG_TAG, "[swr] doStaleWhileRevalidate scheduling continuation after %ld ms", time_to_wait); + INKHttpSchedule(contp, txnp, time_to_wait); + retval = 2; + } + */ + + } else { + INKDebug(SWR_LOG_TAG, + "[swr] doStaleWhileRevalidate : swr request received. nothing to do."); + } + } else { + INKDebug(SWR_LOG_TAG, "[swr] doStaleWhileRevalidate : Not doing SWR, as cache data has expired"); + } + INKDebug(SWR_LOG_TAG, "[swr] doStaleWhileRevalidate : Ends"); + return retval; +} + +/** +* Remove URL from set of URLs being requested asynchronously +*/ +static void +removeURLFromSitesRequested(INKHttpTxn & txnp) +{ + char *url = getURLFromReqHeader(txnp); + if (url == NULL) { + INKError("[swr] removeURLFromSitesRequested : url is NULL"); + return; + } + if (INKMutexLock(swr_mutex) != INK_ERROR) { + INKDebug(SWR_LOG_TAG, "[swr] removeURLFromSitesRequested : removing url=%s", url); + swr_sites_requested.erase(url); + INKMutexUnlock(swr_mutex); + } + INKfree((void *) url); +} + + +static bool +isRequestFromLocalhost(INKHttpTxn & txnp) +{ + INKDebug(SWR_LOG_TAG, "[swr] isRequestFromLocalhost returning : %d", INKHttpIsInternalRequest(txnp)); + return INKHttpIsInternalRequest(txnp); +} + +static void +ignoreOnlyIfCached(INKHttpTxn & txnp) +{ + // Delete only-if-cached from Cache-Control + INKMBuffer req_bufp; + INKMLoc req_loc; + INKHttpTxnClientReqGet(txnp, &req_bufp, &req_loc); + deleteFromHeader(req_bufp, req_loc, "Cache-Control", INK_HTTP_VALUE_ONLY_IF_CACHED, INK_HTTP_LEN_ONLY_IF_CACHED); + INKHandleMLocRelease(req_bufp, INK_NULL_MLOC, req_loc); +} + +static int +plugin_worker_handler(INKCont contp, INKEvent event, void *edata) +{ + switch (event) { + case INK_EVENT_HTTP_READ_REQUEST_HDR: + { + INKDebug(SWR_LOG_TAG, "[swr] MAIN_HANDLER::INK_HTTP_READ_REQUEST_HDR_HOOK"); + INKHttpTxn txnp = (INKHttpTxn) edata; + request_header_values_t pstate; + + parseRequestHeaders(txnp, pstate); + // skip RWW if request is SWR and is from localhost + if (isSWR(txnp) && isRequestFromLocalhost(txnp)) { + INKDebug(SWR_LOG_TAG, "[swr] Disable RWW as this is a SWR request from localhost"); + INKHttpTxnSkipRww(txnp); + } + + if (pstate.swr_can_run) { + // Add hook only if SWR can run + INKHttpTxnHookAdd(txnp, INK_HTTP_CACHE_LOOKUP_COMPLETE_HOOK, contp); + } + + INKHttpTxnReenable(txnp, INK_EVENT_HTTP_CONTINUE); + break; + } + case INK_EVENT_HTTP_SEND_RESPONSE_HDR: + { + INKDebug(SWR_LOG_TAG, "[swr] MAIN_HANDLER::INK_HTTP_SEND_RESPONSE_HDR_HOOK"); + INKHttpTxn txnp = (INKHttpTxn) edata; + addSWRWarningHeader(txnp); + INKHttpTxnReenable(txnp, INK_EVENT_HTTP_CONTINUE); + INKDebug(SWR_LOG_TAG, "[swr] MAIN_HANDLER::INK_HTTP_SEND_RESPONSE_HDR ends"); + break; + } + case INK_EVENT_HTTP_CACHE_LOOKUP_COMPLETE: + { + INKHttpTxn txnp = (INKHttpTxn) edata; + INKDebug(SWR_LOG_TAG, "[swr] MAIN_HANDLER::INK_HTTP_CACHE_LOOKUP_COMPLETE_HOOK"); + + int lookupStatus = 0; + if (INKHttpTxnCacheLookupStatusGet(txnp, &lookupStatus) != INK_SUCCESS) { + INKDebug(SWR_LOG_TAG, "[swr] cache status get failure"); + } else { + request_header_values_t pstate; + bool is_swr_request = false; + if (lookupStatus == INK_CACHE_LOOKUP_MISS || lookupStatus == INK_CACHE_LOOKUP_HIT_STALE) { + parseRequestHeaders(txnp, pstate); + is_swr_request = isSWR(txnp); + } + + if (lookupStatus == INK_CACHE_LOOKUP_MISS) { + INKDebug(SWR_LOG_TAG, "[swr] cache status MISS"); + // Code for background fetch on miss if only_if_cached and background_fetch are set by the client + if (!is_swr_request) { + if (pstate.only_if_cached && pstate.background_fetch) { + // Do background fetch only if client Cache-Control has both only-if-cached and background-fetch + response_header_values_t resp_values; + resp_values.init(); + resp_values.stale_while_revalidate_window = STALE_WHILE_REVALIDATE_WINDOW_INFINITE; + INKDebug(SWR_LOG_TAG, "[swr] doing background fetch"); + // Ignore return value in this case as we do not want a warning header + doStaleWhileRevalidate(contp, edata, resp_values); + } + } else { + // SWR request. Handle background fetch request + if (pstate.only_if_cached && pstate.background_fetch) { + // Delete only-if-cached from Cache-Control + // This will force the core to make a req to the OS + INKDebug(SWR_LOG_TAG, "[swr] ignoreOnlyIfCached"); + ignoreOnlyIfCached(txnp); + } + } + } else if (lookupStatus == INK_CACHE_LOOKUP_HIT_STALE) { + INKDebug(SWR_LOG_TAG, "[swr] cache status HIT STALE"); + + if (!is_swr_request) { + + response_header_values_t my_state; + my_state.init(); + parseResponseHeaders(txnp, my_state); + bool forced_background_fetch = false; + if (pstate.only_if_cached && pstate.background_fetch) { + // Force background fetch only if document age is within max_stale_time + time_t curr_time = INKhrtime() / 1000000000; + time_t max_stale_time = my_state.get_max_stale_time(); + time_t diff = max_stale_time - curr_time; + if(diff < 0) { + forced_background_fetch = true; + } + my_state.stale_while_revalidate_window = STALE_WHILE_REVALIDATE_WINDOW_INFINITE; + } + + if (my_state.must_revalidate == false && doStaleWhileRevalidate(contp, edata, my_state) == 1 && !forced_background_fetch) { + setCacheStatus(txnp, INK_CACHE_LOOKUP_HIT_FRESH); + INKHttpTxnHookAdd(txnp, INK_HTTP_SEND_RESPONSE_HDR_HOOK, contp); + } else { + INKDebug(SWR_LOG_TAG, "[swr] Not serving stale data"); + if (pstate.only_if_cached) { + // Set cache status to miss + // must return an error + setCacheStatus(txnp, INK_CACHE_LOOKUP_MISS); + } + } + } else { + // SWR request. Handle background fetch request + if (pstate.only_if_cached && pstate.background_fetch) { + // Delete only-if-cached from Cache-Control + // This will force the core to make a req to the OS + INKDebug(SWR_LOG_TAG, "[swr] ignoreOnlyIfCached"); + ignoreOnlyIfCached(txnp); + } + } + //FIXME + //The code below should be used when time to wait can be implemented correctly + /* + if (doStaleWhileRevalidate(contp, event, edata)) { + // If doStaleWhileRevalidate returns true, there is an async request happening to OS. + // Do not reenable continuation. + // Wait for timeout + break; + } + */ + } else if (lookupStatus == INK_CACHE_LOOKUP_HIT_FRESH) { + INKDebug(SWR_LOG_TAG, "[swr] cache status HIT FRESH"); + } else if (lookupStatus == INK_CACHE_LOOKUP_SKIPPED) { + INKDebug(SWR_LOG_TAG, "[swr] cache status SKIPPED"); + } + } + INKHttpTxnReenable(txnp, INK_EVENT_HTTP_CONTINUE); + INKDebug(SWR_LOG_TAG, "[swr] MAIN_HANDLER::INK_HTTP_CACHE_LOOKUP_COMPLETE_HOOK ends"); + break; + } + case INK_EVENT_HTTP_TXN_CLOSE: + { + INKHttpTxn txnp = (INKHttpTxn) edata; + INKDebug(SWR_LOG_TAG, "[swr] MAIN_HANDLER::INK_HTTP_TXN_CLOSE_HOOK"); + if (isSWR(txnp)) { + removeURLFromSitesRequested(txnp); + } + INKHttpTxnReenable(txnp, INK_EVENT_HTTP_CONTINUE); + INKContDestroy(contp); + INKDebug(SWR_LOG_TAG, "[swr] MAIN_HANDLER::INK_HTTP_TXN_CLOSE_HOOK ends"); + break; + } + //FIXME + //The code below should be used when time to wait can be implemented correctly + /* + case INK_EVENT_IMMEDIATE: + { + // TIME_TO_WAIT was 0 + INKDebug(SWR_LOG_TAG, "[swr] MAIN_HANDLER::INK_EVENT_IMMEDIATE_HOOK TIME_TO_WAIT was 0"); + INKHttpTxn txnp = (INKHttpTxn) edata; + setCacheStatus(txnp, INK_CACHE_LOOKUP_HIT_FRESH); + INKHttpTxnReenable(txnp, INK_EVENT_HTTP_CONTINUE); + INKDebug(SWR_LOG_TAG, "[swr] MAIN_HANDLER::INK_EVENT_IMMEDIATE_HOOK ends"); + break; + } + case INK_EVENT_TIMEOUT: + { + // We have waited long enough for the async request to complete + INKDebug(SWR_LOG_TAG, "[swr] MAIN_HANDLER::INK_EVENT_TIMEOUT_HOOK done waiting for response"); + INKHttpTxn txnp = (INKHttpTxn) edata; + setCacheStatus(txnp, INK_CACHE_LOOKUP_HIT_FRESH); + INKDebug(SWR_LOG_TAG, "[swr] MAIN_HANDLER::INK_EVENT_TIMEOUT_HOOK txnp = %u", txnp); + INKHttpTxnReenable(txnp, INK_EVENT_HTTP_CONTINUE); + INKDebug(SWR_LOG_TAG, "[swr] MAIN_HANDLER::INK_EVENT_TIMEOUT_HOOK ends"); + break; + } + */ + default: + { + INKHttpTxn txnp = (INKHttpTxn) edata; + INKDebug(SWR_LOG_TAG, "[swr] default event"); + INKHttpTxnReenable(txnp, INK_EVENT_HTTP_CONTINUE); + break; + } + } + + return 1; +} + +static int +plugin_main_handler(INKCont contp, INKEvent event, void *edata) +{ + switch (event) { + case INK_EVENT_HTTP_TXN_START: + { + INKDebug(SWR_LOG_TAG, "[swr] MAIN_HANDLER::INK_HTTP_READ_REQUEST_HDR_HOOK"); + INKHttpTxn txnp = (INKHttpTxn) edata; + + // Create new continuation + INKCont workerCont = INKContCreate(plugin_worker_handler, NULL); + + // Add local hooks for the new continuation + INKHttpTxnHookAdd(txnp, INK_HTTP_READ_REQUEST_HDR_HOOK, workerCont); + INKHttpTxnHookAdd(txnp, INK_HTTP_TXN_CLOSE_HOOK, workerCont); + INKHttpTxnReenable(txnp, INK_EVENT_HTTP_CONTINUE); + break; + } + default: + { + INKDebug(SWR_LOG_TAG, "[swr] default event"); + INKHttpTxn txnp = (INKHttpTxn) edata; + INKHttpTxnReenable(txnp, INK_EVENT_HTTP_CONTINUE); + break; + } + } + + return 1; +} + +static void +parse_config_line(char *line) +{ + std::istringstream conf_line(line); + + std::string key, value; + conf_line >> key; + conf_line >> value; + if (key.empty() || value.empty()) + return; + + if (key == "stale_while_revalidate_window") + STALE_WHILE_REVALIDATE_WINDOW = atol(value.c_str()); + else if (key == "time_to_wait") + TIME_TO_WAIT = atol(value.c_str()); + else if (key == "max_age") + MAX_AGE = atol(value.c_str()); +} + +static bool +read_config(const char *file_name) +{ + INKFile conf_file; + conf_file = INKfopen(file_name, "r"); + + if (conf_file != NULL) { + char buf[1024]; + while (INKfgets(conf_file, buf, sizeof(buf) - 1) != NULL) { + if (strlen(buf) == 0 || buf[0] == '#') + continue; + parse_config_line(buf); + } + INKfclose(conf_file); + } else { + fprintf(stderr, "Failed to open stale while revalidate config file %s\n", file_name); + return false; + } + INKDebug(SWR_LOG_TAG, "[swr] STALE_WHILE_REVALIDATE_WINDOW = %ld", STALE_WHILE_REVALIDATE_WINDOW); + INKDebug(SWR_LOG_TAG, "[swr] TIME_TO_WAIT = %ld", TIME_TO_WAIT); + return true; +} + +void +INKPluginInit(int argc, const char *argv[]) +{ + char default_filename[1024]; + const char *conf_filename; + + if (argc > 1) { + conf_filename = argv[1]; + } else { + sprintf(default_filename, "%s/stale_while_revalidate.conf", INKPluginDirGet()); + conf_filename = default_filename; + } + + if (!read_config(conf_filename)) { + if (argc > 1) { + INKError(SWR_LOG_TAG, "[swr] Plugin conf not valid."); + } else { + INKError(SWR_LOG_TAG, "[swr] No config file specified in plugin.conf"); + } + INKError(SWR_LOG_TAG, "[swr] Continuing with default values for config parameters"); + } + // Creates parent continuation + INKCont mainCont = INKContCreate(plugin_main_handler, NULL); + + // Add global hooks with continuation to be called when the event has to be processed + INKHttpHookAdd(INK_HTTP_TXN_START_HOOK, mainCont); +}
