Skip to content

Commit ddd0a5c

Browse files
feat: migrate to per-queue gating with GateFactory abstraction (#72)
Signed-off-by: Cristobal Sepulveda Cardenas <csep2025@gmail.com>
1 parent dd0f597 commit ddd0a5c

14 files changed

Lines changed: 598 additions & 194 deletions

README.md

Lines changed: 130 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,29 @@ The architecture adheres to the following core principles:
2727

2828
## Table of Contents
2929

30-
- [Overview](#overview)
31-
- [When to use](#when-to-use)
32-
- [Design Principles](#design-principles)
33-
- [Deployment](#deployment)
34-
- [Command line parameters](#command-line-parameters)
35-
- [Request Messages and Consusmption](#request-messages-and-consomption)
30+
- [Async Processor (AP) - User Guide](#async-processor-ap---user-guide)
31+
- [Overview](#overview)
32+
- [When to Use](#when-to-use)
33+
- [Design Principles](#design-principles)
34+
- [Table of Contents](#table-of-contents)
35+
- [Deployment](#deployment)
36+
- [Command line parameters](#command-line-parameters)
37+
- [Dispatch Gates](#dispatch-gates)
38+
- [Per-Queue Dispatch Gates](#per-queue-dispatch-gates)
39+
- [Request Messages and Consumption](#request-messages-and-consumption)
3640
- [Request Merge Policy](#request-merge-policy)
37-
- [Retries](#retries)
38-
- [Results](#results)
39-
- [Implementations](#implementations)
41+
- [Retries](#retries)
42+
- [Results](#results)
43+
- [Implementations](#implementations)
4044
- [Redis Sorted Set (Persisted)](#redis-sorted-set-persisted)
4145
- [Redis Sorted Set Command line parameters](#redis-sorted-set-command-line-parameters)
42-
- [Redis Channels (Ephemeral)](#redis-channels)
46+
- [Redis Channels (Ephemeral)](#redis-channels-ephemeral)
4347
- [Redis Channels Command line parameters](#redis-channels-command-line-parameters)
44-
- [Multiple Queues Configuration File Syntax](#multiple-queues-configuration-file-syntax)
45-
- [GCP Pub/Sub](#gcp-pub-sub)
48+
- [Multiple Queues Configuration File Syntax](#multiple-queues-configuration-file-syntax)
49+
- [GCP Pub/Sub](#gcp-pubsub)
4650
- [GCP PubSub Command line parameters](#gcp-pubsub-command-line-parameters)
47-
- [Multiple Topics Configuration File Syntax](#multiple-topics-configuration-file-syntax)
48-
- [Development](#development)
49-
51+
- [Multiple Topics Configuration File Syntax](#multiple-topics-configuration-file-syntax)
52+
- [Development](#development)
5053

5154
## Deployment
5255

@@ -78,18 +81,76 @@ make deploy-ap-on-k8s
7881
## Command line parameters
7982
- `concurrency`: The number of concurrenct workers, default is 8.
8083
- `request-merge-policy`: Currently only supporting <u>random-robin</u> policy.
81-
- `message-queue-impl`: Implementation of the queueing system. Options are <u>gcp-pubsub</u> for GCP PubSub <u>redis-sortedset</u> for Redis Sorted Set (persisted and sorted) and <u>redis-pubsub</u> for ephemeral Redis-based implementation.
82-
- `dispatch-gate`: Implementation of a dispatcher that is responsible for gating pulling messages from the queues. Options are :
83-
- `noop`: The default. Represents full availability of the system. Always pulling messages.
84-
- `metric-avg-queue-size`: Availability base on the model average queue size metric in Prometheus. If the queue size is non empty, messages will not be pulled from the respective queue.
85-
- `redis`: Availability of the system is pulled periodically from Redis (I.e., managed by external system).
84+
- `message-queue-impl`: Implementation of the queueing system. Options are <u>gcp-pubsub</u> for GCP PubSub, <u>gcp-pubsub-gated</u> for GCP PubSub with per-topic gating, <u>redis-sortedset</u> for Redis Sorted Set (persisted and sorted), and <u>redis-pubsub</u> for ephemeral Redis-based implementation.
85+
86+
- `prometheus-url`: Prometheus server URL for metric-based gates (e.g., http://localhost:9090). For Google Managed Prometheus (GMP), point this to a local proxy or GMP frontend that handles authentication — direct GMP URLs are not supported as the Async Processor does not perform GMP authentication.
87+
This flag is required when using metric-based per-queue gates (e.g., `prometheus-saturation`).
8688

8789
<i>additional parameters may be specified for concrete message queue implementations</i>
8890

91+
## Dispatch Gates
92+
93+
The Async Processor supports dispatch gates to control batch processing based on system capacity. Gates can be configured per-queue (via configuration files).
94+
95+
### Per-Queue Dispatch Gates
96+
97+
For more fine-grained control, configure gates per queue in your configuration file. Each queue can have its own gate type and parameters.
98+
99+
**Gate Types:**
100+
101+
- `constant`: Always returns budget 1.0 (fully open) - no throttling.
102+
- `redis`: Queries Redis for dispatch budget (managed by external system).
103+
- `prometheus-saturation`: Queries Prometheus for pool saturation metric. Returns 1.0 - saturation if below threshold, 0.0 otherwise.
104+
105+
**Example Configuration with Per-Queue Gates:**
106+
107+
```json
108+
[
109+
{
110+
"queue_name": "critical_queue",
111+
"inference_objective": "critical-task",
112+
"request_path_url": "/v1/inference",
113+
"gate_type": "constant"
114+
},
115+
{
116+
"queue_name": "batch_queue",
117+
"inference_objective": "batch-task",
118+
"request_path_url": "/v1/inference",
119+
"gate_type": "prometheus-saturation",
120+
"gate_params": {
121+
"pool": "inference_pool_1",
122+
"threshold": "0.8"
123+
}
124+
},
125+
{
126+
"queue_name": "redis_gated_queue",
127+
"inference_objective": "gated-task",
128+
"request_path_url": "/v1/inference",
129+
"gate_type": "redis",
130+
"gate_params": {
131+
"address": "localhost:6379",
132+
"budget_key": "my-budget-key"
133+
}
134+
}
135+
]
136+
```
137+
138+
**Gate Parameters:**
139+
140+
- `redis`:
141+
- `address` (**required**): Redis server address for the dispatch gate (e.g., `localhost:6379`). Queues sharing the same address will share the same connection pool.
142+
- `budget_key` (optional): Redis key to read dispatch budget from. Default is `dispatch-gate-budget`.
143+
144+
- `prometheus-saturation`:
145+
- `pool`: The inference pool name to query metrics for.
146+
- `threshold`: Saturation threshold (0.0-1.0). When saturation >= threshold, budget is 0.0. Default is 0.8.
147+
- `fallback`: Fallback saturation value (0.0-1.0) used when the metric source returns an error or empty data. Default is 0.0.
148+
- `query`: Custom PromQL expression to query. If omitted, a default query using `inference_extension_flow_control_pool_saturation` with the `pool` label is used.
89149

90-
## Request Messages and Consusmption
150+
## Request Messages and Consumption
91151

92152
The async processor expects request messages to have the following format:
153+
93154
```json
94155
{
95156
"id" : "unique identifier for result mapping",
@@ -100,6 +161,7 @@ The async processor expects request messages to have the following format:
100161
```
101162

102163
Example:
164+
103165
```json
104166
{
105167
"id" : "19933123533434",
@@ -141,7 +203,6 @@ A persisted implementation based on Redis SortedSets.
141203

142204
![Async Processor - Redis Sorted Set architecture](/docs/images/redis_sortedset_architecture.png "AP - Redis SortedSet")
143205

144-
145206
#### Redis Sorted Set Command line parameters
146207
- `redis.ss.addr`: Address of the Redis server. Default is <u>localhost:6379</u>.
147208
- `redis.ss.igw-base-url`: Base URL of the IGW (e.g. https://localhost:30800).<br> Mutually exclusive with `redis.ss.queues-config-file` flag.
@@ -152,6 +213,8 @@ A persisted implementation based on Redis SortedSets.
152213
- `redis.ss.queues-config-file`: The configuration file name when using multiple queues. <br> Mutually exclusive with `redis.ss.igw-base-url`, `redis.ss.request-queue-name`, `redis.ss.request-path-url` and `redis.ss.inference-objective` flags.
153214
- `redis.ss.poll-interval-ms`: Poll interval in milliseconds. Default is <u>1000</u>.
154215
- `redis.ss.batch-size`: Number of messages to process per poll. Default is <u>10</u>.
216+
- `redis.ss.gate-type`: Gate type for single-queue mode (e.g., `redis`, `prometheus-saturation`). Only used when `redis.ss.queues-config-file` is not set.
217+
- `redis.ss.gate-params`: JSON-encoded gate params map for single-queue mode (e.g., `{"address":"localhost:6379"}`). Only used when `redis.ss.queues-config-file` is not set.
155218
156219
### Redis Channels (Ephemeral)
157220
@@ -182,19 +245,31 @@ An example implementation based on Redis channels is provided.
182245

183246
The configuration file when using the `redis.queues-config-file` flag should have the following format:
184247

185-
```json
248+
```json
186249
[
187250
{
188-
"queue_name": "some_channel_name",
251+
"queue_name": "some_queue_name",
189252
"igw_base_url": "http://localhost:30800",
190-
"inference_objective": "some_inference_objective",
191-
"request_path_url": "e.g.: /v1/completions"
253+
"inference_objective": "some_inference_objective",
254+
"request_path_url": "/v1/completions"
192255
},
193-
...
256+
{
257+
"queue_name": "another_queue",
258+
"igw_base_url": "http://localhost:30800",
259+
"inference_objective": "batch_task",
260+
"request_path_url": "/v1/inference"
261+
}
194262
]
195263
```
196264

265+
<u>Note:</u> The ephemeral Redis Channels implementation does not support per-queue dispatch gates. Use the [Redis Sorted Set](#redis-sorted-set-persisted) implementation for per-queue gating.
197266

267+
**Configuration Fields:**
268+
269+
- `queue_name`: The name of the Redis channel for this queue.
270+
- `igw_base_url`: Base URL of the IGW.
271+
- `inference_objective`: The inference objective header value.
272+
- `request_path_url`: The request path URL.
198273

199274
### GCP Pub/Sub
200275

@@ -218,28 +293,49 @@ The GCP PubSub implementation requires the user to configure the following:
218293
- `pubsub.inference-objective`: InferenceObjective to use for requests (set as the HTTP header x-gateway-inference-objective if not empty). <br> Mutually exclusive with `pubsub.topics-config-file` flag.
219294
- `pubsub.request-subscriber-id`: The subscriber ID for the requests topic.<br> Mutually exclusive with `pubsub.topics-config-file` flag.
220295
- `pubsub.result-topic-id`: The results topic ID.
296+
- `pubsub.batch-size`: Number of inflight messages. Default is <u>10</u>.
221297
- `pubsub.topics-config-file`: The configuration file name when using multiple topics. <br> Mutually exclusive with `pubsub.request-subscriber-id`, `pubsub.request-path-url` and `pubsub.inference-objective` flags.
222298

223299
#### Multiple Topics Configuration File Syntax
224300

225301
The configuration file when using the `pubsub.topics-config-file` flag should have the following format:
226302

227-
```json
303+
```json
228304
[
229305
{
230306
"igw_base_url": "http://localhost:30800",
231-
"subscriber_id": "some_subscriber_id",
232-
"inference_objective": "some_inference_objective",
233-
"request_path_url": "e.g.: /v1/completions"
307+
"subscriber_id": "some_subscriber_id",
308+
"inference_objective": "some_inference_objective",
309+
"request_path_url": "e.g.: /v1/completions",
310+
"gate_type": "constant",
311+
"gate_params": {}
234312
},
235-
...
313+
{
314+
"subscriber_id": "another_subscriber",
315+
"inference_objective": "batch_task",
316+
"request_path_url": "/v1/inference",
317+
"gate_type": "prometheus-saturation",
318+
"gate_params": {
319+
"pool": "pool_2",
320+
"threshold": "0.75"
321+
}
322+
}
236323
]
237324
```
238325

326+
**Configuration Fields:**
327+
328+
- `subscriber_id`: The GCP PubSub subscriber ID for this topic.
329+
- `inference_objective`: The inference objective header value.
330+
- `request_path_url`: The request path URL.
331+
- `gate_type`: Required type of dispatch gate for this topic.
332+
- `gate_params` (optional): Parameters for the gate type (e.g., pool name, threshold for prometheus gates).
333+
239334
## Development
240335

241336
A setup based on a KIND cluster with a Redis server for MQ is provided.
242337
In order to deploy everything run:
338+
243339
```bash
244340
make deploy-ap-emulated-on-kind
245341
```
@@ -251,7 +347,7 @@ kubectl exec -n redis redis-master-0 -- redis-cli SUBSCRIBE result-queue
251347
```
252348

253349
Publish a message for async processing:
350+
254351
```bash
255352
kubectl exec -n redis redis-master-0 -- redis-cli PUBLISH request-queue '{"id" : "testmsg", "payload":{ "model":"unsloth/Meta-Llama-3.1-8B", "prompt":"hi"}, "deadline" :"9999999999" }'
256-
```
257-
353+
```

cmd/main.go

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ func main() {
3333
var concurrency int
3434
var requestMergePolicy string
3535
var messageQueueImpl string
36-
var dispatchGateType string
3736

3837
flag.IntVar(&loggerVerbosity, "v", logging.DEFAULT, "number for the log level verbosity")
3938

@@ -43,8 +42,9 @@ func main() {
4342
flag.IntVar(&concurrency, "concurrency", 8, "number of concurrent workers")
4443

4544
flag.StringVar(&requestMergePolicy, "request-merge-policy", "random-robin", "The request merge policy to use. Supported policies: random-robin")
46-
flag.StringVar(&dispatchGateType, "dispatch-gate", "noop", "The dispatch gate policy to use. Supported policies: noop, redis, metric-avg-queue-size, metric-saturation")
47-
flag.StringVar(&messageQueueImpl, "message-queue-impl", "redis-pubsub", "The message queue implementation to use. Supported implementations: redis-pubsub, redis-sortedset, redis-sortedset-gated, gcp-pubsub, gcp-pubsub-gated")
45+
flag.StringVar(&messageQueueImpl, "message-queue-impl", "redis-pubsub", "The message queue implementation to use. Supported implementations: redis-pubsub, redis-sortedset, gcp-pubsub, gcp-pubsub-gated")
46+
47+
var prometheusURL = flag.String("prometheus-url", "", "Prometheus server URL for metric-based gates (e.g., http://localhost:9090)")
4848

4949
opts := zap.Options{
5050
Development: true,
@@ -62,23 +62,8 @@ func main() {
6262
////////setupLog.Info("GIE build", "commit-sha", version.CommitSHA, "build-ref", version.BuildRef)
6363

6464
printAllFlags(setupLog)
65-
// Create dispatch gate
66-
var gate flowcontrol.DispatchGate
67-
switch dispatchGateType {
68-
case "noop":
69-
gate = flowcontrol.ConstOpenGate()
70-
case "redis":
71-
gate = redis.NewRedisDispatchGate()
72-
setupLog.Info("Using Redis-based dispatch gate")
73-
case "metric-avg-queue-size":
74-
gate = flowcontrol.AverageQueueSizeGate()
75-
case "metric-saturation":
76-
gate = flowcontrol.SaturationGate()
77-
setupLog.Info("Using saturation metric dispatch gate")
78-
default:
79-
setupLog.Error(fmt.Errorf("unknown dispatch gate type: %s", dispatchGateType), "Unknown dispatch gate type", "dispatch-gate", dispatchGateType)
80-
os.Exit(1)
81-
}
65+
// Create Gate Factory for per-queue gate instantiation
66+
gateFactory := flowcontrol.NewGateFactory(*prometheusURL)
8267

8368
var policy api.RequestMergePolicy
8469
switch requestMergePolicy {
@@ -94,17 +79,14 @@ func main() {
9479
case "redis-pubsub":
9580
impl = redis.NewRedisMQFlow()
9681
case "redis-sortedset":
97-
impl = redis.NewRedisSortedSetFlow()
98-
case "redis-sortedset-gated":
99-
impl = redis.NewRedisSortedSetFlow(redis.WithDispatchGate(gate))
100-
setupLog.Info("Using Redis sorted-set flow with dispatch gating", "gate-type", dispatchGateType)
82+
impl = redis.NewRedisSortedSetFlow(redis.WithGateFactory(gateFactory))
83+
setupLog.Info("Using Redis sorted-set flow with per-queue gating")
10184
case "gcp-pubsub":
10285
impl = pubsub.NewGCPPubSubMQFlow()
10386
case "gcp-pubsub-gated":
104-
impl = pubsub.NewGCPPubSubMQFlow(pubsub.WithDispatchGate(gate))
105-
setupLog.Info("Using GCP PubSub flow with dispatch gating", "gate-type", dispatchGateType)
87+
impl = pubsub.NewGCPPubSubMQFlow(pubsub.WithGateFactory(gateFactory))
88+
setupLog.Info("Using GCP PubSub flow with per-queue gating")
10689
default:
107-
10890
setupLog.Error(fmt.Errorf("unknown message queue implementation: %s", messageQueueImpl), "Unknown message queue implementation",
10991
"message-queue-impl", messageQueueImpl)
11092
os.Exit(1)

pkg/async/api/api.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package api
22

3-
import "context"
3+
import (
4+
"context"
5+
)
46

57
type Flow interface {
68

@@ -24,6 +26,35 @@ type Characteristics struct {
2426
SupportsMessageLatency bool
2527
}
2628

29+
// DispatchGate defines the interface to determine whether there is enough capacity to forward a request.
30+
type DispatchGate interface {
31+
// Budget returns the Dispatch Budget in the range [0.0, 1.0], representing
32+
// the fraction of system capacity available for new requests.
33+
// A value of 0.0 indicates no available capacity (system at max allowed).
34+
// A value of 1.0 indicates full capacity available (system is idle).
35+
// The system always returns a valid value, even in case of internal error.
36+
Budget(ctx context.Context) float64
37+
}
38+
39+
// GateFactory defines the interface for creating DispatchGate instances.
40+
type GateFactory interface {
41+
CreateGate(gateType string, params map[string]string) (DispatchGate, error)
42+
}
43+
44+
// DispatchGateFunc is a function type that implements DispatchGate.
45+
// This allows any function with the signature func(context.Context) float64
46+
// to be used as a DispatchGate.
47+
type DispatchGateFunc func(context.Context) float64
48+
49+
// Budget implements DispatchGate by calling the function itself.
50+
func (f DispatchGateFunc) Budget(ctx context.Context) float64 {
51+
return f(ctx)
52+
}
53+
54+
func ConstOpenGate() DispatchGate {
55+
return DispatchGateFunc(func(ctx context.Context) float64 { return 1.0 })
56+
}
57+
2758
type RequestMergePolicy interface {
2859
MergeRequestChannels(channels []RequestChannel) EmbelishedRequestChannel
2960
}
@@ -44,6 +75,7 @@ type RequestChannel struct {
4475
IGWBaseURl string
4576
InferenceObjective string
4677
RequestPathURL string
78+
Gate DispatchGate // Dispatch gate for this channel, nil defaults to always-open
4779
}
4880

4981
type EmbelishedRequestChannel struct {

0 commit comments

Comments
 (0)