Repository: trafficserver Updated Branches: refs/heads/master c304f2c08 -> f35b7e060
[TS-3662]: Caching range requests plugin. Project: http://git-wip-us.apache.org/repos/asf/trafficserver/repo Commit: http://git-wip-us.apache.org/repos/asf/trafficserver/commit/f35b7e06 Tree: http://git-wip-us.apache.org/repos/asf/trafficserver/tree/f35b7e06 Diff: http://git-wip-us.apache.org/repos/asf/trafficserver/diff/f35b7e06 Branch: refs/heads/master Commit: f35b7e0609b9def5b3beab26b8c9903e28180a4d Parents: c304f2c Author: John Rushford <[email protected]> Authored: Mon Jun 8 23:50:19 2015 +0000 Committer: Sudheer Vinukonda <[email protected]> Committed: Mon Jun 8 23:50:19 2015 +0000 ---------------------------------------------------------------------- .../cache_range_requests/Makefile.am | 21 + .../experimental/cache_range_requests/README | 36 ++ .../cache_range_requests.cc | 428 +++++++++++++++++++ 3 files changed, 485 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/trafficserver/blob/f35b7e06/plugins/experimental/cache_range_requests/Makefile.am ---------------------------------------------------------------------- diff --git a/plugins/experimental/cache_range_requests/Makefile.am b/plugins/experimental/cache_range_requests/Makefile.am new file mode 100644 index 0000000..5a27cac --- /dev/null +++ b/plugins/experimental/cache_range_requests/Makefile.am @@ -0,0 +1,21 @@ +# 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. + +include $(top_srcdir)/build/plugins.mk + +pkglib_LTLIBRARIES = cache_range_requests.la +cache_range_requests_la_SOURCES = cache_range_requests.cc +cache_range_requests_la_LDFLAGS = $(TS_PLUGIN_LDFLAGS) http://git-wip-us.apache.org/repos/asf/trafficserver/blob/f35b7e06/plugins/experimental/cache_range_requests/README ---------------------------------------------------------------------- diff --git a/plugins/experimental/cache_range_requests/README b/plugins/experimental/cache_range_requests/README new file mode 100644 index 0000000..faddeb5 --- /dev/null +++ b/plugins/experimental/cache_range_requests/README @@ -0,0 +1,36 @@ + +Thousands of range requests for a very large object in the traffic server cache +are likely to increase system load averages due to I/O wait as objects are stored +on a single stripe or disk drive. + +This plugin allows you to remap individual range requests so that they are stored +as individual objects in the ATS cache when subsequent range requests are likely +to use the same range. This spreads range requests over multiple stripes thereby +reducing I/O wait and system load averages. + +This plugin reads the range request header byte range value and then creates a +new cache key url using the original request url with the range value appended +to it. The range header is removed where appropriate from the requests and the +origin server response code is changed from a 206 to a 200 to insure that the +object is written to cache using the new cache key url. The response code sent +to the client will be changed back to a 206 and all requests to the origin server +will contain the range header so that the correct response is received. + +Installation: + + make + sudo make install + +If you don't have the traffic server binaries in your path, then you will need +to specify the path to tsxs manually: + + make TSXS=/opt/trafficserver/bin/tsxs + sudo make TSXS=/opt/trafficserver/bin/tsxs install + +Configuration: + + Add @plugin=cache_range_requests.so to your remap.config rules. + + Or for a global plugin where all range requests are processed, + Add cache_range_requests.so to the plugin.config + http://git-wip-us.apache.org/repos/asf/trafficserver/blob/f35b7e06/plugins/experimental/cache_range_requests/cache_range_requests.cc ---------------------------------------------------------------------- diff --git a/plugins/experimental/cache_range_requests/cache_range_requests.cc b/plugins/experimental/cache_range_requests/cache_range_requests.cc new file mode 100644 index 0000000..0967ff0 --- /dev/null +++ b/plugins/experimental/cache_range_requests/cache_range_requests.cc @@ -0,0 +1,428 @@ +/* + * 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. + */ + +/* + * This plugin looks for range requests and then creates a new + * cache key url so that each individual range requests is written + * to the cache as a individual object so that subsequent range + * requests are read accross different disk drives reducing I/O + * wait and load averages when there are large numbers of range + * requests. + */ + +#include <stdio.h> +#include <string.h> +#include "ts/ts.h" +#include "ts/remap.h" + +#define PLUGIN_NAME "cache_range_requests" + +struct txndata { + char *range_value; + char *request_url; +}; + +static void handle_read_request_header(TSCont, TSEvent, void *); +static void range_header_check(TSHttpTxn txnp); +static void handle_send_origin_request(TSCont, TSHttpTxn, struct txndata *); +static void handle_client_send_response(TSHttpTxn); +static void handle_server_read_response(TSHttpTxn, struct txndata *); +static int remove_header(TSMBuffer, TSMLoc, const char *, int); +static bool set_header(TSMBuffer, TSMLoc, const char *, int, const char *, int); +static void transaction_handler(TSCont, TSEvent, void *); + +/** + * Entry point when used as a global plugin. + * + */ +static void +handle_read_request_header(TSCont txn_contp, TSEvent event, void *edata) +{ + TSHttpTxn txnp = static_cast<TSHttpTxn>(edata); + + TSDebug(PLUGIN_NAME, "Starting handle_read_request_header()"); + + range_header_check(txnp); + + TSDebug(PLUGIN_NAME, "End of handle_read_request_header()"); + TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); +} + +/** + * Reads the client request header and if this is a range request: + * + * 1. creates a new cache key url using the range request information. + * 2. Saves the range request information and then removes the range + * header so that the response retrieved from the origin will + * be written to cache. + * 3. Schedules TS_HTTP_SEND_REQUEST_HDR_HOOK, TS_HTTP_SEND_RESPONSE_HDR_HOOK, + * and TS_HTTP_TXN_CLOSE_HOOK for further processing. + */ +static void +range_header_check(TSHttpTxn txnp) +{ + char cache_key_url[8192] = {0}; + char *req_url; + int length, url_length; + struct txndata *txn_state; + TSMBuffer hdr_bufp; + TSMLoc req_hdrs = NULL; + TSMLoc loc = NULL; + TSCont txn_contp; + + if (TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &hdr_bufp, &req_hdrs)) { + loc = TSMimeHdrFieldFind(hdr_bufp, req_hdrs, "Range", -1); + if (TS_NULL_MLOC != loc) { + const char *hdr_value = TSMimeHdrFieldValueStringGet(hdr_bufp, req_hdrs, loc, 0, &length); + if (!hdr_value || length <= 0) { + TSDebug(PLUGIN_NAME, "range_header_check(): Not a range request."); + } else { + if (NULL == (txn_contp = TSContCreate((TSEventFunc)transaction_handler, NULL))) { + TSError("[%s] TSContCreate(): failed to create the transaction handler continuation.", PLUGIN_NAME); + } else { + txn_state = (struct txndata *)TSmalloc(sizeof(struct txndata)); + txn_state->range_value = TSstrndup(hdr_value, length); + TSDebug(PLUGIN_NAME, "length = %d, txn_state->range_value = %s", length, txn_state->range_value); + txn_state->range_value[length] = '\0'; // workaround for bug in core + + req_url = TSHttpTxnEffectiveUrlStringGet(txnp, &url_length); + snprintf(cache_key_url, 8192, "%s-%s", req_url, txn_state->range_value); + TSDebug(PLUGIN_NAME, "Rewriting cache URL for %s to %s", req_url, cache_key_url); + txn_state->request_url = (char *)TSmalloc(url_length + 1); + strncpy(txn_state->request_url, req_url, url_length); + txn_state->request_url[url_length] = 0; + + // set the cache key. + if (TS_SUCCESS != TSCacheUrlSet(txnp, cache_key_url, strlen(cache_key_url))) { + TSDebug(PLUGIN_NAME, "TSCacheUrlSet(): failed to change the cache url to %s.", cache_key_url); + } + // remove the range request header. + if (remove_header(hdr_bufp, req_hdrs, TS_MIME_FIELD_RANGE, TS_MIME_LEN_RANGE) > 0) { + TSDebug(PLUGIN_NAME, "Removed the Range: header from request"); + } + + TSContDataSet(txn_contp, txn_state); + TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_REQUEST_HDR_HOOK, txn_contp); + TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK, txn_contp); + TSHttpTxnHookAdd(txnp, TS_HTTP_TXN_CLOSE_HOOK, txn_contp); + TSDebug(PLUGIN_NAME, "Added TS_HTTP_SEND_REQUEST_HDR_HOOK, TS_HTTP_SEND_RESPONSE_HDR_HOOK, and TS_HTTP_TXN_CLOSE_HOOK"); + } + } + TSHandleMLocRelease(hdr_bufp, req_hdrs, loc); + } else { + TSDebug(PLUGIN_NAME, "range_header_check(): no range request header."); + } + TSHandleMLocRelease(hdr_bufp, req_hdrs, NULL); + } else { + TSDebug(PLUGIN_NAME, "range_header_check(): failed to retrieve the server request."); + } +} + +/** + * Restores the range request header if the request must be + * satisfied from the origin and schedules the TS_READ_RESPONSE_HDR_HOOK. + */ +static void +handle_send_origin_request(TSCont contp, TSHttpTxn txnp, struct txndata *txn_state) +{ + TSMBuffer hdr_bufp; + TSMLoc req_hdrs = NULL; + + TSDebug(PLUGIN_NAME, "Starting handle_send_origin_request()"); + if (TS_SUCCESS == TSHttpTxnServerReqGet(txnp, &hdr_bufp, &req_hdrs) && txn_state->range_value != NULL) { + if (set_header(hdr_bufp, req_hdrs, TS_MIME_FIELD_RANGE, TS_MIME_LEN_RANGE, txn_state->range_value, + strlen(txn_state->range_value))) { + TSDebug(PLUGIN_NAME, "Added range header: %s", txn_state->range_value); + TSHttpTxnHookAdd(txnp, TS_HTTP_READ_RESPONSE_HDR_HOOK, contp); + } + } + TSHandleMLocRelease(hdr_bufp, req_hdrs, NULL); + TSDebug(PLUGIN_NAME, "End of handle_send_origin_request()"); +} + +/** + * Changes the response code back to a 206 Partial content before + * replying to the client that requested a range. + */ +static void +handle_client_send_response(TSHttpTxn txnp) +{ + bool partial_content_reason = false; + char *p, *reason; + int length; + TSMBuffer response; + TSMLoc resp_hdr; + + TSDebug(PLUGIN_NAME, "Starting handle_client_send_response ()"); + + TSReturnCode result = TSHttpTxnClientRespGet(txnp, &response, &resp_hdr); + TSDebug(PLUGIN_NAME, "result %d", result); + if (TS_SUCCESS == result) { + TSHttpStatus status = TSHttpHdrStatusGet(response, resp_hdr); + // a cached result will have a TS_HTTP_OK with a 'Partial Content' reason + if ((p = (char *)TSHttpHdrReasonGet(response, resp_hdr, &length)) != NULL) { + reason = TSstrndup(p, length + 1); + reason[length] = '\0'; + if (strncasecmp(reason, "Partial Content", length) == 0) { + partial_content_reason = true; + } + } + TSDebug(PLUGIN_NAME, "status %d %s", status, reason); + if (TS_HTTP_STATUS_OK == status && partial_content_reason) { + TSDebug(PLUGIN_NAME, "handle_client_send_response (): Got TS_HTTP_STATUS_OK."); + TSHttpHdrStatusSet(response, resp_hdr, TS_HTTP_STATUS_PARTIAL_CONTENT); + TSDebug(PLUGIN_NAME, "handle_client_send_response (): Set response header to TS_HTTP_STATUS_PARTIAL_CONTENT."); + } + } + TSHandleMLocRelease(response, resp_hdr, NULL); + TSfree(reason); + TSDebug(PLUGIN_NAME, "End of handle_client_send_response ()"); +} + +/** + * After receiving a range request response from the origin, change + * the response code from a 206 Partial content to a 200 OK so that + * the response will be written to cache. + */ +static void +handle_server_read_response(TSHttpTxn txnp, struct txndata *txn_state) +{ + TSMBuffer response; + TSMLoc resp_hdr; + TSHttpStatus status; + + TSDebug(PLUGIN_NAME, "Starting handle_server_read_response ()"); + + if (TS_SUCCESS == TSHttpTxnServerRespGet(txnp, &response, &resp_hdr)) { + status = TSHttpHdrStatusGet(response, resp_hdr); + if (TS_HTTP_STATUS_PARTIAL_CONTENT == status) { + TSDebug(PLUGIN_NAME, "handle_server_read_response (): Got TS_HTTP_STATUS_PARTIAL_CONTENT."); + TSHttpHdrStatusSet(response, resp_hdr, TS_HTTP_STATUS_OK); + TSDebug(PLUGIN_NAME, "handle_server_read_response (): Set response header to TS_HTTP_STATUS_OK."); + bool cacheable = TSHttpTxnIsCacheable(txnp, NULL, response); + TSDebug(PLUGIN_NAME, "handle_server_read_response (): range is cacheable: %d", cacheable); + } else if (TS_HTTP_STATUS_OK == status) { + TSDebug(PLUGIN_NAME, "The origin does not support range requests, attempting to disable cache write."); + if (TS_SUCCESS == TSHttpTxnServerRespNoStoreSet(txnp, 1)) { + TSDebug(PLUGIN_NAME, "Cache write has been disabled for this transaction."); + } else { + TSDebug(PLUGIN_NAME, "Unable to disable cache write for this transaction."); + } + } + } + TSHandleMLocRelease(response, resp_hdr, NULL); + TSDebug(PLUGIN_NAME, "End of handle_server_read_response ()"); +} + +/** + * Remove a header (fully) from an TSMLoc / TSMBuffer. Return the number + * of fields (header values) we removed. + * + * From background_fetch.cc + */ +static int +remove_header(TSMBuffer bufp, TSMLoc hdr_loc, const char *header, int len) +{ + TSMLoc field = TSMimeHdrFieldFind(bufp, hdr_loc, header, len); + int cnt = 0; + + TSDebug(PLUGIN_NAME, "Starting remove_header ()"); + + while (field) { + TSMLoc tmp = TSMimeHdrFieldNextDup(bufp, hdr_loc, field); + + ++cnt; + TSMimeHdrFieldDestroy(bufp, hdr_loc, field); + TSHandleMLocRelease(bufp, hdr_loc, field); + field = tmp; + } + + TSDebug(PLUGIN_NAME, "End of remove_header ()"); + return cnt; +} + +/** + * Set a header to a specific value. This will avoid going to through a + * remove / add sequence in case of an existing header. + * but clean. + * + * From background_fetch.cc + */ +static bool +set_header(TSMBuffer bufp, TSMLoc hdr_loc, const char *header, int len, const char *val, int val_len) +{ + TSDebug(PLUGIN_NAME, "Starting set_header ()"); + + if (!bufp || !hdr_loc || !header || len <= 0 || !val || val_len <= 0) { + return false; + } + + TSDebug(PLUGIN_NAME, "Starting set_header(): header = %s, len = %d, val = %s, val_len = %d\n", header, len, val, val_len); + bool ret = false; + TSMLoc field_loc = TSMimeHdrFieldFind(bufp, hdr_loc, header, len); + + if (!field_loc) { + // No existing header, so create one + if (TS_SUCCESS == TSMimeHdrFieldCreateNamed(bufp, hdr_loc, header, len, &field_loc)) { + if (TS_SUCCESS == TSMimeHdrFieldValueStringSet(bufp, hdr_loc, field_loc, -1, val, val_len)) { + TSMimeHdrFieldAppend(bufp, hdr_loc, field_loc); + ret = true; + } + TSHandleMLocRelease(bufp, hdr_loc, field_loc); + } + } else { + TSMLoc tmp = NULL; + bool first = true; + + while (field_loc) { + if (first) { + first = false; + if (TS_SUCCESS == TSMimeHdrFieldValueStringSet(bufp, hdr_loc, field_loc, -1, val, val_len)) { + ret = true; + } + } else { + TSMimeHdrFieldDestroy(bufp, hdr_loc, field_loc); + } + tmp = TSMimeHdrFieldNextDup(bufp, hdr_loc, field_loc); + TSHandleMLocRelease(bufp, hdr_loc, field_loc); + field_loc = tmp; + } + } + + TSDebug(PLUGIN_NAME, "End of set_header ()"); + return ret; +} + +/** + * Remap initialization. + */ +TSReturnCode +TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size) +{ + TSDebug(PLUGIN_NAME, "cache_range_requests remap init"); + if (!api_info) { + strncpy(errbuf, "[tsremap_init] - Invalid TSRemapInterface argument", errbuf_size - 1); + return TS_ERROR; + } + + if (api_info->tsremap_version < TSREMAP_VERSION) { + snprintf(errbuf, errbuf_size - 1, "[TSRemapInit] - Incorrect API version %ld.%ld", api_info->tsremap_version >> 16, + (api_info->tsremap_version & 0xffff)); + return TS_ERROR; + } + + TSDebug(PLUGIN_NAME, "cache_range_requests remap is successfully initialized"); + return TS_SUCCESS; +} + +/** + * not used. + */ +TSReturnCode +TSRemapNewInstance(int argc, char *argv[], void **ih, char * /*errbuf */, int /* errbuf_size */) +{ + TSDebug(PLUGIN_NAME, "TSRemapNewInstance ()"); + + return TS_SUCCESS; +} + +/** + * not used. + */ +void +TSRemapDeleteInstance(void *ih) +{ + TSDebug(PLUGIN_NAME, "TSRemapDeleteInstance ()"); +} + +/** + * Remap entry point. + */ +TSRemapStatus +TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo * /* rri */) +{ + TSDebug(PLUGIN_NAME, "TSRemapDoRemap()"); + + range_header_check(txnp); + return TSREMAP_NO_REMAP; +} + +/** + * Global plugin initialization. + */ +void +TSPluginInit(int argc, const char *argv[]) +{ + TSPluginRegistrationInfo info; + TSCont txnp_cont; + + TSDebug(PLUGIN_NAME, "Starting TSPluginInit()"); + + info.plugin_name = (char *)PLUGIN_NAME; + info.vendor_name = (char *)"Comcast"; + info.support_email = (char *)"[email protected]"; + + if (TSPluginRegister(TS_SDK_VERSION_3_0, &info) != TS_SUCCESS) { + TSError("[%s] TSPluginInit(): Plugin registration failed.\n", PLUGIN_NAME); + TSError("[%s] Unable to initialize plugin (disabled).\n", PLUGIN_NAME); + return; + } + + if (NULL == (txnp_cont = TSContCreate((TSEventFunc)handle_read_request_header, NULL))) { + TSError("[%s] TSContCreate(): failed to create the transaction continuation handler.", PLUGIN_NAME); + return; + } else { + TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, txnp_cont); + } +} + +/** + * Transaction event handler. + */ +static void +transaction_handler(TSCont contp, TSEvent event, void *edata) +{ + TSHttpTxn txnp = static_cast<TSHttpTxn>(edata); + struct txndata *txn_state = (struct txndata *)TSContDataGet(contp); + + TSDebug(PLUGIN_NAME, "Starting transaction_handler()"); + switch (event) { + case TS_EVENT_HTTP_READ_RESPONSE_HDR: + handle_server_read_response(txnp, txn_state); + break; + case TS_EVENT_HTTP_SEND_REQUEST_HDR: + handle_send_origin_request(contp, txnp, txn_state); + break; + case TS_EVENT_HTTP_SEND_RESPONSE_HDR: + handle_client_send_response(txnp); + break; + case TS_EVENT_HTTP_TXN_CLOSE: + TSDebug(PLUGIN_NAME, "Starting handle_transaction_close()."); + TSfree(txn_state); + TSfree(txn_state->range_value); + TSfree(txn_state->request_url); + TSContDestroy(contp); + break; + default: + TSAssert(!"Unexpected event"); + break; + } + TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); + TSDebug(PLUGIN_NAME, "End of transaction_handler()"); +}
