#[non_exhaustive]pub struct HandlerLimits {Show 16 fields
pub max_id_length: usize,
pub max_metadata_size: usize,
pub max_cancellation_tokens: usize,
pub max_token_age: Duration,
pub push_delivery_timeout: Duration,
pub push_delivery_budget: Duration,
pub executor_drain_timeout: Duration,
pub max_artifacts_per_task: usize,
pub max_context_locks: usize,
pub max_push_configs_per_task: usize,
pub max_parts_per_artifact: usize,
pub max_total_push_configs: usize,
pub subscribe_reattach_interval: Duration,
pub subscribe_max_idle: Duration,
pub subscribe_replay_limit: usize,
pub subscribe_replay_catchup: Duration,
}Expand description
Configurable limits for the request handler.
All fields have sensible defaults. Create with HandlerLimits::default()
and override individual values as needed.
§Example
use a2a_protocol_server::handler::HandlerLimits;
let limits = HandlerLimits::default()
.with_max_id_length(2048)
.with_max_metadata_size(2 * 1024 * 1024);#[non_exhaustive]: build it with Default and the with_* setters,
which cover every field; a struct literal is not available outside this
crate, so a field added later does not break callers.
Fields (Non-exhaustive)§
This struct is marked as non-exhaustive
Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.max_id_length: usizeMaximum allowed length for task/context IDs. Default: 1024.
message.id is bounded too, but never below
MIN_MESSAGE_ID_LENGTH — see
effective_max_message_id_length
for why it cannot simply share this number.
max_metadata_size: usizeMaximum allowed serialized size for metadata fields in bytes. Default: 1 MiB.
max_cancellation_tokens: usizeMaximum cancellation token map entries before cleanup sweep. Default: 10,000.
A sweep threshold, not a hard bound: the sweep only evicts cancelled or aged-out entries whose executor is gone — a token belonging to a live task is never removed, so with more than this many tasks genuinely in flight the map tracks the in-flight count instead.
max_token_age: DurationMaximum age for cancellation tokens. Default: 1 hour.
push_delivery_timeout: DurationTimeout for individual push webhook deliveries. Default: 5 seconds.
Bounds how long the handler waits for a single push notification delivery to complete, preventing one slow webhook from blocking all subsequent deliveries.
§This is a total, and the sender’s retries have to fit inside it
The bound covers the whole PushSender::send call, retries included —
not one HTTP request. A sender whose own schedule is longer never
finishes it, and the attempts it advertises simply do not happen.
The shipped defaults contradict each other. HttpPushSender::new()
is three attempts at a 30-second request timeout with [1s, 2s]
backoff after a 5-second DNS bound — 98 seconds — against this
5-second bound. Measured
2026-08-19 against a real socket: one of the three attempts reaches
the webhook, and the bound fires at 5.001s. So max_attempts and
backoff are, at the defaults, configuration that cannot take effect.
The two numbers pull in opposite directions and neither is obviously
wrong. Raising this bound to fit the retries makes the 30-second
per-event budget in deliver_push_bg reachable by a single config,
which is the amplification ceiling that budget exists to hold.
Shrinking the sender’s schedule to fit gives real webhooks less time
than a slow one legitimately needs. Choosing between them is a
deployment decision, so this documents the arithmetic rather than
picking:
attempts_that_run == 1 + how many whole (request_timeout + backoff)
cycles fit in push_delivery_timeoutA sender that reports PushSender::max_delivery_duration gets the
truncation counted rather than mistaken for a slow endpoint — see
push_outcome::TIMEOUT_TRUNCATED.
push_delivery_budget: DurationTotal time one event’s push deliveries may take, across every registered config. Default: 30 seconds.
This is the amplification ceiling: a task with max_push_configs_per_task
webhooks that all time out would otherwise spend configs x push_delivery_timeout x attempts per event. Deliveries run one after
another, so the configs an event reaches is
min(configs, push_delivery_budget / push_delivery_timeout); the rest
are counted as push_outcome::SKIPPED.
On the blocking send path the same budget covers the whole batch of
events a request produced, not each event, because that delivery is
spawned once per request.
Until 0.12 this was a Duration::from_secs(30) literal in two places
and the term deciding which webhooks were called was not a knob.
executor_drain_timeout: DurationHow long the blocking send path waits, after the executor has finished, for the event queue to close. Default: 5 seconds.
Once the executor returns, everything it wrote is already buffered and
is drained immediately; the only thing left to wait for is the
queue closing, which EventQueueManager::destroy does from the cleanup
guard’s Drop. An executor that returns without reaching a terminal
or interrupted state, on a queue that never closes, used to hold the
blocking SendMessage open forever. When this bound elapses the
response is the task as collected so far, exactly what a closed queue
would have produced, and
Metrics::on_error is called with
error_kind = "executor_drain_timeout".
max_artifacts_per_task: usizeMaximum number of artifacts per task. Default: 1000.
Prevents unbounded memory growth and O(n²) serialization cost when executors emit many artifacts. Once the limit is reached, new artifact updates are rejected.
max_context_locks: usizeMaximum number of per-context locks before cleanup. Default: 10,000.
Context locks serialize concurrent SendMessage requests for the same
context_id. Stale entries (where no other reference is held) are
pruned when this limit is reached. Like
max_cancellation_tokens this is a
prune threshold, not a hard bound — entries currently held by
in-flight requests are never pruned.
max_push_configs_per_task: usizeMaximum number of push notification configs per task. Default: 100.
Enforced by the handler on CreateTaskPushNotificationConfig so the cap
applies uniformly across all store backends. Without it, the SQL
stores (which do not self-enforce) let a client mint unbounded configs
for a single task — a disk-exhaustion vector, and a delivery-amplification
vector since every stream event fans out to all of a task’s configs.
Updating an existing config (same id) does not count against the cap.
max_parts_per_artifact: usizeMaximum number of parts a single artifact may accumulate. Default: 10,000.
max_artifacts_per_task bounds the artifact count, but a stream of
TaskArtifactUpdateEvents with append: true grows one artifact’s
parts without bound. Since executors routinely stream model output
derived from attacker-influenced prompts, this bounds the cumulative
per-artifact (and thus per-task) size. Appends that would exceed the cap
are dropped.
max_total_push_configs: usizeGlobal ceiling on the total number of push configs a store may hold (per-tenant for tenant-scoped stores). Default: 100,000.
Complements max_push_configs_per_task: the per-task cap alone lets a
client mint configs for unboundedly many distinct task ids (100 each),
growing a SQL-backed table without limit. Enforced whenever the store
reports a count (see PushConfigStore::count);
stores that do not report one are unaffected.
subscribe_reattach_interval: DurationHow often a SubscribeToTask stream re-checks whether its task has
finished, once the current turn’s event queue has closed. Default: 250ms.
A task’s queue lives only as long as one executor invocation, so an
agent that parks a task in input_required closes the queue at every
turn boundary. Spec §3.1.6 requires the stream to run until a
terminal state, so it waits here for the next turn rather than
ending. Only an idle stream pays this cost — a live queue delivers
events immediately.
subscribe_max_idle: DurationHow long a SubscribeToTask stream waits for a parked task to make
progress before ending. Default: 5 minutes.
Without a bound, a task left in input_required forever would pin a
connection forever. Ending the stream is safe: §3.5.2 makes
reconnection an expected flow, and the client gets a fresh snapshot
when it resubscribes.
subscribe_replay_limit: usizeHow many logged events a resuming SubscribeToTask replays at most.
Default: 1,000.
A client reconnecting with Last-Event-ID is sent what it missed,
read from the task’s event log. The bound matters because the offset
is client-supplied: Last-Event-ID: 0 on a long-running task asks for
the whole history, and without a cap that is an unbounded read and an
unbounded burst of frames on one connection.
Truncation is not silent data loss. Every replayed frame carries its
own id:, so a client that receives the cap’s worth reconnects at the
last one and continues — the replay is resumable by the same mechanism
that started it.
subscribe_replay_catchup: DurationHow long a resuming SubscribeToTask waits for the task’s event log to
catch up with what has already been broadcast. Default: 2 seconds.
Events reach the log through the background processor, so a position can have been broadcast to live subscribers before it has been appended. A resubscribe attaches its broadcast receiver first and reads the log second; without this wait, a position broadcast just before the receiver existed and appended just after the log was read appears in neither, and the subscriber loses it with no gap it could detect.
The wait is bounded because the processor can be slow for reasons that
are not this subscriber’s problem — a registered webhook is delivered
inline, under push_delivery_budget. On expiry the replay is served
with what the log does hold and the shortfall is logged and counted
under the event_log_catchup persistence-error label, rather than
being passed off as a complete history.
Zero disables the wait.
Implementations§
Source§impl HandlerLimits
impl HandlerLimits
Sourcepub const fn effective_max_message_id_length(&self) -> usize
pub const fn effective_max_message_id_length(&self) -> usize
The bound message.id is actually held to: never below
MIN_MESSAGE_ID_LENGTH.
§Why this is not just max_id_length
It was, for one release, and that was a defect. context_id and
task_id are commonly short and often chosen by the deployment;
message.id is minted by the client and is conventionally a UUID. A
deployment tightening max_id_length to anything under 36 — which is
a reasonable thing to do for the two ids it was documented to cover —
then rejected every message from a conformant client, including
every one this SDK’s own examples send.
Caught by examples/incident-response’s handler-limits check, which
sets max_id_length to 32 and asserts that an id within the bound is
accepted. It failed on a 36-character UUID, which is exactly the
report an adopter would have filed.
The clamp rather than a separate knob, for the same reason
effective_batch_size floors a zero batch: an operator tightening a
bound should not have to know that one identifier has a protocol-
imposed floor, and a configuration that cannot serve a conformant
client is not one worth honouring exactly.
Spelled as a saturating offset from the floor rather than the
self.max_id_length > MIN_MESSAGE_ID_LENGTH it reads as, for the
reason mutants.toml records for InMemoryTaskStore::evict: >
mutates to >=, and at exactly the floor both arms return the same
number, so the weakened operator is an equivalent mutant that no
test can kill. The arithmetic below has no such form —
saturating_sub is 0 at or below the floor, which lands the sum on
the floor, and is the excess above it otherwise. Reported by the
incremental mutation gate on this pull request (shard 1 of run
35523981742); the fix is the spelling, not the logic.
Sourcepub const fn with_subscribe_reattach_interval(
self,
interval: Duration,
) -> HandlerLimits
pub const fn with_subscribe_reattach_interval( self, interval: Duration, ) -> HandlerLimits
Sets how often an idle SubscribeToTask stream re-checks its task.
Sourcepub const fn with_subscribe_max_idle(self, max_idle: Duration) -> HandlerLimits
pub const fn with_subscribe_max_idle(self, max_idle: Duration) -> HandlerLimits
Sets how long a SubscribeToTask stream waits on a parked task.
Sourcepub const fn with_subscribe_replay_limit(self, limit: usize) -> HandlerLimits
pub const fn with_subscribe_replay_limit(self, limit: usize) -> HandlerLimits
Sets how many logged events a resuming SubscribeToTask replays.
Sourcepub const fn with_subscribe_replay_catchup(
self,
catchup: Duration,
) -> HandlerLimits
pub const fn with_subscribe_replay_catchup( self, catchup: Duration, ) -> HandlerLimits
Sets how long a resuming SubscribeToTask waits for the event log to
catch up with what has already been broadcast. Zero disables the wait.
Sourcepub const fn with_max_id_length(self, length: usize) -> HandlerLimits
pub const fn with_max_id_length(self, length: usize) -> HandlerLimits
Sets the maximum allowed length for task/context IDs.
Sourcepub const fn with_max_metadata_size(self, size: usize) -> HandlerLimits
pub const fn with_max_metadata_size(self, size: usize) -> HandlerLimits
Sets the maximum serialized size for metadata fields in bytes.
Sourcepub const fn with_max_cancellation_tokens(self, max: usize) -> HandlerLimits
pub const fn with_max_cancellation_tokens(self, max: usize) -> HandlerLimits
Sets the maximum cancellation token map entries before cleanup.
Sourcepub const fn with_max_token_age(self, age: Duration) -> HandlerLimits
pub const fn with_max_token_age(self, age: Duration) -> HandlerLimits
Sets the maximum age for cancellation tokens.
Sourcepub const fn with_push_delivery_timeout(
self,
timeout: Duration,
) -> HandlerLimits
pub const fn with_push_delivery_timeout( self, timeout: Duration, ) -> HandlerLimits
Sets the timeout for individual push webhook deliveries.
Sourcepub const fn with_push_delivery_budget(self, budget: Duration) -> HandlerLimits
pub const fn with_push_delivery_budget(self, budget: Duration) -> HandlerLimits
Sets the total push-delivery budget per event (per request batch on
the blocking path). See push_delivery_budget.
Sourcepub const fn with_executor_drain_timeout(
self,
timeout: Duration,
) -> HandlerLimits
pub const fn with_executor_drain_timeout( self, timeout: Duration, ) -> HandlerLimits
Sets how long the blocking send path waits for the event queue to
close after the executor finished. See
executor_drain_timeout.
Sourcepub const fn with_max_artifacts_per_task(self, max: usize) -> HandlerLimits
pub const fn with_max_artifacts_per_task(self, max: usize) -> HandlerLimits
Sets the maximum number of artifacts per task.
Sourcepub const fn with_max_push_configs_per_task(self, max: usize) -> HandlerLimits
pub const fn with_max_push_configs_per_task(self, max: usize) -> HandlerLimits
Sets the maximum number of push notification configs per task.
Sourcepub const fn with_max_total_push_configs(self, max: usize) -> HandlerLimits
pub const fn with_max_total_push_configs(self, max: usize) -> HandlerLimits
Sets the global (per-tenant for tenant stores) ceiling on total push notification configs. Enforced only when the store reports a count.
Sourcepub const fn with_max_parts_per_artifact(self, max: usize) -> HandlerLimits
pub const fn with_max_parts_per_artifact(self, max: usize) -> HandlerLimits
Sets the maximum number of parts a single artifact may accumulate.
Sourcepub const fn with_max_context_locks(self, max: usize) -> HandlerLimits
pub const fn with_max_context_locks(self, max: usize) -> HandlerLimits
Sets the maximum number of per-context locks before cleanup.
Trait Implementations§
Source§impl Clone for HandlerLimits
impl Clone for HandlerLimits
Source§fn clone(&self) -> HandlerLimits
fn clone(&self) -> HandlerLimits
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Debug for HandlerLimits
impl Debug for HandlerLimits
Source§impl Default for HandlerLimits
impl Default for HandlerLimits
Source§fn default() -> HandlerLimits
fn default() -> HandlerLimits
Auto Trait Implementations§
impl Freeze for HandlerLimits
impl RefUnwindSafe for HandlerLimits
impl Send for HandlerLimits
impl Sync for HandlerLimits
impl Unpin for HandlerLimits
impl UnsafeUnpin for HandlerLimits
impl UnwindSafe for HandlerLimits
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> FutureExt for T
impl<T> FutureExt for T
Source§fn with_context(self, otel_cx: Context) -> WithContext<Self> ⓘ
fn with_context(self, otel_cx: Context) -> WithContext<Self> ⓘ
Source§fn with_current_context(self) -> WithContext<Self> ⓘ
fn with_current_context(self) -> WithContext<Self> ⓘ
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§impl<T> IntoRequest<T> for T
impl<T> IntoRequest<T> for T
Source§fn into_request(self) -> Request<T>
fn into_request(self) -> Request<T>
T in a tonic::Request