After studying this material, you should be able to:
Standard Advice: Reality for Big Data:
"Test on small samples" "Bugs only appear at full scale"
↓ ↓
Fast iteration Missing the actual bug
| Session Type | Events Generated |
|---|---|
| Typical user session | A handful |
| Idle Netflix tab overnight | Tens of thousands |
Critical Insight: A standard 1% sample statistically eliminates extreme long-tail cases entirely
OLD: Sequential Testing
─────────────────────────────────────────
Test 1 → Test 2 → Test 3 → Test 4 → Test 5
2hr 2hr 2hr 2hr 2hr
= 10 hours total
NEW: Parallel Testing
─────────────────────────────────────────
Test 1 ─┐
Test 2 ─┤
Test 3 ─┼─→ All finish together
Test 4 ─┤
Test 5 ─┘
= 2 hours total (slowest job only)
Branch Name: pgn/cap-experiment
↓
Automatically creates:
┌─────────────────────┐
│ Unique workflow │
│ Unique output table │
│ Unique namespace │
└─────────────────────┘
Why this works:
Old goal: Make one run faster
New goal: Run many experiments simultaneously without interference
Without Persistence:
Laptop sleeps → SSH drops → Multi-hour Spark job DIES
↓
Hours of work lost
Your Laptop Cloud Workbench
│ │
│ SSH connection │
├──────────────────────►│
│ │ ← tmux session lives HERE
│ │ (not on your laptop)
│
Close laptop
│
│ Reconnect later │
├──────────────────────►│
│ │ ← Session still running!
| Component | Tool | Purpose |
|---|---|---|
| Terminal | Ghostty | Local interface |
| Branch management | Git worktrees | One worktree per experiment |
| Session persistence | tmux on cloud | Jobs survive disconnection |
| Compute | Cloud Workbench | Heavy lifting |
Key Principle: Your laptop is disposable. The cloud does the work.
collect_list Problemcollect_list behavior:
─────────────────────────────────────────
Event 1 ─┐
Event 2 ─┤
Event 3 ─┼─→ Entire bag buffered in RAM → OOM risk
... ─┤
Event N ─┘
Problem: Memory scales with INDIVIDUAL session size,
not total dataset size
What inflated each event's weight:
Integer instead of int) using extra memoryThe Fix:
Takeaway: Reduce individual event weight rather than adding more cluster memory
Broadcast Join:
─────────────────────────────────────────
Small table → copied to ALL executors → fast joins
↓
BUT: memory stays pinned
↓
Multiple broadcasts accumulate
↓
Memory pressure builds
The Surgical Fix:
Before: Disable broadcast joins globally
→ Hurts query performance everywhere
After: Lower only AQE's RUNTIME threshold
spark.sql.adaptive.autoBroadcastJoinThreshold = 10MB
→ Freed ~3.1 GB executor memory
→ Initial query planning unaffected
Takeaway: Watch accumulated broadcasts in your execution plan, not just individual dataset sizes
WRONG approach:
─────────────────────────────────────────
All events → Window buffers ENTIRE session → row_number() → filter top N
↑
OOM happens HERE
(before filtering even occurs)
CORRECT approach:
─────────────────────────────────────────
All events → Cap events FIRST → Window buffers smaller session → safe
The Lesson Visualized:
Filtering AFTER buffering: Filtering BEFORE buffering:
┌─────────────────────┐ ┌──────────┐
│ 50,000 events │ │ 50,000 │
│ buffered in memory │ vs │ → cap │
│ THEN filter to 100 │ │ → 100 │
│ → OOM │ │ buffered │
└─────────────────────┘ └──────────┘
Takeaway: Always test fixes against worst-case input skew, not just average load
5 parallel runs × dozens of iterations =
Hundreds of combinations of:
- JAR versions
- Config settings
- Memory spill results
- Stage failure counts
↓
Easy to lose track of what you tried
After every run, the AI agent automatically appended:
| Branch | Cap | Partitions | Spill | OOM | Finding |
|---|---|---|---|---|---|
| pgn/exp-1 | 500 | 200 | Low | No | Baseline stable |
| pgn/exp-2 | 1000 | 200 | High | Yes | Cap too large |
Two-document system:
Document 1: Chronological run log
→ Full history, every result
Document 2: Symptom-to-fix lookup table
→ Distilled findings for teammates
→ "If you see X, try Y"
Key Principle: When running many parallel tests, losing track of results is a bigger risk than job failure
Problem Identified → Isolated in one branch → Validated → Shipped
Fix 1: Slim event bag (remove unnecessary fields)
Fix 2: Unbox primitive types (int vs Integer)
Fix 3: Lower AQE broadcast threshold to 10MB
Fix 4: Move truncation before Window buffering
Each fix was independently validated before merging — the branch isolation made this possible.
Current state after fixes:
─────────────────────────────────────────
Pipeline runs cleanly ✓
Memory pressure reduced ✓
Future state (inevitable):
─────────────────────────────────────────
Upstream adds new event attributes
↓
Each event gets heavier again
↓
Memory pressure returns
↓
WITHOUT changing a single line of your code
| Approach | What It Buys | What It Doesn't Solve |
|---|---|---|
| Tuning | Time and headroom | Unbounded row growth |
| Architecture change | Scalability | Requires more work |
Core Takeaway: If your data model allows unbounded row growth, tuning alone cannot save you permanently. Eventually the architecture must change.
PROBLEM: Big data bugs only appear at full scale
↓
SOLUTION LAYER 1: Branch isolation → parallel experiments
↓
SOLUTION LAYER 2: tmux on cloud → persistent sessions
↓
SOLUTION LAYER 3: AI agent → operational tracking
↓
RESULT: 5 experiment cycles in 24 hours instead of 5 days
↓
FINDINGS: collect_list weight + broadcast accumulation + Window ordering
↓
LESSON: Tuning buys time. Architecture determines ceiling.
collect_list cause OOM based on individual session size rather than total data volume?