api: throttle streaming admin read polls

This commit is contained in:
a.tolmachev
2026-05-02 09:44:28 +00:00
parent c597557a5e
commit 4839cae319
5 changed files with 285 additions and 4 deletions
+106 -1
View File
@@ -1585,6 +1585,9 @@ impl AdminService {
)
})?;
ensure_stream_session_workspace(&session, workspace_id)?;
let session = self
.touch_stream_session_poll_if_ready(workspace_id, session)
.await?;
Ok(stream_session_detail_view(session))
}
@@ -1665,6 +1668,11 @@ impl AdminService {
)
})?;
ensure_async_job_workspace(&job, workspace_id)?;
let job = if matches!(job.status, JobStatus::Completed) {
job
} else {
self.touch_async_job_poll_if_ready(workspace_id, job).await?
};
Ok(async_job_detail_view(job))
}
@@ -1710,6 +1718,7 @@ impl AdminService {
)
})?;
ensure_async_job_workspace(&job, workspace_id)?;
let job = self.touch_async_job_poll_if_ready(workspace_id, job).await?;
match job.status {
JobStatus::Completed => Ok(job.result.unwrap_or(Value::Null)),
@@ -1745,6 +1754,102 @@ impl AdminService {
}
}
async fn touch_stream_session_poll_if_ready(
&self,
workspace_id: &WorkspaceId,
session: StreamSession,
) -> Result<StreamSession, ApiError> {
let poll_interval_ms = self
.streaming_poll_interval_ms(workspace_id, &session.operation_id)
.await?;
let now = OffsetDateTime::now_utc();
let remaining_delay_ms = session.remaining_poll_delay_ms(now, poll_interval_ms);
if remaining_delay_ms > 0 {
return Err(ApiError::rate_limited_with_context(
"stream session poll rate limit exceeded",
json!({
"session_id": session.id.as_str(),
"poll_after_ms": remaining_delay_ms,
}),
));
}
self.registry
.touch_stream_session_poll(&session.id, &now)
.await
.map_err(ApiError::from)
}
async fn touch_async_job_poll_if_ready(
&self,
workspace_id: &WorkspaceId,
job: AsyncJobHandle,
) -> Result<AsyncJobHandle, ApiError> {
let poll_interval_ms = self
.streaming_poll_interval_ms(workspace_id, &job.operation_id)
.await?;
let now = OffsetDateTime::now_utc();
let remaining_delay_ms = job.remaining_poll_delay_ms(now, poll_interval_ms);
if remaining_delay_ms > 0 {
return Err(ApiError::rate_limited_with_context(
"async job poll rate limit exceeded",
json!({
"job_id": job.id.as_str(),
"poll_after_ms": remaining_delay_ms,
}),
));
}
self.registry
.touch_async_job_poll(&job.id, &now)
.await
.map_err(ApiError::from)
}
async fn streaming_poll_interval_ms(
&self,
workspace_id: &WorkspaceId,
operation_id: &OperationId,
) -> Result<u64, ApiError> {
let summary = self
.registry
.get_operation_summary(workspace_id, operation_id)
.await?
.ok_or_else(|| {
ApiError::not_found_with_context(
format!("operation {} was not found", operation_id.as_str()),
json!({ "operation_id": operation_id.as_str() }),
)
})?;
let version = self
.registry
.get_operation_version(workspace_id, operation_id, summary.current_draft_version)
.await?
.ok_or_else(|| {
ApiError::not_found_with_context(
format!(
"operation version {} for {} was not found",
summary.current_draft_version,
operation_id.as_str()
),
json!({
"operation_id": operation_id.as_str(),
"version": summary.current_draft_version,
}),
)
})?;
Ok(version
.snapshot
.execution_config
.streaming
.as_ref()
.and_then(|streaming| streaming.poll_interval_ms)
.unwrap_or(1_000))
}
pub async fn list_operations(
&self,
workspace_id: &WorkspaceId,
@@ -2354,7 +2459,7 @@ impl AdminService {
batch_size,
}),
expires_at,
last_poll_at: Some(created_at),
last_poll_at: None,
created_at,
closed_at: None,
};