Index: trunk/Nebulous-Server/Changes
===================================================================
--- trunk/Nebulous-Server/Changes	(revision 24842)
+++ trunk/Nebulous-Server/Changes	(revision 24915)
@@ -35,4 +35,5 @@
     - change Nebulous::Server->stat_object() to return both the total number of
       instance and just those that are available
+    - add Nebulous::Server->prune_object() API
       
 0.16
Index: trunk/Nebulous-Server/MANIFEST
===================================================================
--- trunk/Nebulous-Server/MANIFEST	(revision 24842)
+++ trunk/Nebulous-Server/MANIFEST	(revision 24915)
@@ -49,3 +49,4 @@
 t/17_server_swap_objects.t
 t/18_server_chmod_object.t
+t/19_server_prune_object.t
 t/75_parse_neb_key.t
Index: trunk/Nebulous-Server/lib/Nebulous/Server.pm
===================================================================
--- trunk/Nebulous-Server/lib/Nebulous/Server.pm	(revision 24842)
+++ trunk/Nebulous-Server/lib/Nebulous/Server.pm	(revision 24915)
@@ -844,4 +844,99 @@
 
 
+sub prune_object
+{
+    my $self = shift;
+
+    my $log = $self->log;
+    $log->debug("entered - @_");
+
+    my ($key) = validate_pos(@_,
+        {
+            type        => SCALAR,
+            callbacks   => {
+                'is valid object key'
+                    => sub { $self->_is_valid_object_key($_[0]) },
+            },
+        },
+    );
+
+    my $sql = $self->sql;
+
+    eval {
+        $key = parse_neb_key($key);
+    };
+    $log->logdie("$@") if $@;
+
+    my $db  = $self->db($key);
+
+    my $rows_removed = 0;
+TRANS: while (1) {
+        eval {
+            # remove key from cache
+            $self->cache->delete($key->path) if defined $self->cache;
+
+            my $so_id;
+            {
+                my $query = $db->prepare_cached( $sql->find_object_by_ext_id );
+                $query->execute( $key->path );
+                $so_id = $query->fetchrow_hashref->{'so_id'};
+                $query->finish;
+            }
+
+            # record the path of the innaccesible files for deferred
+            # deletion
+            my $rows_copied;
+            {
+                my $query = $db->prepare_cached( $sql->copy_dead_instances_to_deleted );
+                $rows_copied = $query->execute( $so_id );
+            }
+
+            # check to see if there is anything to be done
+            unless ($rows_copied > 0) {
+                $db->rollback;
+                return;
+            }
+
+            # In MySQL you can't select from a table your deleting rows from so
+            # we first have to get a list of instances to be removed, and then
+            # removed them.
+            my $rows_found;
+            {
+                my $query = $db->prepare_cached( $sql->find_dead_instances_by_so_id );
+                $rows_found = $query->execute( $so_id );
+                foreach my $row ($query->fetchrow_hashref) {
+                    # remove dead instances
+                    my $query = $db->prepare_cached( $sql->delete_instance_by_ins_id);
+                    $rows_removed += $query->execute( $row->{ins_id} );
+                }
+                $query->finish;
+            }
+
+            # sanity check
+            die("instances inaccessible ($rows_copied) != instances removed ($rows_removed)")
+                unless $rows_copied == $rows_removed;
+            
+            $db->commit;
+            $log->debug("commit");
+        };
+        if ($@) {
+            $db->rollback;
+            $log->debug("rollback");
+            if ($@ =~ $trans_regex) {
+                $log->warn("database error, retrying transaction: $@");
+                sleep TRANS_RETRY_WAIT;
+                redo TRANS;
+            }
+            $log->logdie("error: $@");
+        }
+        last;
+    }
+
+    $log->debug("leaving");
+
+    return $rows_removed;
+}
+
+
 sub lock_object
 {
@@ -1674,17 +1769,7 @@
                 # remove key from cache
                 $self->cache->delete($key->path) if defined $self->cache;
-
-                # record the path of the innaccesible files for deferred
-                # deletion
-                {
-                    my $query = $db->prepare_cached( $sql->copy_instances_to_deleted );
-                    $query->execute( $so_id );
-                }
-                # remove all instances... not strictly nessicary as the delete
-                # from storage_object should cascade but the fkey was specified
-                # without the cascade 
-                {
-                    my $query = $db->prepare_cached( $sql->delete_instance_by_so_id );
-                    $query->execute( $so_id );
+                
+                if ($total > $available) {
+                    $self->prune_object("$key");
                 }
                 
Index: trunk/Nebulous-Server/lib/Nebulous/Server/SQL.pm
===================================================================
--- trunk/Nebulous-Server/lib/Nebulous/Server/SQL.pm	(revision 24842)
+++ trunk/Nebulous-Server/lib/Nebulous/Server/SQL.pm	(revision 24915)
@@ -183,8 +183,4 @@
         WHERE ins_id = ?
     },
-    delete_instance_by_so_id => qq{
-        DELETE FROM instance
-        WHERE so_id = ?
-    },
     get_object_from_uri   => qq{
         SELECT so_id
@@ -218,10 +214,22 @@
         GROUP BY so_id
     },
-    copy_instances_to_deleted => qq{
+    copy_dead_instances_to_deleted => qq{
         INSERT INTO deleted 
         SELECT vol_id, uri, NULL
         FROM instance
+        LEFT JOIN mountedvol
+            USING(vol_id)
         WHERE
             so_id = ?
+            AND (mountedvol.available IS NULL || mountedvol.available = 0)
+    },
+    find_dead_instances_by_so_id => qq{
+        SELECT so_id, ins_id, vol_id, uri
+        FROM instance
+        LEFT JOIN mountedvol
+            USING(vol_id)
+        WHERE
+            so_id = ?
+            AND (mountedvol.available IS NULL || mountedvol.available = 0)
     },
     get_object_instances    => qq{
@@ -329,5 +337,5 @@
     },
     find_object_by_ext_id => qq{
-        SELECT ext_id, ext_id_basename
+        SELECT so_id, ext_id, ext_id_basename
         FROM storage_object
         WHERE ext_id = ?
Index: trunk/Nebulous-Server/t/19_server_prune_object.t
===================================================================
--- trunk/Nebulous-Server/t/19_server_prune_object.t	(revision 24915)
+++ trunk/Nebulous-Server/t/19_server_prune_object.t	(revision 24915)
@@ -0,0 +1,73 @@
+#!/usr/bin/perl
+
+# Copryight (C) 2004-2005  Joshua Hoblitt
+#
+# $Id: 08_server_delete_instance.t,v 1.10.22.1 2008-12-14 22:52:37 eugene Exp $
+
+use strict;
+use warnings FATAL => qw( all );
+
+use Test::More tests => 6;
+
+use lib qw( ./t ./lib );
+
+use Nebulous::Server;
+use Test::Nebulous;
+
+my $neb = Nebulous::Server->new(
+    dsn         => $NEB_DB,
+    dbuser      => $NEB_USER,
+    dbpasswd    => $NEB_PASS,
+);
+
+use Test::DBUnit dsn => $NEB_DB, username => $NEB_USER, password => $NEB_PASS;
+
+Test::Nebulous->setup;
+
+{
+    my $key = "foo";
+    my $uri1 = $neb->create_object($key, 'node01');
+    my $uri2 = $neb->replicate_object($key, 'node02');
+
+    # make one of the instances unavailable
+    my $dbh = test_dbh();
+    $dbh->do("UPDATE mountedvol SET available = 0 WHERE name = 'node01'");
+
+    ok($neb->prune_object($key), "prune object");
+
+    expected_dataset_ok(
+        deleted => [vol_id => 1, uri => $uri1],
+    );
+    
+    expected_dataset_ok(
+        instance => [ins_id => 2, so_id => 1, vol_id => 2, uri => $uri2],
+    );
+}
+
+Test::Nebulous->setup;
+
+{
+    my $key = "foo";
+    my $uri1 = $neb->create_object($key);
+
+    is($neb->prune_object($key), 0, "no dead instances to remove");
+}
+
+Test::Nebulous->setup;
+
+eval {
+    $neb->prune_object();
+};
+like($@, qr/1 was expected/, "no params");
+
+Test::Nebulous->setup;
+
+eval {
+    my $key = "foo";
+    my $uri1 = $neb->create_object($key);
+
+    $neb->prune_object("foo", 2);
+};
+like($@, qr/1 was expected/, "too many params");
+
+Test::Nebulous->cleanup;
