Example: Query Remap Plugin

The sample remap plugin, query_remap.c, maps client requests to a number of servers based on a hash of the request’s URL query parameter. This can be useful for spreading load for a given type of request among backend servers, while still maintaining “stickiness” to a single server for similar requests. For example, a search engine may want to send repeated queries for the same keywords to a server that has likely cached the result from a prior query.

Configuration of query_remap

The query remap plugin will allow the query parameter name to be specified, along with the hostnames of the servers to hash across. Sample remap.config rules using query_remap will look like:

map http://www.example.com/search http://srch1.example.com/search @plugin=query_remap.so @pparam=q @pparam=srch1.example.com @pparam=srch2.example.com @pparam=srch3.example.com
map http://www.example.com/profiles http://prof1.example.com/profiles @plugin=query_remap.so @pparam=user_id @pparam=prof1.example.com @pparam=prof2.example.com

The first @pparam specifies the query param key for which the value will be hashed. The remaining parameters list the hostnames of the servers. A request for http://www.example.com/search?q=apache will match the first rule. The plugin will look for the “q“ parameter and hash the value ‘apache’ to pick from among srch_[1-3]_.example.com to send the request.

If the request does not include a “q“ query parameter and the plugin decides not to modify the request, the default toURL ‘http://srch1.example.com/search’ will be used by TS.

The parameters are passed to the plugin’s tsremap_new_instance function. In query_remap, tsremap_new_instance creates a plugin-defined query_remap_info struct to store its configuration parameters.

typedef struct _query_remap_info {
  char *param_name;
  size_t param_len;
  char **hosts;
  int num_hosts;
} query_remap_info;

The ihandle, an opaque pointer that can be used to pass per-instance data, is set to this struct pointer and will be passed to the TSRemapDoRemap function when it is triggered for a request.

TSRemapNewInstance(int argc, char *argv[], void **ih, char *errbuf ATS_UNUSED, int errbuf_size ATS_UNUSED)
{
  /* Called for each remap rule using this plugin. The parameters are parsed here */
  int i;
  TSDebug(PLUGIN_NAME, "new instance fromURL: %s toURL: %s", argv[0], argv[1]);

  if (argc < 4) {
    TSError("[%s] Missing parameters", PLUGIN_NAME);
    return -1;
  }

  /* initialize the struct to store info about this remap instance
     the argv parameters are:
       0: fromURL
       1: toURL
       2: query param to hash
       3,4,... : server hostnames
  */
  query_remap_info *qri = (query_remap_info *)TSmalloc(sizeof(query_remap_info));

  qri->param_name = TSstrdup(argv[2]);
  qri->param_len  = strlen(qri->param_name);
  qri->num_hosts  = argc - 3;
  qri->hosts      = (char **)TSmalloc(qri->num_hosts * sizeof(char *));

  TSDebug(PLUGIN_NAME, " - Hash using query parameter [%s] with %d hosts", qri->param_name, qri->num_hosts);

  for (i = 0; i < qri->num_hosts; ++i) {
    qri->hosts[i] = TSstrdup(argv[i + 3]);
    TSDebug(PLUGIN_NAME, " - Host %d: %s", i, qri->hosts[i]);
  }

  *ih = (void *)qri;
  TSDebug(PLUGIN_NAME, "created instance %p", *ih);
  return 0;
}

Another way remap plugins may want handle more complex configuration is to specify a configuration filename as a pparam and parse the specified file during instance initialization.

Performing the Remap

The plugin implements the tsremap_remap function, which is called when TS has read the client HTTP request headers and matched the request to a remap rule configured for the plugin. The TSRemapRequestInfo struct contains input and output members for the remap operation.

tsremap_remap uses the configuration information passed via the ihandle and checks the request_query for the configured query parameter. If the parameter is found, the plugin sets a new_host to modify the request host:

TSRemapStatus
TSRemapDoRemap(void *ih, TSHttpTxn rh ATS_UNUSED, TSRemapRequestInfo *rri)
{
  int hostidx           = -1;
  query_remap_info *qri = (query_remap_info *)ih;

  if (!qri || !rri) {
    TSError("[%s] NULL private data or RRI", PLUGIN_NAME);
    return TSREMAP_NO_REMAP;
  }

  int req_query_len;
  const char *req_query = TSUrlHttpQueryGet(rri->requestBufp, rri->requestUrl, &req_query_len);

  if (req_query && req_query_len > 0) {
    char *q, *key;
    char *s = NULL;

    /* make a copy of the query, as it is read only */
    q = (char *)TSstrndup(req_query, req_query_len + 1);

    /* parse query parameters */
    for (key = strtok_r(q, "&", &s); key != NULL;) {
      char *val = strchr(key, '=');
      if (val && (size_t)(val - key) == qri->param_len && !strncmp(key, qri->param_name, qri->param_len)) {
        ++val;
        /* the param key matched the configured param_name
           hash the param value to pick a host */
        hostidx = hash_fnv32(val, strlen(val)) % (uint32_t)qri->num_hosts;
        TSDebug(PLUGIN_NAME, "modifying host based on %s", key);
        break;
      }
      key = strtok_r(NULL, "&", &s);
    }

    TSfree(q);

    if (hostidx >= 0) {
      int req_host_len;
      /* TODO: Perhaps use TSIsDebugTagSet() before calling TSUrlHostGet()... */
      const char *req_host = TSUrlHostGet(rri->requestBufp, rri->requestUrl, &req_host_len);

      if (TSUrlHostSet(rri->requestBufp, rri->requestUrl, qri->hosts[hostidx], strlen(qri->hosts[hostidx])) != TS_SUCCESS) {
        TSDebug(PLUGIN_NAME, "Failed to modify the Host in request URL");
        return TSREMAP_NO_REMAP;
      }
      TSDebug(PLUGIN_NAME, "host changed from [%.*s] to [%s]", req_host_len, req_host, qri->hosts[hostidx]);
      return TSREMAP_DID_REMAP; /* host has been modified */
    }
  }

  /* the request was not modified, TS will use the toURL from the remap rule */
  TSDebug(PLUGIN_NAME, "request not modified");
  return TSREMAP_NO_REMAP;
}