Message Queue using PAGI::FastAPI

4 min read
← Back to Blogs
Message Queue using PAGI::FastAPI

DISCLAIMER: Image is generated using ChatGPT.



Yesterday, I shared a post about API Gateway using PAGI::FastAPI.

Today, in this post, I will talk about Message Queue.

The latest release of PAGI::FastAPI is v1.2.4. Having said, in the release v1.1.0, we added the support for Message Queue facade.

The distribution shipped with the simple memory-based message queue facade. There is even a handy demo app, i.e. eg/memory_queue_demo.pl.

Let’s take you through the app source code:

First prepare the app and queue:

use v5.38;
use PAGI::FastAPI;
use PAGI::FastAPI::Queue;
use Future::AsyncAwait;

my $app = PAGI::FastAPI->new(
    title       => 'PAGI Queue Task Processing Service',
    version     => '1.0.0',
);

my $queue = PAGI::FastAPI::Queue->new( driver => 'Memory' );

Now define the basic app routes:

While working on this post, I found a bug in adding new task. The job_id was null.

I quickly patched the demo app and released PAGI::FastAPI v1.2.5.

Post Task

$app->post('/tasks',
    summary => 'Enqueue a background task',
    handler => async sub ($c) {
        my $item = $c->query_params->{item} // 'default_item';

        my $job_data = {
            task => 'process_payload',
            data => $item,
        };

        my $job_id = await $queue->push('default', $job_data);
        $job_data->{id} = $job_id;

        return {
            status => 'queued',
            job_id => $job_id,
        };
    }
);

Process Task

$app->post('/tasks/process',
    summary => 'Process the next queued task',
    handler => async sub ($c) {
        my $job = await $queue->pop('default');

        unless ($job) {
            $c->status(404);
            return { error => 'No tasks in queue' };
        }

        return {
            status => 'completed',
            job_id => $job->{id},
            result => "Processed " . $job->{data},
        };
    }
);

Task Status

$app->get('/tasks/status',
    summary => 'Get current queue status',
    handler => async sub ($c) {
        my $count = await $queue->size('default');

        return {
            pending_count => $count,
        };
    }
);

ACTION TIME

Start Server

$ pagi-server eg/queue_demo.pl

Submit Task

$ curl -X POST "http://127.0.0.1:5000/tasks?item=report_job_1"
{"status":"queued","job_id":1}

Task Status

$ curl "http://127.0.0.1:5000/tasks/status"
{"pending_count":1}

Process Task

$ curl -X POST "http://127.0.0.1:5000/tasks/process"
{"result":"Processed default_item","job_id":1,"status":"completed"}

Now check the status again:

$ curl "http://127.0.0.1:5000/tasks/status"
{"pending_count":0}

Although, it gives you a quick introduction but it is not suitable for serious message queue.

If you are building production grade message queue, I would highly recommend this companion package: PAGI::FastAPI::Queue::Driver::Redis.

Luckily the distribution shipped with a demo app: eg/redis_queue_demo.pl

We will create app and queue as before first:

use v5.38;
use PAGI::FastAPI;
use PAGI::FastAPI::Queue;
use Future::AsyncAwait;

my $app = PAGI::FastAPI->new(
    title   => 'PAGI Redis Queue Demo',
    version => '1.1.0',
);

my $queue = PAGI::FastAPI::Queue->new(
    driver  => 'PAGI::FastAPI::Queue::Driver::Redis',
    options => {
        host   => '127.0.0.1',
        port   => 6379,
        prefix => 'myapp:queue:',
    },
);

my $job_sequence = 0;

Post Task

$app->post('/tasks',
    summary => 'Enqueue a task to Redis',
    handler => async sub ($c) {
        my $item   = $c->query_params->{item} // 'default_item';
        my $job_id = "job_" . ++$job_sequence . "_" . time();

        await $queue->push('default', {
            job_id => $job_id,
            task   => 'process_payload',
            data   => $item,
        });

        return {
            status => 'queued',
            job_id => $job_id,
            item   => $item,
        };
    }
);

Process Task

$app->post('/tasks/process',
    summary => 'Process next task from Redis',
    handler => async sub ($c) {
        my $job = await $queue->pop('default');

        unless ($job) {
            $c->status(404);
            return { error => 'No tasks in Redis queue' };
        }

        return {
            status => 'completed',
            job_id => $job->{job_id},
            result => "Processed " . $job->{data},
        };
    }
);

Task Status

$app->get('/tasks/status',
    summary => 'Get Redis queue status',
    handler => async sub ($c) {
        my $count = await $queue->size('default');

        return {
            pending_count => $count,
        };
    }
);

For this demo, we would also need redis worker.

Here is the script for redis worker: eg/redis_worker.pl

use v5.38;
use PAGI::FastAPI::Queue;
use PAGI::FastAPI::Queue::Driver::Redis;
use Future::AsyncAwait;
use Future::IO;

my $worker_id = $ARGV[0] // $$;

my $queue = PAGI::FastAPI::Queue->new(
    driver  => 'PAGI::FastAPI::Queue::Driver::Redis',
    options => {
        host   => '127.0.0.1',
        port   => 6379,
        prefix => 'myapp:queue:',
    },
);

say "Worker [$worker_id] started. Waiting for jobs...";

while (1) {
    my $job = $queue->pop('default')->get;

    if ($job) {
        my $id   = $job->{job_id} // 'N/A';
        my $data = $job->{data}   // 'N/A';

        say "Worker [$worker_id] START processing job ID: $id ($data)";
        Future::IO->sleep(1)->get;
        say "Worker [$worker_id] FINISHED job ID: $id";
    } else {
        Future::IO->sleep(0.5)->get;
    }
}

ACTION TIME

Before we start the server, we need Redis server first.

Luckily, we have docker compose config file, docker_compose.yml.

Let’s start the Redis in a docker container:

$ docker compose up -d

Start Server

Now time to start the message queue server:

$ pagi-server eg/redis_queue_demo.pl

Submit Task

In another terminal, we will push 10 tasks:

$ for i in {1..10}; \
  do \
  curl -s -X POST "http://127.0.0.1:5000/tasks?item=job_$i" > /dev/null; \
  done

Task Status

$ curl http://127.0.0.1:5000/tasks/status
{"pending_count":10}

Process Task

To process the submitted tasks, we will use two redis worker in separate terminals at the same time, so both workers get equal opportunity to process the tasks:

$ perl eg/redis_worker.pl "A"
Worker [A] started. Waiting for jobs...
Worker [A] START processing job ID: job_1_1787353306 (default_item)
Worker [A] FINISHED job ID: job_1_1787353306
Worker [A] START processing job ID: job_2_1787353306 (default_item)
Worker [A] FINISHED job ID: job_2_1787353306
Worker [A] START processing job ID: job_3_1787353306 (default_item)
Worker [A] FINISHED job ID: job_3_1787353306
Worker [A] START processing job ID: job_5_1787353306 (default_item)
Worker [A] FINISHED job ID: job_5_1787353306
Worker [A] START processing job ID: job_7_1787353306 (default_item)
Worker [A] FINISHED job ID: job_7_1787353306
Worker [A] START processing job ID: job_9_1787353306 (default_item)
Worker [A] FINISHED job ID: job_9_1787353306
$ perl eg/redis_worker.pl "B"
Worker [B] started. Waiting for jobs...
Worker [B] START processing job ID: job_4_1787353306 (default_item)
Worker [B] FINISHED job ID: job_4_1787353306
Worker [B] START processing job ID: job_6_1787353306 (default_item)
Worker [B] FINISHED job ID: job_6_1787353306
Worker [B] START processing job ID: job_8_1787353306 (default_item)
Worker [B] FINISHED job ID: job_8_1787353306
Worker [B] START processing job ID: job_10_1787353306 (default_item)
Worker [B] FINISHED job ID: job_10_1787353306

Finally check the status:

$ curl http://127.0.0.1:5000/tasks/status
{"pending_count":0}


Happy Hacking !!!