diff --git a/slides/images/01-cover.png b/slides/images/01-cover.png new file mode 100644 index 0000000..3d94b0b Binary files /dev/null and b/slides/images/01-cover.png differ diff --git a/slides/images/02-contract.png b/slides/images/02-contract.png new file mode 100644 index 0000000..703d26d Binary files /dev/null and b/slides/images/02-contract.png differ diff --git a/slides/images/03-scope.png b/slides/images/03-scope.png new file mode 100644 index 0000000..00e038b Binary files /dev/null and b/slides/images/03-scope.png differ diff --git a/slides/images/04-architecture.png b/slides/images/04-architecture.png new file mode 100644 index 0000000..840254f Binary files /dev/null and b/slides/images/04-architecture.png differ diff --git a/slides/images/05-model.png b/slides/images/05-model.png new file mode 100644 index 0000000..cb35810 Binary files /dev/null and b/slides/images/05-model.png differ diff --git a/slides/images/06-claim.png b/slides/images/06-claim.png new file mode 100644 index 0000000..721591a Binary files /dev/null and b/slides/images/06-claim.png differ diff --git a/slides/images/07-state.png b/slides/images/07-state.png new file mode 100644 index 0000000..9dda4b4 Binary files /dev/null and b/slides/images/07-state.png differ diff --git a/slides/images/08-ownership.png b/slides/images/08-ownership.png new file mode 100644 index 0000000..737066f Binary files /dev/null and b/slides/images/08-ownership.png differ diff --git a/slides/images/09-recovery.png b/slides/images/09-recovery.png new file mode 100644 index 0000000..ca07103 Binary files /dev/null and b/slides/images/09-recovery.png differ diff --git a/slides/images/10-retries.png b/slides/images/10-retries.png new file mode 100644 index 0000000..01ee183 Binary files /dev/null and b/slides/images/10-retries.png differ diff --git a/slides/images/11-ui.png b/slides/images/11-ui.png new file mode 100644 index 0000000..8a4a847 Binary files /dev/null and b/slides/images/11-ui.png differ diff --git a/slides/images/12-tests.png b/slides/images/12-tests.png new file mode 100644 index 0000000..89531f2 Binary files /dev/null and b/slides/images/12-tests.png differ diff --git a/slides/images/13-tradeoff.png b/slides/images/13-tradeoff.png new file mode 100644 index 0000000..498dc0d Binary files /dev/null and b/slides/images/13-tradeoff.png differ diff --git a/slides/images/14-takeaway.png b/slides/images/14-takeaway.png new file mode 100644 index 0000000..7a90358 Binary files /dev/null and b/slides/images/14-takeaway.png differ diff --git a/slides/index.html b/slides/index.html new file mode 100644 index 0000000..3a0d089 --- /dev/null +++ b/slides/index.html @@ -0,0 +1,19 @@ + + +
+ + +jobs stores type, payload, status, priority, available time, attempts, max attempts, lock owner, lock deadline, result, and timestamps.',
+ 'job_events is append-only observability: created, claimed, progress, lease renewed, retry scheduled, timeout requeued, succeeded, failed, and manual retry.',
+ 'Database constraints validate row shape: queued jobs have no lock, running jobs must have a lock, and terminal jobs must have a finish timestamp.'
+ ]
+ },
+ {
+ eyebrow: '06 / Safe Claim',
+ title: 'PostgreSQL Chooses the Winner',
+ subtitle: 'Atomic claim uses row locks and SKIP LOCKED to avoid double execution.',
+ image: 'images/06-claim.png',
+ notes: [
+ 'The worker selects the next eligible queued job ordered by priority, available_at, created_at, and id.',
+ 'Inside one transaction, it uses SELECT ... FOR UPDATE SKIP LOCKED, then marks the row running, increments attempts, and sets the lease.',
+ 'When many threads race, one locks the row; the others skip it instead of waiting and accidentally claiming the same job.'
+ ]
+ },
+ {
+ eyebrow: '07 / State',
+ title: 'Status Transitions Are Explicit',
+ subtitle: 'The queue allows only meaningful movement between queued, running, succeeded, and failed.',
+ image: 'images/07-state.png',
+ notes: [
+ 'The normal path is queued -> running -> succeeded.',
+ 'Failure or timeout can move running back to queued when attempts remain.',
+ 'When attempts are exhausted, the job becomes failed. Manual retry can move failed back to queued and resets the attempt count.'
+ ]
+ },
+ {
+ eyebrow: '08 / Ownership',
+ title: 'Current Owner + Attempt Is the Write Token',
+ subtitle: 'A stale worker cannot complete or fail a job after ownership changes.',
+ image: 'images/08-ownership.png',
+ notes: [
+ 'Completion, failure, progress, and lease renewal all filter by job id, running status, locked_by, and attempt.',
+ 'That means a worker that crashed and later wakes up cannot complete attempt 1 after the job was requeued and claimed as attempt 2.',
+ 'This is the practical guardrail that makes lease recovery safe.'
+ ]
+ },
+ {
+ eyebrow: '09 / Recovery',
+ title: 'Worker Failure Is a Timeout, Not a Mystery',
+ subtitle: 'Expired leases are cleaned up deterministically.',
+ image: 'images/09-recovery.png',
+ notes: [
+ 'Workers periodically run cleanup for running jobs whose locked_until is in the past.',
+ 'If attempts remain, the job is requeued and a timeout event is recorded. If attempts are exhausted, it becomes failed.',
+ 'This queue provides at-least-once execution, not exactly-once execution. External side-effect handlers should be idempotent.'
+ ]
+ },
+ {
+ eyebrow: '10 / Retry',
+ title: 'Retries Are Bounded and Scheduled',
+ subtitle: 'Failures return to the queue with deterministic exponential backoff.',
+ image: 'images/10-retries.png',
+ notes: [
+ 'fail_job either schedules the next attempt or marks the job failed when max_attempts is reached.',
+ 'Backoff is deterministic and stored through available_at, so workers do not need hidden memory.',
+ 'Idempotency keys prevent duplicate producer submissions for the same logical job.'
+ ]
+ },
+ {
+ eyebrow: '11 / UI',
+ title: 'Demo-Friendly Observability',
+ subtitle: 'The UI shows state, pressure, attempts, leases, and event history while workers run.',
+ image: 'images/11-ui.png',
+ notes: [
+ 'Dashboard polls jobs, stats, events, and health every 2 seconds.',
+ 'Jobs page uses limit-offset pagination, status/type filters, and refreshes every 2 seconds.',
+ 'Job detail polls the job and its timeline every 1 second; global events use cursor pagination and fetch older pages as the terminal log scrolls.'
+ ]
+ },
+ {
+ eyebrow: '12 / Tests',
+ title: 'Tests Target the Risky Parts',
+ subtitle: 'The most important tests are around ownership, concurrency, and recovery.',
+ image: 'images/12-tests.png',
+ notes: [
+ 'There are service-level tests for successful claim, deterministic ordering, idempotent creation, retry scheduling, exhausted failure, timeout cleanup, stale worker rejection, and lease renewal ownership.',
+ 'The PostgreSQL concurrency test runs multiple claimers and asserts only one claimed event exists.',
+ 'API tests cover job pagination, filters, cursor-based global events, and older-page loading.'
+ ]
+ },
+ {
+ eyebrow: '13 / Trade-off',
+ title: 'PostgreSQL Now, Broker Later',
+ subtitle: 'PostgreSQL is ideal for this interview; Redis or a broker is the scale path.',
+ image: 'images/13-tradeoff.png',
+ notes: [
+ 'The assignment requires PostgreSQL, and PostgreSQL makes the correctness story compact and inspectable.',
+ 'For very high throughput, cross-service distribution, or advanced queue features, I would move to a Redis-backed or dedicated broker design.',
+ 'The important point is that I did not hide over-engineering in the interview implementation; I documented it as a next architecture.'
+ ]
+ },
+ {
+ eyebrow: '14 / Close',
+ title: 'What This Proves',
+ subtitle: 'Minimal implementation with production-aware guarantees.',
+ image: 'images/14-takeaway.png',
+ notes: [
+ 'The system is safe because PostgreSQL owns the claim decision.',
+ 'It is deterministic because state transitions and row shapes are explicit and validated.',
+ 'It is demoable because the UI and events make execution visible without adding unnecessary queue models.'
+ ]
+ }
+];
+
+const deck = document.querySelector('.deck');
+const notes = document.getElementById('notes');
+const progressFill = document.getElementById('progressFill');
+
+let current = Math.max(0, Math.min(slides.length - 1, Number(location.hash.replace('#', '')) - 1 || 0));
+let notesOpen = false;
+
+function renderDeck() {
+ deck.innerHTML = slides.map((slide, index) => `
+
+ `).join('');
+ update(false);
+}
+
+function update(shouldUpdateHash = true) {
+ deck.querySelectorAll('.slide').forEach((slide, index) => {
+ slide.classList.toggle('active', index === current);
+ slide.setAttribute('aria-hidden', index === current ? 'false' : 'true');
+ });
+
+ progressFill.style.transform = `scaleX(${(current + 1) / slides.length})`;
+
+ if (shouldUpdateHash) {
+ location.hash = String(current + 1);
+ }
+
+ renderNotes();
+}
+
+function renderNotes() {
+ const slide = slides[current];
+ notes.innerHTML = `