|
1 | 1 | import re |
| 2 | +from collections import Counter |
2 | 3 | from enum import Enum |
3 | 4 | from typing import Any, Dict, List, Optional, Union |
4 | 5 |
|
|
18 | 19 |
|
19 | 20 | CommandsList = List[str] |
20 | 21 | ValidPort = conint(gt=0, le=65536) |
| 22 | +MAX_INT64 = 2**63 - 1 |
21 | 23 | SERVICE_HTTPS_DEFAULT = True |
22 | 24 | STRIP_PREFIX_DEFAULT = True |
23 | 25 |
|
@@ -85,6 +87,70 @@ class ScalingSpec(CoreModel): |
85 | 87 | ] = Duration.parse("10m") |
86 | 88 |
|
87 | 89 |
|
| 90 | +class IPAddressPartitioningKey(CoreModel): |
| 91 | + type: Annotated[Literal["ip_address"], Field(description="Partitioning type")] = "ip_address" |
| 92 | + |
| 93 | + |
| 94 | +class HeaderPartitioningKey(CoreModel): |
| 95 | + type: Annotated[Literal["header"], Field(description="Partitioning type")] = "header" |
| 96 | + header: Annotated[ |
| 97 | + str, |
| 98 | + Field( |
| 99 | + description="Name of the header to use for partitioning", |
| 100 | + regex=r"^[a-zA-Z0-9-_]+$", # prevent Nginx config injection |
| 101 | + max_length=500, # chosen randomly, Nginx limit is higher |
| 102 | + ), |
| 103 | + ] |
| 104 | + |
| 105 | + |
| 106 | +class RateLimit(CoreModel): |
| 107 | + prefix: Annotated[ |
| 108 | + str, |
| 109 | + Field( |
| 110 | + description=( |
| 111 | + "URL path prefix to which this limit is applied." |
| 112 | + " If an incoming request matches several prefixes, the longest prefix is applied" |
| 113 | + ), |
| 114 | + max_length=4094, # Nginx limit |
| 115 | + regex=r"^/[^\s\\{}]*$", # prevent Nginx config injection |
| 116 | + ), |
| 117 | + ] = "/" |
| 118 | + key: Annotated[ |
| 119 | + Union[IPAddressPartitioningKey, HeaderPartitioningKey], |
| 120 | + Field( |
| 121 | + discriminator="type", |
| 122 | + description=( |
| 123 | + "The partitioning key. Each incoming request belongs to a partition" |
| 124 | + " and rate limits are applied per partition." |
| 125 | + " Defaults to partitioning by client IP address" |
| 126 | + ), |
| 127 | + ), |
| 128 | + ] = IPAddressPartitioningKey() |
| 129 | + rps: Annotated[ |
| 130 | + float, |
| 131 | + Field( |
| 132 | + description=( |
| 133 | + "Max allowed number of requests per second." |
| 134 | + " Requests are tracked at millisecond granularity." |
| 135 | + " For example, `rps: 10` means at most 1 request per 100ms" |
| 136 | + ), |
| 137 | + # should fit into Nginx limits after being converted to requests per minute |
| 138 | + ge=1 / 60, |
| 139 | + le=MAX_INT64 // 60, |
| 140 | + ), |
| 141 | + ] |
| 142 | + burst: Annotated[ |
| 143 | + int, |
| 144 | + Field( |
| 145 | + ge=0, |
| 146 | + le=MAX_INT64, # Nginx limit |
| 147 | + description=( |
| 148 | + "Max number of requests that can be passed to the service ahead of the rate limit" |
| 149 | + ), |
| 150 | + ), |
| 151 | + ] = 0 |
| 152 | + |
| 153 | + |
88 | 154 | class BaseRunConfiguration(CoreModel): |
89 | 155 | type: Literal["none"] |
90 | 156 | name: Annotated[ |
@@ -306,6 +372,7 @@ class ServiceConfigurationParams(CoreModel): |
306 | 372 | Optional[ScalingSpec], |
307 | 373 | Field(description="The auto-scaling rules. Required if `replicas` is set to a range"), |
308 | 374 | ] = None |
| 375 | + rate_limits: Annotated[list[RateLimit], Field(description="Rate limiting rules")] = [] |
309 | 376 |
|
310 | 377 | @validator("port") |
311 | 378 | def convert_port(cls, v) -> PortMapping: |
@@ -358,6 +425,17 @@ def validate_scaling(cls, values): |
358 | 425 | raise ValueError("To use `scaling`, `replicas` must be set to a range.") |
359 | 426 | return values |
360 | 427 |
|
| 428 | + @validator("rate_limits") |
| 429 | + def validate_rate_limits(cls, v: list[RateLimit]) -> list[RateLimit]: |
| 430 | + counts = Counter(limit.prefix for limit in v) |
| 431 | + duplicates = [prefix for prefix, count in counts.items() if count > 1] |
| 432 | + if duplicates: |
| 433 | + raise ValueError( |
| 434 | + f"Prefixes {duplicates} are used more than once." |
| 435 | + " Each rate limit should have a unique path prefix" |
| 436 | + ) |
| 437 | + return v |
| 438 | + |
361 | 439 |
|
362 | 440 | class ServiceConfiguration( |
363 | 441 | ProfileParams, BaseRunConfigurationWithCommands, ServiceConfigurationParams |
|
0 commit comments