| /* |
| * 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. |
| */ |
| #pragma once |
| |
| #include <deque> |
| #include <tuple> |
| #include <atomic> |
| #include <chrono> |
| #include <string> |
| #include <climits> |
| #include <mutex> |
| |
| #include "tscore/ink_config.h" |
| #include "ts/ts.h" |
| #include <yaml-cpp/yaml.h> |
| #include "utilities.h" |
| |
| constexpr auto QUEUE_DELAY_TIME = std::chrono::milliseconds{300}; // Examine the queue every 300ms |
| using QueueTime = std::chrono::time_point<std::chrono::system_clock>; |
| |
| enum { RATE_LIMITER_TYPE_SNI = 0, RATE_LIMITER_TYPE_REMAP, RATE_LIMITER_TYPE_MAX }; |
| |
| // order must align with the above |
| static const char *types[] = { |
| "sni", |
| "remap", |
| }; |
| |
| // no metric for requests we accept; accepted requests should be counted under their usual metrics |
| enum { |
| RATE_LIMITER_METRIC_QUEUED = 0, |
| RATE_LIMITER_METRIC_REJECTED, |
| RATE_LIMITER_METRIC_EXPIRED, |
| RATE_LIMITER_METRIC_RESUMED, |
| |
| RATE_LIMITER_METRIC_MAX |
| }; |
| |
| // order must align with the above |
| static const char *suffixes[] = { |
| "queued", |
| "rejected", |
| "expired", |
| "resumed", |
| }; |
| |
| static const char *RATE_LIMITER_METRIC_PREFIX = "plugin.rate_limiter"; |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Base class for all limiters |
| // |
| template <class T> class RateLimiter |
| { |
| using QueueItem = std::tuple<T, TSCont, QueueTime>; |
| using self_type = RateLimiter; |
| |
| public: |
| RateLimiter() = default; |
| RateLimiter(self_type &&) = delete; |
| self_type &operator=(const self_type &) = delete; |
| self_type &operator=(self_type &&) = delete; |
| |
| virtual ~RateLimiter() = default; |
| |
| virtual bool |
| parseYaml(const YAML::Node &node) |
| { |
| if (node["limit"]) { |
| _limit = node["limit"].as<uint32_t>(); |
| } else { |
| // ToDo: Should we require the limit ? |
| } |
| |
| const YAML::Node &queue = node["queue"]; |
| |
| // If enabled, we default to UINT32_MAX, but the object default is still 0 (no queue) |
| if (queue) { |
| _max_queue = queue["size"] ? queue["size"].as<uint32_t>() : UINT32_MAX; |
| |
| if (queue["max_age"]) { |
| _max_age = std::chrono::seconds(queue["max_age"].as<uint32_t>()); |
| } |
| } |
| |
| const YAML::Node &metrics = node["metrics"]; |
| |
| if (metrics) { |
| std::string prefix = metrics["prefix"] ? metrics["prefix"].as<std::string>() : RATE_LIMITER_METRIC_PREFIX; |
| std::string tag = metrics["tag"] ? metrics["tag"].as<std::string>() : name(); |
| |
| Dbg(dbg_ctl, "Metrics for selector rule: %s(%s, %s)", name().c_str(), prefix.c_str(), tag.c_str()); |
| initializeMetrics(RATE_LIMITER_TYPE_SNI, prefix, tag); |
| } |
| |
| return true; |
| } |
| |
| void |
| initializeMetrics(uint type, std::string tag, std::string prefix = RATE_LIMITER_METRIC_PREFIX) |
| { |
| TSReleaseAssert(type < RATE_LIMITER_TYPE_MAX); |
| memset(_metrics, 0, sizeof(_metrics)); |
| |
| std::string metric_prefix = prefix; |
| metric_prefix.push_back('.'); |
| metric_prefix.append(types[type]); |
| |
| if (!tag.empty()) { |
| metric_prefix.push_back('.'); |
| metric_prefix.append(tag); |
| } else if (!name().empty()) { |
| metric_prefix.push_back('.'); |
| metric_prefix.append(name()); |
| } |
| |
| for (int i = 0; i < RATE_LIMITER_METRIC_MAX; i++) { |
| size_t const metricsz = metric_prefix.length() + strlen(suffixes[i]) + 2; // padding for dot+terminator |
| char *const metric = static_cast<char *>(TSmalloc(metricsz)); |
| snprintf(metric, metricsz, "%s.%s", metric_prefix.data(), suffixes[i]); |
| |
| _metrics[i] = TS_ERROR; |
| |
| if (TSStatFindName(metric, &_metrics[i]) == TS_ERROR) { |
| _metrics[i] = TSStatCreate(metric, TS_RECORDDATATYPE_INT, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM); |
| } |
| |
| if (_metrics[i] != TS_ERROR) { |
| Dbg(dbg_ctl, "established metric '%s' as ID %d", metric, _metrics[i]); |
| } else { |
| TSError("failed to create metric '%s'", metric); |
| } |
| |
| TSfree(metric); |
| } |
| } |
| |
| // Reserve / release a slot from the active resource limits. Reserve will return |
| // false if we are unable to reserve a slot. |
| bool |
| reserve() |
| { |
| std::lock_guard<std::mutex> lock(_active_lock); |
| |
| TSReleaseAssert(_active <= limit()); |
| if (_active < limit()) { |
| ++_active; |
| Dbg(dbg_ctl, "Reserving a slot, active entities == %u", _active.load()); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void |
| free() |
| { |
| { |
| std::lock_guard<std::mutex> lock(_active_lock); |
| --_active; |
| } |
| |
| Dbg(dbg_ctl, "Releasing a slot, active entities == %u", _active.load()); |
| } |
| |
| // Current size of the active_in connections |
| uint32_t |
| active() const |
| { |
| return _active.load(); |
| } |
| |
| // Current size of the queue |
| uint32_t |
| size() const |
| { |
| return _size.load(); |
| } |
| |
| // Is the queue full (at it's max size)? |
| bool |
| full() const |
| { |
| return (_size >= max_queue()); |
| } |
| |
| void |
| push(T elem, TSCont cont) |
| { |
| QueueTime now = std::chrono::system_clock::now(); |
| std::lock_guard<std::mutex> lock(_queue_lock); |
| |
| _queue.push_front(std::make_tuple(elem, cont, now)); |
| ++_size; |
| } |
| |
| QueueItem |
| pop() |
| { |
| QueueItem item; |
| std::lock_guard<std::mutex> lock(_queue_lock); |
| |
| if (!_queue.empty()) { |
| item = std::move(_queue.back()); |
| _queue.pop_back(); |
| --_size; |
| } |
| |
| return item; |
| } |
| |
| void |
| incrementMetric(uint metric) |
| { |
| if (_metrics[metric] != TS_ERROR) { |
| TSStatIntIncrement(_metrics[metric], 1); |
| } |
| } |
| |
| bool |
| hasOldEntity(QueueTime now) |
| { |
| std::lock_guard<std::mutex> lock(_queue_lock); |
| |
| if (!_queue.empty()) { |
| QueueItem item = _queue.back(); |
| |
| std::chrono::milliseconds age = std::chrono::duration_cast<std::chrono::milliseconds>(now - std::get<2>(item)); |
| |
| return (age >= max_age()); |
| } |
| |
| return false; |
| } |
| |
| const std::string & |
| name() const |
| { |
| return _name; |
| } |
| |
| uint32_t |
| limit() const |
| { |
| return _limit; |
| } |
| |
| uint32_t |
| max_queue() const |
| { |
| return _max_queue; |
| } |
| |
| std::chrono::milliseconds |
| max_age() const |
| { |
| return _max_age; |
| } |
| |
| void |
| setName(const std::string &name) |
| { |
| _name = name; |
| } |
| |
| protected: |
| std::string _name = "_limiter_"; // The name/descr (e.g. SNI name) of this limiter |
| uint32_t _limit = UINT32_MAX; // No limit unless specified ... |
| uint32_t _max_queue = 0; // No queue by default |
| std::chrono::milliseconds _max_age = std::chrono::milliseconds::zero(); // Max age (ms) in the queue |
| |
| private: |
| std::atomic<uint32_t> _active = 0; // Current active number of txns. This has to always stay <= limit above |
| std::atomic<uint32_t> _size = 0; // Current size of the pending queue of txns. This should aim to be < _max_queue |
| |
| std::mutex _queue_lock, _active_lock; // Resource locks |
| std::deque<QueueItem> _queue; // Queue for the pending TXN's. ToDo: Should also move (see below) |
| |
| int _metrics[RATE_LIMITER_METRIC_MAX]; |
| }; |