Implement a Pub/Sub Messaging System in Go
This challenge asks you to build a fundamental component of many distributed systems: a publish-subscribe (pub/sub) messaging system. A pub/sub system allows publishers to send messages without knowing who the subscribers are, and subscribers to receive messages without knowing who the publishers are. This decoupling is crucial for building scalable and resilient applications.
Problem Description
Your task is to implement a concurrent, in-memory publish-subscribe messaging system in Go. The system should support the following operations:
- Publish: A publisher sends a message to a specific topic.
- Subscribe: A subscriber registers interest in a specific topic, and will receive messages published to that topic.
- Unsubscribe: A subscriber can stop receiving messages for a topic.
The system should be thread-safe, meaning multiple goroutines can publish and subscribe concurrently without data corruption or race conditions. Messages should be delivered to all active subscribers of a topic.
Key Requirements:
PublisherInterface: Define an interface for publishing messages.SubscriberInterface: Define an interface for receiving messages.PubSubStructure: Implement a centralPubSubstructure that manages topics, subscribers, and message routing.- Topic-Based Routing: Messages published to a topic should only be delivered to subscribers of that same topic.
- Concurrency Safety: The
PubSubsystem must be safe for concurrent use by multiple goroutines. - Channel-Based Delivery: Subscribers should receive messages via Go channels.
- Unsubscribe Mechanism: Subscribers must be able to cleanly unsubscribe from topics.
Expected Behavior:
- When a message is published to a topic, all currently subscribed subscribers for that topic should receive the message.
- Subscribers should receive messages in the order they are published to a given topic.
- Unsubscribing should immediately stop message delivery for that subscriber on that topic.
- Publishing to a topic with no subscribers should not cause an error.
Edge Cases:
- Subscribing to a topic that doesn't exist yet.
- Unsubscribing from a topic the subscriber is not subscribed to.
- Publishing or subscribing with empty topic names.
- Handling a large number of publishers, subscribers, and messages.
Examples
Example 1: Basic Publish and Subscribe
// Assume a PubSub instance `ps` is created and initialized
// Create a subscriber
sub1Chan := make(chan string)
ps.Subscribe("news", sub1Chan)
// Publish a message
ps.Publish("news", "Breaking news: Go 2.0 announced!")
// In a separate goroutine (or after a short delay to allow delivery)
// read from sub1Chan: "Breaking news: Go 2.0 announced!"
// Subscribe another
sub2Chan := make(chan string)
ps.Subscribe("news", sub2Chan)
// Publish another message
ps.Publish("news", "Weather update: Sunny day expected.")
// Read from sub1Chan: "Weather update: Sunny day expected."
// Read from sub2Chan: "Weather update: Sunny day expected."
Explanation:
The first message is published to "news" and received by sub1Chan. The second message is published to "news" and received by both sub1Chan and sub2Chan.
Example 2: Unsubscribing
// Assume a PubSub instance `ps` is created and initialized
subChanA := make(chan string)
ps.Subscribe("alerts", subChanA)
subChanB := make(chan string)
ps.Subscribe("alerts", subChanB)
ps.Publish("alerts", "System warning: High CPU usage.")
// Both subChanA and subChanB receive the message.
ps.Unsubscribe("alerts", subChanA)
ps.Publish("alerts", "Critical alert: Disk space low.")
// Only subChanB receives this message.
Explanation:
Initially, both subscribers receive the "System warning". After subChanA unsubscribes, it no longer receives messages, so only subChanB receives the "Critical alert".
Example 3: Multiple Topics and Unsubscribe from Non-Existent Topic
// Assume a PubSub instance `ps` is created and initialized
subFinance := make(chan string)
ps.Subscribe("finance", subFinance)
subSports := make(chan string)
ps.Subscribe("sports", subSports)
ps.Publish("finance", "Dow Jones up 200 points.")
// subFinance receives: "Dow Jones up 200 points."
ps.Publish("sports", "Local team wins championship!")
// subSports receives: "Local team wins championship!"
ps.Unsubscribe("finance", subSports) // No-op, subSports is not subscribed to finance
ps.Unsubscribe("sports", subFinance) // No-op, subFinance is not subscribed to sports
ps.Publish("finance", "Inflation rate stabilizes.")
// subFinance receives: "Inflation rate stabilizes."
// subSports receives nothing.
Explanation: Messages are routed correctly to their respective topics. Attempting to unsubscribe a subscriber from a topic they are not subscribed to has no effect.
Constraints
- The
PubSubsystem must be implemented entirely in memory. No external message brokers (like Kafka, RabbitMQ) or persistent storage should be used. - The system should handle at least 1000 concurrent publishers and 1000 concurrent subscribers without significant performance degradation.
- Message delivery should be best-effort, meaning message loss due to concurrent access is not expected, but external factors like channel buffer overflows (if not managed) are potential concerns to be mindful of.
- Topic names and message payloads will be represented as
stringtypes. - The system should be able to handle an unbounded number of topics.
Notes
- Consider using Go's built-in concurrency primitives like
sync.Mutex,sync.RWMutex, and channels. - A map is likely a good data structure to store topics and their associated subscribers.
- Think about how to manage the lifecycle of subscriber channels, especially when unsubscribing.
- How will you ensure that publishing and subscribing operations are atomic and safe from race conditions?
- The
Subscribemethod should return an identifier or the channel itself, which can then be used forUnsubscribe. For simplicity, you can just pass the channel directly toUnsubscribe.