You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by zw...@apache.org on 2021/04/09 22:39:40 UTC

[trafficserver] branch 9.1.x updated: New rate_limit plugin for simple resource limitations (#7623)

This is an automated email from the ASF dual-hosted git repository.

zwoop pushed a commit to branch 9.1.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/9.1.x by this push:
     new d540ecc  New rate_limit plugin for simple resource limitations (#7623)
d540ecc is described below

commit d540eccd5421d935daf6023da4df6ec4e4534127
Author: Leif Hedstrom <zw...@apache.org>
AuthorDate: Fri Apr 9 16:39:15 2021 -0600

    New rate_limit plugin for simple resource limitations (#7623)
    
    * New rate_limit plugin for simple resource limitations
    
    This current version has only one limiter, which implements
    a basic active_in limitation. However, at least for now,
    these connections that are queued will still count against
    the active connection metrics and limits that are in the core.
    
    I'm open for a refactoring of this, if or when we want to have
    different types of limiters. Similar to the policies for the
    cache_promote plugin.
    
    (cherry picked from commit a8e98d6f56dfa10d243364425b6cc75ccfff63fa)
---
 doc/admin-guide/plugins/index.en.rst          |   4 +
 doc/admin-guide/plugins/rate_limit.en.rst     | 115 ++++++++++++++++
 plugins/Makefile.am                           |   1 +
 plugins/experimental/rate_limit/Makefile.inc  |  21 +++
 plugins/experimental/rate_limit/limiter.cc    | 157 ++++++++++++++++++++++
 plugins/experimental/rate_limit/limiter.h     | 182 ++++++++++++++++++++++++++
 plugins/experimental/rate_limit/rate_limit.cc | 137 +++++++++++++++++++
 7 files changed, 617 insertions(+)

diff --git a/doc/admin-guide/plugins/index.en.rst b/doc/admin-guide/plugins/index.en.rst
index 6f73b82..c9037c9 100644
--- a/doc/admin-guide/plugins/index.en.rst
+++ b/doc/admin-guide/plugins/index.en.rst
@@ -164,6 +164,7 @@ directory of the |TS| source tree. Experimental plugins can be compiled by passi
    MP4 <mp4.en>
    Multiplexer <multiplexer.en>
    MySQL Remap <mysql_remap.en>
+   Rate Limit <rate_limit.en>
    Signed URLs <url_sig.en>
    Slice <slice.en>
    SSL Headers <sslheaders.en>
@@ -228,6 +229,9 @@ directory of the |TS| source tree. Experimental plugins can be compiled by passi
 :doc:`Prefetch <prefetch.en>`
    Pre-fetch objects based on the requested URL path pattern.
 
+:doc:`Rate Limit <rate_limit.en>`
+   Simple transaction rate limiting.
+
 :doc:`Remap Purge <remap_purge.en>`
    This remap plugin allows the administrator to easily setup remotely
    controlled ``PURGE`` for the content of an entire remap rule.
diff --git a/doc/admin-guide/plugins/rate_limit.en.rst b/doc/admin-guide/plugins/rate_limit.en.rst
new file mode 100644
index 0000000..aea3b46
--- /dev/null
+++ b/doc/admin-guide/plugins/rate_limit.en.rst
@@ -0,0 +1,115 @@
+.. 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.
+
+.. _admin-plugins-rate-limit:
+
+Rate Limit Plugin
+********************
+
+The :program:`rate_limit` plugin provides basic mechanism for how much
+traffic a particular service (remap rule) is allowed to do. Currently,
+the only implementation is a limit on how many active client transactions
+a service can have. However, it would be easy to refactor this plugin to
+allow for adding new limiter policies later on.
+
+The limit counters and queues are per remap rule only, i.e. there is
+(currently) no way to group transaction limits from different remap rules
+into a single rate limiter.
+
+All configuration is done via :file:`remap.config`, and the following options
+are available:
+
+.. program:: rate-limit
+
+.. option:: --limit
+
+   The maximum number of active client transactions.
+
+.. option:: --queue
+
+   When the limit (above) has been reached, all new transactions are placed
+   on a FIFO queue. This option (optional) sets an upper bound on how many
+   queued transactions we will allow. When this threshold is reached, all
+   additional transactions are immediately served with an error message.
+
+   The queue is effectively disabled if this is set to `0`, which implies
+   that when the transaction limit is reached, we immediately start serving
+   error responses.
+
+   The default queue size is `UINT_MAX`, which is essentially unlimited.
+
+.. option:: --error
+
+   An optional HTTP status error code, to be used together with the
+   :option:`--queue` option above. The default is `429`.
+
+.. option:: --retry
+
+   An optional retry-after value, which if set will cause rejected (e.g. `429`)
+   responses to also include a header `Retry-After`.
+
+.. option:: --header
+
+   This is an optional HTTP header name, which will be added to the client
+   request header IF the transaction was delayed (queued). The value of the
+   header is the delay, in milliseconds. This can be useful to for example
+   log the delays for later analysis.
+
+   It is recommended that an `@` header is used here, e.g. `@RateLimit-Delay`,
+   since this header will not leave the ATS server instance.
+
+.. option:: --maxage
+
+   An optional `max-age` for how long a transaction can sit in the delay queue.
+   The value (default 0) is the age in milliseconds.
+
+Examples
+--------
+
+This example shows a simple rate limiting of `128` concurrently active client
+transactions, with a maximum queue size of `256`. The default of HTTP status
+code `429` is used when queue is full.
+
+    map http://cdn.example.com/ http://some-server.example.com \
+      @plugin=rate_limit.so @pparam=--limit=128 @pparam=--queue=256
+
+
+This example would put a hard transaction (in) limit to 256, with no backoff
+queue, and add a header with the transaction delay if it was queued:
+
+    map http://cdn.example.com/ http://some-server.example.com \
+      @plugin=rate_limit.so @pparam=--limit=256 @pparam=--queue=0 \
+      @pparam=--header=@RateLimit-Delay
+
+This final example will limit the active transaction, queue size, and also
+add a `Retry-After` header once the queue is full and we return a `429` error:
+
+    map http://cdn.example.com/ http://some-server.example.com \
+      @plugin=rate_limit.so @pparam=--limit=256 @pparam=--queue=1024 \
+      @pparam=--retry=3600 @pparam=--header=@RateLimit-Delay
+
+In this case, the response would look like this when the queue is full:
+
+    HTTP/1.1 429 Too Many Requests
+    Date: Fri, 26 Mar 2021 22:42:38 GMT
+    Connection: keep-alive
+    Server: ATS/10.0.0
+    Cache-Control: no-store
+    Content-Type: text/html
+    Content-Language: en
+    Retry-After: 3600
+    Content-Length: 207
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index 52ce5d8..785e495 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -79,6 +79,7 @@ include experimental/memory_profile/Makefile.inc
 include experimental/metalink/Makefile.inc
 include experimental/money_trace/Makefile.inc
 include experimental/mp4/Makefile.inc
+include experimental/rate_limit/Makefile.inc
 include experimental/redo_cache_lookup/Makefile.inc
 include experimental/remap_stats/Makefile.inc
 include experimental/slice/Makefile.inc
diff --git a/plugins/experimental/rate_limit/Makefile.inc b/plugins/experimental/rate_limit/Makefile.inc
new file mode 100644
index 0000000..250ce13
--- /dev/null
+++ b/plugins/experimental/rate_limit/Makefile.inc
@@ -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.
+
+pkglib_LTLIBRARIES += experimental/rate_limit/rate_limit.la
+
+experimental_rate_limit_rate_limit_la_SOURCES = \
+  experimental/rate_limit/rate_limit.cc \
+  experimental/rate_limit/limiter.cc
diff --git a/plugins/experimental/rate_limit/limiter.cc b/plugins/experimental/rate_limit/limiter.cc
new file mode 100644
index 0000000..5960e97
--- /dev/null
+++ b/plugins/experimental/rate_limit/limiter.cc
@@ -0,0 +1,157 @@
+/*
+ * 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 "limiter.h"
+
+///////////////////////////////////////////////////////////////////////////////
+// This is the continuation that gets scheduled periodically to process the
+// deque of waiting TXNs.
+//
+int
+RateLimiter::queue_process_cont(TSCont cont, TSEvent event, void *edata)
+{
+  RateLimiter *limiter = static_cast<RateLimiter *>(TSContDataGet(cont));
+  QueueTime now        = std::chrono::system_clock::now(); // Only do this once per "loop"
+
+  // Try to enable some queued txns (if any) if there are slots available
+  while (limiter->size() > 0 && limiter->reserve()) {
+    auto [txnp, contp, start_time]  = limiter->pop();
+    std::chrono::microseconds delay = std::chrono::duration_cast<std::chrono::milliseconds>(now - start_time);
+
+    limiter->delayHeader(txnp, delay);
+    TSDebug(PLUGIN_NAME, "Enabling queued txn after %ldms", static_cast<long>(delay.count()));
+    // Since this was a delayed transaction, we need to add the TXN_CLOSE hook to free the slot when done
+    TSHttpTxnHookAdd(txnp, TS_HTTP_TXN_CLOSE_HOOK, contp);
+    TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+  }
+
+  // Kill any queued txns if they are too old
+  if (limiter->max_age > std::chrono::milliseconds::zero() && limiter->size() > 0) {
+    now = std::chrono::system_clock::now(); // Update the "now", for some extra accuracy
+
+    while (limiter->size() > 0 && limiter->hasOldTxn(now)) {
+      // The oldest object on the queue is too old on the queue, so "kill" it.
+      auto [txnp, contp, start_time] = limiter->pop();
+      std::chrono::milliseconds age  = std::chrono::duration_cast<std::chrono::milliseconds>(now - start_time);
+
+      limiter->delayHeader(txnp, age);
+      TSDebug(PLUGIN_NAME, "Queued TXN is too old (%ldms), erroring out", static_cast<long>(age.count()));
+      TSHttpTxnStatusSet(txnp, static_cast<TSHttpStatus>(limiter->error));
+      TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK, contp);
+      TSHttpTxnReenable(txnp, TS_EVENT_HTTP_ERROR);
+    }
+  }
+
+  return TS_EVENT_NONE;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// The main rate limiting continuation.
+//
+int
+RateLimiter::rate_limit_cont(TSCont cont, TSEvent event, void *edata)
+{
+  RateLimiter *limiter = static_cast<RateLimiter *>(TSContDataGet(cont));
+
+  switch (event) {
+  case TS_EVENT_HTTP_TXN_CLOSE:
+    limiter->release();
+    TSContDestroy(cont); // We are done with this continuation now
+    TSHttpTxnReenable(static_cast<TSHttpTxn>(edata), TS_EVENT_HTTP_CONTINUE);
+    return TS_EVENT_CONTINUE;
+    break;
+
+  case TS_EVENT_HTTP_POST_REMAP:
+    limiter->push(static_cast<TSHttpTxn>(edata), cont);
+    return TS_EVENT_NONE;
+    break;
+
+  case TS_EVENT_HTTP_SEND_RESPONSE_HDR: // This is only applicable when we set an error in remap
+    limiter->retryAfter(static_cast<TSHttpTxn>(edata), limiter->retry);
+    TSContDestroy(cont); // We are done with this continuation now
+    TSHttpTxnReenable(static_cast<TSHttpTxn>(edata), TS_EVENT_HTTP_CONTINUE);
+    return TS_EVENT_CONTINUE;
+    break;
+
+  default:
+    TSDebug(PLUGIN_NAME, "Unknown event %d", static_cast<int>(event));
+    TSError("Unknown event in %s", PLUGIN_NAME);
+    break;
+  }
+  return TS_EVENT_NONE;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Setup the continuous queue processing continuation
+//
+void
+RateLimiter::setupQueueCont()
+{
+  _queue_cont = TSContCreate(queue_process_cont, TSMutexCreate());
+  TSReleaseAssert(_queue_cont);
+  TSContDataSet(_queue_cont, this);
+  _action = TSContScheduleEveryOnPool(_queue_cont, QUEUE_DELAY_TIME.count(), TS_THREAD_POOL_TASK);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Add a header with the delay imposed on this transaction. This can be used
+// for logging, and other types of metrics.
+//
+void
+RateLimiter::delayHeader(TSHttpTxn txnp, std::chrono::microseconds delay) const
+{
+  if (header.size() > 0) {
+    TSMLoc hdr_loc   = nullptr;
+    TSMBuffer bufp   = nullptr;
+    TSMLoc field_loc = nullptr;
+
+    if (TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc)) {
+      if (TS_SUCCESS == TSMimeHdrFieldCreateNamed(bufp, hdr_loc, header.c_str(), header.size(), &field_loc)) {
+        if (TS_SUCCESS == TSMimeHdrFieldValueIntSet(bufp, hdr_loc, field_loc, -1, static_cast<int>(delay.count()))) {
+          TSMimeHdrFieldAppend(bufp, hdr_loc, field_loc);
+        }
+        TSHandleMLocRelease(bufp, hdr_loc, field_loc);
+      }
+      TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
+    }
+  }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Add a header with the delay imposed on this transaction. This can be used
+// for logging, and other types of metrics.
+//
+void
+RateLimiter::retryAfter(TSHttpTxn txnp, unsigned retry) const
+{
+  if (retry > 0) {
+    TSMLoc hdr_loc   = nullptr;
+    TSMBuffer bufp   = nullptr;
+    TSMLoc field_loc = nullptr;
+
+    if (TS_SUCCESS == TSHttpTxnClientRespGet(txnp, &bufp, &hdr_loc)) {
+      if (TS_SUCCESS == TSMimeHdrFieldCreateNamed(bufp, hdr_loc, "Retry-After", 11, &field_loc)) {
+        if (TS_SUCCESS == TSMimeHdrFieldValueIntSet(bufp, hdr_loc, field_loc, -1, retry)) {
+          TSDebug(PLUGIN_NAME, "Added a Retry-After: %u", retry);
+          TSMimeHdrFieldAppend(bufp, hdr_loc, field_loc);
+        }
+        TSHandleMLocRelease(bufp, hdr_loc, field_loc);
+      }
+      TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
+    }
+  }
+}
diff --git a/plugins/experimental/rate_limit/limiter.h b/plugins/experimental/rate_limit/limiter.h
new file mode 100644
index 0000000..947f6a9
--- /dev/null
+++ b/plugins/experimental/rate_limit/limiter.h
@@ -0,0 +1,182 @@
+/*
+ * 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 <deque>
+#include <tuple>
+#include <climits>
+#include <atomic>
+#include <cstdio>
+#include <chrono>
+#include <cstring>
+#include <string>
+
+#include <ts/ts.h>
+
+constexpr char const PLUGIN_NAME[] = "rate_limit";
+constexpr auto QUEUE_DELAY_TIME    = std::chrono::milliseconds{100}; // Examine the queue every 100ms
+
+using QueueTime = std::chrono::time_point<std::chrono::system_clock>;
+using QueueItem = std::tuple<TSHttpTxn, TSCont, QueueTime>;
+
+///////////////////////////////////////////////////////////////////////////////
+// Configuration object for a rate limiting remap rule.
+//
+class RateLimiter
+{
+public:
+  RateLimiter() : _queue_lock(TSMutexCreate()), _active_lock(TSMutexCreate()) {}
+
+  ~RateLimiter()
+  {
+    if (_action) {
+      TSActionCancel(_action);
+    }
+    if (_queue_cont) {
+      TSContDestroy(_queue_cont);
+    }
+    TSMutexDestroy(_queue_lock);
+    TSMutexDestroy(_active_lock);
+  }
+
+  // Reserve / release a slot from the active connect limits. Reserve will return
+  // false if we are unable to reserve a slot.
+  bool
+  reserve()
+  {
+    TSReleaseAssert(_active <= limit);
+    TSMutexLock(_active_lock);
+    if (_active < limit) {
+      ++_active;
+      TSMutexUnlock(_active_lock); // Reduce the critical section, release early
+      TSDebug(PLUGIN_NAME, "Reserving a slot, active txns == %u", active());
+      return true;
+    } else {
+      TSMutexUnlock(_active_lock);
+      return false;
+    }
+  }
+
+  void
+  release()
+  {
+    TSMutexLock(_active_lock);
+    --_active;
+    TSMutexUnlock(_active_lock);
+    TSDebug(PLUGIN_NAME, "Releasing a slot, active txns == %u", active());
+  }
+
+  // Current size of the active_in connections
+  unsigned
+  active() const
+  {
+    return _active.load();
+  }
+
+  // Current size of the queue
+  unsigned
+  size() const
+  {
+    return _size.load();
+  }
+
+  // Is the queue full (at it's max size)?
+  bool
+  full() const
+  {
+    return (_size == max_queue);
+  }
+
+  void
+  push(TSHttpTxn txnp, TSCont cont)
+  {
+    QueueTime now = std::chrono::system_clock::now();
+
+    TSMutexLock(_queue_lock);
+    _queue.push_front(std::make_tuple(txnp, cont, now));
+    ++_size;
+    TSMutexUnlock(_queue_lock);
+  }
+
+  QueueItem
+  pop()
+  {
+    QueueItem item;
+
+    TSMutexLock(_queue_lock);
+    if (!_queue.empty()) {
+      item = std::move(_queue.back());
+      _queue.pop_back();
+      --_size;
+    }
+    TSMutexUnlock(_queue_lock);
+
+    return item;
+  }
+
+  bool
+  hasOldTxn(QueueTime now) const
+  {
+    TSMutexLock(_queue_lock);
+    if (!_queue.empty()) {
+      QueueItem item = _queue.back();
+      TSMutexUnlock(_queue_lock); // A little ugly but this reduces the critical section for the lock a little bit.
+
+      std::chrono::milliseconds age = std::chrono::duration_cast<std::chrono::milliseconds>(now - std::get<2>(item));
+
+      return (age >= max_age);
+    } else {
+      TSMutexUnlock(_queue_lock);
+      return false;
+    }
+  }
+
+  void delayHeader(TSHttpTxn txpn, std::chrono::microseconds delay) const;
+  void retryAfter(TSHttpTxn txpn, unsigned after) const;
+
+  // Continuation creation and scheduling
+  void setupQueueCont();
+
+  void
+  setupTxnCont(void *ih, TSHttpTxn txnp, TSHttpHookID hook)
+  {
+    TSCont cont = TSContCreate(rate_limit_cont, nullptr);
+    TSReleaseAssert(cont);
+
+    TSContDataSet(cont, ih);
+    TSHttpTxnHookAdd(txnp, hook, cont);
+  }
+
+  // These are the configurable portions of this limiter, public so sue me.
+  unsigned limit                    = 100;      // Arbitrary default, probably should be a required config
+  unsigned max_queue                = UINT_MAX; // No queue limit, but if sets will give an immediate error if at max
+  unsigned error                    = 429;      // Error code when we decide not to allow a txn to be processed (e.g. queue full)
+  unsigned retry                    = 0;        // If > 0, we will also send a Retry-After: header with this retry value
+  std::chrono::milliseconds max_age = std::chrono::milliseconds::zero(); // Max age (ms) in the queue
+  std::string header; // Header to put the latency metrics in, e.g. @RateLimit-Delay
+
+private:
+  static int queue_process_cont(TSCont cont, TSEvent event, void *edata);
+  static int rate_limit_cont(TSCont cont, TSEvent event, void *edata);
+
+  std::atomic<unsigned> _active = 0; // Current active number of txns. This has to always stay <= limit above
+  std::atomic<unsigned> _size   = 0; // Current size of the pending queue of txns. This should aim to be < _max_queue
+
+  TSMutex _queue_lock, _active_lock; // Resource locks
+  std::deque<QueueItem> _queue;      // Queue for the pending TXN's
+  TSCont _queue_cont = nullptr;      // Continuation processing the queue periodically
+  TSAction _action   = nullptr;      // The action associated with the queue continuation, needed to shut it down
+};
diff --git a/plugins/experimental/rate_limit/rate_limit.cc b/plugins/experimental/rate_limit/rate_limit.cc
new file mode 100644
index 0000000..37d8c65
--- /dev/null
+++ b/plugins/experimental/rate_limit/rate_limit.cc
@@ -0,0 +1,137 @@
+/*
+ * 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 <unistd.h>
+#include <getopt.h>
+#include <stdlib.h>
+#include <ts/ts.h>
+#include <ts/remap.h>
+
+#include "limiter.h"
+
+///////////////////////////////////////////////////////////////////////////////
+// Setup stuff for the remap plugin
+//
+TSReturnCode
+TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
+{
+  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, "plugin is successfully initialized");
+  return TS_SUCCESS;
+}
+
+void
+TSRemapDeleteInstance(void *ih)
+{
+  delete static_cast<RateLimiter *>(ih);
+}
+
+TSReturnCode
+TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSED */, int /* errbuf_size ATS_UNUSED */)
+{
+  static const struct option longopt[] = {
+    {const_cast<char *>("limit"), required_argument, nullptr, 'l'},
+    {const_cast<char *>("queue"), required_argument, nullptr, 'q'},
+    {const_cast<char *>("error"), required_argument, nullptr, 'e'},
+    {const_cast<char *>("retry"), required_argument, nullptr, 'r'},
+    {const_cast<char *>("header"), required_argument, nullptr, 'h'},
+    {const_cast<char *>("maxage"), required_argument, nullptr, 'm'},
+    // EOF
+    {nullptr, no_argument, nullptr, '\0'},
+  };
+
+  RateLimiter *limiter = new RateLimiter();
+  TSReleaseAssert(limiter);
+  // argv contains the "to" and "from" URLs. Skip the first so that the
+  // second one poses as the program name.
+  --argc;
+  ++argv;
+
+  while (true) {
+    int opt = getopt_long(argc, (char *const *)argv, "", longopt, nullptr);
+
+    switch (opt) {
+    case 'l':
+      limiter->limit = strtol(optarg, nullptr, 10);
+      break;
+    case 'q':
+      limiter->max_queue = strtol(optarg, nullptr, 10);
+      break;
+    case 'e':
+      limiter->error = strtol(optarg, nullptr, 10);
+      break;
+    case 'r':
+      limiter->retry = strtol(optarg, nullptr, 10);
+      break;
+    case 'm':
+      limiter->max_age = std::chrono::milliseconds(strtol(optarg, nullptr, 10));
+      break;
+    case 'h':
+      limiter->header = optarg;
+      break;
+    }
+    if (opt == -1) {
+      break;
+    }
+  }
+
+  limiter->setupQueueCont();
+
+  TSDebug(PLUGIN_NAME, "Added active_in limiter rule (limit=%u, queue=%u, error=%u", limiter->limit, limiter->max_queue,
+          limiter->error);
+  *ih = static_cast<void *>(limiter);
+
+  return TS_SUCCESS;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// This is the main "entry" point for the plugin, called for every request.
+//
+TSRemapStatus
+TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri)
+{
+  RateLimiter *limiter = static_cast<RateLimiter *>(ih);
+
+  if (limiter) {
+    if (!limiter->reserve()) {
+      if (!limiter->max_queue || limiter->full()) {
+        // We are running at limit, and the queue has reached max capacity, give back an error and be done.
+        TSHttpTxnStatusSet(txnp, static_cast<TSHttpStatus>(limiter->error));
+        limiter->setupTxnCont(ih, txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK);
+        TSDebug(PLUGIN_NAME, "Rejecting request, we're at capacity and queue is full");
+      } else {
+        limiter->setupTxnCont(ih, txnp, TS_HTTP_POST_REMAP_HOOK);
+        TSDebug(PLUGIN_NAME, "Adding rate limiting hook, we are at capacity");
+      }
+    } else {
+      limiter->setupTxnCont(ih, txnp, TS_HTTP_TXN_CLOSE_HOOK);
+      TSDebug(PLUGIN_NAME, "Adding txn-close hook, we're not at capacity");
+    }
+  }
+
+  return TSREMAP_NO_REMAP;
+}