Limit the event response size

This commit is contained in:
Andras Schmelczer 2026-09-22 20:16:04 +01:00
commit 3d71abcca1
3 changed files with 34 additions and 5 deletions

View file

@ -7,6 +7,8 @@ use super::{
}; };
use anyhow::{Context as _, Result, ensure}; use anyhow::{Context as _, Result, ensure};
use crate::consts::MAX_EVENTS_PAGE_BYTES;
impl Database { impl Database {
pub async fn get_missing_document( pub async fn get_missing_document(
tx: &mut Transaction<'_>, tx: &mut Transaction<'_>,
@ -172,7 +174,12 @@ impl Database {
}) })
} }
pub async fn events_after(&self, vault: &VaultId, after: VaultUpdateId) -> Result<EventBatch> { pub async fn events_after(
&self,
vault: &VaultId,
after: VaultUpdateId,
page_size: usize,
) -> Result<EventBatch> {
let mut tx = self.create_readonly_transaction(vault).await?; let mut tx = self.create_readonly_transaction(vault).await?;
let head_event_id = Self::latest_event_id(&mut tx).await?; let head_event_id = Self::latest_event_id(&mut tx).await?;
@ -182,23 +189,36 @@ impl Database {
); );
let rows: Vec<(VaultUpdateId, String)> = sqlx::query_as( let rows: Vec<(VaultUpdateId, String)> = sqlx::query_as(
"SELECT event_id, request_id FROM events WHERE event_id > ? ORDER BY event_id", "SELECT event_id, request_id FROM events WHERE event_id > ? ORDER BY event_id LIMIT ?",
) )
.bind(after) .bind(after)
.bind(i64::try_from(page_size)?)
.fetch_all(&mut *tx) .fetch_all(&mut *tx)
.await?; .await?;
let mut events = Vec::with_capacity(rows.len()); let mut events = Vec::with_capacity(rows.len());
let mut bytes = 0;
let mut end_event_id = after;
for (event_id, request_id) in rows { for (event_id, request_id) in rows {
events.push(EventRecord { let event = EventRecord {
event_id, event_id,
request_id: request_id.parse()?, request_id: request_id.parse()?,
event: Self::event_by_id(&mut tx, event_id).await?, event: Self::event_by_id(&mut tx, event_id).await?,
}); };
let size = serde_json::to_vec(&event)?.len();
// One manifest is atomic and may exceed this budget. Always allow
// one event so even such a page advances; never accumulate history.
if !events.is_empty() && bytes + size > MAX_EVENTS_PAGE_BYTES {
break;
}
bytes += size;
end_event_id = event_id;
events.push(event);
} }
Ok(EventBatch { Ok(EventBatch {
head_event_id, head_event_id,
end_event_id: Some(end_event_id),
events, events,
}) })
} }

View file

@ -21,4 +21,7 @@ pub const WEBSOCKET_SEND_TIMEOUT: Duration = Duration::from_secs(10);
pub const DEFAULT_MERGEABLE_FILE_EXTENSIONS: &[&str] = &["md", "txt"]; pub const DEFAULT_MERGEABLE_FILE_EXTENSIONS: &[&str] = &["md", "txt"];
pub const DEFAULT_EVENTS_PAGE_SIZE: usize = 64;
pub const MAX_EVENTS_PAGE_BYTES: usize = 1024 * 1024;
pub const SUPPORTED_API_VERSION: u32 = 4; pub const SUPPORTED_API_VERSION: u32 = 4;

View file

@ -8,12 +8,14 @@ use serde::Deserialize;
use super::VaultPath; use super::VaultPath;
use crate::{ use crate::{
app_state::{AppState, database::models::EventBatch}, app_state::{AppState, database::models::EventBatch},
consts::DEFAULT_EVENTS_PAGE_SIZE,
errors::{SyncServerError, client_error, server_error}, errors::{SyncServerError, client_error, server_error},
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct EventsQuery { pub struct EventsQuery {
pub after: i64, pub after: i64,
pub page_size: Option<usize>,
} }
#[axum::debug_handler] #[axum::debug_handler]
@ -33,7 +35,11 @@ pub async fn events(
state state
.database .database
.events_after(&vault_id, query.after) .events_after(
&vault_id,
query.after,
query.page_size.unwrap_or(DEFAULT_EVENTS_PAGE_SIZE),
)
.await .await
.map(Json) .map(Json)
.map_err(server_error) .map_err(server_error)