Project 10: Three-Node PubSub Laboratory
Build a repeatable three-node laboratory that makes topology, process-group membership, cross-node fanout, partitions, reconnection, and safe PubSub pool migration directly observable.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 4 — Expert |
| Time Estimate | 20–30 hours |
| Language | Elixir (alternatives: Erlang, Gleam) |
| Prerequisites | OTP supervision, distributed Erlang basics, Phoenix.PubSub, node monitoring, revision-based recovery |
| Key Topics | Full-mesh topology, :pg, Phoenix.PubSub.PG2, partitions, missed transient events, pool-size migration |
1. Learning Objectives
By completing this project, you will be able to:
- Separate node discovery, node connectivity, process-group membership, and application message delivery as distinct layers.
- Prove a cluster topology from every node rather than inferring it from one successful connection.
- Explain
:pgstrong eventual consistency, temporary membership divergence, and non-transitive views. - Trace how Phoenix.PubSub.PG2 performs local subscription and cross-node forwarding.
- Predict and observe component-local delivery during a network partition.
- Demonstrate that healed membership does not replay transient messages missed during disconnection.
- Repair application state using an authoritative revision rather than inventing replay semantics for PubSub.
- Perform a safe
pool_sizemigration with a compatibility stage and continuous sequence probes. - Capture node security, cleanup, and failure evidence in a reproducible lab report.
2. Theoretical Foundation
2.1 Core Concepts
Distribution has layers
A distributed Elixir application can fail even when every component works individually because several independent layers are often collapsed into the word “cluster.” Discovery produces candidate node names or addresses. Distributed Erlang establishes authenticated connections. Topology determines which nodes are directly connected. :pg propagates process-group membership over those direct connections. Phoenix.PubSub uses adapter processes and those membership views to forward broadcasts. Finally, the application interprets delivered messages and repairs missed state.
No layer automatically supplies the layer below it. Phoenix.PubSub does not discover nodes. A discovery library does not guarantee all expected distribution connections remain healthy. A full Node.list/0 snapshot does not prove every PubSub shard has converged. Converged membership does not replay a message missed during a partition.
:pg process groups
OTP’s :pg module implements distributed named process groups. Processes join locally, and membership changes propagate across directly connected nodes. Official OTP documentation describes the model as strong eventual consistency: replicas may temporarily observe updates in different orders, but connected replicas converge after communication stabilizes.
Membership is non-transitive. If alpha and gamma are both connected to beta but not to each other, beta may see both sets of members while alpha and gamma do not see one another’s group membership. A hub-and-spoke drawing is therefore not equivalent to a full mesh for group propagation. Group membership is also ephemeral. If a local scope process restarts, its local membership is not preserved automatically; supervised adapter processes must rejoin.
Phoenix.PubSub.PG2 forwarding
Phoenix.PubSub maintains local subscriber registrations and uses an adapter for cluster forwarding. With the PG2 adapter, pool shards distribute forwarding work. A broadcast enters a local shard, is sent to corresponding remote adapter members visible through the process group, and each remote node dispatches locally. The application subscriber does not receive a durability guarantee or broker acknowledgement. A successful call means the broadcast operation was accepted according to the current adapter view, not that every subscriber processed it.
Pool size matters because the shard used to broadcast must be understood by old and new nodes during a rolling deployment. Official Phoenix.PubSub documentation provides a compatibility mechanism: deploy the new pool_size while temporarily retaining the old broadcast_pool_size, then deploy again with the final broadcast size. Increasing from 1 to 2 without the compatibility stage can create migration-induced gaps even in an otherwise healthy cluster.
Partitions and transient loss
When gamma is isolated from alpha and beta, each connected component continues local work. Alpha and beta can exchange PubSub messages; gamma can deliver only within its component. This is availability, not global consistency. Reconnecting restores distribution and allows membership to converge, but Phoenix.PubSub has no historical log from which to replay revision 42.
Applications therefore distinguish notifications from facts. A notification says, “revision 42 may now exist.” Each subscriber tracks the last applied revision. A gap, reconnect, or node-up event triggers an authorized reload from the authoritative database or event log. The durable system decides what catch-up means; PubSub merely lowers latency while connectivity is healthy.
Topology evidence and security
Distribution is privileged remote-code infrastructure, not a generic public transport. A shared cookie authenticates nodes but is not a complete production security design. Network isolation, TLS distribution where appropriate, certificate and cookie rotation, restricted node names and ports, least-privilege runtime identities, and explicit discovery boundaries are part of the system.
The lab must be disposable. It should use isolated names, deterministic ports or containers, training credentials, and a teardown check that finds no surviving nodes, listeners, proxy rules, or partition hooks.
2.2 Why This Matters
Cluster tests often prove only the happy path: start two nodes on one laptop, publish once, and celebrate when both print a line. That test does not expose partial topology, temporary membership disagreement, node churn, partition behavior, or rolling configuration compatibility. Production incidents happen in those omitted states.
A three-node lab is the smallest topology that reveals non-transitivity. With two nodes, connected and disconnected are the only shapes. With three, alpha and gamma can both see beta while remaining disconnected from each other. Each node can then hold a different but locally reasonable view. This makes abstract distributed-systems language concrete.
The sequence probe turns visibility into evidence. Publishing monotonically numbered revisions before, during, and after a partition shows exactly which component observed each transient message. Comparing that stream with an authoritative revision demonstrates recovery without pretending the missing message appears later.
2.3 Historical Context / Background
Distributed Erlang was designed to make sending to a remote PID syntactically similar to local messaging. That transparency is productive, but latency, connectivity, and failure semantics remain distributed. Earlier Phoenix.PubSub versions supported multiple adapters and Erlang process-group mechanisms; current PG2 naming is historical while its implementation is based on modern :pg/:pg2 compatibility according to the package documentation.
The operational lesson has remained stable: an API may be location-transparent, but failures are not. Modern deployment platforms automate DNS discovery and rolling releases, yet the application must still validate topology, mixed-version compatibility, and state repair.
2.4 Common Misconceptions
- “Phoenix.PubSub discovers application nodes.” Discovery and connection happen elsewhere.
- “If alpha and gamma both connect to beta, they see each other through beta.”
:pgmembership is non-transitive. - “Strong eventual consistency means every read is current.” Views may temporarily diverge before convergence.
- “A healed partition replays missed broadcasts.” PubSub is transient; only current membership converges.
- “
Node.list/0on alpha proves the full topology.” Every node needs a symmetric connectivity snapshot. - “A shared cookie makes distribution safe on an untrusted network.” It is only one credential in a larger security boundary.
- “Changing
pool_sizeis a normal one-step rolling config update.” Safe migration requires the documented compatibility phase.
3. Project Specification
3.1 What You Will Build
Build a local laboratory with nodes alpha@lab, beta@lab, and gamma@lab. Each node runs the same supervised application, one Phoenix.PubSub instance, a probe subscriber, a topology observer, a revision tracker, and a small status endpoint or terminal panel.
The lab controller starts and stops nodes, requests connectivity snapshots from each one, waits for adapter group convergence, publishes numbered probes, injects a deterministic partition, heals it, and validates application catch-up.
The authoritative revision store may be a small disposable PostgreSQL table or one deliberately centralized training service. It must be clearly separate from PubSub. When alpha commits revision 42 during gamma’s isolation, gamma misses the transient probe. After reconnection, gamma queries the authority and updates from 41 to 42.
The final drill changes PubSub from pool size 1 to 2. A continuous publisher emits sequence probes throughout both deployment stages. The first stage runs the new pool while broadcasting compatibly with the old size; the second removes compatibility after all nodes are upgraded.
3.2 Functional Requirements
- Three named nodes: Start three isolated BEAM nodes with predictable logical identities.
- Topology snapshots: Collect each node’s direct connections and render a matrix.
- Membership snapshots: Query every PubSub shard/process group from every node.
- Sequence probes: Publish a monotonic sequence with publisher node, event ID, and authoritative revision.
- Observation ledger: Record which node observed which sequence and when.
- Deterministic partition: Isolate gamma from alpha/beta without crashing the application.
- Component-local assertions: Prove alpha/beta share revision 42 while gamma misses it.
- Heal and converge: Restore connectivity and wait for node and membership convergence separately.
- Authoritative repair: Detect gamma’s revision gap and reload revision 42.
- Pool migration: Execute the documented 1→2 compatibility rollout under continuous publication.
- Cleanup: Remove network rules and stop all nodes even when a drill fails.
- Security report: Document credentials, network boundary, naming, and why the setup is training-only.
3.3 Non-Functional Requirements
- Reproducibility: A fresh checkout can run the full lab with deterministic node names and steps.
- Isolation: The lab cannot connect to unrelated local or production clusters.
- Observability: Every delivery includes sequence, node, component, topology generation, and timestamps.
- Boundedness: Observation buffers, log retention, and probe rates are explicitly limited.
- Convergence budget: Full-mesh and group convergence have measured deadlines rather than unbounded sleeps.
- Recovery: Missed transient messages are labeled, not hidden; durable revision catch-up is independently verified.
- Migration safety: Continuous probes show zero pool-migration-induced gaps while the network remains healthy.
- Cleanup safety: Teardown is idempotent and reports residual nodes or partition controls.
3.4 Example Usage / Output
$ ./lab cluster status
node matrix:
alpha -> [beta,gamma]
beta -> [alpha,gamma]
gamma -> [alpha,beta]
topology=full_mesh PASS
pubsub shard=0 members alpha=3 beta=3 gamma=3 converged=true
publish sequence=41 revision=41 from=alpha
observed alpha=12ms beta=18ms gamma=21ms PASS
partition applied component_A=[alpha,beta] component_B=[gamma]
publish sequence=42 revision=42 from=alpha
observed=[alpha,beta] gamma=MISS_EXPECTED
partition healed
node_views_converged=true pg_views_converged=true
gamma last_observed=41 authoritative=42 gap=1
gamma reload revision=42 source=authoritative_store PASS
pool_migration stage=compat pool_size=2 broadcast_pool_size=1 probes=500 gaps=0
pool_migration stage=final pool_size=2 broadcast_pool_size=2 probes=500 gaps=0
cleanup residual_nodes=0 residual_partition_rules=0
lab result=PASS
3.5 Real World Outcome
Run three terminals or open the lab’s topology page. The normal-state view shows a 3×3 adjacency matrix with symmetric direct connections, one PubSub shard containing one forwarding member per node, and three subscribers on the selected topic. Publish revision 41 and watch all three node timelines add the same event with different arrival latencies.
Activate “Partition Gamma.” The topology view splits into two colored components. Alpha and beta show node-down for gamma; gamma shows isolation from both. Publishing revision 42 produces green delivery markers only on alpha and beta and an explicit gray MISS_EXPECTED marker on gamma. The system must never paint gamma green merely because membership later heals.
Heal the partition. Node and process-group views converge. Gamma’s revision card remains 41 until its recovery path compares against the authoritative revision. It then reloads 42 and labels the transition recovered_from_authority, not replayed_by_pubsub.
Finally, start a continuous probe and perform the pool-size migration. The dashboard overlays rollout stage, node version, configured pool size, broadcast pool size, and any gap. Completion evidence shows no gaps during healthy-network migration, while the earlier partition gap remains documented as expected transient loss.
4. Solution Architecture
4.1 High-Level Design
LAB CONTROLLER
start | inspect | partition | heal | migrate | teardown
│
┌────────────────────┼────────────────────┐
v v v
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ alpha@lab │ │ beta@lab │ │ gamma@lab │
│ PubSub shards │◀▶│ PubSub shards │◀▶│ PubSub shards │
│ probe subscriber │ │ probe subscriber │ │ probe subscriber │
│ topology observer│◀▶│ topology observer│◀▶│ topology observer│
│ revision tracker │ │ revision tracker │ │ revision tracker │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
└─────────────────────┼─────────────────────┘
v
┌───────────────────────┐
│ Authoritative revision│
│ store / API │
└───────────────────────┘
Partition drill:
{alpha,beta} X {gamma}
After heal:
connectivity converges -> :pg membership converges
missed messages do not return -> revision tracker reloads facts
4.2 Key Components
| Component | Responsibility | Key Decisions |
|---|---|---|
| Node launcher | Start isolated named runtimes | Stable lab names and per-run credential |
| Topology observer | Report direct peers and node up/down | Snapshot from every node |
| Membership observer | Report adapter shard group members | Poll with deadline; retain divergent views |
| Probe publisher | Emit monotonic sequence/revision events | Fixed rate and bounded run length |
| Probe subscriber | Record observed events | Bounded ETS/ring buffer plus export |
| Revision tracker | Detect gaps and reload facts | PubSub never treated as replay log |
| Partition controller | Apply and remove deterministic isolation | Idempotent cleanup in failure path |
| Migration orchestrator | Roll nodes through compatibility/final stages | Continuous probes during every restart |
| Evidence aggregator | Produce matrix, timeline, and verdicts | Correlate monotonic and wall-clock time |
4.3 Data Structures
Probe
run_id isolates repeated lab runs
sequence monotonic integer
event_id immutable logical ID
revision authoritative fact revision
publisher_node alpha/beta/gamma
published_monotonic latency baseline
Observation
run_id + sequence + observer_node unique evidence key
received_monotonic
topology_generation
pool_stage
NodeView
observer_node
captured_at
direct_peers
connected_component
GroupView
observer_node
scope
shard
visible_members
captured_at
RevisionCursor
tenant_or_stream
node
last_observed_revision
last_authoritative_revision
recovery_reason
4.4 Algorithm Overview
Algorithm: Prove full mesh
FOR each expected node N:
request direct peer snapshot from N
assert peers(N) = expected_nodes - N
assert every edge is symmetric
THEN request group view from every N and shard
WAIT until expected membership appears everywhere or deadline expires
PRESERVE intermediate divergent snapshots as evidence
Algorithm: Partition and recover
publish revision 41 and await all observations
isolate gamma from alpha and beta
wait for topology to show components {alpha,beta} and {gamma}
commit authoritative revision 42
publish transient probe 42 from alpha
assert alpha and beta observe; gamma does not
heal partition
wait separately for node and group convergence
compare gamma cursor 41 with authority 42
reload current fact or durable events
set gamma cursor 42 with recovery_reason=authoritative_reload
Algorithm: Safe pool migration 1→2
start continuous sequence probes
stage A: roll every node to pool_size=2, broadcast_pool_size=1
assert each node healthy and probes remain gap-free
stage B: roll every node to pool_size=2, broadcast_pool_size=2/final
assert each node healthy and probes remain gap-free
stop probes and compare published/observed ranges per node
Complexity analysis
- Full topology validation is O(n²); intentional for a small lab and necessary for symmetric evidence.
- Membership evidence is O(n × s × m), for nodes, shards, and visible members.
- Probe storage is O(n × p) for
pbounded probes observed acrossnnodes. - The lab uses three nodes; production observability should aggregate rather than render a quadratic matrix at very large scale.
5. Implementation Guide
5.1 Development Environment Setup
Use containers or isolated local loopback addresses. Do not reuse a production cookie or open distribution ports to an untrusted network.
$ elixir --version
Elixir 1.20.x (compiled with Erlang/OTP 29)
$ ./lab doctor
loopback aliases=available
distribution ports=closed_before_start
expected nodes=[alpha@lab,beta@lab,gamma@lab]
training credential=generated_for_run
partition backend=container_network
doctor result=PASS
The controller may wrap mix release, containers, or three explicit iex --name commands. Commands are setup interfaces; do not hide the actual node names, cookie source, port restrictions, or cleanup behavior.
5.2 Project Structure
three_node_pubsub_lab/
├── lib/
│ ├── lab/topology_observer.ex
│ ├── lab/membership_observer.ex
│ ├── lab/probe_publisher.ex
│ ├── lab/probe_subscriber.ex
│ ├── lab/revision_tracker.ex
│ └── lab/evidence_reporter.ex
├── config/
│ ├── runtime.exs # node/pool stage inputs
│ └── lab_topology.example # non-secret outline
├── scripts/
│ ├── lab # lifecycle interface
│ └── partition_backends/ # platform-specific isolation
├── test/
│ ├── distributed/
│ └── migration/
├── docs/
│ ├── topology-contract.md
│ ├── security-boundary.md
│ └── failure-predictions.md
└── evidence/
5.3 The Core Question You’re Answering
“What does cluster Pub/Sub guarantee when node views diverge, and how does the application recover facts after connectivity returns?”
Your answer must distinguish current connected component, current membership view, transient message delivery, and authoritative application state. “The cluster heals” is incomplete unless it states what converges and what does not replay.
5.4 Concepts You Must Understand First
- Distributed Erlang nodes
- What makes a node name resolvable and a connection authenticated?
- Which failures appear as
nodedownand when? - Book Reference: “Designing for Scalability with Erlang/OTP” — distributed architecture chapters.
- Topology and discovery
- Does discovery return candidates or guarantee live full-mesh connectivity?
- How will every node prove its direct peers?
- Reference: Erlang distributed-system documentation and chosen discovery adapter docs.
:pgmembership- What does strong eventual consistency permit during convergence?
- Why is membership non-transitive?
- Reference: OTP 29
pgdocumentation.
- Phoenix.PubSub adapter behavior
- What is local subscription versus cluster forwarding?
- How do pool shards and remote members relate?
- Reference: Phoenix.PubSub and Phoenix.PubSub.PG2 documentation.
- Revision-based recovery
- Which system stores authoritative facts?
- How does a subscriber detect and repair a gap?
- Book Reference: “Designing Data-Intensive Applications, 2nd Edition” — distributed-system trouble.
5.5 Questions to Guide Your Design
- Proof quality
- Are topology and membership sampled from all three nodes?
- Are intermediate divergent views preserved instead of waiting silently?
- Partition mechanism
- Can isolation be applied and removed deterministically?
- What cleanup runs after an assertion failure or interrupted shell?
- Clock and ordering
- Which results use monotonic time, and which display wall-clock time?
- Does sequence, not timestamp, determine missing messages?
- Recovery
- Is the source a current-state reload or durable event replay?
- How are stale and missing revisions labeled differently?
- Migration
- Which config is deployed in stage A and stage B?
- How does a probe distinguish network loss from migration loss?
5.6 Thinking Exercise
Draw both topologies:
FULL MESH HUB AND SPOKE
alpha ───── beta alpha ───── beta ───── gamma
│ ╲ │ alpha X gamma direct connection
│ ╲ │
└──── gamma
For each node, list Node.list and possible :pg members. Then partition {alpha,beta} from {gamma} and predict:
- node monitor signals;
- adapter membership visible from each component;
- delivery of sequences 41, 42, and 43;
- what happens to a subscriber that joins during isolation;
- what membership converges after heal;
- why sequence 42 remains absent on gamma;
- which authoritative operation repairs gamma.
Record predictions before running the lab and compare every mismatch.
5.7 The Interview Questions They’ll Ask
- “Does Phoenix.PubSub discover or connect Erlang nodes?”
- “What consistency model does
:pgprovide?” - “Are
:pgmembership views transitive through a hub?” - “What happens to PubSub broadcasts during a partition?”
- “How does an application repair state after the partition heals?”
- “How do you safely increase Phoenix.PubSub pool size during a rolling deploy?”
5.8 Hints in Layers
Hint 1: Prove the transport before the abstraction
Render the direct-peer matrix before inspecting PubSub. A wrong matrix invalidates later assumptions.
Hint 2: Number every probe
Use one bounded stream of monotonically increasing sequence numbers. Do not infer loss from wall-clock gaps.
Hint 3: Keep convergence and recovery separate
Produce two verdicts after heal:
membership_converged=true
application_revision_recovered=true source=authoritative_reload
Hint 4: Migrate while noisy
A quiet rolling deployment proves only startup. Continuous probes reveal compatibility gaps.
5.9 Books That Will Help
| Topic | Book | Chapter / Section |
|---|---|---|
| Distributed OTP | “Designing for Scalability with Erlang/OTP” | Distribution and distributed architectures |
| Network partitions | “Designing Data-Intensive Applications, 2nd Edition” | Distributed-System Trouble |
| Rollouts and stability | “Release It!, 2nd Edition” | Stability Patterns |
| Architecture evidence | “Software Architecture in Practice, 4th Edition” | Quality Attribute Scenarios |
5.10 Implementation Phases
Phase 1: Topology and Membership Observatory (8–12 hours)
Goals
- Start three isolated nodes.
- Prove full mesh and adapter membership from every node.
Tasks
- Create lifecycle and teardown interfaces.
- Add node monitoring and symmetric peer snapshots.
- Add per-shard group snapshots with deadlines.
- Add bounded sequence publisher/subscribers.
- Export a normal-state topology report.
Checkpoint: Revision 41 reaches all nodes, every node reports two peers, every shard view converges, and teardown leaves no residual runtime.
Phase 2: Partition and Authoritative Recovery (8–14 hours)
Goals
- Observe component-local fanout.
- Repair missed facts after heal.
Tasks
- Implement deterministic gamma isolation and cleanup.
- Capture intermediate node/group views.
- Commit and publish revision 42 during isolation.
- Heal connectivity and wait for convergence.
- Detect gamma’s gap and reload authoritative state.
Checkpoint: Gamma explicitly misses transient 42, later converges membership, and recovers revision 42 only through the authority.
Phase 3: Safe Pool Migration and Certification (8–14 hours)
Goals
- Exercise mixed configuration safely.
- Produce repeatable migration evidence.
Tasks
- Define stage-A and stage-B runtime configurations.
- Start a fixed-rate sequence probe.
- Roll alpha, beta, and gamma one at a time through compatibility.
- Roll them again to final configuration.
- Attribute gaps, validate cleanup, and publish the lab report.
Checkpoint: At least 1,000 healthy-network probes across the two stages show zero migration gap, and the report still preserves the expected partition loss separately.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale |
|---|---|---|---|
| Node environment | Host processes; containers | Containers or isolated loopback with explicit teardown | Deterministic partition and port boundary |
| Topology | Full mesh; hub/spoke | Full mesh for reference lab; demonstrate hub/spoke failure separately | Matches adapter membership expectations |
| Observation storage | Unbounded process list; bounded ETS/ring buffer | Bounded store plus export | Avoids evidence causing mailbox/memory failure |
| Catch-up | Assume replay; query authority | Query/replay from durable authority | PubSub has no replay log |
| Timing | Wall clock only; sequence + monotonic time | Sequence + monotonic time | Correct gap and latency evidence |
| Pool rollout | One stage; compatibility then final | Two-stage documented migration | Supports mixed old/new nodes |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Local unit | Validate matrices, gaps, and verdicts | Symmetric edges, missing sequence ranges |
| Multi-node integration | Verify real distribution | Full mesh, shard membership, cross-node probe |
| Partition | Verify component-local behavior | Gamma isolation and heal |
| Recovery | Verify durable catch-up | Cursor 41 to authority 42 |
| Churn | Verify process/node lifecycle | Subscriber crash, adapter restart, node restart |
| Migration | Verify mixed pool configuration | Stage A and B under continuous probes |
| Security/cleanup | Verify isolation | Wrong credential denial, no residual ports/rules |
6.2 Critical Test Cases
- Full mesh: Every node sees the other two directly and group views converge.
- Hub/spoke counterexample: Alpha and gamma both see beta but not each other; non-transitive membership is exposed.
- Normal fanout: Sequence 41 appears once on all expected subscribers.
- Partition delivery: Sequence 42 remains inside
{alpha,beta}; gamma’s miss is expected. - Local gamma publish: During isolation, gamma’s publish remains in its component and does not reach alpha/beta.
- Heal convergence: Node and group views converge within measured budgets.
- No replay: Sequence 42 does not spontaneously arrive on gamma after heal.
- Revision repair: Gamma reloads authoritative revision 42 and records the source.
- Adapter restart: Local subscriptions/members recover through supervision; temporary divergence is measured.
- Pool migration: Continuous healthy-network probes show no stage-induced gaps.
- Wrong credential: An unauthorized fourth node cannot join.
- Cleanup: Interrupted drill still removes partition controls and terminates nodes.
6.3 Test Data
run_id: lab-2026-07-15-01
nodes: [alpha@lab, beta@lab, gamma@lab]
topic: lab:tenant42:orders
initial_pool_size: 1
target_pool_size: 2
probes:
41 -> before partition, expected [alpha,beta,gamma]
42 -> during partition, publisher alpha, expected [alpha,beta]
43 -> during partition, publisher gamma, expected [gamma]
44 -> after heal, expected [alpha,beta,gamma]
authoritative revisions:
before partition: 41
during partition: 42
gamma recovery target: 42
migration:
stage A: pool_size=2 broadcast_pool_size=1 probes=1000
stage B: pool_size=2 broadcast_pool_size=2 probes=1000
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution |
|---|---|---|
| Assuming discovery equals topology | Candidates exist but probes miss nodes | Capture direct-peer matrix from every node |
| Checking only beta | Hub appears healthy while edges disagree | Require symmetric all-node snapshots |
| Waiting with arbitrary sleep | Flaky convergence test | Poll explicit conditions with deadline and evidence |
| Expecting replay after heal | Gamma never sees 42 | Reload/replay from durable authority |
| Hiding expected loss | Dashboard reports false success | Label expected transient misses and repair separately |
| One-step pool increase | Gaps during mixed rollout | Use broadcast_pool_size compatibility stage |
| Shared production cookie | Lab can join privileged cluster | Generate isolated per-run credential and network |
| Failed teardown | Later runs start in a partitioned state | Make cleanup idempotent and verify residual state |
7.2 Debugging Strategies
- Work bottom-up: node names/resolution → direct connections → group views → adapter forwarding → subscriber receipt.
- Inspect every node: a global-looking view from one node may be uniquely complete.
- Preserve divergence: log membership snapshots with observer identity and time.
- Compare components: classify each delivery against the component graph at publication time.
- Use sequence ranges: calculate missing sets per node rather than reading interleaved terminal logs.
- Separate restart from reconnect: a restarted process has a new PID and may need to rejoin even if the node never disconnected.
- Run cleanup doctor: before a new drill, confirm no old nodes, ports, or network filters remain.
7.3 Performance Traps
- Excessive topology polling adds distribution traffic and can distort convergence measurements.
- Broadcasting probes too quickly can make subscriber mailboxes the bottleneck instead of the network fault under study.
- Storing every observation forever creates quadratic evidence growth.
- Large PubSub pool sizes add processes and group membership traffic; bigger is not automatically faster.
- Blocking report generation in subscriber processes delays receipt and produces misleading gaps.
- DNS or discovery retry storms can hide the original partition under control-plane load.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add a live adjacency matrix and node-event timeline.
- Add a fourth unauthorized node and prove join rejection.
- Compare a subscriber restart with a full node restart.
8.2 Intermediate Extensions
- Replace static seeds with one discovery mechanism and keep topology proof unchanged.
- Add asymmetric packet loss or latency rather than complete isolation.
- Track Phoenix.Tracker/Presence convergence alongside PubSub delivery.
- Compare state reload with durable event-log replay.
8.3 Advanced Extensions
- Run nodes across three local networks and route only explicit distribution ports.
- Configure TLS distribution and automate certificate rotation in the lab.
- Test a rolling OTP/Elixir/Phoenix.PubSub version upgrade under probes.
- Build a Jepsen-inspired history checker for published, observed, and recovered revisions.
9. Real-World Connections
9.1 Industry Applications
- Realtime SaaS: Multiple Phoenix nodes fan out tenant updates while clients reconnect across deploys.
- Fleet and IoT control: Regional partitions require local operation plus later authoritative catch-up.
- Collaboration tools: Presence and editing notifications tolerate transient loss but documents require durable state.
- Operations dashboards: Cluster health must distinguish stale views from durable business loss.
- Rolling platform upgrades: Mixed configuration must be explicitly compatible under live traffic.
9.2 Related Open Source Projects
- Phoenix.PubSub: Realtime local and distributed PubSub.
- Erlang/OTP
pg: Distributed process groups. - libcluster: Pluggable node discovery/topology strategies.
- Phoenix.Tracker: Eventually consistent distributed presence tracking.
9.3 Interview Relevance
This lab equips you to reject vague claims such as “Erlang distribution is transparent” or “PubSub works across the cluster.” You can draw the exact topology, state the membership consistency model, predict delivery per connected component, explain non-replay after heal, and describe a safe rolling configuration change with evidence.
10. Resources
10.1 Essential Reading
- “Designing for Scalability with Erlang/OTP” by Francesco Cesarini and Steve Vinoski — distribution and scalable architecture chapters.
- “Designing Data-Intensive Applications, 2nd Edition” by Martin Kleppmann et al. — distributed failure, consistency, and recovery.
- “Release It!, 2nd Edition” by Michael Nygard — stability and deployment failure patterns.
- “Software Architecture in Practice, 4th Edition” by Bass et al. — measurable quality-attribute scenarios.
10.2 Video Resources
- Official Erlang/OTP conference talks on distribution, partitions, and process groups.
- Phoenix conference talks that instrument multi-node PubSub and Presence behavior.
10.3 Tools & Documentation
- OTP
pg— consistency and non-transitive membership semantics. - Distributed Erlang — node naming and distribution behavior.
- Phoenix.PubSub — adapters and safe pool-size migration.
- Phoenix.Tracker — distributed presence convergence.
- libcluster — optional discovery layer.
10.4 Related Projects in This Series
- Project 2: Registry Topic Bus introduces local membership without distribution.
- Project 6: Distributed Presence Workspace introduces eventually consistent presence semantics.
- Project 9: Durable Notification Outbox supplies the authoritative revision recovery path.
- Project 11: Tenant-Safe Broker Edge Bridge adds durable cross-service delivery beyond a BEAM cluster.
- Project 12: Multi-Tenant Real-Time Event Platform turns this lab into a production-style chaos certification.
11. Self-Assessment Checklist
11.1 Understanding
- I can separate discovery, connectivity, membership, forwarding, and application recovery.
- I can explain strong eventual consistency without claiming immediate agreement.
- I can draw a non-transitive membership example with three nodes.
- I can explain why membership convergence does not replay sequence 42.
- I can describe the two stages of a safe pool-size increase.
- I can defend the distribution security boundary.
11.2 Implementation
- Three nodes start with isolated names and credentials.
- Every node reports the expected direct peers and shard members.
- Sequence 41 reaches all nodes before partition.
- Sequences 42 and 43 remain in their expected components.
- Heal converges node and group views within measured budgets.
- Gamma recovers revision 42 from the authority.
- Continuous migration probes show zero healthy-network gaps.
- Teardown reports no residual nodes or partition controls.
11.3 Growth
- I documented at least one prediction that the lab disproved.
- I can diagnose a hub-and-spoke topology from per-node evidence.
- I can explain when a durable broker or event log is required instead of PubSub.
- I can present the partition timeline and migration evidence in an interview.
12. Submission / Completion Criteria
Minimum Viable Completion
- Three nodes form and prove a symmetric full mesh.
- A numbered probe reaches all nodes in normal state.
- Gamma isolation produces the predicted component-local delivery.
- Healing restores topology and membership visibility.
- Gamma catches up from an authoritative revision source.
Full Completion
- All minimum criteria plus captured intermediate membership divergence, bounded evidence, security documentation, and idempotent teardown.
- The lab distinguishes node convergence, group convergence, transient non-replay, and durable state recovery.
- A two-stage 1→2 pool migration runs under at least 1,000 continuous probes with zero migration-induced gaps.
- The final report includes matrices, timelines, sequence sets, convergence durations, and residual-state checks.
Excellence (Going Above & Beyond)
- The lab supports static and dynamic discovery while retaining the same evidence contract.
- TLS distribution and credential rotation are automated and verified.
- Asymmetric latency/loss tests produce explainable convergence and delivery histories.
- A history checker automatically validates component-aware delivery and eventual revision recovery.
This guide was expanded from LEARN_ELIXIR_PUBSUB_DEEP_DIVE.md. For the complete expanded project index, see README.md.