Dancer2::Plugin::Minion (aka, Using Minion in Dancer Apps, Revisited)

In 2018, I wrote about my experience using Minion at my employer at that time. Since then, I changed to another employer, where again, Minion was the appropriate solution to the issue of long-running jobs in our application. They desired to have a more Dancer-ish, polished, and integrated solution, and thanks to them, Dancer2::Plugin::Minion was born.

I don't see a lot of value rehashing the rationale behind this - I think this was covered pretty well in the 2018 article - so feel free to pop back there if you're looking for some backstory and analysis. With that, let's move on to...

The Code!

We're going to build an image uploader that lets a child upload a picture of an item on their Christmas list to Santa. We're going to save that image to a public image directory, and then it will generate a variety of sizes of thumbnails for that image. We don't want the client to have to wait on the generation to complete (they have other things to upload to Santa after all!), so we will use Minion to generate those thumbnails in the background.

It requires a minimal amount of configuration to set up Minion in your Dancer2 apps:

plugins:
    Minion:
        dsn: sqlite:test.db
        backend: SQLite

Fill in whatever values for DSN and backend match your existing Minion setup. And that's it - your Dancer2 app can now talk to Minion!

The plugin exports a keyword, minion, that exposes all of Minion to your Dancer2 application. I'm not promising it will be as seamless as an experience as the one when you are building Mojolicious applications, but it is really powerful.

Two more keywords are created by the plugin, add_task and enqueue, that map directly to the same methods available in Minion. These tasks are common enough to warrant having their own keywords to save you the little bit of additional typing to use them.

Lastly, the keyword minion_app creates a Minion application, which you can then mount via Plack (if you want to have an admin dashboard for Minion in your Dancer app), or if you want to enable the Minion CLI in your Dancer apps. I will demonstrate this below.

Let's see what this looks like:

use Dancer2;
use Dancer2::Plugin::Minion;
use Plack::Builder;
use File::Basename 'fileparse';

add_task thumbnails => sub {
    my ($job, $original) = @_;
    require Image::Imlib2::Thumbnail;
    my $thumb = Image::Imlib2::Thumbnail->new;
    my ($base, $dir, $ext) = fileparse( $original, qr/\.[^.]*?$/ );
    $_->{name} = "$base-$_->{name}" for @{ $thumb->sizes };
    my @generated = $thumb->generate($original, $dir);
    $job->finish(\@generated);
};

This creates a task, thumbnails, to automatically generate multiple thumbnails when provided with an original image.

# This exposes all of the minion commands
if (@ARGV && $ARGV[0] eq 'minion') {
    minion_app()->start;
    exit 0;
}

This bit of magic exposes the entire Minion CLI to your app - so you can use the various subcommands like worker and job.

set views => '.';

get '/' => sub {
    template 'upload';
};

This sets up a simple file uploader app. It also tells Dancer2 to look for its templates in the same directory as the app.

post '/' => sub {
    my $file = upload('file');
    my $name = $file->basename;
    my $target = path('public', $name);
    $file->copy_to($target);
    enqueue(thumbnails => [$target]);
    redirect "/$name";
};

Here, we consume a file upload, stash it on disk, and call the thumbnails job to render multiple thumbnails. This might take a while, so we don't want to tie up the browser waiting for them to finish.

builder {
    # mount the container app at /dashboard/
    # note that the trailing slash is very important
    mount '/dashboard/' => minion_app( 'https://northpole.com/' )->start;
    mount '/' => start;
};

Finally, we create the Plack application by mounting our Dancer2 application to the root URL, and attaching the Minion dashboard to the /dashboard/ URL (the trailing / is required!). It also sets the "Back to Site" link on the dashboard to the North Pole website (https://northpole.com/).

Now, Let's Put it All Together

Here's everything you need to make this example work (put all files in the same directory):

# Save this as app.psgi
use Dancer2;
use Dancer2::Plugin::Minion;
use Plack::Builder;
use File::Basename 'fileparse';

add_task thumbnails => sub {
    my ($job, $original) = @_;
    require Image::Imlib2::Thumbnail;
    my $thumb = Image::Imlib2::Thumbnail->new;
    my ($base, $dir, $ext) = fileparse( $original, qr/\.[^.]*?$/ );
    $_->{name} = "$base-$_->{name}" for @{ $thumb->sizes };
    my @generated = $thumb->generate($original, $dir);
    $job->finish(\@generated);
};

# This exposes all of the minion commands
if (@ARGV && $ARGV[0] eq 'minion') {
    minion_app()->start;
    exit 0;
}

set views => '.';

get '/' => sub {
    template 'upload';
};

post '/' => sub {
    my $file = upload('file');
    my $name = $file->basename;
    my $target = path('public', $name);
    $file->copy_to($target);
    enqueue(thumbnails => [$target]);
    redirect "/$name";
};

builder {
    # mount the container app at /dashboard/
    # note that the trailing slash is very important
    mount '/dashboard/' => minion_app( 'https://northpole.com/' )->start;
    mount '/' => start;
};

# Save as cpanfile
requires 'Dancer2';
requires 'Dancer2::Plugin::Minion';
requires 'Image::Imlib2::Thumbnail';

# Save as config.yml
plugins:
    Minion:
        dsn: sqlite:test.db
        backend: SQLite

# Save as upload.tt
<html>
  <head>
    <title>Upload your wish pictures</title>
  </head>
  <body>
    <h1>Upload your wish pictures</h1>

    <form action="/" enctype="multipart/form-data" method="post">
      <input type="file" name="file">
      <input type="submit">
    </form>
  </body>
</html>

To run the web app:

plackup app.psgi

And finally, to run the Minion worker:

perl app.psgi minion worker

And remember, you can also run other Minion commands this way:

perl app.psgi minion job 
perl app.psgi minion job -b kill ...

You can see a sample of the app running:

And the dashboard, too:

Future Plans

To be honest, I don't know what the future holds for this module. My own uses of it have been pretty minimal compared to the potential of what you can use it for. In my mind, this leaves the future a pretty blank slate. Is there something you'd like to see? I'd love to hear from you! Reach out at cromedome at cpan dot org and let me know your thoughts and ideas for this plugin.

Giving Credit Where Credit is Due

Dancer2::Plugin::Minion saw the light of day thanks to the wonderful people at Clearbuilt. They are the nicest group of people you could ever hope to work for/with, and I am extremely grateful for them giving me the time to not only build this module out, but so much more.

Other Notes

There is no plugin for Dancer 1 at the time of this writing, nor do I expect there will ever be one, at least not of my doing.

Author

This article has been written by Jason Crome (CromeDome) (with much assistance from Joel Berger) for the Twelve Days of Dancer.

Copyright

No copyright retained. Enjoy.

Jason A. Crome / CromeDome