// Copyright 2025 Envoy Project Authors
//
// Licensed 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.

syntax = "proto3";

package envoy.service.rate_limit_quota.v3;

import "envoy/type/v3/ratelimit_strategy.proto";

import "google/protobuf/duration.proto";

import "xds/annotations/v3/status.proto";

import "udpa/annotations/status.proto";
import "validate/validate.proto";

option java_package = "io.envoyproxy.envoy.service.rate_limit_quota.v3";
option java_outer_classname = "RlqsProto";
option java_multiple_files = true;
option go_package = "github.com/envoyproxy/go-control-plane/envoy/service/rate_limit_quota/v3;rate_limit_quotav3";
option (udpa.annotations.file_status).package_version_status = ACTIVE;
option (xds.annotations.v3.file_status).work_in_progress = true;

// [#protodoc-title: Rate Limit Quota Service (RLQS)]

// The Rate Limit Quota Service (RLQS) is a Envoy global rate limiting service that allows to
// delegate rate limit decisions to a remote service. The service will aggregate the usage reports
// from multiple data plane instances, and distribute Rate Limit Assignments to each instance
// based on its business logic. The logic is outside of the scope of the protocol API.
//
// The protocol is designed as a streaming-first API. It utilizes watch-like subscription model.
// The data plane groups requests into Quota Buckets as directed by the filter config,
// and periodically reports them to the RLQS server along with the Bucket identifier, :ref:`BucketId
// <envoy_v3_api_msg_service.rate_limit_quota.v3.BucketId>`. Once RLQS server has collected enough
// reports to make a decision, it'll send back the assignment with the rate limiting instructions.
//
// The first report sent by the data plane is interpreted by the RLQS server as a "watch" request,
// indicating that the data plane instance is interested in receiving further updates for the
// ``BucketId``. From then on, RLQS server may push assignments to this instance at will, even if
// the instance is not sending usage reports. It's the responsibility of the RLQS server
// to determine when the data plane instance didn't send ``BucketId`` reports for too long,
// and to respond with the :ref:`AbandonAction
// <envoy_v3_api_msg_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.AbandonAction>`,
// indicating that the server has now stopped sending quota assignments for the ``BucketId`` bucket,
// and the data plane instance should :ref:`abandon
// <envoy_v3_api_field_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.abandon_action>`
// it.
//
// If for any reason the RLQS client doesn't receive the initial assignment for the reported bucket,
// in order to prevent memory exhaustion, the data plane will limit the time such bucket
// is retained. The exact time to wait for the initial assignment is chosen by the filter,
// and may vary based on the implementation.
// Once the duration ends, the data plane will stop reporting bucket usage, reject any enqueued
// requests, and purge the bucket from the memory. Subsequent requests matched into the bucket
// will re-initialize the bucket in the "no assignment" state, restarting the reports.
//
// Refer to Rate Limit Quota :ref:`configuration overview <config_http_filters_rate_limit_quota>`
// for further details.

// Defines the Rate Limit Quota Service (RLQS).
service RateLimitQuotaService {
  // Main communication channel: the data plane sends usage reports to the RLQS server,
  // and the server asynchronously responding with the assignments.
  rpc StreamRateLimitQuotas(stream RateLimitQuotaUsageReports)
      returns (stream RateLimitQuotaResponse) {
  }
}

message RateLimitQuotaUsageReports {
  // The usage report for a bucket.
  //
  // .. note::
  //   Note that the first report sent for a ``BucketId`` indicates to the RLQS server that
  //   the RLQS client is subscribing for the future assignments for this ``BucketId``.
  message BucketQuotaUsage {
    // ``BucketId`` for which request quota usage is reported.
    BucketId bucket_id = 1 [(validate.rules).message = {required: true}];

    // Time elapsed since the last report.
    google.protobuf.Duration time_elapsed = 2 [(validate.rules).duration = {
      required: true
      gt {}
    }];

    // Requests the data plane has allowed through.
    uint64 num_requests_allowed = 3;

    // Requests throttled.
    uint64 num_requests_denied = 4;
  }

  // All quota requests must specify the domain. This enables sharing the quota
  // server between different applications without fear of overlap.
  // E.g., "envoy".
  //
  // Should only be provided in the first report, all subsequent messages on the same
  // stream are considered to be in the same domain. In case the domain needs to be
  // changes, close the stream, and reopen a new one with the different domain.
  string domain = 1 [(validate.rules).string = {min_len: 1}];

  // A list of quota usage reports. The list is processed by the RLQS server in the same order
  // it's provided by the client.
  repeated BucketQuotaUsage bucket_quota_usages = 2 [(validate.rules).repeated = {min_items: 1}];
}

message RateLimitQuotaResponse {
  // Commands the data plane to apply one of the actions to the bucket with the
  // :ref:`bucket_id <envoy_v3_api_field_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.bucket_id>`.
  message BucketAction {
    // Quota assignment for the bucket. Configures the rate limiting strategy and the duration
    // for the given :ref:`bucket_id
    // <envoy_v3_api_field_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.bucket_id>`.
    //
    // **Applying the first assignment to the bucket**
    //
    // Once the data plane receives the ``QuotaAssignmentAction``, it must send the current usage
    // report for the bucket, and start rate limiting requests matched into the bucket
    // using the strategy configured in the :ref:`rate_limit_strategy
    // <envoy_v3_api_field_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.QuotaAssignmentAction.rate_limit_strategy>`
    // field. The assignment becomes bucket's ``active`` assignment.
    //
    // **Expiring the assignment**
    //
    // The duration of the assignment defined in the :ref:`assignment_time_to_live
    // <envoy_v3_api_field_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.QuotaAssignmentAction.assignment_time_to_live>`
    // field. When the duration runs off, the assignment is ``expired``, and no longer ``active``.
    // The data plane should stop applying the rate limiting strategy to the bucket, and transition
    // the bucket to the "expired assignment" state. This activates the behavior configured in the
    // :ref:`expired_assignment_behavior <envoy_v3_api_field_extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings.expired_assignment_behavior>`
    // field.
    //
    // **Replacing the assignment**
    //
    // * If the rate limiting strategy is different from bucket's ``active`` assignment, or
    //   the current bucket assignment is ``expired``, the data plane must immediately
    //   end the current assignment, report the bucket usage, and apply the new assignment.
    //   The new assignment becomes bucket's ``active`` assignment.
    // * If the rate limiting strategy is the same as the bucket's ``active`` (not ``expired``)
    //   assignment, the data plane should extend the duration of the ``active`` assignment
    //   for the duration of the new assignment provided in the :ref:`assignment_time_to_live
    //   <envoy_v3_api_field_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.QuotaAssignmentAction.assignment_time_to_live>`
    //   field. The ``active`` assignment is considered unchanged.
    message QuotaAssignmentAction {
      // A duration after which the assignment is be considered ``expired``. The process of the
      // expiration is described :ref:`above
      // <envoy_v3_api_msg_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.QuotaAssignmentAction>`.
      //
      // * If unset, the assignment has no expiration date.
      // * If set to ``0``, the assignment expires immediately, forcing the client into the
      //   :ref:`"expired assignment"
      //   <envoy_v3_api_field_extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings.ExpiredAssignmentBehavior.expired_assignment_behavior_timeout>`
      //   state. This may be used by the RLQS server in cases when it needs clients to proactively
      //   fall back to the pre-configured :ref:`ExpiredAssignmentBehavior
      //   <envoy_v3_api_msg_extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings.ExpiredAssignmentBehavior>`,
      //   f.e. before the server going into restart.
      //
      // .. attention::
      //   Note that :ref:`expiring
      //   <envoy_v3_api_msg_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.QuotaAssignmentAction>`
      //   the assignment is not the same as :ref:`abandoning
      //   <envoy_v3_api_msg_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.AbandonAction>`
      //   the assignment. While expiring the assignment just transitions the bucket to
      //   the "expired assignment" state; abandoning the assignment completely erases
      //   the bucket from the data plane memory, and stops the usage reports.
      google.protobuf.Duration assignment_time_to_live = 2 [(validate.rules).duration = {gte {}}];

      // Configures the local rate limiter for the request matched to the bucket.
      // If not set, allow all requests.
      type.v3.RateLimitStrategy rate_limit_strategy = 3;
    }

    // Abandon action for the bucket. Indicates that the RLQS server will no longer be
    // sending updates for the given :ref:`bucket_id
    // <envoy_v3_api_field_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.bucket_id>`.
    //
    // If no requests are reported for a bucket, after some time the server considers the bucket
    // inactive. The server stops tracking the bucket, and instructs the the data plane to abandon
    // the bucket via this message.
    //
    // **Abandoning the assignment**
    //
    // The data plane is to erase the bucket (including its usage data) from the memory.
    // It should stop tracking the bucket, and stop reporting its usage. This effectively resets
    // the data plane to the state prior to matching the first request into the bucket.
    //
    // **Restarting the subscription**
    //
    // If a new request is matched into a bucket previously abandoned, the data plane must behave
    // as if it has never tracked the bucket, and it's the first request matched into it:
    //
    // 1. The process of :ref:`subscription and reporting
    //    <envoy_v3_api_field_extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings.reporting_interval>`
    //    starts from the beginning.
    //
    // 2. The bucket transitions to the :ref:`"no assignment"
    //    <envoy_v3_api_field_extensions.filters.http.rate_limit_quota.v3.RateLimitQuotaBucketSettings.no_assignment_behavior>`
    //    state.
    //
    // 3. Once the new assignment is received, it's applied per
    //    "Applying the first assignment to the bucket" section of the :ref:`QuotaAssignmentAction
    //    <envoy_v3_api_msg_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.QuotaAssignmentAction>`.
    message AbandonAction {
    }

    // ``BucketId`` for which request the action is applied.
    BucketId bucket_id = 1 [(validate.rules).message = {required: true}];

    oneof bucket_action {
      option (validate.required) = true;

      // Apply the quota assignment to the bucket.
      //
      // Commands the data plane to apply a rate limiting strategy to the bucket.
      // The process of applying and expiring the rate limiting strategy is detailed in the
      // :ref:`QuotaAssignmentAction
      // <envoy_v3_api_msg_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.QuotaAssignmentAction>`
      // message.
      QuotaAssignmentAction quota_assignment_action = 2;

      // Abandon the bucket.
      //
      // Commands the data plane to abandon the bucket.
      // The process of abandoning the bucket is described in the :ref:`AbandonAction
      // <envoy_v3_api_msg_service.rate_limit_quota.v3.RateLimitQuotaResponse.BucketAction.AbandonAction>`
      // message.
      AbandonAction abandon_action = 3;
    }
  }

  // An ordered list of actions to be applied to the buckets. The actions are applied in the
  // given order, from top to bottom.
  repeated BucketAction bucket_action = 1 [(validate.rules).repeated = {min_items: 1}];
}

// The identifier for the bucket. Used to match the bucket between the control plane (RLQS server),
// and the data plane (RLQS client), f.e.:
//
// * the data plane sends a usage report for requests matched into the bucket with ``BucketId``
//   to the control plane
// * the control plane sends an assignment for the bucket with ``BucketId`` to the data plane
//   Bucket ID.
//
// Example:
//
// .. validated-code-block:: yaml
//   :type-name: envoy.service.rate_limit_quota.v3.BucketId
//
//   bucket:
//     name: my_bucket
//     env: staging
//
// .. note::
//   The order of ``BucketId`` keys do not matter. Buckets ``{ a: 'A', b: 'B' }`` and
//   ``{ b: 'B', a: 'A' }`` are identical.
message BucketId {
  map<string, string> bucket = 1 [(validate.rules).map = {
    min_pairs: 1
    keys {string {min_len: 1}}
    values {string {min_len: 1}}
  }];
}
