| /* |
| * 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 <cstdlib> |
| |
| #include "txn_limiter.h" |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // These continuations are "helpers" to the TXN limiter object. Putting them |
| // outside the class implementation is just cleaner. |
| // |
| static int |
| txn_limit_cont(TSCont cont, TSEvent event, void *edata) |
| { |
| auto *limiter = static_cast<TxnRateLimiter *>(TSContDataGet(cont)); |
| |
| switch (event) { |
| case TS_EVENT_HTTP_TXN_CLOSE: |
| limiter->free(); |
| 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_SSN_CLOSE: |
| limiter->free(); |
| TSContDestroy(cont); // We are done with this continuation now |
| TSHttpSsnReenable(static_cast<TSHttpSsn>(edata), TS_EVENT_HTTP_CONTINUE); |
| return TS_EVENT_NONE; |
| break; |
| |
| case TS_EVENT_HTTP_POST_REMAP: |
| limiter->push(static_cast<TSHttpTxn>(edata), cont); |
| limiter->incrementMetric(RATE_LIMITER_METRIC_QUEUED); |
| return TS_EVENT_NONE; |
| break; |
| |
| case TS_EVENT_HTTP_SEND_RESPONSE_HDR: // This is only applicable when we set an error in remap |
| 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); |
| limiter->incrementMetric(RATE_LIMITER_METRIC_REJECTED); |
| return TS_EVENT_CONTINUE; |
| break; |
| |
| default: |
| Dbg(dbg_ctl, "Unknown event %d", static_cast<int>(event)); |
| TSError("Unknown event in %s", PLUGIN_NAME); |
| break; |
| } |
| return TS_EVENT_NONE; |
| } |
| |
| static int |
| txn_queue_cont(TSCont cont, TSEvent /* event ATS_UNUSED */, void * /* edata ATS_UNUSED */) |
| { |
| auto *limiter = static_cast<TxnRateLimiter *>(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::milliseconds delay = std::chrono::duration_cast<std::chrono::milliseconds>(now - start_time); |
| |
| delayHeader(txnp, limiter->header(), delay); |
| Dbg(dbg_ctl, "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); |
| limiter->incrementMetric(RATE_LIMITER_METRIC_RESUMED); |
| TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); |
| } |
| |
| // Kill any queued txns if they are too old |
| if (limiter->size() > 0 && limiter->max_age() > std::chrono::milliseconds::zero()) { |
| now = std::chrono::system_clock::now(); // Update the "now", for some extra accuracy |
| |
| while (limiter->size() > 0 && limiter->hasOldEntity(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); |
| |
| delayHeader(txnp, limiter->header(), age); |
| Dbg(dbg_ctl, "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); |
| limiter->incrementMetric(RATE_LIMITER_METRIC_EXPIRED); |
| TSHttpTxnReenable(txnp, TS_EVENT_HTTP_ERROR); |
| } |
| } |
| |
| return TS_EVENT_NONE; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Parse the configurations for the TXN limiter. |
| // |
| bool |
| TxnRateLimiter::initialize(int argc, const char *argv[]) |
| { |
| 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' }, |
| {const_cast<char *>("prefix"), required_argument, nullptr, 'p' }, |
| {const_cast<char *>("tag"), required_argument, nullptr, 't' }, |
| {const_cast<char *>("conntrack"), no_argument, nullptr, 'c' }, |
| // EOF |
| {nullptr, no_argument, nullptr, '\0'}, |
| }; |
| optind = 1; |
| std::string prefix = RATE_LIMITER_METRIC_PREFIX; |
| std::string tag = ""; |
| |
| while (true) { |
| int opt = getopt_long(argc, const_cast<char *const *>(argv), "", longopt, nullptr); |
| |
| switch (opt) { |
| case 'l': |
| this->_limit = strtol(optarg, nullptr, 10); |
| break; |
| case 'q': |
| this->_max_queue = strtol(optarg, nullptr, 10); |
| break; |
| case 'e': |
| this->_error = strtol(optarg, nullptr, 10); |
| break; |
| case 'r': |
| this->_retry = strtol(optarg, nullptr, 10); |
| break; |
| case 'm': |
| this->_max_age = std::chrono::milliseconds(strtol(optarg, nullptr, 10)); |
| break; |
| case 'h': |
| this->_header = optarg; |
| break; |
| case 'p': |
| prefix = optarg; |
| break; |
| case 't': |
| tag = optarg; |
| break; |
| case 'c': |
| this->_conntrack = true; |
| break; |
| } |
| if (opt == -1) { |
| break; |
| } |
| } |
| |
| if (this->max_queue() > 0) { |
| _queue_cont = TSContCreate(txn_queue_cont, TSMutexCreate()); |
| TSReleaseAssert(_queue_cont); |
| TSContDataSet(_queue_cont, this); |
| _action = TSContScheduleEveryOnPool(_queue_cont, QUEUE_DELAY_TIME.count(), TS_THREAD_POOL_TASK); |
| } |
| |
| this->initializeMetrics(RATE_LIMITER_TYPE_REMAP, tag, prefix); |
| |
| return true; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Sets up a transaction based continuation for this transaction. |
| // |
| void |
| TxnRateLimiter::setupTxnCont(TSHttpTxn txnp, TSHttpHookID hook) |
| { |
| TSCont cont = TSContCreate(txn_limit_cont, nullptr); |
| TSReleaseAssert(cont); |
| |
| TSContDataSet(cont, this); |
| TSHttpTxnHookAdd(txnp, hook, cont); |
| } |
| |
| // This only needs the TS_HTTP_SSN_CLOSE_HOOK, for now at least, so not passed as argument. |
| void |
| TxnRateLimiter::setupSsnCont(TSHttpSsn ssnp) |
| { |
| TSCont cont = TSContCreate(txn_limit_cont, nullptr); |
| TSReleaseAssert(cont); |
| |
| TSContDataSet(cont, this); |
| TSHttpSsnHookAdd(ssnp, TS_HTTP_SSN_CLOSE_HOOK, cont); |
| } |