refactor: enhance batch processing for cipher operations

This commit is contained in:
qaz741wsd856 2025-11-30 04:31:45 +00:00
parent de43671573
commit d107ba8a28
4 changed files with 69 additions and 28 deletions

View file

@ -159,7 +159,7 @@ You can configure the following environment variables in `wrangler.toml` under t
* **`IMPORT_BATCH_SIZE`** (Optional, Default: `30`) * **`IMPORT_BATCH_SIZE`** (Optional, Default: `30`)
Number of records to process in each batch when importing data. This helps manage memory usage and processing time for large imports. Number of records to process in each batch when importing and deleting data. This helps manage memory usage and processing time for large imports.
* Set to `0` to disable batching (all records imported in a single batch) * Set to `0` to disable batching (all records imported in a single batch)
* Defaults to `30` records per batch if not specified * Defaults to `30` records per batch if not specified

View file

@ -1,9 +1,10 @@
use super::get_batch_size;
use axum::{extract::State, Json}; use axum::{extract::State, Json};
use chrono::Utc; use chrono::Utc;
use serde::Deserialize; use serde::Deserialize;
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
use worker::{query, Env}; use worker::{query, D1PreparedStatement, Env};
use crate::auth::Claims; use crate::auth::Claims;
use crate::db; use crate::db;
@ -11,6 +12,27 @@ use crate::error::AppError;
use crate::models::cipher::{Cipher, CipherData, CipherRequestData, CreateCipherRequest}; use crate::models::cipher::{Cipher, CipherData, CipherRequestData, CreateCipherRequest};
use axum::extract::Path; use axum::extract::Path;
/// Execute D1 statements in batches, allowing batch_size 0 to run everything at once.
async fn execute_in_batches(
db: &worker::D1Database,
statements: Vec<D1PreparedStatement>,
batch_size: usize,
) -> Result<(), AppError> {
if statements.is_empty() {
return Ok(());
}
if batch_size == 0 {
db.batch(statements).await?;
} else {
for chunk in statements.chunks(batch_size) {
db.batch(chunk.to_vec()).await?;
}
}
Ok(())
}
#[worker::send] #[worker::send]
pub async fn create_cipher( pub async fn create_cipher(
claims: Claims, claims: Claims,
@ -198,20 +220,24 @@ pub async fn soft_delete_ciphers_bulk(
) -> Result<Json<()>, AppError> { ) -> Result<Json<()>, AppError> {
let db = db::get_db(&env)?; let db = db::get_db(&env)?;
let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(); let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
let batch_size = get_batch_size(&env);
let mut statements = Vec::with_capacity(payload.ids.len());
for id in payload.ids { for id in payload.ids {
query!( let stmt = query!(
&db, &db,
"UPDATE ciphers SET deleted_at = ?1, updated_at = ?1 WHERE id = ?2 AND user_id = ?3", "UPDATE ciphers SET deleted_at = ?1, updated_at = ?1 WHERE id = ?2 AND user_id = ?3",
now, now,
id, id,
claims.sub claims.sub
) )
.map_err(|_| AppError::Database)? .map_err(|_| AppError::Database)?;
.run()
.await?; statements.push(stmt);
} }
execute_in_batches(&db, statements, batch_size).await?;
Ok(Json(())) Ok(Json(()))
} }
@ -246,19 +272,23 @@ pub async fn hard_delete_ciphers_bulk(
Json(payload): Json<CipherIdsData>, Json(payload): Json<CipherIdsData>,
) -> Result<Json<()>, AppError> { ) -> Result<Json<()>, AppError> {
let db = db::get_db(&env)?; let db = db::get_db(&env)?;
let batch_size = get_batch_size(&env);
let mut statements = Vec::with_capacity(payload.ids.len());
for id in payload.ids { for id in payload.ids {
query!( let stmt = query!(
&db, &db,
"DELETE FROM ciphers WHERE id = ?1 AND user_id = ?2", "DELETE FROM ciphers WHERE id = ?1 AND user_id = ?2",
id, id,
claims.sub claims.sub
) )
.map_err(|_| AppError::Database)? .map_err(|_| AppError::Database)?;
.run()
.await?; statements.push(stmt);
} }
execute_in_batches(&db, statements, batch_size).await?;
Ok(Json(())) Ok(Json(()))
} }
@ -318,23 +348,28 @@ pub async fn restore_ciphers_bulk(
) -> Result<Json<BulkRestoreResponse>, AppError> { ) -> Result<Json<BulkRestoreResponse>, AppError> {
let db = db::get_db(&env)?; let db = db::get_db(&env)?;
let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(); let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
let batch_size = get_batch_size(&env);
let ids = payload.ids;
let mut update_statements = Vec::with_capacity(ids.len());
let mut restored_ciphers = Vec::new(); for id in ids.iter() {
let stmt = query!(
for id in payload.ids {
// Update the cipher to clear deleted_at
query!(
&db, &db,
"UPDATE ciphers SET deleted_at = NULL, updated_at = ?1 WHERE id = ?2 AND user_id = ?3", "UPDATE ciphers SET deleted_at = NULL, updated_at = ?1 WHERE id = ?2 AND user_id = ?3",
now, now,
id, id.clone(),
claims.sub claims.sub
) )
.map_err(|_| AppError::Database)? .map_err(|_| AppError::Database)?;
.run()
.await?;
// Fetch the restored cipher update_statements.push(stmt);
}
execute_in_batches(&db, update_statements, batch_size).await?;
let mut restored_ciphers = Vec::with_capacity(ids.len());
for id in ids {
let cipher_db: Option<crate::models::cipher::CipherDBModel> = query!( let cipher_db: Option<crate::models::cipher::CipherDBModel> = query!(
&db, &db,
"SELECT * FROM ciphers WHERE id = ?1 AND user_id = ?2", "SELECT * FROM ciphers WHERE id = ?1 AND user_id = ?2",

View file

@ -11,14 +11,7 @@ use crate::models::cipher::{Cipher, CipherData};
use crate::models::folder::Folder; use crate::models::folder::Folder;
use crate::models::import::ImportRequest; use crate::models::import::ImportRequest;
/// Get the batch size from environment variable IMPORT_BATCH_SIZE. use super::{get_batch_size};
/// Defaults to 30 if not set or invalid.
fn get_batch_size(env: &Env) -> usize {
env.var("IMPORT_BATCH_SIZE")
.ok()
.and_then(|v| v.to_string().parse::<usize>().ok())
.unwrap_or(30)
}
/// Execute statements in batches. If batch_size is 0, execute all in one batch. /// Execute statements in batches. If batch_size is 0, execute all in one batch.
async fn execute_in_batches( async fn execute_in_batches(

View file

@ -6,3 +6,16 @@ pub mod sync;
pub mod folders; pub mod folders;
pub mod import; pub mod import;
pub mod purge; pub mod purge;
/// Shared helper for reading an environment variable into usize.
pub(crate) fn get_env_usize(env: &worker::Env, var_name: &str, default: usize) -> usize {
env.var(var_name)
.ok()
.and_then(|value| value.to_string().parse::<usize>().ok())
.unwrap_or(default)
}
/// Convenience helper for cipher batch size using IMPORT_BATCH_SIZE.
pub(crate) fn get_batch_size(env: &worker::Env) -> usize {
get_env_usize(env, "IMPORT_BATCH_SIZE", 30)
}