PDF upload and optimization database flow
Last updated: 10 September 2026
1. Result
A PDF is uploaded directly to evequality/incoming/..., recorded in MySQL, dispatched through the PDF SQS queue, optimized by a PDF Lambda using Ghostscript and qpdf, verified, and atomically published.
The final PDF key is stored in media_versions.object_key. media_assets.active_version_id points to the live version. Publication also writes the full CDN URL to the legacy pdfSolutions.pdflink column.
If the PDF is too large for the configured Lambda contract or the process times out, the same job is rerouted to the scale-to-zero EC2 heavy worker.
This document describes implemented code and Terraform, not a claim that these resources are already live.
2. Main AWS and database flow
3. Database identity
Each legacy pdfSolutions row has two independent assets:
media_assets.media_type | Role | Legacy field updated after publication |
|---|---|---|
pdf | content | pdfSolutions.pdflink |
image | thumbnail | pdfSolutions.pdflogo |
For pdfSolutions.id = 70, the PDF content identity is:
1legacy_table = pdfSolutions
2legacy_id = 70
3role = contentmedia_assets.external_code contains pdfSolutions.pdf_code. A replacement PDF keeps the same asset but advances its source revision and creates a new version.
4. Stage-by-stage database changes
Stage A — authorize direct upload
The admin UI calls POST /admin/api/media/uploads. Laravel checks pdfs.update, requires .pdf plus application/pdf, validates the declared size and creates:
| Table | Fields written |
|---|---|
media_uploads | media asset/admin/revision, shared bucket, temporary key, original filename, expected bytes/MIME/checksum, optional multipart contract, status = authorized, expiry |
admin_audit_events | media_upload.authorized |
Temporary key example:
1incoming/pdf/{media_asset_ulid}/revision-{number}/{upload_ulid}.pdfAt the default 100 MiB multipart threshold, the browser uses signed per-part requests. All bytes travel directly from browser to S3.
Stage B — verify upload and create work
The confirmation endpoint completes multipart upload where applicable, reads S3 metadata and validates size, MIME, checksum and the real PDF signature.
| Table | Transition or write |
|---|---|
media_uploads | authorized -> uploaded -> confirmed; sets confirmation and S3 version fields |
media_assets | advances source_revision |
media_jobs | inserts media_type = pdf, profile_version = pdf-v1, processor = pdf_lambda, status = pending, generation 1, default three attempts |
outbox_messages | inserts pending media.job.requested for the PDF queue |
admin_audit_events | records media_upload.confirmed and job ID |
Job creation is idempotent for asset, source revision and profile.
Stage C — outbox and PDF SQS
Every minute, the outbox dispatcher publishes pending work:
1outbox_messages: pending -> dispatching -> dispatched
2media_jobs: pending -> dispatchedThe PDF queue is encrypted, long-polls for 20 seconds, retains messages for four days, has a 360-second visibility timeout and redrives after three receives to a DLQ retained for 14 days.
The PDF Lambda event-source mapping has batch size one and partial-batch failure reporting.
Stage D — Lambda attempt and lease
The ARM64 PDF Lambda configuration is:
| Setting | Value |
|---|---|
| Reserved concurrency | 1 |
| Memory | 2,048 MiB |
| Ephemeral storage | 2,048 MiB |
| Function/application timeout | 300 seconds |
| Default Lambda source-size limit | 100 MiB |
When the function claims work:
| Table | Transition or write |
|---|---|
media_job_attempts | inserts attempt number, processor = pdf_lambda, status = claimed, lease UUID, Lambda owner and lease timestamps |
media_jobs | increments attempts; dispatched -> processing |
media_job_attempts | claimed -> processing |
The local work directory must have at least three times the source size free, with a 100 MB floor.
Stage E — PDF optimization and validation
The processor performs these operations without rasterizing every page:
qpdf --checkvalidates the source structure.qpdf --show-encryptionrejects encrypted PDFs.pdfinforecords the source page count.- Ghostscript uses the
/ebookprofile, PDF 1.6, duplicate-image detection and font compression. - qpdf linearizes the result for faster web access.
- If the optimized file is not smaller, the safe original is retained as the final
document.pdfinstead of publishing a larger file. - qpdf validates the output and
pdfinfoproves that the page count did not change.
The result is uploaded as:
1pdf/{board-class}/{subject}/{service}/{unit}/{chapter}/solution-{id}-{pdf_code}/document.pdfLater replacements can be isolated under versions/r{revision}. The verifier checks object existence, non-empty size, PDF signature and positive unchanged page count.
Stage F — publication transaction
The publisher first rejects stale leases, old generations, superseded source revisions, blocked assets and cancelled jobs. It then commits:
| Table | Fields or transition |
|---|---|
media_jobs | processing -> publish_pending -> published; clears error/availability |
media_versions | inserts format = pdf, processor, final object_key, object_prefix, bytes, source/final page metadata, verification_status = verified, verification and publication timestamps |
media_assets | sets active_version_id to the new PDF version |
pdfSolutions | sets pdflink = {EVE_MEDIA_DOMAIN}/{media_versions.object_key} |
media_job_attempts | processing -> succeeded; sets completion time |
publication_audits | records previous/new version, job, attempt, generation and actor |
Example:
1media_versions.object_key = pdf/wb12/.../solution-70-PDF001/document.pdf
2pdfSolutions.pdflink = https://evevideoquality.rayandmartin.in/pdf/wb12/.../solution-70-PDF001/document.pdfThe new version table stores a relative key so delivery-domain changes do not require rewriting every version. The legacy field keeps the full URL for old consumers.
Important media_versions fields
| Field | Meaning |
|---|---|
object_key | Primary relative S3 key ending in document.pdf |
object_prefix | Directory containing the published PDF |
metadata.object_keys | Verified output inventory; currently one PDF key |
file_size_bytes | Published PDF size |
media_job_attempt_id | Relation to the producing attempt, not a PDF URL |
inventory_object_key | Currently null; the inventory is embedded in metadata.object_keys |
5. PDF status timeline
The uploaded -> authorized recovery clears confirmation_started_at so the administrator can correct/retry confirmation before the authorization expires.
6. Heavy-worker fallback
If media_uploads.expected_bytes exceeds the default 100 MiB Lambda limit, or the Lambda processing command times out:
| Table | Change |
|---|---|
media_job_attempts | Lambda attempt becomes failed, error_code = heavy_fallback, reason stored in error_context |
media_jobs | processor becomes heavy_worker, status becomes pending, generation increases, retry budget extends, fallback_reason is set |
outbox_messages | a new media.job.heavy_fallback.{generation} message targets the video/heavy queue |
The CloudWatch video-queue alarm or one-minute reconciler starts the conversion ASG at desired capacity one. The EC2 worker claims a new attempt and runs the same PdfProcessor, but with the longer heavy-media timeout. Old-generation messages cannot activate output.
7. Table ownership summary
| Table | PDF responsibility |
|---|---|
pdfSolutions | Legacy catalogue and full PDF/thumbnail links |
media_assets | Stable content/thumbnail identity and active version pointer |
media_uploads | Temporary original and integrity/multipart contract |
media_jobs | PDF Lambda or heavy-worker lifecycle |
media_job_attempts | Durable processor lease and attempt result |
outbox_messages | Reliable PDF/heavy SQS publication |
media_versions | Verified PDF version history and final object key |
publication_audits | Publication and rollback history |
admin_audit_events | Admin authorization/confirmation audit |
8. Failure, retry, rollback and cleanup
- Structural, encryption, page-count, command, disk or upload failures are stored on
media_job_attempts.error_code; bounded context is stored inerror_context. - With retry budget remaining,
media_jobsreturns todispatchedwith exponential delay and SQS retries the message. - Exhausted jobs become
failed; SQS moves repeatedly failed messages to the PDF DLQ after three receives. - Candidate output from a failed attempt is deleted best-effort.
- The one-minute recovery command marks dead attempts
lease_expiredand retries only within budget. - Manual retry increases generation and creates a fresh outbox row, preventing old messages from publishing.
- Rollback first verifies the stored PDF object and metadata, then changes
active_version_id, updatespdflinkand creates a publication audit. - The hourly source purger deletes
incoming/pdf/...only after the same revision is verified, published and active for the configured retention period, default 24 hours. - Cleanup progress/failure is stored in
media_uploads.source_delete_started_at,source_deleted_atandsource_delete_error_code.
9. PDF thumbnail
The PDF processor does not generate a thumbnail. pdfSolutions.pdflogo belongs to a separate image asset (media_type = image, role = thumbnail). Its upload, Sharp variants, image queue and publication are described in the image and thumbnail upload flow.