Partitioned async in-memory queue that simulates Kafka's partitioning. Messages are routed to partitions based on user_id hash, ensuring: - All messages for the same user go to the same partition - Each partition is consumed by exactly one worker task - Per-user message orderin
| 50 | |
| 51 | |
| 52 | class PartitionedMemoryQueue(QueueInterface): |
| 53 | """ |
| 54 | Partitioned async in-memory queue that simulates Kafka's partitioning. |
| 55 | |
| 56 | Messages are routed to partitions based on user_id hash, ensuring: |
| 57 | - All messages for the same user go to the same partition |
| 58 | - Each partition is consumed by exactly one worker task |
| 59 | - Per-user message ordering is preserved (same as Kafka behavior) |
| 60 | |
| 61 | Partitioning modes: |
| 62 | - Hash (default): Uses hash(key) % num_partitions (Kafka-like) |
| 63 | - Round-robin: Sequential assignment (user1->p0, user2->p1, ...) |
| 64 | """ |
| 65 | |
| 66 | def __init__(self, num_partitions: int = 1, round_robin: bool = False): |
| 67 | self._num_partitions = max(1, num_partitions) |
| 68 | self._round_robin = round_robin |
| 69 | self._partitions: List[asyncio.Queue[QueueMessage]] = [ |
| 70 | asyncio.Queue() for _ in range(self._num_partitions) |
| 71 | ] |
| 72 | |
| 73 | self._user_partition_map: Dict[str, int] = {} |
| 74 | self._next_partition: int = 0 |
| 75 | self._partition_lock = asyncio.Lock() |
| 76 | |
| 77 | mode = "round-robin" if round_robin else "hash" |
| 78 | logger.info( |
| 79 | "Initialized PartitionedMemoryQueue with %d partitions, mode=%s", |
| 80 | self._num_partitions, |
| 81 | mode, |
| 82 | ) |
| 83 | |
| 84 | @property |
| 85 | def num_partitions(self) -> int: |
| 86 | return self._num_partitions |
| 87 | |
| 88 | @property |
| 89 | def round_robin(self) -> bool: |
| 90 | return self._round_robin |
| 91 | |
| 92 | async def get_partition_stats(self) -> Dict[str, any]: |
| 93 | """Get statistics about partition distribution.""" |
| 94 | async with self._partition_lock: |
| 95 | partition_counts = [0] * self._num_partitions |
| 96 | for partition_id in self._user_partition_map.values(): |
| 97 | partition_counts[partition_id] += 1 |
| 98 | |
| 99 | return { |
| 100 | "mode": "round-robin" if self._round_robin else "hash", |
| 101 | "num_partitions": self._num_partitions, |
| 102 | "total_users": len(self._user_partition_map), |
| 103 | "users_per_partition": partition_counts, |
| 104 | "queue_sizes": [p.qsize() for p in self._partitions], |
| 105 | } |
| 106 | |
| 107 | def _get_partition_key(self, message: QueueMessage) -> str: |
| 108 | if message.HasField("user_id") and message.user_id: |
| 109 | return message.user_id |
no outgoing calls