Batch & Async Signing
Sign many PDFs in parallel with a result per input, and offload signing to Tokio's blocking pool
Bulk signing — invoices, payroll, certificates — is CPU-bound work that parallelises well.
sign_batch runs one signer and one config over many PDFs across a small thread pool and returns a
Result per input, so one bad document never aborts the batch. The tokio feature adds async
wrappers for the same work inside an async server.
Parallel batch — sign_batch
use mudrit_keystore::PfxSigner;
use mudrit_pdfsign::{sign_batch, BatchOptions, SignConfig};
let signer = PfxSigner::from_file("samples/ABC12.pfx", "ABC12")?;
let cfg = SignConfig::builder().place("F", [350, 60, 560, 160])?.build();
let inputs: Vec<Vec<u8>> = vec![std::fs::read("a.pdf")?, std::fs::read("b.pdf")?];
let results = sign_batch(&inputs, &signer, &cfg, &BatchOptions::default());
for (i, r) in results.iter().enumerate() {
match r {
Ok(pdf) => println!("#{i}: {} bytes", pdf.len()),
Err(e) => eprintln!("#{i}: {e}"), // this input failed; the rest still succeeded
}
}The results come back in the same order as inputs. A failing document yields an Err in its
slot — the batch never aborts on one error (failure isolation).
Options — BatchOptions
| Option | Default | Effect |
|---|---|---|
threads | available CPUs | Worker thread count (clamped to at least 1). Set with BatchOptions::threads(n) |
One hardware token → threads(1)
Parallel signing is safe for software signers (PfxSigner — RSA/ECDSA signing is reentrant). A
single hardware token, though, has one PKCS#11 session that is not safe for concurrent signing.
For one token use BatchOptions::threads(1) — the work is then sequential but still gets per-item
results. With several tokens, sign each token's batch on its own.
let opts = BatchOptions::default().threads(1); // serialize onto the single tokenThe + Sync bound
sign_batch shares signer across worker threads, so it must be Sync. The Signer trait
requires only Send (a live PKCS#11 session is Send but not Sync), so the parallel paths add the
+ Sync bound at the use site rather than forcing it on every backend. &PfxSigner satisfies it
directly. For a type-erased signer, wrap it:
let signed = sign_batch(&inputs, &signer as &(dyn Signer + Sync), &cfg, &opts);Async — sign_pdf_async / sign_batch_async
Signing is inherently blocking — CPU-bound crypto, and for hardware backends a synchronous
PKCS#11 / CNG call. The async wrappers (feature tokio) don't pretend otherwise: they offload the
blocking work onto Tokio's blocking thread-pool (spawn_blocking) so it never stalls an async
runtime. That is the honest, correct shape for an axum / actix server — no executor thread is
blocked, and you avoid the easy mistake of calling the blocking API directly from an async task.
use std::sync::Arc;
use mudrit_keystore::PfxSigner;
use mudrit_pdfsign::{sign_pdf_async, sign_batch_async, SignConfig, BatchOptions};
let signer = Arc::new(PfxSigner::from_file("samples/ABC12.pfx", "ABC12")?);
let cfg = SignConfig::builder().place("F", [350, 60, 560, 160])?.build();
// one document
let signed = sign_pdf_async(std::fs::read("in.pdf")?, signer.clone(), cfg.clone()).await?;
// a whole batch
let results = sign_batch_async(inputs, signer, cfg, BatchOptions::default()).await?;The signer is shared as an Arc
Because the work runs on another thread, the signer must be Send + Sync + 'static and is passed as
Arc<S>. For a type-erased signer use Arc<dyn Signer + Send + Sync> as S. sign_batch_async
resolves to the same per-input Vec<Result<...>> as the blocking sign_batch.