Skip to content

Threaded worker - #7

Merged
fangpenlin merged 12 commits into
LaunchPlatform:masterfrom
mahmoud:threaded-worker
Feb 16, 2026
Merged

fangpenlin merged 12 commits into
LaunchPlatform:masterfrom
mahmoud:threaded-worker

Conversation

@mahmoud

@mahmoud mahmoud commented Feb 10, 2026 •

Copy link
Copy Markdown
Contributor

NB: Claude commits are from @Fazel94's branch here

Tests are pretty extensive, and I've also integration tested within my application. Haven't stress tested or run for very long in production, but seems to work well enough.

claude and others added 12 commits November 19, 2025 12:14
This commit adds ThreadPoolExecutor support to enable concurrent task processing
within a single worker instance, addressing the TODO at line 348 in app.py.

Key changes:
- Added MAX_WORKER_THREADS configuration option (default: 1 for backward compatibility)
- Created _process_task_in_thread() method for thread-safe task processing
- Each thread uses its own database session to avoid session conflicts
- Modified process_tasks() to use ThreadPoolExecutor when MAX_WORKER_THREADS > 1
- Added graceful executor shutdown on worker exit
- Preserved sequential processing when MAX_WORKER_THREADS = 1

Benefits:
- Enables concurrent processing of multiple tasks within a single worker
- Improves throughput for I/O-bound tasks
- Maintains backward compatibility with sequential processing by default
- Thread-safe with proper database session management per thread

Configuration:
- Set MAX_WORKER_THREADS to desired concurrency level (e.g., 4, 8, 10)
- Set to 0 to use default (number of CPUs * 5)
- Set to 1 to disable thread pool (sequential processing)
This commit fixes critical issues for thread-based executor to work correctly:

1. **SQLAlchemy Session Context Issue**:
   - Changed _process_task_in_thread to accept task_id instead of task object
   - Each worker thread now reloads the task in its own session
   - This prevents "DetachedInstanceError" when task objects cross thread boundaries

2. **Thread-Safe Connection Pool**:
   - Added conditional pool selection based on MAX_WORKER_THREADS
   - Uses QueuePool (thread-safe) when MAX_WORKER_THREADS > 1
   - Keeps SingletonThreadPool for backwards compatibility when MAX_WORKER_THREADS = 1
   - Configures pool_size dynamically based on number of worker threads

The previous implementation used SingletonThreadPool which is NOT thread-safe,
causing "connection already closed" errors when multiple threads accessed the pool.

These fixes ensure that:
- Each thread has its own database session
- Task objects are not shared between threads
- Connection pool is thread-safe for concurrent access
- No session conflicts or detached instance errors occur
This commit adds both unit tests and acceptance tests for the thread
executor functionality.

Unit Tests (tests/unit/test_thread_executor.py):
- Test default pool is SingletonThreadPool (backward compatibility)
- Test thread pool uses QueuePool when MAX_WORKER_THREADS > 1
- Test queue pool size configuration based on MAX_WORKER_THREADS
- Test various MAX_WORKER_THREADS configurations (1, 2, 4, 8, 16, 0)
- Test configuration via environment variable BQ_MAX_WORKER_THREADS
- Total: 13 unit tests

Acceptance Tests (tests/acceptance/test_thread_executor.py):
- test_thread_executor_with_multiple_threads: Verifies concurrent task
  processing with 4 worker threads processing 16 tasks
- test_thread_executor_session_isolation: Verifies each thread has its
  own database session without conflicts
- test_sequential_processing_backward_compatibility: Verifies that
  MAX_WORKER_THREADS=1 maintains sequential processing behavior

Test Fixtures (tests/acceptance/fixtures/thread_processors.py):
- slow_task: Simulates I/O-bound work with configurable sleep time
- concurrent_task: Tests concurrent execution and session isolation

All unit tests pass successfully (13/13).
Acceptance tests verify thread safety, session isolation, and concurrent
task execution.
CRITICAL BUG FIX: The thread executor was missing a commit after dispatch(),
causing race conditions and transaction issues.

Problem:
1. dispatch() updates tasks to PROCESSING state (uncommitted)
2. Worker threads immediately start and reload tasks
3. Worker threads see PENDING state (not PROCESSING) due to MVCC
4. FOR UPDATE locks held until main thread commits/rollbacks
5. db.close() rolls back PROCESSING state, causing inconsistency

Fix:
- Added db.commit() after dispatch() and before submitting to thread pool
- This ensures:
  * Worker threads see correct PROCESSING state
  * FOR UPDATE locks are released promptly
  * No rollback of dispatch changes
  * Proper transaction boundaries

Impact:
- Fixes state consistency (PENDING → PROCESSING → DONE)
- Releases locks faster, improving concurrency
- Prevents transaction rollback issues
- Enables dead worker detection (requires PROCESSING state)

Testing:
- All 13 unit tests pass
- Transaction flow verified
- No deadlocks or race conditions
This document analyzes Celery's thread pool implementation and identifies
potential improvements for BeanQueue's thread executor.

Key findings:
- Celery uses continuous task feeding (not batch-and-wait)
- Prefetch multiplier pattern for optimization
- Pool monitoring and statistics
- Late acknowledgement option for crash recovery

Proposed improvements prioritized by impact:
1. Optimize BATCH_SIZE defaults (match MAX_WORKER_THREADS)
2. Implement continuous task feeding
3. Add WORKER_PREFETCH_MULTIPLIER config
4. Add pool statistics/monitoring
5. Optional late acknowledgement mode

Current implementation is correct and safe but can improve throughput
with these patterns for I/O-bound workloads.
This commit adds extensive test coverage for the thread executor:

Unit Tests (tests/unit/test_thread_executor_advanced.py) - 17 tests:
- Engine recreation with different configurations
- BATCH_SIZE independence from MAX_WORKER_THREADS
- Pool size calculations (zero, large, overflow)
- Session factory integration with engine pools
- Config validation and mode detection
- Multiple app instances with independent pools
- Engine caching and custom engine override
- Parametrized tests for various batch/thread combinations

Acceptance Tests (tests/acceptance/test_thread_executor_edge_cases.py) - 5 tests:
- Handling task failures gracefully
- More threads than tasks scenario
- More tasks than threads scenario
- Retry policy with thread executor
- Task state transitions (PENDING → PROCESSING → DONE)

Test Fixtures (tests/acceptance/fixtures/thread_processors.py):
- Added failing_task: conditionally fails to test error handling
- Added retry_task: tests retry policy with thread executor

Test Results:
- All 73 unit tests pass (100%)
- Tests cover edge cases, error conditions, and various configurations
- Ensures thread safety, session isolation, and proper state transitions

Coverage areas:
- Pool configuration and sizing
- Error handling and retry logic
- State transition correctness
- Multiple thread/task combinations
- Session management across threads
This test simulates a real-world data processing pipeline with financial
transactions to verify the thread executor works correctly with realistic
workloads.

Test scenarios:
- 50 transaction processing tasks (0.1s each)
- 15 merchant analysis tasks (0.15s each)
- 5 report generation tasks (0.2s each)

Results show 6.82x speedup with 8 worker threads, processing 45.46 tasks/sec
with 100% success rate and proper data integrity.
Explains what works and what doesn't when using custom models with
the thread executor. Key findings:

WORKS:
- Thread executor with standard bq.Task/Event models (fully tested)
- Custom processors with any logic
- Storing custom data in task kwargs or metadata

LIMITATION:
- Custom model classes require polymorphic mapper configuration
- BeanQueue doesn't configure polymorphic inheritance currently
- Causes SQLAlchemy FlushError when creating events

RECOMMENDED WORKAROUNDS:
1. Use task kwargs for custom fields (best approach)
2. Use JSON metadata field for structured data
3. Disable events if not needed (EVENT_MODEL=None)

All approaches work perfectly with the thread executor.
This test attempted to verify thread executor compatibility with custom
Task/Event models that extend the base classes with additional fields.

The test reveals a SQLAlchemy polymorphic inheritance limitation:
- Custom Task subclasses cause FlushError when Events are created
- Error: "Expected bq.Task, got CustomTask"
- Would require polymorphic mapper configuration in BeanQueue

Test kept for documentation purposes. The workaround (using task kwargs
instead of custom model fields) is documented in test_custom_fields_summary.md
and works perfectly with the thread executor.
@fangpenlin

Copy link
Copy Markdown
Contributor

Hey @mahmoud, thanks for contributing. Seems like a big PR, I will find a time to review it.

@fangpenlin fangpenlin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

over all looks good. the AI generated code is a bit verbose and contain some parts may not be really needed or useful. will find a time to clean it up

@fangpenlin
fangpenlin merged commit 5a20173 into LaunchPlatform:master Feb 16, 2026
@mahmoud
mahmoud deleted the threaded-worker branch April 21, 2026 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants