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.
|
This is a deliberately small PostgreSQL-backed job queue for the interview assignment.
|
||||||
|
|
||||||
@@ -14,6 +14,41 @@ The important parts are:
|
|||||||
- idempotent job creation
|
- idempotent job creation
|
||||||
- demo UI with job state and event polling
|
- 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
|
## Run
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -60,52 +95,6 @@ docker compose up -d --build worker
|
|||||||
docker compose logs -f 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
|
## Useful Commands
|
||||||
|
|
||||||
Run backend tests locally with SQLite fallback:
|
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.
|
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
|
## High-Level Architecture
|
||||||
|
|
||||||
|

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

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

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

|
||||||
|
|
||||||
### `Job`
|
### `Job`
|
||||||
|
|
||||||
`Job` is the source of truth for queue state.
|
`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
|
## Indexes
|
||||||
|
|
||||||
|

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

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

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

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

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

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

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

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

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

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