❌

Normal view

There are new articles available, click to refresh the page.
Today β€” 18 January 2025Main stream

Learning Notes #56 – Push vs Pull Architecture

15 January 2025 at 16:16

Today, i learnt about push vs pull architecture, the choice between push and pull architectures can significantly influence system performance, scalability, and user experience. Both approaches have their unique advantages and trade-offs. Understanding these architectures and their ideal use cases can help developers and architects make informed decisions.

What is Push Architecture?

Push architecture is a communication pattern where the server actively sends data to clients as soon as it becomes available. This approach eliminates the need for clients to repeatedly request updates.

How it Works

  • The server maintains a connection with the client.
  • When new data is available, the server β€œpushes” it to the connected clients.
  • In a message queue context, producers send messages to a queue, and the queue actively delivers these messages to subscribed consumers without explicit requests.

Examples

  • Notifications in Mobile Apps: Users receive instant updates, such as chat messages or alerts.
  • Stock Price Updates: Financial platforms use push to provide real-time market data.
  • Message Queues with Push Delivery: Systems like RabbitMQ or Kafka configured to push messages to consumers.
  • Server-Sent Events (SSE) and WebSockets: These are common implementations of push.

Advantages

  • Low Latency: Clients receive updates instantly, improving responsiveness.
  • Reduced Redundancy: No need for clients to poll servers frequently, reducing bandwidth consumption.

Challenges

  • Complexity: Maintaining open connections, especially for many clients, can be resource-intensive.
  • Scalability: Requires robust infrastructure to handle large-scale deployments.

What is Pull Architecture?

Pull architecture involves clients actively requesting data from the server. This pattern is often used when real-time updates are not critical or predictable intervals suffice.

How it Works

  • The client periodically sends requests to the server.
  • The server responds with the requested data.
  • In a message queue context, consumers actively poll the queue to retrieve messages when ready.

Examples

  • Web Browsing: A browser sends HTTP requests to fetch pages and resources.
  • API Data Fetching: Applications periodically query APIs to update information.
  • Message Queues with Pull Delivery: Systems like SQS or Kafka where consumers poll for messages.
  • Polling: Regularly checking a server or queue for updates.

Advantages

  • Simpler Implementation: No need for persistent connections; standard HTTP requests or queue polling suffice.
  • Server Load Control: The server can limit the frequency of client requests to manage resources better.

Challenges

  • Latency: Updates are only received when the client requests them, which might lead to delays.
  • Increased Bandwidth: Frequent polling can waste resources if no new data is available.

AspectPush ArchitecturePull Architecture
LatencyLow – Real-time updatesHigher – Dependent on polling frequency
ComplexityHigher – Requires persistent connectionsLower – Simple request-response model
Bandwidth EfficiencyEfficient – Updates sent only when neededLess efficient – Redundant polling possible
ScalabilityChallenging – High client connection overheadEasier – Controlled client request intervals
Message Queue FlowMessages actively delivered to consumersConsumers poll the queue for messages
Use CasesReal-time applications (e.g., chat, live data)Non-critical updates (e.g., periodic reports)

Before yesterdayMain stream

Learning Notes #10 – Lazy Queues | RabbitMQ

26 December 2024 at 06:54

What Are Lazy Queues?

  • Lazy Queues are designed to store messages primarily on disk rather than in memory.
  • They are optimized for use cases involving large message backlogs where minimizing memory usage is critical.

Key Characteristics

  1. Disk-Based Storage – Messages are stored on disk immediately upon arrival, rather than being held in memory.
  2. Low Memory Usage – Only minimal metadata for messages is kept in memory.
  3. Scalability – Can handle millions of messages without consuming significant memory.
  4. Message Retrieval – Retrieving messages is slower because messages are fetched from disk.
  5. Durability – Messages persist on disk, reducing the risk of data loss during RabbitMQ restarts.

Trade-offs

  • Latency: Fetching messages from disk is slower than retrieving them from memory.
  • Throughput: Not suitable for high-throughput, low-latency applications.

Choose Lazy Queues if

  • You need to handle very large backlogs of messages.
  • Memory is a constraint in your system.Latency and throughput are less critical.

Implementation

Pre-requisites

1. Install and run RabbitMQ on your local machine.


docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:4.0-management

2. Install the pika library


pip install pika

Producer (producer.py)

This script sends a persistent message to a Lazy Queue.

import pika

# RabbitMQ connection parameters for localhost
connection_params = pika.ConnectionParameters(host="localhost")

# Connect to RabbitMQ
connection = pika.BlockingConnection(connection_params)
channel = connection.channel()

# Custom Exchange and Routing Key
exchange_name = "custom_exchange"
routing_key = "custom_routing_key"
queue_name = "lazy_queue_example"

# Declare the custom exchange
channel.exchange_declare(
    exchange=exchange_name,
    exchange_type="direct",  # Direct exchange routes messages based on the routing key
    durable=True
)

# Declare a Lazy Queue
channel.queue_declare(
    queue=queue_name,
    durable=True,
    arguments={"x-queue-mode": "lazy"}  # Configure the queue as lazy
)

# Bind the queue to the custom exchange with the routing key
channel.queue_bind(
    exchange=exchange_name,
    queue=queue_name,
    routing_key=routing_key
)

# Publish a message
message = "Hello from the Producer via Custom Exchange!"
channel.basic_publish(
    exchange=exchange_name,
    routing_key=routing_key,
    body=message,
    properties=pika.BasicProperties(delivery_mode=2)  # Persistent message
)

print(f"Message sent to Lazy Queue via Exchange: {message}")

# Close the connection
connection.close()

Consumer (consumer.py)

import pika

# RabbitMQ connection parameters for localhost
connection_params = pika.ConnectionParameters(host="localhost")

# Connect to RabbitMQ
connection = pika.BlockingConnection(connection_params)
channel = connection.channel()

# Custom Exchange and Routing Key
exchange_name = "custom_exchange"
routing_key = "custom_routing_key"
queue_name = "lazy_queue_example"

# Declare the custom exchange
channel.exchange_declare(
    exchange=exchange_name,
    exchange_type="direct",  # Direct exchange routes messages based on the routing key
    durable=True
)

# Declare the Lazy Queue
channel.queue_declare(
    queue=queue_name,
    durable=True,
    arguments={"x-queue-mode": "lazy"}  # Configure the queue as lazy
)

# Bind the queue to the custom exchange with the routing key
channel.queue_bind(
    exchange=exchange_name,
    queue=queue_name,
    routing_key=routing_key
)

# Callback function to process messages
def callback(ch, method, properties, body):
    print(f"Received message: {body.decode()}")
    ch.basic_ack(delivery_tag=method.delivery_tag)  # Acknowledge the message

# Start consuming messages
channel.basic_consume(queue=queue_name, on_message_callback=callback, auto_ack=False)

print("Waiting for messages. To exit, press CTRL+C")
try:
    channel.start_consuming()
except KeyboardInterrupt:
    print("Stopped consuming.")

# Close the connection
connection.close()

Explanation

  1. Producer
    • Defines a custom exchange (custom_exchange) of type direct.
    • Declares a Lazy Queue (lazy_queue_example).
    • Binds the queue to the exchange using a routing key (custom_routing_key).
    • Publishes a persistent message via the custom exchange and routing key.
  2. Consumer
    • Declares the same exchange and Lazy Queue to ensure they exist.
    • Consumes messages routed to the queue through the custom exchange and routing key.
  3. Custom Exchange and Binding
    • The direct exchange type routes messages based on an exact match of the routing key.
    • Binding ensures the queue receives messages published to the exchange with the specified key.
  4. Lazy Queue Behavior
    • Messages are stored directly on disk to minimize memory usage.

Learning Notes #9 – Quorum Queues | RabbitMQ

25 December 2024 at 16:42

What Are Quorum Queues?

  • Quorum Queues are distributed queues built on the Raft consensus algorithm.
  • They are designed for high availability, durability, and data safety by replicating messages across multiple nodes in a RabbitMQ cluster.
  • Its a replacement of Mirrored Queues.

Key Characteristics

  1. Replication:
    • Messages are replicated across a quorum (a majority of nodes).
    • A quorum consists of an odd number of replicas (e.g., 3, 5, 7) to ensure a majority can elect a leader during failovers.
  2. Leader-Follower Architecture:
    • Each Quorum Queue has one leader and multiple followers.
    • The leader handles all write and read operations, while followers replicate messages and provide redundancy.
  3. Durability:
    • Messages are written to disk on all quorum nodes, ensuring persistence even if nodes fail.
  4. High Availability:
    • If the leader node fails, RabbitMQ elects a new leader from the remaining quorum, ensuring continued operation.
  5. Consistency:
    • Quorum Queues prioritize consistency over availability.
    • Messages are acknowledged only after replication is successful on a majority of nodes.
  6. Message Ordering:
    • Message ordering is preserved during normal operations but may be disrupted during leader failovers.

Use Cases

  • Mission-Critical Applications – Systems where message loss is unacceptable (e.g., financial transactions, order processing).
  • Distributed Systems – Environments requiring high availability and fault tolerance.
  • Data Safety – Applications prioritizing consistency over throughput (e.g., event logs, audit trails).

Setups

Using rabbitmqctl


rabbitmqctl add_queue quorum_queue --type quorum

Using python


channel.queue_declare(queue="quorum_queue", arguments={"x-queue-type": "quorum"})

References:

  1. https://www.rabbitmq.com/docs/quorum-queues

Learning Notes #7 – AMQP Protocol and RabbitMQ | An Overview

24 December 2024 at 18:22

Today, i learned about AMQP Protocol, Components of RabbitMQ (Connections, Channels, Queues, Exchanges, Bindings and Different Types of Exchanges, Acknowledgement and Publisher Confirmation). I learned these all from CloudAMQP In this blog, you will find a crisp details on these topics.

1. Overview of AMQP Protocol

  • Advanced Message Queuing Protocol (AMQP) is an open standard for messaging middleware. It enables systems to exchange messages in a reliable and flexible manner.
  • Key components:
    • Producers: Applications that send messages.
    • Consumers: Applications that receive messages.
    • Broker: Middleware (e.g., RabbitMQ) that manages message exchanges.
    • Message: A unit of data transferred between producer and consumer.

2. How AMQP Works in RabbitMQ

  • RabbitMQ implements AMQP to facilitate message exchange. It acts as the broker, managing queues, exchanges, and bindings.
  • AMQP Operations:
    1. Producer sends a message to an exchange.
    2. The exchange routes the message to one or more queues based on bindings.
    3. Consumer retrieves the message from the queue.

3. Connections and Channels

Connections

A connection is a persistent, long-lived TCP connection between a client application and the RabbitMQ broker. Connections are relatively resource-intensive because they involve socket communication and the overhead of establishing and maintaining the connection. Each connection is uniquely identified by the broker and can be shared across multiple threads or processes.

When an application establishes a connection to RabbitMQ, it uses it as a gateway to interact with the broker. This includes creating channels, declaring queues and exchanges, publishing messages, and consuming messages. Connections should ideally be reused across the application to reduce overhead and optimize resource usage.

Channels

A channel is a lightweight, logical communication pathway established within a connection. Channels provide a way to perform multiple operations concurrently over a single connection. They are less resource-intensive than connections and are designed to handle operations such as queue declarations, message publishing, and consuming.

Using channels allows applications to:

  1. Scale efficiently: Instead of opening multiple connections, applications can open multiple channels over a single connection.
  2. Isolate operations: Each channel operates independently. For instance, one channel can consume messages while another publishes.

How They Work Together

When a client connects to RabbitMQ, it first establishes a connection. Within that connection, it can open multiple channels. Each channel operates as a virtual connection, allowing concurrent tasks without needing separate TCP connections.


import pika

# Establish a connection to RabbitMQ
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))

# Create multiple channels on the same connection
channel1 = connection.channel()
channel2 = connection.channel()

# Declare queues on each channel
channel1.queue_declare(queue='queue1')
channel2.queue_declare(queue='queue2')

# Publish messages on different channels
channel1.basic_publish(exchange='', routing_key='queue1', body='Message for Queue 1')
channel2.basic_publish(exchange='', routing_key='queue2', body='Message for Queue 2')

print("Messages sent to both queues!")

# Close the connection
connection.close()

Best Practices (Not Tried; Got this from the video)

  1. Reusing Connections: Establish one connection per application or service and share it across threads or processes for efficiency.
  2. Using Channels for Parallelism: Open separate channels for different operations like publishing and consuming.
  3. Graceful Cleanup: Always close channels and connections when done to avoid resource leaks.

4. Queues

  • Act as message storage.
  • Can be:
    • Durable: Survives broker restarts.
    • Exclusive: Used by a single connection.
    • Auto-delete: Deleted when the last consumer disconnects.

# Declaring a durable queue
channel.queue_declare(queue='durable_queue', durable=True)

# Sending a persistent message
channel.basic_publish(
    exchange='',  # Default exchange
    routing_key='durable_queue',
    body='Persistent message',
    properties=pika.BasicProperties(delivery_mode=2)  # Persistent
)

5. Exchanges

An exchange in RabbitMQ is a routing mechanism that determines how messages sent by producers are directed to queues. Exchanges act as intermediaries between producers and queues, enabling flexible and efficient message routing based on routing rules and patterns.

Types of Exchanges

RabbitMQ supports four types of exchanges, each with its unique routing mechanism:

1. Direct Exchange

  • Routes messages to queues based on an exact match of the routing key.
  • If the routing key in the message matches the binding key of a queue, the message is routed to that queue.
  • Use case: Task queues where each task has a specific destination.

Example:

  • Queue queue1 is bound to the exchange with the routing key info.
  • A message with the routing key info is routed to queue1.

channel.exchange_declare(exchange='direct_exchange', exchange_type='direct')
channel.queue_declare(queue='direct_queue')
channel.queue_bind(exchange='direct_exchange', queue='direct_queue', routing_key='info')
channel.basic_publish(exchange='direct_exchange', routing_key='info', body='Direct message')

2. Fanout Exchange

  • Broadcasts messages to all queues bound to the exchange, ignoring routing keys.
  • Use case: Broadcasting events to multiple consumers, such as notifications or logs.

Example:

  • All queues bound to the exchange receive the same message.

channel.exchange_declare(exchange='fanout_exchange', exchange_type='fanout')
channel.queue_declare(queue='queue1')
channel.queue_declare(queue='queue2')

channel.queue_bind(exchange='fanout_exchange', queue='queue1')
channel.queue_bind(exchange='fanout_exchange', queue='queue2')

channel.basic_publish(exchange='fanout_exchange', routing_key='', body='Broadcast message')

3. Topic Exchange

  • Routes messages to queues based on pattern matching of routing keys.
  • Routing keys are dot-separated words, and queues can bind with patterns using wildcards:
    • * matches exactly one word.
    • # matches zero or more words.
  • Use case: Complex routing scenarios, such as logging systems with multiple log levels and sources.

Example:

  • Queue queue1 is bound with the pattern logs.info.*.
  • A message with the routing key logs.info.app1 is routed to queue1.

channel.exchange_declare(exchange='topic_exchange', exchange_type='topic')
channel.queue_declare(queue='topic_queue')

channel.queue_bind(exchange='topic_exchange', queue='topic_queue', routing_key='logs.info.*')

channel.basic_publish(exchange='topic_exchange', routing_key='logs.info.app1', body='Topic message')

4. Headers Exchange

  • Routes messages based on message header attributes instead of routing keys.
  • Headers can specify conditions like x-match:
    • x-match = all: All specified headers must match.
    • x-match = any: At least one specified header must match.
  • Use case: Advanced filtering scenarios.

Example:

  • Queue queue1 is bound with headers format=json and type=report.

channel.exchange_declare(exchange='headers_exchange', exchange_type='headers')
channel.queue_declare(queue='headers_queue')

channel.queue_bind(
    exchange='headers_exchange',
    queue='headers_queue',
    arguments={'format': 'json', 'type': 'report', 'x-match': 'all'}
)

channel.basic_publish(
    exchange='headers_exchange',
    routing_key='',
    body='Headers message',
    properties=pika.BasicProperties(headers={'format': 'json', 'type': 'report'})
)

Exchange Lifecycle

  1. Declaration: Exchanges must be explicitly declared before use. If an exchange is not declared and a producer tries to publish a message to it, an error will occur.
  2. Binding: Queues are bound to exchanges with routing keys or header arguments.
  3. Publishing: Producers publish messages to exchanges with optional routing keys.

Durable and Non-Durable Exchanges

  • Durable Exchange: Survives broker restarts. Useful for critical applications.
  • Non-Durable Exchange: Deleted when the broker restarts. Suitable for transient tasks.

# Declare a durable exchange
channel.exchange_declare(exchange='durable_exchange', exchange_type='direct', durable=True)

Default Exchange

RabbitMQ provides a built-in default exchange (unnamed exchange) that routes messages directly to a queue with a name matching the routing key.


channel.queue_declare(queue='default_queue')
channel.basic_publish(exchange='', routing_key='default_queue', body='Default exchange message')

Best Practices for Exchanges

  • Use durable exchanges for critical applications that require persistence across broker restarts.
  • Use direct exchanges for targeted delivery when routing keys are predictable.
  • Use fanout exchanges for broadcasting to multiple queues.
  • Use topic exchanges for complex routing needs, especially with hierarchical routing keys.
  • Use headers exchanges for advanced filtering based on metadata.

6. Bindings

Bindings connect queues to exchanges with routing rules.


# Binding a queue with a routing key
channel.queue_bind(exchange='direct_logs', queue='error_logs', routing_key='error')

7. Consumer Acknowledgments

Two acknowledgment types:

  • Manual: Consumer explicitly sends an acknowledgment.
  • Automatic: RabbitMQ assumes successful processing.
# Auto ACK
channel.basic_consume(queue='test_queue', on_message_callback=lambda ch, method, properties, body: print(body), auto_ack=True)

# Manual ACK
def callback(ch, method, properties, body):
    print(f"Received {body}")
    ch.basic_ack(delivery_tag=method.delivery_tag)

channel.basic_consume(queue='test_queue', on_message_callback=callback)
channel.start_consuming()

8. Publisher Confirmations

  • Guarantees that RabbitMQ successfully received a message.
  • Enable publisher confirms for robust systems.

# Enable delivery confirmation
channel.confirm_delivery()

# Publish and handle confirmation
try:
    channel.basic_publish(exchange='direct_logs', routing_key='error', body='Message with confirmation')
    print("Message successfully published!")
except pika.exceptions.UnroutableError:
    print("Message could not be routed.")

9. Virtual Hosts (vhosts)

  • Logical partitions to segregate exchanges, queues, and users.
  • Use vhosts for multi-tenant setups.

Tomm, I am planning to explore more on RabbitMQ. Let’s see tomm.

❌
❌