Skip to content

ECHO-465 Non-blocking uploads with presigned URLs + Kubernetes worker optimization#318

Merged
dtrn2048 merged 26 commits intomainfrom
feat/nonblocking-uploads
Oct 7, 2025
Merged

ECHO-465 Non-blocking uploads with presigned URLs + Kubernetes worker optimization#318
dtrn2048 merged 26 commits intomainfrom
feat/nonblocking-uploads

Conversation

@dtrn2048
Copy link
Copy Markdown
Contributor

@dtrn2048 dtrn2048 commented Oct 3, 2025

Summary

Implements non-blocking file uploads using S3 presigned URLs and reconfigures workers for Kubernetes horizontal pod autoscaling.

Changes

Non-blocking Upload System

  • Frontend: New uploadConversationChunkWithPresignedUrl function
  • Backend: Endpoints for presigned URL generation and upload confirmation
  • API server no longer handles file transfers
  • Direct client to S3 uploads
  • Retry logic: 3 attempts with exponential backoff
  • Rate limiting: 10 requests/min per conversation
  • Progress tracking through upload stages

Worker Configuration

  • API: Switched from uvicorn to gunicorn (2 workers/pod)
  • Network workers: 3×50 → 2×20 threads per pod
  • CPU workers: 4×6 → 2×4 threads per pod
  • All settings configurable via environment variables:
    • API_WORKERS=2
    • NETWORK_WORKER_PROCESSES=2, THREADS=20
    • CPU_WORKER_PROCESSES=2, THREADS=4

Other

  • Fixed DIRECTUS_PUBLIC_URL handling for absolute/relative URLs
  • Added dependencies: gunicorn, trankit, torch
  • Updated vite proxy to localhost:8001

Files Changed

Backend:

  • dembrane/api/participant.py: Presigned URL endpoints
  • dembrane/s3.py: URL generation logic
  • dembrane/service/conversation.py: Chunk handling updates
  • prod*.sh: Worker configuration scripts
  • pyproject.toml: Dependencies

Frontend:

  • lib/api.ts: Upload implementation
  • config.ts: URL handling
  • vite.config.ts: Proxy config

Notes

  • No breaking changes to existing upload functionality
  • Worker settings now environment-configurable
  • Risk: LOW, easy rollback

Commits:

  • feat: optimize workers for Kubernetes horizontal scaling
  • Update configuration and dependencies

Summary by CodeRabbit

  • New Features

    • Direct-to-storage uploads via presigned POSTs with per-file progress, retries, finish hook, and retained legacy upload path; server endpoints to request and confirm uploads with per-conversation rate limiting.
  • Bug Fixes

    • Chunk creation now records file URLs consistently; safer URL logging and stricter filename/key validation to prevent path traversal; clearer upload/confirmation error handling.
  • Chores

    • Kubernetes-friendly startup scripts, switched API runtime to Gunicorn and added dependency, improved public-URL resolution and dev proxy target, expanded .gitignore, and NER anonymization pipeline disabled.

- Added .env and notes.md to .gitignore to prevent sensitive files from being tracked.
- Changed API proxy target in vite.config.ts from localhost:8000 to localhost:8001.
- Enhanced DIRECTUS_PUBLIC_URL logic in config.ts to support both absolute and relative URLs.
- Added trankit and torch as dependencies in pyproject.toml and updated requirements files accordingly, ensuring compatibility with existing packages.
- API: Switch from uvicorn to gunicorn (2 workers/pod)
- Network workers: 3×50 → 2×20 (per pod)
- CPU workers: 4×6 → 2×4 (per pod)
- Add gunicorn==21.2.* dependency
- All configurations via environment variables
- Designed for K8s horizontal pod scaling

Configuration:
- API_WORKERS=2 (default)
- NETWORK_WORKER_PROCESSES=2, THREADS=20 (default)
- CPU_WORKER_PROCESSES=2, THREADS=4 (default)

All values configurable via env vars without code changes.

Risk: LOW - No breaking changes, easy rollback
Tested: Shell syntax validated, dependencies verified
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Oct 3, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds an S3 presigned‑post upload flow (frontend + backend) with confirm endpoint and rate limiting, hardens S3 key validation, adjusts frontend config and Vite proxy, makes runtime scripts Kubernetes‑friendly, adds Gunicorn dependency, and disables the NER/anonymization pipeline. Also updates .gitignore.

Changes

Cohort / File(s) Summary of changes
Ignore rules
\.gitignore
Added ignore patterns: *.env and notes_*.md.
Frontend config & proxy
echo/frontend/src/config.ts, echo/frontend/vite.config.ts
DIRECTUS_PUBLIC_URL now computed via IIFE (resolves absolute vs relative with fallback); Vite /api proxy target changed http://localhost:8000/http://localhost:8001/.
Frontend uploads (presigned)
echo/frontend/src/lib/api.ts
New presigned POST upload implementation: uploadConversationChunkWithPresignedUrl (presign request, S3 POST upload with retries/backoff, progress reporting, confirm step). uploadConversationChunk now delegates to presigned flow; legacy proxied upload retained as uploadConversationChunkLegacy. Multi-chunk orchestrator updated to use per‑chunk presigned uploads and finish-hook wiring.
Backend API (presigned endpoints)
echo/server/dembrane/api/participant.py
Added endpoints/models: get_chunk_upload_url (rate‑limited) returns presigned POST upload_url, fields, file_url; confirm_chunk_upload verifies object existence (retry/backoff) and creates chunk records. Maintains error mappings and logging.
S3 utilities & validation
echo/server/dembrane/s3.py
Added generate_presigned_post(file_name, content_type, ...) returning url, fields, and key; strengthened get_sanitized_s3_key against path traversal and leading slashes; presigned POST includes ACL, Content-Type, and content-length-range.
Conversation service behavior / logging
echo/server/dembrane/service/conversation.py
Added module logger and sanitize_url_for_logging; unify chunk path to always use file_url; require either file or transcript; only enqueue background processing when file present and log presigned vs proxied upload paths.
NER / anonymization disabled
echo/server/dembrane/ner.py, echo/server/dembrane/quote_utils.py
Trankit/Torch anonymization code commented out/disabled; anonymize_sentence removed from active use; quote generation uses raw transcripts.
Prod / runtime scripts (Kubernetes-ready)
echo/server/prod.sh, echo/server/prod-worker.sh, echo/server/prod-worker-cpu.sh, echo/server/run-worker.sh, echo/server/run-worker-cpu.sh
Replace hard-coded startup commands with env-configurable Gunicorn/Dramatiq invocations, add startup logging and configurable workers/processes/threads, adjust defaults and add watch flags where applicable.
Dependencies / pyproject
echo/server/pyproject.toml
Added runtime dependency gunicorn; removed trankit and its mypy override block.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

LGTM.

Pre-merge checks and finishing touches

❌ Failed checks (3 warnings)
Check name Status Explanation Resolution
Linked Issues Check ⚠️ Warning The implementation correctly adds presign and confirm endpoints with retry and progress handling and persists metadata on confirm, but it does not provide an abort mechanism for the client upload and lacks explicit metrics instrumentation for latency and error rates as defined in the acceptance criteria. Introduce a client-side abort capability in the upload flow and add basic metrics or counters to expose presign and upload latencies as well as failure rates.
Out of Scope Changes Check ⚠️ Warning The pull request includes multiple unrelated changes beyond the presigned upload feature, such as gitignore tweaks, DIRECTUS_PUBLIC_URL logic, anonymization removal, Vite proxy updates, and Kubernetes worker script modifications that fall outside the linked issue’s scope. Separate the unrelated infrastructural and service configuration updates into dedicated pull requests to isolate and review the presigned upload functionality independently.
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly references the two core changes—presigned URL uploads and Kubernetes worker optimization—while including the ticket number for context, making it clear, concise, and focused on the main updates.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/nonblocking-uploads

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 36a01c0 and 9fb1f01.

⛔ Files ignored due to path filters (2)
  • echo/server/requirements-dev.lock is excluded by !**/*.lock
  • echo/server/requirements.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • echo/frontend/src/lib/api.ts (3 hunks)
  • echo/server/dembrane/api/participant.py (4 hunks)
  • echo/server/dembrane/quote_utils.py (2 hunks)
  • echo/server/run-worker-cpu.sh (1 hunks)
  • echo/server/run-worker.sh (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-03T13:45:53.929Z
Learnt from: dtrn2048
PR: Dembrane/echo#318
File: echo/server/dembrane/api/participant.py:96-124
Timestamp: 2025-10-03T13:45:53.929Z
Learning: In `echo/server/dembrane/api/participant.py`, the in-memory rate limiter for presigned URL generation (`check_rate_limit`) is intentionally process-local (not Redis-backed). This is acceptable because users are authenticated municipal employees (paid customers), normal usage is 6-10 req/min, and rate limit violations are monitored. An upgrade path to Redis-based limiting (slowapi) is documented in `notes_PRESIGNED_URL_IMPLEMENTATION.md` if abuse is detected.

Applied to files:

  • echo/server/dembrane/api/participant.py
📚 Learning: 2025-10-06T16:19:09.311Z
Learnt from: dtrn2048
PR: Dembrane/echo#318
File: echo/server/dembrane/quote_utils.py:18-18
Timestamp: 2025-10-06T16:19:09.311Z
Learning: In the echo repository, the anonymization feature using trankit + torch for PII redaction in echo/server/dembrane/ner.py and echo/server/dembrane/quote_utils.py was intentionally disabled (imports commented out) as of PR #318 because the feature is not ready. This is a temporary state accepted by the maintainer dtrn2048.

Applied to files:

  • echo/server/dembrane/quote_utils.py
🧬 Code graph analysis (1)
echo/server/dembrane/api/participant.py (3)
echo/server/dembrane/s3.py (3)
  • get_sanitized_s3_key (200-236)
  • get_file_size_bytes_from_s3 (251-254)
  • generate_presigned_post (154-197)
echo/server/dembrane/utils.py (2)
  • generate_uuid (13-14)
  • get (67-79)
echo/server/dembrane/service/conversation.py (5)
  • ConversationServiceException (28-29)
  • get_by_id_or_raise (53-90)
  • ConversationNotFoundException (32-33)
  • ConversationNotOpenForParticipationException (36-37)
  • create_chunk (213-308)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: ci-check-server
🔇 Additional comments (3)
echo/server/run-worker.sh (1)

1-3: Add robust signal handling and env-configurability

  • Include set -euo pipefail for safer execution
  • Allow NETWORK_PROCESSES, NETWORK_THREADS, and NETWORK_QUEUE env vars with sane defaults (2, 20, network)
  • Use exec so signals propagate correctly in containers

Confirm that the NETWORK_THREADS default of 20 aligns with ECHO-465.

echo/server/run-worker-cpu.sh (1)

1-3: Use exec with strict mode and env-configurable defaults for CPU worker

  • Enable set -euo pipefail and replace shell with exec for proper SIGTERM/SIGINT handling in containers.
  • Make CPU_PROCESSES, CPU_THREADS, and CPU_QUEUE configurable with defaults (2, 4, cpu).
  • Confirm that 4 threads is the intended default per ECHO-465.
-#!/usr/bin/env bash
-
-dramatiq --queues cpu --processes 2 --threads 1 dembrane.tasks
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+PROCESSES="${CPU_PROCESSES:-2}"
+THREADS="${CPU_THREADS:-4}"
+QUEUE="${CPU_QUEUE:-cpu}"
+
+exec dramatiq --queues "$QUEUE" --processes "$PROCESSES" --threads "$THREADS" dembrane.tasks

LGTM.

echo/frontend/src/lib/api.ts (1)

369-503: LGTM on the presigned URL flow.

Solid work here:

  • 3-attempt retry with exponential backoff (1s, 2s)
  • Progress tracking split 0-90% S3, 90-100% confirm
  • Comprehensive error handling at each stage
  • Good logging for debugging

This is how you ship non-blocking uploads. 🚀


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Oct 3, 2025

Hi @dtrn2048!

Thank you for contributing to Dembrane ECHO! Before we consider your Pull Request, we ask that you sign our Contributor License Agreement (CLA). This is only required for your first Pull Request.

Please review the CLA, and sign it by adding your GitHub username to the contributors.yml file. Thanks!

@coderabbitai coderabbitai bot added the Feature label Oct 3, 2025
cursor[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
echo/frontend/src/lib/api.ts (1)

671-777: Finish hook only fires for the first conversation

Line [671] seeds conversationId once, while Line [689] spins up a fresh conversation per file. When you drop in multiple files, only that first conversation ever reaches the /finish call on Line [769], so every other conversation stays stuck without ETL/summarization. Multi-file uploads silently die after item one. Let's track every created conversation id and finish each one before we return.

-  let conversationId: string | null = null;
+  const conversationIds = new Set<string>();
...
-      if (!conversationId) {
-        conversationId = conversation.id;
-      }
+      conversationIds.add(conversation.id);
...
-  if (conversationId && failures.length === 0) {
-    console.log(`[Upload] Triggering finish hook for conversation ${conversationId}`);
-    try {
-      await finishConversation(conversationId);
-      console.log(`[Upload] Finish hook triggered successfully`);
-    } catch (error) {
-      console.error('[Upload] Failed to trigger finish hook:', error);
-      // Don't throw - uploads succeeded, this is just post-processing
-    }
-  }
+  if (conversationIds.size > 0 && failures.length === 0) {
+    for (const id of conversationIds) {
+      console.log(`[Upload] Triggering finish hook for conversation ${id}`);
+      try {
+        await finishConversation(id);
+        console.log(`[Upload] Finish hook triggered successfully for ${id}`);
+      } catch (error) {
+        console.error(`[Upload] Failed to trigger finish hook for ${id}:`, error);
+        // Don't throw - uploads succeeded, this is just post-processing
+      }
+    }
+  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 387119c and ae7503b.

⛔ Files ignored due to path filters (2)
  • echo/server/requirements-dev.lock is excluded by !**/*.lock
  • echo/server/requirements.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • .gitignore (0 hunks)
  • echo/frontend/src/config.ts (1 hunks)
  • echo/frontend/src/lib/api.ts (3 hunks)
  • echo/frontend/vite.config.ts (1 hunks)
  • echo/server/dembrane/api/participant.py (4 hunks)
  • echo/server/dembrane/s3.py (2 hunks)
  • echo/server/dembrane/service/conversation.py (4 hunks)
  • echo/server/prod-worker-cpu.sh (1 hunks)
  • echo/server/prod-worker.sh (1 hunks)
  • echo/server/prod.sh (1 hunks)
  • echo/server/pyproject.toml (2 hunks)
💤 Files with no reviewable changes (1)
  • .gitignore
🧰 Additional context used
🧬 Code graph analysis (2)
echo/server/dembrane/service/conversation.py (1)
echo/server/dembrane/directus.py (2)
  • DirectusBadRequest (27-28)
  • directus_client_context (32-40)
echo/server/dembrane/api/participant.py (3)
echo/server/dembrane/utils.py (2)
  • generate_uuid (13-14)
  • get (67-79)
echo/server/dembrane/service/conversation.py (4)
  • get_by_id_or_raise (46-83)
  • ConversationNotFoundException (25-26)
  • ConversationNotOpenForParticipationException (29-30)
  • create_chunk (206-283)
echo/server/dembrane/s3.py (3)
  • get_sanitized_s3_key (200-236)
  • generate_presigned_post (154-197)
  • get_file_size_bytes_from_s3 (251-254)
🪛 Shellcheck (0.11.0)
echo/server/prod.sh

[info] 16-16: Double quote to prevent globbing and word splitting.

(SC2086)


[info] 19-19: Double quote to prevent globbing and word splitting.

(SC2086)


[info] 22-22: Double quote to prevent globbing and word splitting.

(SC2086)

echo/server/prod-worker.sh

[info] 14-14: Double quote to prevent globbing and word splitting.

(SC2086)


[info] 15-15: Double quote to prevent globbing and word splitting.

(SC2086)

echo/server/prod-worker-cpu.sh

[info] 14-14: Double quote to prevent globbing and word splitting.

(SC2086)


[info] 15-15: Double quote to prevent globbing and word splitting.

(SC2086)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: ci-check-server
🔇 Additional comments (2)
echo/frontend/vite.config.ts (1)

38-38: LGTM! Port shift tracks the backend reconfig.

Clean change, aligns with the API server updates mentioned in the PR objectives. No issues here.

echo/frontend/src/config.ts (1)

9-19: LGTM! Solid URL resolution pattern.

The IIFE handles absolute URLs, relative paths, and fallback cases cleanly. Good defensive logic for the Vite proxy setup and dynamic port scenarios.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ae7503b and ad07378.

📒 Files selected for processing (1)
  • .gitignore (1 hunks)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ad07378 and 2f47705.

📒 Files selected for processing (2)
  • echo/server/dembrane/api/participant.py (3 hunks)
  • echo/server/dembrane/service/conversation.py (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
echo/server/dembrane/service/conversation.py (2)
echo/server/dembrane/directus.py (2)
  • DirectusBadRequest (27-28)
  • directus_client_context (32-40)
echo/server/dembrane/utils.py (1)
  • generate_uuid (13-14)
echo/server/dembrane/api/participant.py (3)
echo/server/dembrane/s3.py (3)
  • get_file_size_bytes_from_s3 (251-254)
  • get_sanitized_s3_key (200-236)
  • generate_presigned_post (154-197)
echo/server/dembrane/service/conversation.py (4)
  • ConversationNotFoundException (25-26)
  • ConversationNotOpenForParticipationException (29-30)
  • get_by_id_or_raise (46-83)
  • create_chunk (206-283)
echo/server/dembrane/utils.py (2)
  • generate_uuid (13-14)
  • get (67-79)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci-check-server
🔇 Additional comments (2)
echo/server/dembrane/service/conversation.py (2)

3-11: LGTM—Logger setup is clean.

Standard Python logging pattern with qualified module name. No issues here.


279-281: Clean background task trigger.

Comment and logging are clear. No issues.

- Introduced a new function `sanitize_url_for_logging` to remove sensitive query parameters and fragments from URLs before logging.
- Updated logging statements in the `ConversationService` to use the new sanitization function for file URLs.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (2)
echo/server/dembrane/service/conversation.py (1)

262-264: Problem solved. Logging is now safe.

Both branches sanitize file_url before logging—credentials no longer leak into log streams. The messages clearly distinguish API uploads from presigned URL uploads, which is clutch for debugging the new flow. This closes out the past review concern.

echo/server/dembrane/api/participant.py (1)

436-450: Return 404 when the conversation is gone.

If conversation_service.create_chunk raises ConversationNotFoundException, we fall through to the catch-all and ship a 500, which is the wrong contract. Catch it explicitly and surface the expected 404 before the generic handler.

     except ConversationNotOpenForParticipationException as e:
         logger.error(f"Conversation not open for participation: {conversation_id}")
         raise HTTPException(
             status_code=403, 
             detail="Conversation not open for participation"
         ) from e
+    except ConversationNotFoundException as e:
+        logger.error(f"Conversation not found while confirming upload: {conversation_id}")
+        raise HTTPException(status_code=404, detail="Conversation not found") from e
     except HTTPException:
         # Re-raise HTTP exceptions as-is
         raise
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2f47705 and 0af65d8.

📒 Files selected for processing (3)
  • .gitignore (1 hunks)
  • echo/server/dembrane/api/participant.py (3 hunks)
  • echo/server/dembrane/service/conversation.py (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
echo/server/dembrane/service/conversation.py (1)
echo/server/dembrane/directus.py (2)
  • DirectusBadRequest (27-28)
  • directus_client_context (32-40)
echo/server/dembrane/api/participant.py (3)
echo/server/dembrane/utils.py (2)
  • generate_uuid (13-14)
  • get (67-79)
echo/server/dembrane/s3.py (3)
  • get_file_size_bytes_from_s3 (251-254)
  • get_sanitized_s3_key (200-236)
  • generate_presigned_post (154-197)
echo/server/dembrane/service/conversation.py (4)
  • get_by_id_or_raise (53-90)
  • ConversationNotFoundException (32-33)
  • ConversationNotOpenForParticipationException (36-37)
  • create_chunk (213-290)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: ci-check-server
🔇 Additional comments (6)
.gitignore (1)

14-15: Ship it. Tight addition—keeping env blobs and scratch pads out of git is exactly the move.

echo/server/dembrane/service/conversation.py (5)

2-5: Ship it. Imports are clean.

Standard library imports for logging and URL parsing—exactly what you need for the sanitization helper. No bloat, no drama.


12-13: LGTM. Logger setup is textbook.

Module-scoped logger with a qualified name—perfect for tracing in production when you're hunting down that one rogue chunk upload at 3AM.


22-26: Nailed it. URL sanitization is solid.

This helper strips sensitive query params and fragments—exactly what the past review asked for. Presigned URL signatures stay out of your log aggregators. Clean implementation, zero complaints.


273-273: Better, but still needs upstream validation.

Always using file_url for path is the right move—it fixes the presigned URL case. However, if both file_obj and file_url are None, path will still be None, and downstream split_audio_chunk will blow up with FileNotFoundError. The past review requested API-level validation to reject requests where neither is provided. This change improves the situation but doesn't close the loop.

Add a guard in create_chunk before chunk creation:

+        if file_url is None and transcript is None:
+            raise ConversationServiceException("Either file_obj/file_url or transcript must be provided")
+
         with directus_client_context() as client:
             chunk = client.create_item(

Or enforce at the API boundary in dembrane/api/participant.py before calling create_chunk.


286-287: Good call. Observability FTW.

Logging before the async task dispatch gives you a clear trail when you're debugging chunk processing in prod. Helps correlate API calls with background job execution. Clean and useful.

- Updated `prod-worker-cpu.sh` and `prod-worker.sh` to quote the `$PROCESSES` and `$THREADS` variables for improved handling of values with spaces or special characters.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (5)
echo/server/dembrane/api/participant.py (5)

95-123: Rate limiter still process-local (known issue).

The past review comment about multi-worker/pod rate limit bypass remains unaddressed. This is expected if it's a known limitation being tracked separately.


323-324: Redundant local import (known issue).

Past review comment about the duplicate get_sanitized_s3_key import applies here.


332-339: Redundant local import (known issue).

Past review comment about the duplicate generate_presigned_post import applies here.


342-342: Double-slash risk in file_url (known issue).

Past review comment about normalizing STORAGE_S3_ENDPOINT to prevent double slashes applies here.


388-417: Catch ValueError from get_sanitized_s3_key (known issue).

Past review comment about handling ValueError from get_sanitized_s3_key at line 390 applies here.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0af65d8 and 77b742e.

📒 Files selected for processing (3)
  • echo/server/dembrane/api/participant.py (3 hunks)
  • echo/server/prod-worker-cpu.sh (1 hunks)
  • echo/server/prod-worker.sh (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
echo/server/dembrane/api/participant.py (3)
echo/server/dembrane/utils.py (2)
  • generate_uuid (13-14)
  • get (67-79)
echo/server/dembrane/s3.py (3)
  • get_file_size_bytes_from_s3 (251-254)
  • get_sanitized_s3_key (200-236)
  • generate_presigned_post (154-197)
echo/server/dembrane/service/conversation.py (4)
  • get_by_id_or_raise (53-90)
  • ConversationNotFoundException (32-33)
  • ConversationNotOpenForParticipationException (36-37)
  • create_chunk (213-290)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: ci-check-server
🔇 Additional comments (2)
echo/server/dembrane/api/participant.py (2)

442-444: ConversationNotFoundException now handled correctly.

The past review comment about catching ConversationNotFoundException explicitly has been addressed. 404 contract is now correct.


395-417: LGTM: Retry logic handles S3 eventual consistency.

The 3-attempt backoff (100ms → 500ms → 2s) with clear logging is solid. Ships 400 after exhausting retries, which is the right contract for "upload failed or S3 unavailable."

- Updated `prod.sh` to quote the `$WORKERS`, `$TIMEOUT`, and `$MAX_REQUESTS` variables to ensure proper handling of values with spaces or special characters.
- Added validation to ensure a chunk has either an audio file or a transcript before creation, raising a `ConversationServiceException` if both are missing.
- Updated error handling in the API to catch `ConversationServiceException` and return a 400 HTTP error with a detailed message.
- Improved logging for chunk creation failures and clarified processing behavior based on the presence of audio files.
cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2661a06 and b1e313a.

📒 Files selected for processing (2)
  • echo/server/dembrane/ner.py (1 hunks)
  • echo/server/dembrane/quote_utils.py (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: ci-check-server
🔇 Additional comments (1)
echo/server/dembrane/quote_utils.py (1)

160-160: Using raw transcripts without anonymization.

LGTM from a code perspective—you're just pulling chunk.transcript directly. But the privacy implications are massive. Raw transcripts might contain names, emails, phone numbers, etc. Make sure the legal team signed off on this before it ships.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

… and ensure proper function definition

fix: Remove unused import of get_sanitized_s3_key in get_chunk_upload_url
fix: Add missing newline at end of run-worker scripts for better compatibility
@dtrn2048 dtrn2048 changed the title feat: Non-blocking uploads with presigned URLs + Kubernetes worker optimization ECHO-465 Non-blocking uploads with presigned URLs + Kubernetes worker optimization Oct 6, 2025
@linear
Copy link
Copy Markdown

linear bot commented Oct 6, 2025

spashii
spashii previously approved these changes Oct 7, 2025
Copy link
Copy Markdown
Member

@spashii spashii left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All changes are safe except vite.config.ts - 8001 port change. I think you should try out the dev containers. The networking / containers setup is managed through it.

@dtrn2048 dtrn2048 added this pull request to the merge queue Oct 7, 2025
Merged via the queue into main with commit 78dc3f7 Oct 7, 2025
12 checks passed
@dtrn2048 dtrn2048 deleted the feat/nonblocking-uploads branch October 7, 2025 09:30
spashii pushed a commit that referenced this pull request Nov 18, 2025
… optimization (#318)

## Summary

Implements non-blocking file uploads using S3 presigned URLs and
reconfigures workers for Kubernetes horizontal pod autoscaling.

## Changes

### Non-blocking Upload System
- Frontend: New `uploadConversationChunkWithPresignedUrl` function
- Backend: Endpoints for presigned URL generation and upload
confirmation
- API server no longer handles file transfers
- Direct client to S3 uploads
- Retry logic: 3 attempts with exponential backoff
- Rate limiting: 10 requests/min per conversation
- Progress tracking through upload stages

### Worker Configuration
- API: Switched from uvicorn to gunicorn (2 workers/pod)
- Network workers: 3×50 → 2×20 threads per pod
- CPU workers: 4×6 → 2×4 threads per pod
- All settings configurable via environment variables:
  - `API_WORKERS=2`
  - `NETWORK_WORKER_PROCESSES=2`, `THREADS=20`
  - `CPU_WORKER_PROCESSES=2`, `THREADS=4`

### Other
- Fixed DIRECTUS_PUBLIC_URL handling for absolute/relative URLs
- Added dependencies: gunicorn, trankit, torch
- Updated vite proxy to localhost:8001

## Files Changed

**Backend:**
- `dembrane/api/participant.py`: Presigned URL endpoints
- `dembrane/s3.py`: URL generation logic
- `dembrane/service/conversation.py`: Chunk handling updates
- `prod*.sh`: Worker configuration scripts
- `pyproject.toml`: Dependencies

**Frontend:**
- `lib/api.ts`: Upload implementation
- `config.ts`: URL handling
- `vite.config.ts`: Proxy config

## Notes

- No breaking changes to existing upload functionality
- Worker settings now environment-configurable
- Risk: LOW, easy rollback

---

**Commits:**
- feat: optimize workers for Kubernetes horizontal scaling
- Update configuration and dependencies

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Direct-to-storage uploads via presigned POSTs with per-file progress,
retries, finish hook, and retained legacy upload path; server endpoints
to request and confirm uploads with per-conversation rate limiting.

* **Bug Fixes**
* Chunk creation now records file URLs consistently; safer URL logging
and stricter filename/key validation to prevent path traversal; clearer
upload/confirmation error handling.

* **Chores**
* Kubernetes-friendly startup scripts, switched API runtime to Gunicorn
and added dependency, improved public-URL resolution and dev proxy
target, expanded .gitignore, and NER anonymization pipeline disabled.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants