For [[http://linkfluence.net/][$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:
#+BEGIN_SRC perl
# $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;
}
#+END_SRC
and the worker look like this
#+BEGIN_SRC perl
package MyWorker;
use Moose;
extends 'XXX::Worker';
sub foo {
my ($self, $context, $job) = @_;
# do something
$self->job_success();
}
#+END_SRC
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
[[http://search.cpan.org/perldoc?Devel::Declare][Devel::Declare]].
The syntax I want for my worker is this one:
#+BEGIN_SRC perl
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");
};
#+END_SRC
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:
#+BEGIN_SRC perl
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;
#+END_SRC
The =init_meta= method is provided by Moose: (from the POD)
#+BEGIN_QUOTE
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.
#+END_QUOTE
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:
#+BEGIN_SRC perl
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;
#+END_SRC
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
#+BEGIN_SRC perl
work foo {};
work bar {};
#+END_SRC
in my worker, I add this method to *->meta->local\_work*.
And the class for our keyword work:
#+BEGIN_SRC perl
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;
#+END_SRC
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
#+BEGIN_SRC perl
my ($self, $context, $job) = @_;
#+END_SRC
as this will always be my 3 arguments for a work method.
Now, for each new worker, I write something like this:
#+BEGIN_SRC perl
package MyWorker;
use Moose;
extends 'XXX::Worker';
use XXX::Meta;
work foo {};
#+END_SRC
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)