use strict;
package ObjStore::Job;
use ObjStore;
use base 'ObjStore::HV';  #use fields? XXX
use vars qw($VERSION);
$VERSION = '0.02';

sub new {
    # $id should be probably be formed with:
    #    join('.', `hostname`, $$, $id++);

    my ($class, $near, $id, $priority) = @_;
    # uses real pointers so must be per-database...
    my $t = $near->database_of->hash->{'ObjStore::Job::Table'};
    my $o = shift->SUPER::new($t);
    # $id is a string!
    if ($id) { $$o{id} = "$id"; }
    else {     $$o{id} = "$$t{nextid}"; ++$$t{nextid}; }
    $$o{priority} = defined $priority? $priority : 10;
    $$o{job_table} = $$t{SELF};
    $$o{state} = 'R';
    $$o{why} = '';   #why killed
    $t->add($o);
    $o;
}

sub runnable {
    my $state = shift->{state};
    $state ne 'D' and $state ne 'K';
}
sub running {
    my $state = shift->{state};
    $state eq 'R' or $state eq 'S';
}

use ObjStore::notify qw(work set_priority signal acknowledge);
sub do_work {
    my ($o, $slices) = @_;
    # override this method!
    my $used = int rand 8;
    warn "$o->work(): consuming $used slices";
    $$o{state} = 'S' if $used == 0;  #avoid 'L' state
    $slices - $used;  # how many left
}
sub do_set_priority {
    my ($o, $pri) = @_;
    my $t = $$o{job_table}->focus;
    $t->remove($o);
    $$o{priority} = $pri;
    $t->add($o);
    ()
}
sub do_signal {
    my ($o, $sig) = @_;
    return if !$o->runnable;
    if    ($sig eq 'kill')      { $$o{state} = 'K'; $$o{why} = 'signal'; }
    elsif ($sig eq 'suspend')   { $$o{state} = 'T'; }
    elsif ($sig eq 'resume')    { $$o{state} = 'R'; }
    else { warn "$o->signal($sig): unknown signal"; }
    ()
}
sub do_acknowledge {  #like wait(2)
    my ($o) = @_;
    return if $o->runnable;
    $o->cancel();
    ()
}

sub cancel {
    my ($o) = @_;
    $$o{job_table}->focus->remove($o);
}

1;

=head1 NAME

ObjStore::Job - Jobs for a Non-Preemptive Idle-Time Job Scheduler

=head1 SYNOPSIS

=over 4

=item 1

Add an C<ObjStore::Job::Table> to your database.

=item 2

Sub-class C<ObjStore::Job> and override the C<do_work> method.

=item 3

  package ObjStore::Job
  use ObjStore::Mortician;

Maybe all jobs should have delayed destruction by default.

=back

=head1 DESCRIPTION

=head1 JOB STATES

 R running
 L infinite loop detected
 S sleeping                - will retry every second
 T suspended
 D done
 K killed

=head1 SCHEDULING PRIORITIES

=over 4

=item * HIGH PRIORITY <= 0

Allowed to consume all available pizza slices.

=item * TIME-SLICED 1-20

Given pizza slices proportional to the priority until either all the
pizza slices are consumed or all the jobs are asleep (feast induced
slumber :-).

=item * IDLE > 20

Given all remaining pizza slices.

=back

=head1 TRANSACTION STRATEGY

The whole scheduling operation occurs within a single transaction.
While this means that any job can kill the entire transaction, this
seems a better choice than wrapping every job in its own
mini-transaction.  Since transactions are relatively expensive, it is
assumed that most of the time all jobs will complete without error.

=head1 BUGS

Too bad you can't store CODEREFs in the database.

Time does not necessarily transmute into pizza.

=cut
