GCP PubSub - Batching with orderingKey
Tags: pubsub, gcp, backend, javascript
Published: 2024-04-24 13:07:31

**Introduction**
Google Cloud Pub/Sub (GCP Pub/Sub) is a cornerstone of modern application architecture, offering a fully-managed real-time messaging service that seamlessly connects independent applications. In the dynamic landscape of cloud services, scalability is a critical factor. GCP Pub/Sub provides two primary scaling strategies: vertical scaling through batching with ordering keys, and horizontal scaling with multiple consumers on the same subscription. This blog post delves into the intricacies of both approaches, their implementation, testing methodologies, and the results they yield.

**Sample Code used to push messages with two diffrent orderingKey**

```javascript
// Importing the PubSub module from the Google Cloud Pub/Sub library
import { PubSub } from '@google-cloud/pubsub';

// Defining the topic name
export const TOPIC = '<TOPIC_NAME>';

// Creating a Pub/Sub client with the specified projectId
export const client = new PubSub({
    projectId: '<projectId>'
});

// Function to check if a number is even
function isEven(number) {
    return number % 2 === 0;
}

// Looping through numbers from 0 to 9
for (let i = 0; i < 10; i++) {
    // Publishing a message to the specified topic
    const res = await client.topic(TOPIC, { enableMessageOrdering: true })
        .publishMessage({ data: Buffer.from(i.toString(), 'utf8'), orderingKey: isEven(i) ? 'EVEN' : 'ODD' });
    // Logging the result of the publishing operation
    console.log(res);
}
```

**Vertical Scaling: Batching with Ordering Key**
Vertical scaling is synonymous with optimizing the processing capabilities of individual messages. By implementing batching with an ordering key, subscribers can collectively process messages in a batch. This method significantly reduces overhead and enhances processing efficiency. However, the subtleties of message delivery with ordering keys demand a comprehensive understanding.

**Test Scenario and Results**
A test scenario was established to examine vertical scaling further, where 100 messages were published with alternating ordering keys. Despite the activation of ordering, GCP Pub/Sub did not sustain the anticipated sequence during batch processing. This discrepancy underscored a notable limitation of vertical scaling in managing voluminous message flows.

```javascript
// Importing the PubSub module from the Google Cloud Pub/Sub library
import { PubSub } from '@google-cloud/pubsub';

// Defining the subscription name
export const SUB = '<TEST-SUB>';

// Creating a Pub/Sub client with the specified projectId
export const client = new PubSub({
    projectId: '<projectId>'
});

// ANSI color codes for console output
const RED = "\u001b[31m"; // Red color
const BLUE = "\u001b[34m"; // Blue color

// Creating a subscription with increased message flow to process multiple messages concurrently
const subscription = client.subscription(SUB, {
    flowControl: {
        maxMessages: 100 // Increased message flow to process multiple messages concurrently
    }
});

// Function to check if a number is even
function isEven(number) {
    return number % 2 === 0;
}

// Function to deserialize message data
function deserializeMessage(message) {
    return message.data.toString('utf8');
}

// Function to introduce random delay
function randomSleep() {
    const randomMilliseconds = Math.floor(Math.random() * (3000 + 1));
    return new Promise(resolve => setTimeout(resolve, randomMilliseconds));
}

// Asynchronous message handler function
async function handler(message) {
    // Introducing random delay to simulate processing time
    await randomSleep();
    // Logging received message with appropriate color based on its ordering
    console.log(isEven(message) ? BLUE : RED, 'received msg', 'ack msg', message);
}

// Function to process messages
function messageProcessor(message) {
    return Promise.resolve(message)
        .then(deserializeMessage)
        .then(handler)
        .then(() => message.ack())
        .catch(e => {
            // Handling errors encountered while processing messages
            console.debug('Error encountered while handling message.', {
                errorMessage: e.message,
                errorName: e.name,
                messageId: message.id,
                orderingKey: message.orderingKey
            });
            message.nack(); // Nacknowledging the message
        });
}

// Event listener for incoming messages
subscription.on('message', messageProcessor);

// Event listener for subscription errors
subscription.on('error', e => {
    console.error('Error encountered on subscription. Closing connection');
    console.error(e);
    // Closing subscription connection on error
    subscription.close((err) => {
        if (err) {
            console.error('Error on closing subscription');
            console.error(err);
            process.exit(1);
        }
    });
});

```

**Result: we lost the order of message**

![Order of message delivery when maxMessage increased](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h9hc6onwxm7w218w4bda.png)

**Horizontal Scaling: Ordering Keys & Multiple Consumers**
Horizontal scaling, in contrast, entails the distribution of workload across an array of subscribers. GCP Pub/Sub ensures that messages tagged with an ordering key are consistently delivered to the same subscription that previously received messages with the identical ordering key. This mechanism is pivotal for maintaining the sequence of messages across various consumers.

**Test Scenario and Results**
For the horizontal scaling test, two distinct subscribers (emulating separate pods) were configured for the same subscription. Each subscriber was tasked with handling messages bearing alternating ordering keys. The outcome aligned with expectations; every message sharing an ordering key was routed to the same subscriber, thereby preserving the order.

**Code**

```javascript
import { PubSub } from '@google-cloud/pubsub'

export const SUB ='<TEST-SUB>'

export const client = new PubSub({
    projectId: '<projectId>'
})

const RED = "\u001b[31m";
const BLUE = "\u001b[34m";

const subscription = client.subscription(SUB, {
    flowControl: {
        maxMessages: 1 
    }
})

function isEven(number) {
    return number % 2 === 0;
}


function deserializeMessage (message) {
    return message.data.toString('utf8')
}

function randomSleep() {
    const randomMilliseconds = Math.floor(Math.random() * (3000 + 1));
    return new Promise(resolve => setTimeout(resolve, randomMilliseconds));
}

async function handler(message){
    await randomSleep()
    console.log(isEven(message)?BLUE:RED,'received msg','ack msg',message)
}


function messageProcessor (message) {
    return Promise.resolve(message)
        .then(deserializeMessage)
        .then(handler)
        .then(() => message.ack())
        .catch(e => {
            console.debug('Error encountered while handling message.',
                { errorMessage: e.message, errorName: e.name, messageId: message.id, orderingKey: message.orderingKey })
            message.nack()
        })
}

subscription.on('message', messageProcessor)

subscription.on('error', e => {
    console.error('Error encountered on subscription. Closing connection')
    console.error(e)
    subscription.close((err) => {
        if (err) {
            console.error('Error on closing subscription')
            console.error(err)
            process.exit(1)
        }
    })
})

```

**Result: on delivery Order Was preserved**

![Order preserved](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6392mnqkrkpnky7wobzo.png)

**Conclusion**
The decision between vertical and horizontal scaling in GCP Pub/Sub hinges on the specific requirements of the message flow and the system architecture. Vertical scaling is advantageous for its efficiency in processing smaller batches of messages, while horizontal scaling excels in maintaining order across high-volume message streams. Both methods have their merits and limitations, and the choice ultimately depends on the balance between order preservation and processing efficiency.

By understanding the strengths and weaknesses of each scaling approach, developers can make informed decisions that align with their application's needs, ensuring robust and scalable message handling in their cloud infrastructure. The exploration of these scaling strategies underscores the importance of thorough testing and evaluation in optimizing cloud-based messaging services.

refferance:
[Batch messaging By GCP Doc](https://cloud.google.com/pubsub/docs/batch-messaging)
[Google Cloud Pub/Sub Ordered Delivery by Kamal Aboul-Hosn](https://medium.com/google-cloud/google-cloud-pub-sub-ordered-delivery-1e4181f60bc8)