Imported Upstream version 3.3.3
[debian/amanda] / perl / Amanda / Changer / multi.pm
1 # Copyright (c) 2008-2012 Zmanda, Inc.  All Rights Reserved.
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
11 # for more details.
12 #
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
16 #
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
19
20 package Amanda::Changer::multi;
21
22 use strict;
23 use warnings;
24 use vars qw( @ISA );
25 @ISA = qw( Amanda::Changer );
26
27 use File::Glob qw( :glob );
28 use File::Path;
29 use Amanda::Config qw( :getconf );
30 use Amanda::Debug;
31 use Amanda::Changer;
32 use Amanda::MainLoop;
33 use Amanda::Device qw( :constants );
34
35 =head1 NAME
36
37 Amanda::Changer::multi
38
39 =head1 DESCRIPTION
40
41 This changer operates with a list of device, specified in the tpchanger
42 string.
43
44 See the amanda-changers(7) manpage for usage information.
45
46 =cut
47
48 # STATE
49 #
50 # The device state is shared between all changers accessing the same changer.
51 # It is a hash with keys:
52 #   current_slot - the unaliased device name of the current slot
53 #   slots - see below
54 #
55 # The 'slots' key is a hash, with unaliased device name as keys and hashes
56 # as values.  Each slot's hash has keys:
57 #   pid           - the pid that reserved that slot.
58 #   state         - SLOT_FULL/SLOT_EMPTY/SLOT_UNKNOWN
59 #   device_status - the status of the device after the open or read_label
60 #   device_error  - error message from the device
61 #   f_type        - the F_TYPE of the fileheader.
62 #   label         - the label, if known, of the volume in this slot
63
64 # $self is a hash with keys:
65 #   slot           : slot number of the current slot
66 #   slots          : An array with all slot names
67 #   unaliased      : A hash with slot number as keys and unaliased device name
68 #                    as value
69 #   slot_name      : A hash with slot number as keys and device name as value
70 #   number         : A hash with unaliased device name as keys and slot number
71 #                    as value
72 #   config         : The Amanda::Changer::Config for this changer
73 #   state_filename : The filename of the state file
74 #   first_slot     : The number of the first slot
75 #   last_slot      : The number of the last slot + 1
76
77 sub new {
78     my $class = shift;
79     my ($config, $tpchanger) = @_;
80     my $devices = $tpchanger;
81     $devices =~ s/^chg-multi://g;
82     my (@slots) = Amanda::Util::expand_braced_alternates($devices);
83
84     unless (scalar @slots != 0) {
85         return Amanda::Changer->make_error("fatal", undef,
86             message => "no devices specified");
87     }
88
89     my $properties = $config->{'properties'};
90     my $first_slot = 1;
91     if (exists $properties->{'first-slot'}) {
92         $first_slot = @{$properties->{'first-slot'}->{'values'}}[0];
93     }
94
95     my %number = ();
96     my %unaliased = ();
97     my %slot_name = ();
98     my $last_slot = $first_slot;
99     foreach my $slot_name (@slots) {
100         my $unaliased_name = Amanda::Device::unaliased_name($slot_name);
101         $number{$unaliased_name} = $last_slot;
102         $unaliased{$last_slot} = $unaliased_name;
103         $slot_name{$last_slot} = $slot_name;
104         $last_slot++;
105     }
106
107     if (!defined $config->{changerfile} ||
108         $config->{changerfile} eq "") {
109         return Amanda::Changer->make_error("fatal", undef,
110             reason => "invalid",
111             message => "no changerfile specified for changer '$config->{name}'");
112     }
113
114     my $state_filename = Amanda::Config::config_dir_relative($config->{'changerfile'});
115     my $lock_timeout = $config->{'lock-timeout'};
116     Amanda::Debug::debug("Using state file: $state_filename");
117
118     my $self = {
119         slots => \@slots,
120         unaliased => \%unaliased,
121         slot_name => \%slot_name,
122         number => \%number,
123         config => $config,
124         state_filename => $state_filename,
125         first_slot => $first_slot,
126         last_slot => $last_slot,
127         'lock-timeout' => $lock_timeout,
128     };
129
130     bless ($self, $class);
131     return $self;
132 }
133
134 sub load {
135     my $self = shift;
136     my %params = @_;
137     my $old_res_cb = $params{'res_cb'};
138     my $state;
139
140     $self->validate_params('load', \%params);
141
142     return if $self->check_error($params{'res_cb'});
143
144     $self->with_locked_state($self->{'state_filename'},
145                                      $params{'res_cb'}, sub {
146         my ($state, $res_cb) = @_;
147
148         $params{'state'} = $state;
149         # overwrite the callback for _load_by_xxx
150         $params{'res_cb'} = $res_cb;
151
152         if (exists $params{'slot'} or exists $params{'relative_slot'}) {
153             $self->_load_by_slot(%params);
154         } elsif (exists $params{'label'}) {
155             $self->_load_by_label(%params);
156         }
157     });
158 }
159
160 sub info_key {
161     my $self = shift;
162     my ($key, %params) = @_;
163     my %results;
164
165     return if $self->check_error($params{'info_cb'});
166
167     # no need for synchronization -- all of these values are static
168
169     if ($key eq 'num_slots') {
170         $results{$key} = $self->{last_slot} - $self->{first_slot};
171     } elsif ($key eq 'vendor_string') {
172         $results{$key} = 'chg-multi'; # mostly just for testing
173     } elsif ($key eq 'fast_search') {
174         $results{$key} = 0;
175     }
176
177     $params{'info_cb'}->(undef, %results) if $params{'info_cb'};
178 }
179
180 sub reset {
181     my $self = shift;
182     my %params = @_;
183
184     return if $self->check_error($params{'finished_cb'});
185
186     $self->with_locked_state($self->{'state_filename'},
187                                      $params{'finished_cb'}, sub {
188         my ($state, $finished_cb) = @_;
189         my $slot;
190
191         $params{state} = $state;
192         $slot = $self->{first_slot};
193         $self->{slot} = $slot;
194         $self->_set_current($state, $slot);
195
196         $finished_cb->();
197     });
198 }
199
200 sub eject {
201     my $self = shift;
202     my %params = @_;
203     my $slot;
204
205     return if $self->check_error($params{'finished_cb'});
206
207     $self->with_locked_state($self->{'state_filename'},
208                                      $params{'finished_cb'}, sub {
209         my ($state, $finished_cb) = @_;
210         my $drive;
211
212         $params{state} = $state;
213         if (!exists $params{'drive'}) {
214             $drive = $self->_get_current($params{state});
215         } else {
216             $drive = $params{'drive'};
217         }
218         if (!defined $self->{unaliased}->{$drive}) {
219             return $self->make_error("failed", $finished_cb,
220                 reason => "invalid",
221                 message => "Invalid slot '$drive'");
222         }
223
224         Amanda::Debug::debug("ejecting drive $drive");
225         my $device = Amanda::Device->new($self->{slot_name}->{$drive});
226         if ($device->status() != $DEVICE_STATUS_SUCCESS) {
227             return $self->make_error("failed", $finished_cb,
228                 reason => "device",
229                 message => $device->error_or_status);
230         }
231         if (my $err = $self->{'config'}->configure_device($device)) {
232             return $self->make_error("failed", $params{'res_cb'},
233                         reason => "device",
234                         message => $err);
235         }
236         $device->eject();
237         if ($device->status() != $DEVICE_STATUS_SUCCESS) {
238             return $self->make_error("failed", $finished_cb,
239                 reason => "invalid",
240                 message => $device->error_or_status);
241         }
242         undef $device;
243
244         $finished_cb->();
245     });
246 }
247
248 sub update {
249     my $self = shift;
250     my %params = @_;
251     my @slots_to_check;
252     my $state;
253     my $set_to_unknown = 0;
254
255     my $user_msg_fn = $params{'user_msg_fn'};
256     $user_msg_fn ||= sub { Amanda::Debug::info("chg-multi: " . $_[0]); };
257
258     my $steps = define_steps
259         cb_ref => \$params{'finished_cb'};
260
261     step lock => sub {
262         $self->with_locked_state($self->{'state_filename'},
263                                  $params{'finished_cb'}, sub {
264             my ($state, $finished_cb) = @_;
265
266             $params{state} = $state;
267             $params{'finished_cb'} = $finished_cb;
268
269             $steps->{'handle_assignment'}->();
270         });
271     };
272
273     step handle_assignment => sub {
274         $state = $params{state};
275         # check for the SL=LABEL format, and handle it here
276         if (exists $params{'changed'} and
277             $params{'changed'} =~ /^\d+=\S+$/) {
278             my ($slot, $label) = ($params{'changed'} =~ /^(\d+)=(\S+)$/);
279
280             # let's list the reasons we *can't* do what the user has asked
281             my $whynot;
282             if (!exists $self->{unaliased}->{$slot}) {
283                 $whynot = "slot $slot does not exist";
284             }
285
286             if ($whynot) {
287                 return $self->make_error("failed", $params{'finished_cb'},
288                         reason => "unknown", message => $whynot);
289             }
290
291             $user_msg_fn->("recording volume '$label' in slot $slot");
292             # ok, now erase all knowledge of that label
293             while (my ($sl, $inf) = each %{$state->{'slots'}}) {
294                 if ($inf->{'label'} and $inf->{'label'} eq $label) {
295                     $inf->{'label'} = undef;
296                 }
297             }
298
299             # and add knowledge of the label to the given slot
300             my $unaliased = $self->{unaliased}->{$slot};
301             $state->{'slots'}->{$unaliased}->{'label'} = $label;
302
303             # that's it -- no changer motion required
304             return $params{'finished_cb'}->(undef);
305         } elsif (exists $params{'changed'} and
306                $params{'changed'} =~ /^(.+)=$/) {
307             $params{'changed'} = $1;
308             $set_to_unknown = 1;
309             $steps->{'calculate_slots'}->();
310         } else {
311             $steps->{'calculate_slots'}->();
312         }
313     };
314
315     step calculate_slots => sub {
316         if (exists $params{'changed'}) {
317             # parse the string just like use-slots, using a hash for uniqueness
318             my %changed;
319             for my $range (split ',', $params{'changed'}) {
320                 my ($first, $last) = ($range =~ /(\d+)(?:-(\d+))?/);
321                 $last = $first unless defined($last);
322                 for ($first .. $last) {
323                     $changed{$_} = undef;
324                 }
325             }
326
327             @slots_to_check = keys %changed;
328             @slots_to_check = grep { exists $self->{'unaliased'}->{$_} } @slots_to_check;
329         } else {
330             @slots_to_check = keys %{ $self->{unaliased} };
331         }
332
333         # sort them so we don't confuse the user with a "random" order
334         @slots_to_check = sort @slots_to_check;
335
336         $steps->{'update_slot'}->();
337     };
338
339     # TODO: parallelize, we have one drive by slot
340
341     step update_slot => sub {
342         return $steps->{'done'}->() if (!@slots_to_check);
343         my $slot = shift @slots_to_check;
344         if ($self->_is_slot_in_use($state, $slot)) {
345             $user_msg_fn->("Slot $slot is already in use");
346             return $steps->{'update_slot'}->();
347         }
348
349         if ($set_to_unknown == 1) {
350             $user_msg_fn->("removing entry for slot $slot");
351             my $unaliased = $self->{unaliased}->{$slot};
352             delete $state->{slots}->{$unaliased};
353             return $steps->{'update_slot'}->();
354         } else {
355             $user_msg_fn->("scanning slot $slot");
356             $params{'slot'} = $slot;
357             $params{'res_cb'} = $steps->{'slot_loaded'};
358             $self->_load_by_slot(%params);
359         }
360     };
361
362     step slot_loaded => sub {
363         my ($err, $res) = @_;
364         if ($err) {
365             return $params{'finished_cb'}->($err);
366         }
367
368         my $slot = $res->{'this_slot'};
369         my $dev = $res->{device};
370         $self->_update_slot_state(state => $state, dev => $dev, slot =>$slot);
371         if ($dev->status() == $DEVICE_STATUS_SUCCESS) {
372             my $label = $dev->volume_label;
373             $user_msg_fn->("recording volume '$label' in slot $slot");
374         } else {
375             my $status = $dev->error_or_status;
376             $user_msg_fn->("recording device error '" . $status . "' in slot $slot");
377         }
378         $res->release(
379             finished_cb => $steps->{'released'},
380             unlocked => 1,
381             state => $state);
382     };
383
384     step released => sub {
385         my ($err) = @_;
386         if ($err) {
387             return $params{'finished_cb'}->($err);
388         }
389
390         $steps->{'update_slot'}->();
391     };
392
393     step done => sub {
394         $params{'finished_cb'}->(undef);
395     };
396 }
397
398 sub inventory {
399     my $self = shift;
400     my %params = @_;
401
402     return if $self->check_error($params{'inventory_cb'});
403
404     $self->with_locked_state($self->{'state_filename'},
405                              $params{'inventory_cb'}, sub {
406         my ($state, $inventory_cb) = @_;
407
408         my @inventory;
409         my $current = $self->_get_current($state);
410         foreach ($self->{first_slot} .. ($self->{last_slot} - 1)) {
411             my $slot = "$_";
412             my $unaliased = $self->{unaliased}->{$slot};
413             my $s = { slot => $slot,
414                       state => $state->{slots}->{$unaliased}->{state} || Amanda::Changer::SLOT_UNKNOWN,
415                       reserved => $self->_is_slot_in_use($state, $slot) };
416             if (defined $state->{slots}->{$unaliased} and
417                 exists $state->{slots}->{$unaliased}->{device_status}) {
418                 $s->{'device_status'} =
419                               $state->{slots}->{$unaliased}->{device_status};
420                 if ($s->{'device_status'} != $DEVICE_STATUS_SUCCESS) {
421                     $s->{'device_error'} =
422                               $state->{slots}->{$unaliased}->{device_error};
423                 } else {
424                     $s->{'device_error'} = undef;
425                 }
426                 $s->{'f_type'} = $state->{slots}->{$unaliased}->{f_type};
427                 $s->{'label'} = $state->{slots}->{$unaliased}->{label};
428             } else {
429                 $s->{'device_status'} = undef;
430                 $s->{'device_error'} = undef;
431                 $s->{'f_type'} = undef;
432                 $s->{'label'} = undef;
433             }
434             if ($slot eq $current) {
435                 $s->{'current'} = 1;
436             }
437             push @inventory, $s;
438         }
439         $inventory_cb->(undef, \@inventory);
440     })
441 }
442
443 sub _load_by_slot {
444     my $self = shift;
445     my %params = @_;
446     my $slot;
447
448     if (exists $params{'relative_slot'}) {
449         if ($params{'relative_slot'} eq "current") {
450             $slot = $self->_get_current($params{state});
451         } elsif ($params{'relative_slot'} eq "next") {
452             if (exists $params{'slot'}) {
453                 $slot = $params{'slot'};
454             } else {
455                 $slot = $self->_get_current($params{state});
456             }
457             $slot = $self->_get_next($slot);
458             $self->{slot} = $slot if ($params{'set_current'});
459             $self->_set_current($params{state}, $slot) if ($params{'set_current'});
460         } else {
461             return $self->make_error("failed", $params{'res_cb'},
462                 reason => "invalid",
463                 message => "Invalid relative slot '$params{relative_slot}'");
464         }
465     } else {
466         $slot = $params{'slot'};
467     }
468
469     if (exists $params{'except_slots'} and exists $params{'except_slots'}->{$slot}) {
470         return $self->make_error("failed", $params{'res_cb'},
471             reason => "notfound",
472             message => "all slots have been loaded");
473     }
474
475     if (!$self->_slot_exists($slot)) {
476         return $self->make_error("failed", $params{'res_cb'},
477             reason => "notfound",
478             message => "Slot $slot not defined");
479     }
480
481     if ($self->_is_slot_in_use($params{state}, $slot)) {
482         my $unaliased = $self->{unaliased}->{$slot};
483         return $self->make_error("failed", $params{'res_cb'},
484             reason => "volinuse",
485             slot => $slot,
486             message => "Slot $slot is already in use by process '$params{state}->{slots}->{$unaliased}->{pid}'");
487     }
488
489     $self->{slot} = $slot if ($params{'set_current'});
490     $self->_set_current($params{state}, $slot) if ($params{'set_current'});
491
492     $self->_make_res($params{state}, $params{'res_cb'}, $slot);
493 }
494
495 sub _load_by_label {
496     my $self = shift;
497     my %params = @_;
498     my $label = $params{'label'};
499     my $slot;
500     my $slot_name;
501     my $state = $params{state};
502
503     foreach $slot (keys %{$state->{slots}}) {
504         if (defined $state->{slots}->{$slot} &&
505             $state->{slots}->{$slot}->{label} &&
506             $state->{slots}->{$slot}->{label} eq $label) {
507             $slot_name = $slot;
508             last;
509         }
510     }
511
512     if (defined $slot_name &&
513         $state->{slots}->{$slot_name}->{label} eq $label) {
514
515         $slot = $self->{number}->{$slot_name};
516         delete $params{'label'};
517         $params{'slot'} = $slot;
518         $self->_load_by_slot(%params);
519     } else {
520         return $self->make_error("failed", $params{'res_cb'},
521                                 reason => "notfound",
522                                 message => "Label '$label' not found");
523     }
524 }
525
526
527 sub _make_res {
528     my $self = shift;
529     my ($state, $res_cb, $slot) = @_;
530     my $res;
531
532     my $unaliased = $self->{unaliased}->{$slot};
533     my $slot_name = $self->{slot_name}->{$slot};
534     my $device = Amanda::Device->new($slot_name);
535     if ($device->status != $DEVICE_STATUS_SUCCESS) {
536         return $self->make_error("failed", $res_cb,
537                 reason => "device",
538                 message => "opening '$slot': " . $device->error_or_status());
539     }
540
541     if (my $err = $self->{'config'}->configure_device($device)) {
542         return $self->make_error("failed", $res_cb,
543                 reason => "device",
544                 message => $err);
545     }
546
547     $res = Amanda::Changer::multi::Reservation->new($self, $device, $slot);
548     $state->{slots}->{$unaliased}->{pid} = $$;
549     $device->read_label();
550
551     $self->_update_slot_state(state => $state, dev => $res->{device}, slot => $slot);
552     $res_cb->(undef, $res);
553 }
554
555
556 # Internal function to determine whether a slot exists.
557 sub _slot_exists {
558     my ($self, $slot) = @_;
559
560     return 1 if defined $self->{unaliased}->{$slot};
561     return 0;
562 }
563
564 sub _update_slot_state {
565     my $self = shift;
566     my %params = @_;
567     my $state = $params{state};
568     my $dev = $params{dev};
569     my $slot = $params{slot};
570     my $unaliased = $self->{unaliased}->{$slot};
571     $state->{slots}->{$unaliased}->{device_status} = "".scalar($dev->status);
572     if ($dev->status != $DEVICE_STATUS_SUCCESS) {
573         $state->{slots}->{$unaliased}->{device_error} = $dev->error;
574     } else {
575         $state->{slots}->{$unaliased}->{device_error} = undef;
576     }
577     my $label = $dev->volume_label;
578     $state->{slots}->{$unaliased}->{state} = Amanda::Changer::SLOT_FULL;
579     $state->{slots}->{$unaliased}->{label} = $label;
580     my $volume_header = $dev->volume_header;
581     if (defined $volume_header) {
582         $state->{slots}->{$unaliased}->{f_type} = "".scalar($volume_header->{type});
583     } else {
584         delete $state->{slots}->{$unaliased}->{f_type};
585     }
586 }
587 # Internal function to determine if a slot (specified by number) is in use by a
588 # drive, and return the path for that drive if so.
589 sub _is_slot_in_use {
590     my ($self, $state, $slot) = @_;
591
592     return 0 if !defined $state;
593     return 0 if !defined $state->{slots};
594     return 0 if !defined $self->{unaliased}->{$slot};
595     my $unaliased = $self->{unaliased}->{$slot};
596     return 0 if !defined $state->{slots}->{$unaliased};
597     return 0 if !defined $state->{slots}->{$unaliased}->{pid};
598
599     #check if PID is still alive
600     my $pid = $state->{slots}->{$unaliased}->{pid};
601     if (Amanda::Util::is_pid_alive($pid) == 1) {
602         return 1;
603     }
604
605     delete $state->{slots}->{$unaliased}->{pid};
606     return 0;
607 }
608
609 # Internal function to get the next slot after $slot.
610 # skip over except_slot and slot in use.
611 sub _get_next {
612     my ($self, $slot, $except_slot) = @_;
613     my $next_slot;
614
615     $next_slot = $slot + 1;
616     $next_slot = $self->{'first_slot'} if $next_slot >= $self->{'last_slot'};
617
618     return $next_slot;
619 }
620
621 # Get the 'current' slot
622 sub _get_current {
623     my ($self, $state) = @_;
624
625     return $self->{slot} if defined $self->{slot};
626     if (defined $state->{current_slot}) {
627         my $slot = $self->{number}->{$state->{current_slot}};
628         # return the slot if it exist.
629         return $slot if defined $slot and
630                                 $slot >= $self->{'first_slot'} and
631                                 $slot < $self->{'last_slot'};
632         Amanda::Debug::debug("statefile current_slot is not configured");
633     }
634     # return the first slot
635     return $self->{first_slot};
636 }
637
638 # Set the 'current' slot
639 sub _set_current {
640     my ($self, $state, $slot) = @_;
641
642     $self->{slot} = $slot;
643     $state->{current_slot} = $self->{unaliased}->{$slot};
644 }
645
646 package Amanda::Changer::multi::Reservation;
647 use vars qw( @ISA );
648 @ISA = qw( Amanda::Changer::Reservation );
649 use Amanda::Device qw( :constants );
650
651 sub new {
652     my $class = shift;
653     my ($chg, $device, $slot) = @_;
654     my $self = Amanda::Changer::Reservation::new($class);
655
656     $self->{'chg'} = $chg;
657     $self->{'device'} = $device;
658     $self->{'this_slot'} = $slot;
659
660     return $self;
661 }
662
663 sub set_label {
664     my $self = shift;
665     my %params = @_;
666
667     my $chg = $self->{chg};
668     $chg->with_locked_state($chg->{'state_filename'},
669                             $params{'finished_cb'}, sub {
670         my ($state, $finished_cb) = @_;
671         my $label = $params{'label'};
672         my $slot = $self->{'this_slot'};
673         my $unaliased = $chg->{unaliased}->{$slot};
674         my $dev = $self->{'device'};
675
676         $state->{slots}->{$unaliased}->{label} =  $label;
677         $state->{slots}->{$unaliased}->{device_status} =
678                                 "".$dev->status;
679         if ($dev->status != $DEVICE_STATUS_SUCCESS) {
680             $state->{slots}->{$unaliased}->{device_error} = $dev->error;
681         } else {
682             $state->{slots}->{$unaliased}->{device_error} = undef;
683         }
684         my $volume_header = $dev->volume_header;
685         if (defined $volume_header) {
686             $state->{slots}->{$unaliased}->{f_type} =
687                                 "".$volume_header->{type};
688         } else {
689             $state->{slots}->{$unaliased}->{f_type} = undef;
690         }
691         $finished_cb->();
692     });
693 }
694
695 sub do_release {
696     my $self = shift;
697     my %params = @_;
698
699     # if we're in global cleanup and the changer is already dead,
700     # then never mind
701     return unless $self->{'chg'};
702
703     $self->{'device'}->eject() if (exists $self->{'device'} and
704                                    exists $params{'eject'} and
705                                    $params{'eject'});
706
707     # unref the device, for good measure
708     $self->{'device'} = undef;
709
710     if (exists $params{'unlocked'}) {
711         my $state = $params{state};
712         my $slot = $self->{'this_slot'};
713         my $unaliased = $self->{chg}->{unaliased}->{$slot};
714         delete $state->{slots}->{$unaliased}->{pid};
715         return $params{'finished_cb'}->();
716     }
717
718     $self->{chg}->with_locked_state($self->{chg}->{'state_filename'},
719                                     $params{'finished_cb'}, sub {
720         my ($state, $finished_cb) = @_;
721         $params{state} = $state;
722         my $slot = $self->{'this_slot'};
723         my $unaliased = $self->{chg}->{unaliased}->{$slot};
724         delete $state->{slots}->{$unaliased}->{pid};
725         $finished_cb->();
726     });
727 }