docs: add readme architecture diagrams
83
README.md
@@ -1,4 +1,4 @@
|
||||
# Minimal Senior-Level Job Queue
|
||||
# Job Queue
|
||||
|
||||
This is a deliberately small PostgreSQL-backed job queue for the interview assignment.
|
||||
|
||||
@@ -14,6 +14,41 @@ The important parts are:
|
||||
- idempotent job creation
|
||||
- demo UI with job state and event polling
|
||||
|
||||
## Architecture
|
||||
|
||||

|
||||
|
||||
|
||||
There is no queue table and no worker table. Workers are ephemeral process threads with generated ids. The queue is internal and ordered by:
|
||||
|
||||
```text
|
||||
priority DESC, available_at ASC, created_at ASC, id ASC
|
||||
```
|
||||
|
||||
## Statuses
|
||||
|
||||

|
||||
|
||||
The database also validates row shape:
|
||||
|
||||
- queued jobs cannot have locks or finish timestamps
|
||||
- running jobs must have a lock owner and lease deadline
|
||||
- terminal jobs must have a finish timestamp and no lock
|
||||
|
||||
## At-Least-Once Execution
|
||||
|
||||

|
||||
|
||||
This queue provides at-least-once execution, not exactly-once execution.
|
||||
|
||||
A worker can perform an external side effect and crash before marking a job succeeded. The lease will expire and the job can run again. Real handlers should therefore be idempotent.
|
||||
|
||||
## Why PostgreSQL
|
||||
|
||||
The assignment requires PostgreSQL, and PostgreSQL gives a compact solution for safe concurrent claiming through `SELECT ... FOR UPDATE SKIP LOCKED`. This keeps the implementation transactional, inspectable, and easy to demo.
|
||||
|
||||
For a high-throughput distributed production queue, Redis-backed systems such as BullMQ or Sidekiq-style designs are common. That is documented as the next architecture, not implemented here.
|
||||
|
||||
## Run
|
||||
|
||||
```powershell
|
||||
@@ -60,52 +95,6 @@ docker compose up -d --build worker
|
||||
docker compose logs -f worker
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
React UI
|
||||
|
|
||||
Django API ---- PostgreSQL
|
||||
|
|
||||
Django worker process
|
||||
|
|
||||
N worker threads from env
|
||||
```
|
||||
|
||||
There is no queue table and no worker table. Workers are ephemeral process threads with generated ids. The queue is internal and ordered by:
|
||||
|
||||
```text
|
||||
priority DESC, available_at ASC, created_at ASC, id ASC
|
||||
```
|
||||
|
||||
## Statuses
|
||||
|
||||
```text
|
||||
queued -> running
|
||||
running -> succeeded
|
||||
running -> queued retry after failure or timeout
|
||||
running -> failed attempts exhausted
|
||||
failed -> queued manual retry
|
||||
```
|
||||
|
||||
The database also validates row shape:
|
||||
|
||||
- queued jobs cannot have locks or finish timestamps
|
||||
- running jobs must have a lock owner and lease deadline
|
||||
- terminal jobs must have a finish timestamp and no lock
|
||||
|
||||
## At-Least-Once Execution
|
||||
|
||||
This queue provides at-least-once execution, not exactly-once execution.
|
||||
|
||||
A worker can perform an external side effect and crash before marking a job succeeded. The lease will expire and the job can run again. Real handlers should therefore be idempotent.
|
||||
|
||||
## Why PostgreSQL
|
||||
|
||||
The assignment requires PostgreSQL, and PostgreSQL gives a compact solution for safe concurrent claiming through `SELECT ... FOR UPDATE SKIP LOCKED`. This keeps the implementation transactional, inspectable, and easy to demo.
|
||||
|
||||
For a high-throughput distributed production queue, Redis-backed systems such as BullMQ or Sidekiq-style designs are common. That is documented as the next architecture, not implemented here.
|
||||
|
||||
## Useful Commands
|
||||
|
||||
Run backend tests locally with SQLite fallback:
|
||||
|
||||
BIN
assets/images/at-least-once-lease-recovery.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
assets/images/backend/api-surface.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/images/backend/database-erd.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/images/backend/database-indexes.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/images/backend/expired-lease-cleanup.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
assets/images/backend/exponential-backoff.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/images/backend/high-level-architecture.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
assets/images/backend/job-state-machine.png
Normal file
|
After Width: | Height: | Size: 1013 KiB |
BIN
assets/images/backend/lease-renewal.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
assets/images/backend/ownership-stale-worker.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/images/backend/skip-locked-claim.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
assets/images/backend/worker-runtime-loop.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/images/frontend/data-flow.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/images/frontend/terminal-event-feed.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
assets/images/frontend/ui-architecture.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/images/job-status-state-machine.png
Normal file
|
After Width: | Height: | Size: 914 KiB |
BIN
assets/images/project-architecture.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
@@ -1,4 +1,4 @@
|
||||
# Backend README
|
||||
# Backend
|
||||
|
||||
This backend is a minimal production-aware PostgreSQL job queue implemented with Django and Django REST Framework.
|
||||
|
||||
@@ -23,6 +23,8 @@ All queue behavior is implemented in service functions under `jobs/services.py`.
|
||||
|
||||
## High-Level Architecture
|
||||
|
||||

|
||||
|
||||
```text
|
||||
PostgreSQL
|
||||
|
|
||||
@@ -91,6 +93,8 @@ This is an at-least-once queue. A worker can perform a side effect and then cras
|
||||
|
||||
### Statuses
|
||||
|
||||

|
||||
|
||||
`Job.status` is intentionally small:
|
||||
|
||||
```text
|
||||
@@ -114,6 +118,8 @@ Unsupported transitions are rejected by service-level ownership checks and by da
|
||||
|
||||
### Ownership
|
||||
|
||||

|
||||
|
||||
A worker owns a job only when all of these are true:
|
||||
|
||||
```text
|
||||
@@ -143,6 +149,8 @@ This is one of the most important correctness properties in the project.
|
||||
|
||||
## Database Model
|
||||
|
||||

|
||||
|
||||
### `Job`
|
||||
|
||||
`Job` is the source of truth for queue state.
|
||||
@@ -240,6 +248,8 @@ This allows many jobs with no idempotency key while preventing duplicate client-
|
||||
|
||||
## Indexes
|
||||
|
||||

|
||||
|
||||
### Claim Index
|
||||
|
||||
```text
|
||||
@@ -405,6 +415,8 @@ Table jobs_jobevent {
|
||||
|
||||
## Claiming Algorithm
|
||||
|
||||

|
||||
|
||||
The worker claims a job in a short transaction:
|
||||
|
||||
```python
|
||||
@@ -471,6 +483,8 @@ Then a `succeeded` event is inserted.
|
||||
|
||||
## Failure And Retry Algorithm
|
||||
|
||||

|
||||
|
||||
On handler failure, the worker calls `fail_job`.
|
||||
|
||||
If attempts remain:
|
||||
@@ -515,6 +529,8 @@ max delay -> 300 seconds
|
||||
|
||||
## Lease Renewal
|
||||
|
||||

|
||||
|
||||
Long-running handlers can renew their lease:
|
||||
|
||||
```python
|
||||
@@ -540,6 +556,8 @@ The demo `demo.timeout` handler intentionally does not renew leases so the UI ca
|
||||
|
||||
## Expired Lease Cleanup
|
||||
|
||||

|
||||
|
||||
Every worker thread periodically runs cleanup:
|
||||
|
||||
```text
|
||||
@@ -570,6 +588,8 @@ There is no separate cron process. This is intentional for the interview version
|
||||
|
||||
## Worker Runtime
|
||||
|
||||

|
||||
|
||||
Run workers with:
|
||||
|
||||
```powershell
|
||||
@@ -643,6 +663,8 @@ Example:
|
||||
|
||||
## API
|
||||
|
||||

|
||||
|
||||
Base URL in local development:
|
||||
|
||||
```text
|
||||
@@ -1094,4 +1116,3 @@ POST /api/jobs/{id}/retry/
|
||||
GET /api/schema/
|
||||
GET /api/docs/
|
||||
```
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Vite React demo UI for the minimal job queue.
|
||||
|
||||

|
||||
|
||||
It reuses the visual style from the previous advanced frontend, but only keeps:
|
||||
|
||||
- dashboard
|
||||
@@ -11,11 +13,15 @@ It reuses the visual style from the previous advanced frontend, but only keeps:
|
||||
|
||||
The UI polls:
|
||||
|
||||

|
||||
|
||||
- jobs and stats every 2 seconds
|
||||
- events every 1 second
|
||||
|
||||
No WebSockets are used.
|
||||
|
||||

|
||||
|
||||
## Run
|
||||
|
||||
```powershell
|
||||
|
||||