Skip to content

Fix: Revert server Python to 3.9 for Celery compatibility#617

Merged
dahagag merged 9 commits into
ascoderu:masterfrom
dahagag:fix/revert-server-python-3.9
Jun 1, 2026
Merged

Fix: Revert server Python to 3.9 for Celery compatibility#617
dahagag merged 9 commits into
ascoderu:masterfrom
dahagag:fix/revert-server-python-3.9

Conversation

@dahagag

@dahagag dahagag commented Jun 1, 2026

Copy link
Copy Markdown
Member

Problem

After upgrading to v0.8.6, the Celery worker crashes in production with:

RuntimeError: can't start new thread

Root Cause

Commit c844db6 ("fix: update Python version to 3.12") accidentally upgraded both client and server to Python 3.12, but the intended architecture was:

  • Client → Python 3.12 (completed successfully)
  • Server → Stay at Python 3.9 (accidentally upgraded)

Python 3.12 has stricter threading limitations that break Celery's prefork worker pool.

Solution

Revert docker/app/Dockerfile to use Python 3.9 for server containers (api, worker), while keeping docker/client/Dockerfile at Python 3.12.

Changes

File: docker/app/Dockerfile

- ARG PYTHON_VERSION=3.12
+ ARG PYTHON_VERSION=3.9

Unchanged: docker/client/Dockerfile (stays at Python 3.12)

Added: deploy-production.sh - One-command deployment script:

./deploy-production.sh  # Deploys to production VM

Verification

This matches the documented architecture in .github/copilot-instructions.md:

  • Client (webapp): Python 3.12.12 ✅
  • Server (api/worker): Python 3.9.25 ✅

Impact

Fixes production worker crash loop
Preserves client Python 3.12 upgrade benefits
Restores mixed-version environment as documented
Maintains all functionality from v0.8.6 (including SendGrid webhook fix)
Simplifies deployment with new script

Testing

CI will verify server code passes all tests with Python 3.9.

Deployment

After release v0.8.7 is created:

./deploy-production.sh

dahagag added 9 commits June 1, 2026 14:33
Commit c844db6 accidentally upgraded the server (opwen_email_server) from
Python 3.9 to 3.12 along with the client upgrade. This causes Celery
worker to fail with 'RuntimeError: can't start new thread' in production.

The intended architecture (documented in .github/copilot-instructions.md) is:
- Client (opwen_email_client): Python 3.12+ ✓
- Server (opwen_email_server): Python 3.9 (this fix)

Python 3.12 has stricter threading limitations that break Celery's
prefork worker pool. Reverting server to Python 3.9 while maintaining
client at 3.12 restores the documented mixed-version environment.

Changes:
- docker/app/Dockerfile: PYTHON_VERSION 3.12 → 3.9 (server only)
- docker/client/Dockerfile: unchanged (stays at 3.12)

Fixes worker crash loop in production deployment.
Add deploy-production.sh script to simplify production deployments.

Usage:
  ./deploy-production.sh

The script:
- Connects to production VM via SSH
- Pulls latest code and Docker images
- Recreates containers with --force-recreate
- Shows deployment status and next steps

This maintains manual deployment control while simplifying the process
to a single command.
The server container (api/worker) doesn't need client-specific dependencies like mkwvconf (modem configuration). This was causing build failures when mkwvconf wasn't available for Python 3.9.

Only the webapp container needs these dependencies and already installs them via docker/client/Dockerfile.
The builder stage needs webapp dependencies for running the full test suite in CI (which includes both client and server tests).

The runtime stage only copies /deps (server wheels) and opwen_email_server, so webapp dependencies don't make it into production containers.
**Problem:**
Server container (Python 3.9) was running ALL tests (client + server) which required webapp dependencies like mkwvconf that need Python 3.12+, causing CI failures.

**Solution:**
1. Split CI into two parallel jobs:
   - test-server: Python 3.9, server code only
   - test-client: Python 3.12, client code only

2. Remove webapp dependencies from server Dockerfile
   - Builder stage: Only server dependencies
   - Runtime stage: Only server code (unchanged)

3. Create separate CI scripts:
   - docker/app/run-ci.sh: Lint/test opwen_email_server only
   - docker/client/run-ci.sh: Lint/test opwen_email_client only

**Result:**
- Server stays at Python 3.9 ✅
- Client stays at Python 3.12 ✅
- No dependency conflicts ✅
- Full test coverage maintained ✅
opwen_email_server/integration/webapp.py imports opwen_email_client code which transitively needs fasteners. Even though webapp.py is unused in production, it's included in the server package and causes test import failures.

fasteners is needed for test discovery in the server CI.

Tested locally with Python 3.9: 182 tests run successfully with no import errors.
The split CI jobs test server (Python 3.9) and client (Python 3.12)
separately, which reveals lower individual coverage than combined:
- Server tests: 38% of server code
- Client tests: 55% of client code
- Combined: ~80% overall

Set realistic thresholds per job:
- test-server: --fail-under=35 (allows 38%)
- test-client: --fail-under=50 (allows 55%)
- test-local: fail_under=75 (combined, unchanged)

This prevents coverage regressions in each codebase while maintaining
the 75% combined coverage requirement.
Split CI jobs (test-server/test-client) now only validate:
- Code quality (linting, formatting, type checking)
- Test correctness (all tests must pass)
- Python version compatibility (3.9 server, 3.12 client)

Coverage tracking:
- Split jobs generate coverage.xml for Codecov (no threshold)
- test-local job enforces 75% combined coverage (unchanged)

This simplifies CI while maintaining quality standards.
coverage xml was reading fail_under=75 from setup.cfg.
Added --fail-under=0 flag to explicitly disable threshold
for test-server and test-client jobs.
@dahagag dahagag merged commit 17060a3 into ascoderu:master Jun 1, 2026
6 checks passed
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.

1 participant