November 2014 Archives

Using Minion with a REST API

Minion is a job queue for the Mojolicious real-time web framework. Below is example usage that sends an email using a REST API. The email address is handed off to minion and then minion processes the task. Everything is in a self-contained file as a Mojolicious::Lite app.

In addition, a config file is shared between minion and the web app.

One cool thing about Minion is that it has support for a Postgres backend using Mojo::Pg. Given that, workers can be running jobs on different physical boxes as long as they use a compatible "Pg" connect string.

The web app is started like so:

$ /opt/perl minion_email.pl daemon

A Minion worker is started with:

$ /opt/perl minion_email.pl minion worker

Example usage:

$ curl -s -X POST http://localhost:3000/api/v1/stats -d '{"api_key":"68b329da9893e34099c7d8ad5cb9c940","username"}' | ~/jq . -                       
{
  "status": "success",
  "message": {
    "inactive_workers": 1,
    "finished_jobs": "41",
    "active_jobs": 0,
    "failed_jobs": 0,
    "active_workers": 0,
    "inactive_jobs": 0
  }
}
$ curl -s -X POST http://localhost:3000/api/v1/email -d '{"api_key":"68b329da9893e34099c7d8ad5cb9c940","username":"fnord","email":"joy@example.com"}' | ~/jq . -
{
  "jobid": "139",
  "message": "Email send to joy@example.com",
  "status": "success"
}
$ curl -s -X POST http://localhost:3000/api/v1/status -d '{"api_key":"68b329da9893e34099c7d8ad5cb9c940","username":"fnord","jobid":"139"}' | ~/jq . -               
{
  "status": "success",
  "message": {
    "retries": 0,
    "retried": null,
    "args": [
      "joy@example.com"
    ],
    "result": {
      "status": "success",
      "msg": "Mail to joy@example.com sent"
    },
    "state": "finished",
    "worker": 669,
    "id": "139",
    "priority": 0,
    "delayed": "1",
    "finished": "1415590829.85986",
    "started": "1415590829.30462",
    "created": "1415590825.31839",
    "task": "email"
  }
}
$ curl -s -X POST http://localhost:3000/api/v1/stats -d '{"api_key":"68b329da9893e34099c7d8ad5cb9c940","username":"fnord"}' | ~/jq . -
{
  "status": "success",
  "message": {
    "finished_jobs": "42",
    "inactive_workers": 1,
    "inactive_jobs": 0,
    "failed_jobs": 0,
    "active_workers": 0,
    "active_jobs": 0
  }
}
$ cat /opt/minion_email/config
{
    smtp_host => "*",
    smtp_port => '*',
    smtp_user => '*',
    smtp_pass => '*',
    pg_string => 'postgresql://username:password@localhost/jobs',
    secret => '*',
};

Below is the Mojolicious::Lite example script:

use Mojolicious::Lite;

use v5.20;
use experimental 'signatures';

use Email::Sender::Simple qw(sendmail);
use Email::Simple;
use Email::Simple::Creator;
use Email::Sender::Transport::SMTP::TLS;
use Email::Valid;

use Mojo::JSON 'encode_json';

plugin Config => {file => '/opt/minion_email/config'};
plugin Minion => {Pg => app->config->{pg_string}};

app->secrets([app->config->{secret}]);

app->minion->add_task(email => sub ($job, $email) {
    my $mail = Email::Simple->create(
        header => [
            To     => $email,
            From    => 'signup@host.com',
            Subject => "Super Signup Email",
        ],
        body => "Thank you for signing up with us.\nGlobal conquest in an hour\n",
    );

    my $transport = Email::Sender::Transport::SMTP::TLS->new({
            host => app->config->{smtp_host},
            port => app->config->{smtp_port},
            username => app->config->{smtp_user},
            password => app->config->{smtp_pass},
            timeout => 10,
    });

    eval {
        sendmail($mail, {transport => $transport });
    };
    if ($@) {
        $job->app->log->debug("error: $@");
        $job->finish({ status => "error", msg => $@});
    }
    else {
        $job->finish({ status => "success", msg => "Mail to $email sent"});
    }
});

get '/' => 'index';

under (sub {
    my $self = shift;

    return($self->render(json => {status => "error", data => { message => "No JSON found" }})) unless $self->req->json;

    my $username = $self->req->json->{username};
    my $api_key = $self->req->json->{api_key};

    unless ($username) {
        $self->render(json => {status => "error", data => { message => "No username found" }});

        return undef;
    }

    unless ($api_key) {
        $self->render(json => {status => "error", data => { message => "No API Key found" }});

        return undef;
    }

    unless ("fnord" eq $username) {
        $self->render(json => {status => "error", data => { message => "Credentials mis-match" }});

        return undef;
    }

    unless ("68b329da9893e34099c7d8ad5cb9c940" eq $api_key) {
        $self->render(json => {status => "error", data => { message => "Credentials mis-match" }});

        return undef;
    }

    return 1;
});

post '/api/v1/email' => sub ($c) {
    my $email = $c->req->json->{email};

    unless (Email::Valid->address($email)) {
        return($c->render(json => {status => "error", message => "Email does not seem to be valid"}));
    }

    my $jobid = $c->minion->enqueue(email => [$email]);

    return($c->render(json => {status => "success", message => "Email send to $email", jobid => $jobid}));
};

post '/api/v1/status' => sub ($c) {
    my $jobid = $c->req->json->{jobid};

    unless ($jobid =~ m/^\d+$/) {
        return($c->render(json => {status => "error", message => "JobID not an unsigned integer"}));
    }

    my $job = $c->minion->job($jobid);
    if ($job) {
        return($c->render(json => {status => "success", message => $job->info}));
    }
    else {
        return($c->render(json => {status => "error", message => "Job not: found for $jobid"}));
    }
};

post '/api/v1/stats' => sub ($c) {
    my $stats = $c->minion->stats;
    return($c->render(json => {status => "success", message => $stats}));
};

app->start;

__DATA__

@@ index.html.ep

Try:<br>

    POST /email/v1/email
    POST /email/v1/status

About Brian Medley

user-pic I blog about Perl.