Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,7 @@
## 2026-04-17 - ORM Counting vs func.count().scalar()
**Learning:** Using `db.query(Model).filter(...).count()` can be slower and have more ORM overhead than `db.query(func.count(Model.id)).filter(...).scalar() or 0` or doing an early `.first()` exit.
**Action:** When counting records or verifying existence, prefer early `.first()` exits combined with `func.count().scalar()` for performance in high-traffic APIs.

## 2026-04-20 - Async File I/O in Voice Submission
**Learning:** Saving audio recordings (up to 10MB) synchronously in a FastAPI async endpoint blocks the main event loop, significantly increasing tail latency for all concurrent users during high-traffic periods.
**Action:** Wrap blocking synchronous File I/O operations like `f.write()` in `run_in_threadpool` to offload them to a separate thread, keeping the event loop responsive for other requests.
49 changes: 44 additions & 5 deletions backend/routers/voice.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
import logging
import os
import uuid
import hashlib
from datetime import datetime, timezone

from backend.database import get_db
from backend.models import Issue
from backend.cache import blockchain_last_hash_cache
from backend.schemas import (
VoiceTranscriptionResponse,
TextTranslationRequest,
Expand Down Expand Up @@ -248,12 +250,44 @@ async def submit_voice_issue(
audio_filename = f"{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex}{file_extension}"
audio_file_path = os.path.join(AUDIO_STORAGE_DIR, audio_filename)

with open(audio_file_path, 'wb') as f:
f.write(audio_content)

# Store relative path for portability
relative_audio_path = os.path.join("data", "audio_recordings", audio_filename)
# Performance optimization: Wrap blocking synchronous File I/O in threadpool
def _save_audio_file():
with open(audio_file_path, 'wb') as f:
f.write(audio_content)

def _delete_audio_file_best_effort():
try:
if os.path.exists(audio_file_path):
os.remove(audio_file_path)
except Exception as cleanup_error:
logger.warning("Failed to delete orphaned audio file %s: %s", audio_file_path, cleanup_error)

await run_in_threadpool(_save_audio_file)
Comment thread
RohanExploit marked this conversation as resolved.

try:
# Store relative path for portability
relative_audio_path = os.path.join("data", "audio_recordings", audio_filename)

# Blockchain feature: calculate integrity hash for the report
# Performance Boost: Use thread-safe cache to eliminate DB query for last hash
prev_hash = blockchain_last_hash_cache.get("last_hash")
if prev_hash is None:
# Cache miss: Fetch only the last hash from DB
prev_issue = await run_in_threadpool(
lambda: db.query(Issue.integrity_hash).order_by(Issue.id.desc()).first()
)
prev_hash = prev_issue[0] if prev_issue and prev_issue[0] else ""
blockchain_last_hash_cache.set(data=prev_hash, key="last_hash")

# Simple but effective SHA-256 chaining
# Format must match backend/routers/issues.py for a consistent chain
hash_content = f"{final_description}|{issue_category.value}|{prev_hash}"
except Exception:
db.rollback()
await run_in_threadpool(_delete_audio_file_best_effort)
raise
integrity_hash = hashlib.sha256(hash_content.encode()).hexdigest()

# Create issue in database
reference_id = generate_reference_id()

Expand All @@ -267,6 +301,8 @@ async def submit_voice_issue(
location=location,
source='voice',
status='open',
integrity_hash=integrity_hash,
previous_integrity_hash=prev_hash,
# Voice-specific fields
submission_type='voice',
original_language=voice_result.get('source_language'),
Expand All @@ -280,6 +316,9 @@ async def submit_voice_issue(
db.commit()
db.refresh(new_issue)

# Update cache for next report AFTER successful DB commit
blockchain_last_hash_cache.set(data=integrity_hash, key="last_hash")

logger.info(f"Voice issue created: ID={new_issue.id}, Language={voice_result.get('source_language')}, Confidence={voice_result.get('confidence')}")

return IssueCreateResponse(
Expand Down
Loading