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