Why I sometimes hate require

Can you spot the error in this code?

package Foo;
use strict;
use warnings;

use Moo;
use MooX::Types::Mooselike::Base qw/Num/;

has count => (
    is => 'ro',
    isa => Num,
);

1;

Not yet? Here's the error I get compiling it:

$ perl -c Foo.pm 
Bareword "Num" not allowed while "strict subs" in use at Foo.pm line 9.
Foo.pm had compilation errors.

What the heck? I imported Num, so why is it not allowed? (Spot the error yet?)

Here's a hint: I'm using a Mac.

Does this make it clearer:

$ perl -MAcme::require::case -c Foo.pm 
MooX/Types/Mooselike/Base.pm has incorrect case (maybe you want MooX/Types/MooseLike/Base.pm instead?) at Foo.pm line 7.
BEGIN failed--compilation aborted at Foo.pm line 7.

Aha! I said "Mooselike" instead of "MooseLike". And the Mac OSX filesystem is case insensitive. Here's what happens internally when I say 'use MooX::Types::Mooselike::Base qw/Num/;'

BEGIN {
    require MooX::Types::Mooselike::Base;
    if ( my $code = MooX::Types::Mooselike::Base->can("import") ) {
        $code->( 'MooX::Types::Mooselike::Base', 'Num' );
    }
}

The standard require call works fine despite the typo because the file system is case insensitive. But the package name is wrong, so the import method is not found and thus not called.

The Acme::require::case module overrides CORE::GLOBAL::require to do a case-sensitive search for the file requested and fail if the name requested differs from the case the file system thinks is correct.

To help me catch these sorts of stupid typo errors, I've added it to an alias for prove.

alias prove="PERL5OPT=-MAcme::require::case prove"

I'll probably add it to my editor's compiler config as well.

Posted in perl programming | Tagged , , | 8 Responses

Crossing the Perl Rubicon

Yes, this is another blog post reacting to Matt S. Trout's Pumpkin Perl idea. (His followups are here and here.)

I've considered a long, thoughtful reply about what I see as the problems Perl 5 faces, why various proposals I've heard fall short, and what I think is meaningful for the future of Perl 5. But I'll probably never have the time to write that.

Instead, I realized that a recent email exchange with some other Porters summed up my thoughts about Matt's plan well enough to share. It will have to do.


Here was my original email, more or less, with some added emphasis:

If we go all the way, and rebrand from "Perl 5" to "Pumpkin Perl 1", then we open the door to a future "Pumpkin Perl 2" that breaks backward compatibility with a major version bump.

I'm neutral on whether it will do anything for internal and external perceptions, but at least it would solve this major version problem we're currently stuck with. That is not a trivial benefit if we seriously want to evolve Perl in a significant way (whether with this interpreter or a future rewrite). It doesn't get us out of the box we're in, but it at least opens the flap.

I'm not sure what such a bump would mean internally with respect to patchlevel.h and PERL_REVISION and PERL_API_REVISION. Possibly those go to 6 and any external version displayed is PERL_REVISION - 4 and we do the version monkey dance like Java did. There are plenty of devils in the details...

Then, in a very thoughtful response, Jan Dubois raised some concerns about what I said about breaking backward compatibility. Here is a short excerpt of his email (reprinted with permission):

>> If we go all the way, and rebrand from "Perl 5" to "Pumpkin Perl 1",
>> then we open the door to a future "Pumpkin Perl 2" that breaks
>> backward compatibility with a major version bump.
>
> I actively dislike the suggestion above, in case it actually is part
> of the plan.
>
> If there ever is a version of Perl that breaks backwards
> compatibility, then I sure hope it will use a *different* name, and
> not just a bump in version numbers. Not being able to rely on
> /usr/bin/perl being a compatible implementation seems like a sure way
> to make Perl even less popular...

And here is my response, with slight editing and some emphasis added:

I don't think there is currently a "plan". There's just an idea.

That said, I don't see much reason to rebrand *unless* there is a plan that is at least conceptually along those lines.

Whether such a binary gets called "pumpkin" or whatever is a bikeshed color issue.

If it's still recognizably "Perl 5" as we know it -- perhaps with a little less of some things and a little more of others -- then having a name like "Pumpkin" that carries from 1 to 2 would be very helpful for continuity.

This is *exactly* what Perl 6 prevents currently. We can't bump to 6. We probably shouldn't bump to v20 and then have annual releases be v22, v24, etc. or the significance of a major version bump is lost. ("Oops v30 was the one that broke backcompat, didn't you realize that?")

IF we think there will ever be a future Perl 5-like language/interpreter that is a significant departure from what we have today (that isn't Perl 6), then we really need a way to signify that with some sort of major bump or change in either version or nomenclature.

(I realize that's a huge if. Caps doesn't do it justice, it's so huge.)

Some have made very good arguments that there's no point changing a name *until* we have this hypothetical future Perl 5-like thing ready to release -- that until we have something to show that no one will pay attention.

That may well be. On the flip side, we set up a double barrier to change: not only must the actual work be done, but said doers must contemplate (and then wage) the secondary battle over version and nomenclature.

What Matt proposes -- at least in my interpretation of it -- is to cross the version/nomenclature Rubicon *now* when there *isn't* anything at stake. Once that happens, contemplation of more significant change can focus on the design and technical issues, because at the least the first cultural barrier will have been trail-blazed.

Again, I'm not wildly agitating *for* this Pumpkin idea -- I don't trivialize the big "if". But if I ignore 90% of what people think it will or won't do, I still think it does give this glimmer of a future that to date has seemed impossible and *that* is the part I think is worth discussing.


In summary, if we really are going to have Perl 5 and Perl 6 as sibling languages, with each evolving independently, then we need to decide whether we ever anticipate the possibility of Perl 5 needing to make a major break. If so, then Perl 5 will have to cross the Rubicon eventually.

Do we do it now to pave the way? Or do we do it later when the break appears?

Those two questions -- "Will we ever or won't we?" and "Now or later?" -- are the ones that matter.

Posted in p5p, perl programming | Tagged , , , | 3 Responses

Adventures in Benchmarking, Part 1

I created Path::Tiny because I wanted a faster file path utility. But believing it would be faster isn't the same thing as demonstrating it, so I decided to create some benchmarks. That got complicated, but now I have a pretty nice benchmarking toolkit.

This article describes my setup and process and shows results for object creation and basic file path manipulations for Path::Tiny and three other file path utilities: Path::Class, IO::All and File::Fu.

In subsequent articles, I'll show benchmarks for file input and output, including some startling facts about Unicode slurping.

tl;dr: Path::Tiny is really fast!

Goodbye Benchmark, hello Benchmark::Forking

Hopefully, you've heard of the good-old Benchmark module. It's a good starting point for benchmarking Perl programs. Or is it?

Even if you've used it, you might not realize that Benchmark runs tests in alphabetical order, doing all the loops for one test before going onto the next. Because prior runs affect Perl's memory allocation, test names can affect benchmarking results.

Here's a demonstration of what can happen. All the test subs are the same -- they just slurp a 1 megabyte file -- so the only difference is the name:

use v5.10;
use strict;
use warnings;
use Benchmark qw( cmpthese );
use Path::Tiny;

my $file = "ascii-large";

my $count = -1;
cmpthese(
    $count,
    {
        map { $_ => sub { path($file)->slurp } } ("a" .. "l")
    }
);

And here is the result:

$ perl bench.pl 
    Rate    l    h    i    j    f    b    g    c    a    e    k    d
l 1599/s   -- -14% -15% -19% -20% -20% -21% -21% -21% -23% -23% -23%
h 1866/s  17%   --  -1%  -6%  -6%  -7%  -8%  -8%  -8% -10% -10% -10%
i 1879/s  17%   1%   --  -5%  -6%  -6%  -8%  -8%  -8%  -9%  -9%  -9%
j 1981/s  24%   6%   5%   --  -1%  -1%  -3%  -3%  -3%  -4%  -4%  -4%
f 1995/s  25%   7%   6%   1%   --  -0%  -2%  -2%  -2%  -3%  -4%  -4%
b 1999/s  25%   7%   6%   1%   0%   --  -2%  -2%  -2%  -3%  -3%  -4%
g 2033/s  27%   9%   8%   3%   2%   2%   --  -0%  -0%  -2%  -2%  -2%
c 2035/s  27%   9%   8%   3%   2%   2%   0%   --  -0%  -2%  -2%  -2%
a 2036/s  27%   9%   8%   3%   2%   2%   0%   0%   --  -1%  -2%  -2%
e 2067/s  29%  11%  10%   4%   4%   3%   2%   2%   2%   --  -0%  -0%
k 2070/s  29%  11%  10%   4%   4%   4%   2%   2%   2%   0%   --  -0%
d 2074/s  30%  11%  10%   5%   4%   4%   2%   2%   2%   0%   0%   --

Wow! Sucks to be called "l", doesn't it! The better letters outperform you by 30%!

With volatility like that, how can you possibly draw useful conclusions?

I hoped that someone had already encountered this and figured out a fix, so I checked CPAN. There, I found Benchmark::Forking, which monkeypatches Benchmark to run each test in a subprocess.

Here is that code. Notice that Benchmark::Forking is a drop-in replacement.

use v5.10;
use strict;
use warnings;
use Benchmark::Forking qw( cmpthese );
use Path::Tiny;

my $file = "ascii-large";

my $count = -1;
cmpthese(
    $count,
    {
        map { $_ => sub { path($file)->slurp } } ("a" .. "l")
    }
);

And now the results. Yes, there's variance, but it's much, much smaller:

$ perl forking.pl 
    Rate   l   c   i   g   k   b   e   a   d   h   j   f
l 1982/s  -- -1% -1% -2% -3% -3% -4% -4% -4% -4% -5% -5%
c 2000/s  1%  --  0% -1% -2% -2% -3% -4% -4% -4% -4% -4%
i 2000/s  1%  0%  -- -1% -2% -2% -3% -4% -4% -4% -4% -4%
g 2018/s  2%  1%  1%  -- -1% -1% -2% -3% -3% -3% -4% -4%
k 2036/s  3%  2%  2%  1%  --  0% -1% -2% -2% -2% -3% -3%
b 2036/s  3%  2%  2%  1%  0%  -- -1% -2% -2% -2% -3% -3%
e 2055/s  4%  3%  3%  2%  1%  1%  -- -1% -1% -1% -2% -2%
a 2074/s  5%  4%  4%  3%  2%  2%  1%  --  0%  0% -1% -1%
d 2074/s  5%  4%  4%  3%  2%  2%  1%  0%  --  0% -1% -1%
h 2074/s  5%  4%  4%  3%  2%  2%  1%  0%  0%  -- -1% -1%
j 2093/s  6%  5%  5%  4%  3%  3%  2%  1%  1%  1%  --  0%
f 2093/s  6%  5%  5%  4%  3%  3%  2%  1%  1%  1%  0%  --

Benchmark::Forking FTW!

DRY benchmarks

I wrote a lot of benchmarks. After the first half dozen, I realized that I was copying and pasting the same files over and over. What a huge code smell! While there might be a module out there to implement modular benchmarks (let me know in comments), I opted for some quick and dirty code generation instead.

I grouped snippets of benchmarking code into subdirectories and wrote a harness that read the snippets, wrapped a shell of code around them, dumped them into files, ran them, and gathered the results.

Here is the code shell, which I kept in a HERE document. The words in all caps were replaced by substitution to customize each test. (DIY templating.)

#!/usr/bin/env perl
use v5.10;
use strict;
use warnings;

use Benchmark qw( :hireswallclock );
use Benchmark::Forking qw( timethese );
use JSON -convert_blessed_universally;
use File::pushd qw/tempd/;

use File::Fu;
use IO::All;
use Path::Class;
use Path::Tiny;

my $count = COUNT;
my $corpus = path("CORPUS");
my $test = path("TEST");
my $result;

TIMETHESE

print JSON->new->allow_blessed->convert_blessed->encode($result);

The variables like $corpus are fixtures that I wanted available to all the benchmark snippets. The $result variable gets set in the snippet and communicated back to the harness via JSON.

The TIMETHESE marker is replaced by a snippet of benchmarking. Here is one for slurping an ASCII file. It determines a file to slurp based on the name of the test, and then gets the results of timing tests (the "none" stops output so we don't mess up the JSON return).

my $size = $test->basename;
my $file = $corpus->child("ascii-$size");
die "Couldn't find $file" unless $file->exists;

$result = timethese (
    $count,
    {
        'Path::Tiny'  => sub { my $s = path($file)->slurp },
        'Path::Class' => sub { my $s = file($file)->slurp },
        'IO::All'     => sub { my $s = io($file)->slurp },
        'File::Fu'    => sub { my $s = File::Fu->file($file)->read },
    },
    "none"
);

I created that file as "small", so it would slurp the "ascii-small" corpus. Then I just symlinked it to "medium", "large", and "huge" and had tests for those corpuses (corpi?) corpora as well without any extra code to write. DRY!

You can see the benchmark harness code on Github.

Pictures worth a thousand words

With so many benchmarks, trying to make sense of it from ASCII tables would be crazy. I was inspired by the cool charts in Ricardo's blog about file finder speed, so I copied his code out of his repository and customized it.

It used Chart::Clicker, and getting that installed wasn't as easily as I'd have liked. Finally, I figured out I had to install Cairo development libraries, and even then, I had to force install Chart::Clicker despite some test failures. Nevertheless, it worked.

Here's what my code does:

  • Reads in benchmark data from a JSON file
  • Pivots the data from benchmark-module to module-benchmark
  • Chooses an order to display benchmarks (e.g. descending by worst performance)
  • Converts data to logarithms
  • Adds data series to a chart object
  • Formats the chart

The worst part was getting the chart formatting the way I wanted it. It took a lot of trial and error.

It's more or less what you would do with a spreadsheet, just done in Perl instead. And once it was written, I could use the same code to generate charts for all the different benchmark groups -- something I wouldn't want to do over and over in Excel.

You can also see the chart code on Github.

Next, I'll show some results.

Comparing file object creation

One of the first design goals I had for Path::Tiny was making object construction as cheap and lazy as possible because I'd seen in Ricardo's file-finding exercise just how costly that can be when iterating over a lot of files.

I was particularly curious how construction times might vary along several dimensions:

  • file objects versus directory objects
  • paths that exist or not
  • single level versus nested paths
  • absolute versus relative paths

I wound up with twelve benchmarks across those variations, which I plotted on a log-scaled axis.

File object construction

As I had hoped, Path::Tiny absolutely crushed the others across all benchmarks.

Path::Class and File::Fu drop off precipitously for absolute files and relative files that are nested, possibly because they create directory objects inside the file object, so two objects get created for every file object.

The flip side is that Path::Class spikes for a single-level, relative file (whether it exists or not), possibly because it's not creating an expensive directory object.

IO::All is consistent, but slow. It seems to be doing a lot of work in its constructor.

Object construction is a very synthetic baseline. In the real world, we actually want to manipulate those paths and the files or directories they represent. Still, it's built-in overhead and any method that returns another object is going to incur that overhead more than once.

Comparing file path manipulation

Next, I looked at several types of path manipulation:

  • Getting a child path (file or subdirectory)
  • Finding a basename
  • Relative to absolute (and vice versa)
  • Finding a parent directory
  • Finding the contents of a directory

I wound up with ten benchmarks. These are also plotted on a log scale. While the axis scaling differs from the last chart, you'll still be able to see how speeds drop off from the prior chart.

File object manipulation benchmark chart

Once again, Path::Tiny comes out on top!

File::Fu performs pretty well, and is nearly as fast as Path::Tiny at generating a new object for a file in a directory.

Everything performs similarly at converting an absolute path to a relative one, in part because it's sufficiently tricky that Path::Tiny falls back to using File::Spec like the other modules.

The slowest benchmark is the directory listing one. This consisted of reading my $HOME directory, which has about thirty mixed files or directories.

Unlike the other benchmarks, which are mostly manipulations of the path, this one requires reading a directory handle and constructing objects from the results. Assuming directly reading is equally efficient, you're seeing the cumulative effects of the object construction times.

Conclusions

Good benchmarking is difficult. Using Benchmark::Forking eliminates some of the noise, letting us have more confidence in the trends we see.

By taking some extra time to write a harness for my benchmark snippets and a chart generator, I was able to create a lot of benchmarks and visualize them with very little code. Writing new benchmarks is now just a matter of putting snippets into a directory.

The charts show Path::Tiny outperforming on every single benchmark, often by a fairly wide margin. (Remember, those charts were log scale!)

The next article in this series will look at file slurping, the disastrous costs of the common way to do strict Unicode decoding, how File::Slurp lies about binmodes, and how Path::Tiny wound up as the Unicode slurping champion.

Update: Ricardo informs me the correct word is "corpora".

Posted in perl programming | Tagged , , | 9 Responses

Goodbye Path::Class, hello Path::Tiny

I like Path::Class, but it's clunky and slow. So I wrote Path::Tiny to scratch my itch.

It's smaller (roughly half the lines of code), comes in a single file, and is generally faster. Among other things, it has lots of handy UTF-8 input and output methods.

The downside is that it's less portable and less extensible, but let's be honest, most of us are developing only for Unix or Windows anyway. And when was the last time you subclassed Path::Class for something? I'll bet never. YAGNI.

Here's the synopsis:

use Path::Tiny;
 
# creating Path::Tiny objects
 
$dir = path("/tmp");
$foo = path("foo.txt");
 
$subdir = $dir->child("foo");
$bar = $subdir->child("bar.txt");
 
# reading files
 
$guts = $file->slurp;
$guts = $file->slurp_utf8;
 
@lines = $file->lines;
@lines = $file->lines_utf8;
 
$head = $file->lines( {count => 1} );
 
# writing files
 
$bar->spew( @data );
$bar->spew_utf8( @data );
 
# reading directories
 
for ( $dir->children ) { ... }
 
$iter = $dir->iterator;
while ( my $next = $iter->() ) { ... }

It does require a very new File::Spec that fixes some ugly, tricky bugs, but, otherwise, it's core only for any recent Perl.

Check it out!

Update: If you use Moose, there is also MooseX::Types::Path::Tiny.

Update 2: I didn't mention it before, but note that stringifying the Path::Tiny returns a (possibly cleaned up) copy of the original path.

Posted in perl programming | Tagged , , | 8 Responses

My second week of Dancer, now with queues and transactional email

A couple weeks ago, I wrote about my initial efforts with Dancer, Xslate and Bootstrap. Last week, I added the ability to send password reset emails. In the process, I've learned how to write Dancer plugins.

Designing a password reset system

Since I've never done it before, I decided to reinvent the wheel and make it work the way I think it should.

I broke the problem down into three big chunks:

  1. generate and store a random token
  2. send the token to a user's registered email address
  3. receive the token and prompt a password change

Then I made a couple additional architectural decisions.

First, I decided to make the token system generic, as I expect there will be other things I'll use tokens for, such as email address confirmation. Regardless of the action that happens when the token is received, the first two steps above are nearly identical. All I need to do is make sure that tokens have a type and can store whatever additional data is needed to complete the action.

Second, I decided to break email sending out of the web app itself. Instead, an email is generated within the app and dropped into an asynchronous message queue. A separate program monitors the queue and sends the emails. In addition to insulating the web app from latency from sending email, a message queue makes unit testing a lot easier. All I need to do is see if the web app dropped the right message into the queue. I don't have to mock up an email delivery system.

Creating the token model

I decided it was sufficient to have a token be a URL-friendly, base64-encoded random value associated with a username, a "type" (e.g. password reset), an expiration, and an arbitrary "value" field.

For what it's worth, here's the trivial code for generating a random value:

use MIME::Base64 qw/encode_base64url/;
use Data::Entropy::Algorithms qw/rand_bits/;

$token = encode_base64url( rand_bits(192) ),

One of the reasons for having a generic "value" field is that I'm using MongoDB for my data store, so any JSON-serializable data can go in there "for free". If I wasn't using a document-based data store, I'd have to think more carefully about the value field semantics, or I'd have to serialize to/from the field with JSON or Sereal or something like that.

Speaking of MongoDB, I've been pretty pleased with Mongoose as a MongoDB->Moose mapper and the related Dancer::Plugin::Mongoose. I can specify my model classes and their associated database connection parameters directly in the Dancer config.yml file. If I were using a relational database, I'd probably look into Dancer::Plugin::DBIC, instead.

Loose coupling

Since I didn't want to send the email directly from the application, I needed a message queue. Since I'm using MongoDB and have other (backend) reasons for using it for other message queues, I decided to use it here as well. Otherwise, I might have explored Amazon SQS, Gearman, or Redis. Generally, my approach is to use a small number of versatile tools that I can develop expertise in rather than spread my expertise across a giant toolbox. This, of course, is why Perl is my #1 tool.

I had already written MongoDBx::Queue, so that was done. What I needed was to get Dancer to use it. A Dancer plugin for it would need to do a few useful things:

  1. Gather config data for (one or more) queues
  2. Instantiate a singleton for the life of the app
  3. Extend the Dancer DSL to provide access.

Again, I prefer loose coupling and frameworks, so instead of writing Dancer::Plugin::MongoDBx::Queue, instead I wrote Dancer::Plugin::Queue, which is a generic queue interface. Then I wrote Dancer::Plugin::Queue::MongoDB to implement the generic mechanism using MongoDBx::Queue.

Other Dancers who might favor other message queue systems just need to write similar implementation plugins and then message queues become an interchangeable component, just like template systems and session management. Loose coupling for the win!

Here is a slightly simplified version of the resulting code:

# generate reset token
my $token = schema("token")->new(
  user => $user->username,
  type => 'p',            # password reset type
);
$token->save;

# queue the reset email
queue("mx_out")->add_msg(
  {
    to      => $user->email,
    from    => 'support@example.com',
    subject => 'Did you forget your password?',
    body    => template(
      'emails/password_reset',
      {
        username    => $user->username,
        token_url   => uri_for( '/confirm/' . $token->token ),
      },
    ),
  }
);

Once the reset email data goes into the queue, it waits for a separate worker process to retrieve the message and send it. At the moment, I'm using the Postmark email service to send my transactional emails. The worker is a pretty short Perl program that polls the message queue, retrieves new messages and hands them off via WWW::Postmark.

While I was at it...

When working on Dancer::Plugin::Queue, I realized what I was doing was similar to a lot of other plugins I had examined. In the case of D::P::Queue, I had the added step of creating a role to define the generic interface, but leaving that aside, a lot of plugins are doing this:

  1. Loading a class
  2. Loading some config options
  3. Creating a singleton

It's ridiculous to do that for any particular CPAN module you want to use within Dancer, so I wrote it generically as Dancer::Plugin::Adapter.

If I weren't committed to using WWW::Postmark via a message queue, this is how I could use it directly within a Dancer app with Dancer::Plugin::Adapter:

In the config.yml:

plugins:
  Adapter:
    postmark:
      class: WWW::Postmark
      options: POSTMARK_API_TEST

In the application:

use Dancer::Plugin::Adapter;

get '/send_email' => sub {
  eval {
    service("postmark")->send(
      from    => 'me@domain.tld',
      to      => 'you@domain.tld, them@domain.tld',
      subject => 'an email message',
      body    => "hi guys, what's up?"
    );
  };
  return $@ ? "Error: $@" : "Mail sent";
};

As long as the module needs only static data to initialize, Dancer::Plugin::Adapter does all the repetitive work. In my (not so) humble opinion, that makes a lot of useful CPAN modules trivial to use as singletons within Dancer. Enjoy!

Pulling it together

Once I could generate and send the reset email, I needed a handler to respond to someone clicking on the reset link. Most of the work was doing some rudimentary validation on the submitted token -- ensuring it was valid, not expired, and so on.

I decided to treat a password reset token as a one-shot, password-equivalent-login and existing-password-revocation. I also track that the login happened via token, so that the password change form and logic can skip requiring the current password.

Here is a simplified version of that code:

get '/confirm/:token' => sub {
  unless ( params->{token} =~ /^[a-zA-Z0-9_=-]{32}$/ ) {
    return template 'error' => { error => "Invalid token" };
  }

  my $token = schema("token")->find_token( params->{token} );
  if ( $token ) {
    $token->delete; # one-shot token, so delete from database
  }
  else {
    return template 'error' => { error => "Token not found" };
  }
  
  my $user = schema("user")->find_user( $token->user );
  unless ($user) {
    return template 'error' => { error => "Token has invalid user" };
  }

  if ( time() > $token->expiration ) {
    return template 'error' => { error => "Token has expired" };
  }

  if ( $token->type eq 'p' ) { # password reset
    session user  => $user->username; # treat as logged in
    session token => 1;               # note they arrived via token
    $user->scramble_password;
    $user->save;
    redirect '/change_password';
  }
  else {
    return template 'error' => { error => "Token not recognized" };
  }
};

You can see how the token confirmation logic is nearly completely generic. I can add additional token types as needed, just with different logic for each.

Summary

After another week of work, the application continues to take shape. Here's what I got done:

  • Wrote and shipped a message queue plugin system and MongoDB implmentation
  • Wrote and shipped a generic CPAN module adapter plugin
  • Added a password reset feature that generates reset tokens and emails users
  • Added a password reset token handler

It's still only the rough outline of an application, but Dancer feels less foreign and I'm about ready to get past basic user-account housekeeping and into the real feature set of the application.

Posted in perl programming | Tagged , , , | 10 Responses

© 2009-2013 David Golden All Rights Reserved