=head1 NAME Sub::Slice::Backend - API for Sub::Slice backends =head1 SYNOPSIS use Sub::Slice; use Sub::Slice::Backend::MyBackend; our %Options = ( ...options for your backend... ); sub create_token { my $job = new Sub::Slice ( backend => 'MyBackend', storage_options => \%Options, ); return $job->token; } sub do_work { my $job = new Sub::Slice ( backend => 'MyBackend', storage_options => \%Options, token => shift() ); ... } =head1 DESCRIPTION This is the API which storage backends for Sub::Slice must implement. Sub::Slice comes with a default Filesystem backend using Storable to serialise data. If you'd rather store the Sub::Slice data somewhere else or in another format, you can create an adaptor class satisfying this API to plug Sub::Slice into your storage system. =head2 Module naming conventions If a string matching \w+ is passed to Sub::Slice for the backend, the module is assumed to be in the Sub::Slice::Backend namespace (such as the example in the SYNOPSIS). If you have a module in a namespace other than the top-level one, e.g. MySystem::StorageModule, that exposes this API, Sub::Slice will happily use this as a backend module without you needing to create a wrapper in the Sub::Slice::Backend namespace. my $job = new Sub::Slice ( backend => 'MySystem::StorageModule', storage_options => \%Options, ); =head1 METHODS =over 4 =item $backend = new Sub::Slice::Backend::YOURCLASS(\%options) Contructor takes a hash of options =item $id = $backend->new_id() Generates a new ID. =item $job = $backend->load_job($id) Loads an existing job from persistent storage =item $backend->save_job($job) Saves a job to persistent storage =item $backend->delete_job($id) Removes a job from persistent storage =item $backend->store($job, $key, $data) Store data for job =item $data = $backend->fetch($job, $key) Fetch data for job =item $backend->store_blob($job, $key, $data) Store BLOB data for job =item $data = $backend->fetch_blob($job, $key) Fetch BLOB data for job =item $count = $backend->cleanup($age) Deletes any leftover data which has been unmodified for at least $age days (default 1 day). $count will equal the number of files or items deleted, or may be undef if cleanup wasn't able to scan for leftover data (eg. because a directory didn't exist). Should die if it doesn't manage to clean up (eg. because of a permissions problem). Normally, tasks should clean up after themselves, so unless you are experiencing errors, cleanup will have nothing to do. =back =head1 Strategies for issuing IDs The storage API requires that you can issue an ID for a new record before storing data into it. In an Oracle database you normally use a sequence to generate a unique ID in advance of storing a record so this isn't a problem. In MySQL, if you are planning on using an AUTO_INCREMENT column to generate ids, you're probably best off inserting an empty row. Alternatively in most database engines you can simulate an Oracle sequence using a counter table (see the example in the MySQL manual for a neat example using LAST_INSERT_ID). If your storage engine doesn't have a unique ID mechanism, a couple of strategies are open to you: you can use a perl GUID generator (such as Data::UUID), or you can roll your own sequence generator as in the example below. =head1 Worked Example Here's a sample implementation. It's not a particularly good one in that it doesn't handle concurrent processes writing to the DBM file, but it does illustrate the steps required to write a backend. package Sub::Slice::Backend::MLDBM; use GDBM_File; use Storable(); use MLDBM qw(GDBM_File Storable); The constructor takes a hashref of storage options, which you're free to define the meaning of. These are what the caller will pass in as the storage_options to Sub::Slice. sub new { my ($class, $options) = @_; my $dbm = $options->{DBM} or die("You must supply a DBM"); my %db; tie(%db, 'MLDBM', $dbm, GDBM_WRCREAT, 0666) or die("unable to tie to $dbm"); my $self = {$db => \%db}; return bless($self, $class); } sub DESTROY { my $self = shift; untie %{$self->{db}} if tied %{$self->{db}}; } The C routine should create a unique ID which can be used to store the job against. Here we use a sentinel key to store a sequence counter against: sub new_id { my ($self) = @_; my $id = ++$self->{db}->{__COUNT__}; return $id; } The delete_job routine takes an ID rather than a job: sub delete_job { my ($self, $id) = @_; delete $self->{db}->{$id}; } This allows it to be used in cleaner processes without loading the job first. The load_job routine should return the job given the ID: sub load_job { my ($self, $id) = @_; return $self->{db}->{$id}; } The save_job should persist the job (against the ID): sub save_job { my ($self, $job) = @_; $self->{db}->{$job->{id}} = $job; } Store and fetch methods are passed the job object so they can hang data on it, if the backend desires: sub store { my ($self, $job, $key, $value) = @_; $job->{data}{$key} = $value; } sub fetch { my ($self, $job, $key) = @_; return $job->{data}{$key}; } If there is no optimisation you want to perform for BLOB data, the following implementation is legitimate: sub store_blob { shift()->store(@_); } sub fetch_blob { shift()->fetch(@_); } =head1 VERSION $Revision: 1.8 $ on $Date: 2004/12/17 14:41:21 $ by $Author: johna $ =head1 AUTHOR John Alden =head1 COPYRIGHT (c) BBC 2004. This program is free software; you can redistribute it and/or modify it under the GNU GPL. See the file COPYING in this distribution, or http://www.gnu.org/licenses/gpl.txt =cut