Implementing Eventual Consistency with a Message Queue in Go
Eventual consistency is a consistency model used in distributed systems where data changes are propagated asynchronously. This means that after an update, different replicas of the data may temporarily hold different values, but eventually, all replicas will converge to the same value. This challenge asks you to implement a simplified version of eventual consistency using a message queue to propagate updates between two data stores.
Problem Description
You are tasked with building a system that manages data across two data stores (Store A and Store B) using a message queue for asynchronous updates. Store A is the primary data store where writes initially occur. Updates to Store A are then published to a message queue, which consumes the messages and applies the updates to Store B. The goal is to simulate eventual consistency – Store B will not immediately reflect changes made to Store A, but it will eventually be consistent.
Your implementation should include the following components:
- Store A (Primary): A simple in-memory data store that accepts write requests.
- Store B (Replica): Another in-memory data store that receives updates from the message queue.
- Message Queue: A channel in Go that acts as the message queue. Messages will be simple strings representing the key-value pairs to be updated in Store B.
- Producer: A goroutine that simulates writes to Store A and publishes the updates to the message queue.
- Consumer: A goroutine that consumes messages from the message queue and applies the updates to Store B.
The system should handle concurrent writes to Store A and ensure that updates are eventually applied to Store B. You should also include a mechanism to check the consistency of the two stores.
Examples
Example 1:
Input:
Store A: {"key1": "value1"}
Producer sends: "key1:value2"
Store B: {"key1": "value1"}
Output:
Store A: {"key1": "value1"}
Store B: {"key1": "value2"} (after consumer processes the message)
Explanation:
The producer writes "value2" for "key1" to Store A. It then publishes "key1:value2" to the message queue. The consumer reads this message and updates Store B, eventually making it consistent with the intended state.
Example 2:
Input:
Store A: {"key1": "value1", "key2": "value2"}
Producer sends: "key1:value3", "key2:value4" (in that order)
Store B: {"key1": "value1", "key2": "value2"}
Output:
Store A: {"key1": "value1", "key2": "value2"}
Store B: {"key1": "value3", "key2": "value4"} (after consumer processes both messages)
Explanation:
The producer sends two updates. The consumer processes them sequentially, updating Store B accordingly. The order of processing is important for eventual consistency.
Example 3: (Concurrent Writes)
Input:
Store A: {"key1": "value1"}
Producer sends: "key1:value2", "key1:value3" (concurrently)
Store B: {"key1": "value1"}
Output:
Store A: {"key1": "value1"}
Store B: {"key1": "value3"} (eventually, one of the updates will be applied)
Explanation:
Concurrent updates demonstrate the eventual nature of the consistency. Store B will eventually reflect one of the updates, but there's no guarantee which one without additional conflict resolution mechanisms (not required for this challenge).
Constraints
- Data Store Size: Each data store (Store A and Store B) can hold a maximum of 10 key-value pairs.
- Message Size: Messages in the queue should be strings in the format "key:value".
- Concurrency: The producer should simulate concurrent writes (e.g., sending multiple messages within a short time frame).
- Performance: The solution should be reasonably efficient. While absolute performance is not the primary focus, avoid unnecessary delays or blocking operations.
- Error Handling: Basic error handling is expected (e.g., handling cases where a key doesn't exist in a store).
Notes
- This is a simplified simulation of eventual consistency. Real-world systems often involve more complex mechanisms for conflict resolution, data replication, and failure handling.
- Focus on demonstrating the core concept of asynchronous updates and eventual convergence.
- Consider using goroutines and channels effectively to implement the producer, consumer, and message queue.
- You can use a simple map (e.g.,
map[string]string) to represent the in-memory data stores. - The "consistency check" can be a simple function that compares the contents of Store A and Store B and reports any discrepancies. It should be called periodically to observe the eventual convergence.
- No external libraries are required. Use only standard Go libraries.