Franck Cuny @franckcuny

Software and Operation engineer.

Modules I like Devel::Declare

For $work, I've been working on a job queue system, using Moose, Catalyst (for a REST API) and DBIx::Class to store the jobs and some meta (yeah I know, there is not enough job queue system already, the world really needs a new one ...).

Basicaly, I've got a XXX::Worker class that all the workers extends. This class provide methods for fetching job, add a new job, mark a job as fail, retry, ...

The main loop in the XXX::Worker class look like this:

# $context is a hashref with some info the job or method may need
while (1) {
    my @jobs = $self->fetch_jobs();
    foreach my $job (@jobs) {
        my $method = $job->{funcname};
        $self->$method($context, $job);
    }
    $self->wait;
}

and the worker look like this

package MyWorker;
use Moose;
extends 'XXX::Worker';

sub foo {
    my ($self, $context, $job) = @_;

    # do something
    $self->job_success();
}

But as I'm using Moose, I want to add more sugar to the syntax, so writing a new worker would be really more easy.

Here comes Devel::Declare.

The syntax I want for my worker is this one:

work foo {
    $self->logger->info("start to work on job");
    # do something with $job
};

work bar {
    # do something with $job
};

success foo {
    $self->logger->info("woot job success");
};

fail bar {
    $self->logger->info("ho noez this one failed");
};

Where with work I write the code the writer will execute on a task, success, a specific code that will be executed after a job is marked as successfull, and fail for when the job fail.

I will show how to add the work keyword. I start by writing a new package:

package XXX::Meta;

use Moose;
use Moose::Exporter;
use Moose::Util::MetaRole;

use Devel::Declare;

use XXX::Meta::Class;
use XXX::Keyword::Work;

Moose::Exporter->setup_import_methods();

sub init_meta {
    my ($me, %options) = @_;

    my $for = $options{for_class};

    XXX::Keyword::Work->install_methodhandler(into => $for,);

    Moose::Util::MetaRole::apply_metaclass_roles(
        for_class       => $for,
        metaclass_roles => ['XXX::Meta::Class'],
    );

}

1;

The init_meta method is provided by Moose: (from the POD)

The init_meta method sets up the metaclass object for the class specified by for_class. This method injects a a meta accessor into the class so you can get at this object. It also sets the class's superclass to base_class, with Moose::Object as the default.

So I inject into the class that will use XXX::Meta a new metaclass, XXX::Meta::Class.

Let's take a look to XXX::Meta::Class:

package XXX::Meta::Class;

use Moose::Role;
use Moose::Meta::Class;
use MooseX::Types::Moose qw(Str ArrayRef ClassName Object);

has work_metaclass => (
    is      => 'ro',
    isa     => Object,
    builder => '_build_metaclass',
    lazy    => 1,
);

has 'local_work' => (
    traits     => ['Array'],
    is         => 'ro',
    isa        => ArrayRef [Str],
    required   => 1,
    default    => sub { [] },
    auto_deref => 1,
    handles    => {'_add_work' => 'push',}
);

sub _build_metaclass {
    my $self = shift;
    return Moose::Meta::Class->create_anon_class(
        superclasses => [$self->method_metaclass],
        cache        => 1,
    );
}

sub add_local_method {
    my ($self, $method, $name, $code) = @_;

    my $method_name = $method . "_" . $name;
    my $body        = $self->work_metaclass->name->wrap(
        $code,
        original_body => $code,
        name          => $method_name,
        package_name  => $self->name,
    );

    my $method_add = "_add_" . $method;
    $self->add_method($method_name, $body);
    $self->$method_add($method_name);
}

1;

Here I add to the ->meta provided by Moose local_work, which is an array that contains all my work methods. So each time I do something like

work foo {};

work bar {};

in my worker, I add this method to ->meta->local_work.

And the class for our keyword work:

package XXX::Keyword::Work;

use strict;
use warnings;

use Devel::Declare ();
use Sub::Name;

use base 'Devel::Declare::Context::Simple';

sub install_methodhandler {
    my $class = shift;
    my %args  = @_;
    {
        no strict 'refs';
        *{$args{into} . '::work'} = sub (&) { };
    }

    my $ctx = $class->new(%args);
    Devel::Declare->setup_for(
        $args{into},
        {   work => {
                const => sub { $ctx->parser(@_) }
            },
        }
    );
}

sub parser {
    my $self = shift;
    $self->init(@_);

    $self->skip_declarator;
    my $name = $self->strip_name;
    $self->strip_proto;
    $self->strip_attrs;

    my $inject = $self->scope_injector_call();
    $self->inject_if_block(
        $inject . " my (\$self, \$content, \$job) = \@_; ");

    my $pack = Devel::Declare::get_curstash_name;
    Devel::Declare::shadow_sub(
        "${pack}::work",
        sub (&) {
            my $work_method = shift;
            $pack->meta->add_local_method('work', $name, $work_method);
        }
    );
    return;
}

1;

The install_methodhandler add the work keyword, with a block of code. This code is sent to the parser, that will add more sugar. With the inject_if_block, I inject the following line

my ($self, $context, $job) = @_;

as this will always be my 3 arguments for a work method.

Now, for each new worker, I write something like this:

package MyWorker;
use Moose;
extends 'XXX::Worker';
use XXX::Meta;

work foo {};

The next step is too find the best way to reduce the first four lines to two.

(some of this code is ripped from other modules that use Devel::Declare. The best way to learn what you can do with this module is to read code from other modules that use it)