LP#1895052: Avoid foreign targets when local items exist
authorMike Rylander <mrylander@gmail.com>
Mon, 28 Jun 2021 19:06:26 +0000 (15:06 -0400)
committerGalen Charlton <gmc@equinoxOLI.org>
Fri, 13 Aug 2021 22:02:22 +0000 (18:02 -0400)
This commit adds a new YAOUS that allows a pickup library to specify
that it does not want its holds to have foreign (prox > 0) copies
directly targeted if there is a local copy in an available status (on
the shelf).  The setting is an interval, and after the age of the hold
has passed that interval, foreign direct targetting is allowed.

This does not change the calculation of the potential list, so
op-capture will be availalbe (all else being equal) without
retargetting.

This setting (circ.pickup_hold_stalling.hard) is meant to be used in
concert with the other new setting in the parent commit
(circ.pickup_hold_stalling.soft), and should generally have a value the
same or smaller than the soft setting.  Doing this allows tiered
targetting, where no remote items are targeted via the hard setting for,
say, 3 days, where all capture is restricted to only the pickup, and
then, with a soft setting of 5 days, the next 2 days allow only direct
target capture of foreign copies.  After 5 days, normal, global
targetting and op-capture resumes.

An alternative use for this setting is to ignore the parent-commit soft
setting and allow op-capture everywhere, but only direct targetting at
the pickup library.  The effect of this, if used globally throughout an
entire Evergreen instance, would be that the pull list would only
represent pickup-local holds, but serendipitous scans of items that
could fill remote holds could capture for transit.

Signed-off-by: Mike Rylander <mrylander@gmail.com>
Signed-off-by: Jason Stephenson <jason@sigio.com>
Signed-off-by: John Amundson <jamundson@cwmars.org>
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
Open-ILS/src/perlmods/lib/OpenILS/Utils/HoldTargeter.pm
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.data.stalling-YAOUS.sql

index 56e9c84..31b8852 100644 (file)
@@ -298,6 +298,34 @@ sub hold {
     return $self->{hold};
 }
 
+sub inside_hard_stall_interval {
+    my ($self) = @_;
+    if (defined $self->{inside_hard_stall_interval}) {
+        $self->log_hold('already looked up hard stalling state: '.$self->{inside_hard_stall_interval});
+        return $self->{inside_hard_stall_interval};
+    }
+
+    my $hard_stall_interval =
+        $self->parent->get_ou_setting(
+            $self->hold->pickup_lib, 'circ.pickup_hold_stalling.hard', $self->editor) || '0 seconds';
+
+    $self->log_hold('hard stalling interval '.$hard_stall_interval);
+
+    my $hold_request_time = $dt_parser->parse_datetime(clean_ISO8601($self->hold->request_time));
+    my $hard_stall_time = $hold_request_time->clone->add(
+        seconds => OpenILS::Utils::DateTime->interval_to_seconds($hard_stall_interval)
+    );
+
+    if (DateTime->compare($hard_stall_time, DateTime->now(time_zone => 'local')) > 0) {
+        $self->{inside_hard_stall_interval} = 1
+    } else {
+        $self->{inside_hard_stall_interval} = 0
+    }
+
+    $self->log_hold('hard stalling state: '.$self->{inside_hard_stall_interval});
+    return $self->{inside_hard_stall_interval};
+}
+
 # Debug message
 sub message {
     my ($self, $message) = @_;
@@ -355,6 +383,12 @@ sub recall_copies {
     return $self->{recall_copies};
 }
 
+sub in_use_copies {
+    my ($self, $in_use_copies) = @_;
+    $self->{in_use_copies} = $in_use_copies if $in_use_copies;
+    return $self->{in_use_copies};
+}
+
 # Maps copy ID's to their hold proximity
 sub copy_prox_map {
     my ($self, $copy_prox_map) = @_;
@@ -720,6 +754,7 @@ sub compile_weighted_proximity_map {
     my %prox_map;
     for my $copy_hash (@{$self->copies}) {
         my $prox = $copy_prox_map{$copy_hash->{id}};
+        $copy_hash->{proximity} = $prox;
         $prox_map{$prox} ||= [];
 
         my $weight = $self->parent->get_ou_setting(
@@ -730,6 +765,20 @@ sub compile_weighted_proximity_map {
         push(@{$prox_map{$prox}}, $copy_hash) foreach (1 .. $weight);
     }
 
+    # We need to grab the proximity for copies targeted by other holds
+    # that belong to this pickup lib for hard-stalling tests later. We'll
+    # just grab them all in case it's useful later.
+    for my $copy_hash (@{$self->in_use_copies}) {
+        my $prox = $copy_prox_map{$copy_hash->{id}};
+        $copy_hash->{proximity} = $prox;
+    }
+
+    # We also need the proximity for the previous target.
+    if ($self->{valid_previous_copy}) {
+        my $prox = $copy_prox_map{$self->{valid_previous_copy}->{id}};
+        $self->{valid_previous_copy}->{proximity} = $prox;
+    }
+
     return $self->{weighted_prox_map} = \%prox_map;
 }
 
@@ -806,6 +855,10 @@ sub filter_copies_by_status {
 sub filter_copies_in_use {
     my $self = shift;
 
+    # Copies that are targeted, but could contribute to pickup lib
+    # hard (foreign) stalling.  These are Available-status copies.
+    $self->in_use_copies([grep {$_->{current_copy}} @{$self->copies}]);
+
     # A copy with a 'current_copy' value means it's in use by another hold.
     $self->copies([
         grep {!$_->{current_copy}} @{$self->copies}
@@ -920,7 +973,7 @@ sub attempt_force_recall_target {
 sub attempt_to_find_copy {
     my $self = shift;
 
-    return undef unless @{$self->copies};
+    $self->log_hold("attempting to find a copy normally");
 
     my $max_loops = $self->parent->get_ou_setting(
         $self->hold->pickup_lib,
@@ -1102,13 +1155,40 @@ sub find_nearest_copy {
     my $hold = $self->hold;
     my %seen;
 
+    # See if there are in-use (targeted) copies "here".
+    my $have_local_copies = 0;
+    if ($self->inside_hard_stall_interval) { # But only if we're inside the hard age.
+        if (grep { $_->{proximity} <= 0 } @{$self->in_use_copies}) {
+            $have_local_copies = 1;
+        }
+        $self->log_hold("inside hard stall interval and does ".
+            ($have_local_copies ? "" : "not "). "have in-use local copies");
+    }
+
     # Pick a copy at random from each tier of the proximity map,
     # starting at the lowest proximity and working up, until a
     # copy is found that is suitable for targeting.
+    my $no_copies = 1;
     for my $prox (sort {$a <=> $b} keys %prox_map) {
         my @copies = @{$prox_map{$prox}};
         next unless @copies;
 
+        $no_copies = 0;
+        $have_local_copies = 1 if ($prox <= 0);
+
+        $self->log_hold("inside hard stall interval and does ".
+            ($have_local_copies ? "" : "not "). "have testable local copies")
+                if ($self->inside_hard_stall_interval && $prox > 0);
+
+        if ($have_local_copies and $self->inside_hard_stall_interval) {
+            # Unset valid_previous_copy if it's not local and we have local copies now
+            $self->{valid_previous_copy} = undef if (
+                $self->{valid_previous_copy}
+                and $self->{valid_previous_copy}->{proximity} > 0
+            );
+            last if ($prox > 0); # No point in looking further "out".
+        }
+
         my $rand = int(rand(scalar(@copies)));
 
         while (my ($c) = splice(@copies, $rand, 1)) {
@@ -1122,6 +1202,14 @@ sub find_nearest_copy {
         }
     }
 
+    if ($no_copies and $have_local_copies and $self->inside_hard_stall_interval) {
+        # Unset valid_previous_copy if it's not local and we have local copies now
+        $self->{valid_previous_copy} = undef if (
+            $self->{valid_previous_copy}
+            and $self->{valid_previous_copy}->{proximity} > 0
+        );
+    }
+
     return undef;
 }
 
index 397cfa3..791ba23 100644 (file)
@@ -3607,10 +3607,19 @@ INSERT into config.org_unit_setting_type
         'Pickup Library Soft stalling interval',
         'coust', 'label'),
     oils_i18n_gettext('circ.pickup_hold_stalling.soft',
-        'When set for the pickup library, this specifies that only items scanned at the pickup library can be opportunistically captured for this time period.  Example "5 days".  This setting takes precedence over "Soft stalling interval" (circ.hold_stalling.soft).',
+        'When set for the pickup library, this specifies that for holds with a request time age smaller than this interval only items scanned at the pickup library can be opportunistically captured. Example "5 days". This setting takes precedence over "Soft stalling interval" (circ.hold_stalling.soft) when the interval is in force.',
         'coust', 'description'),
     'interval', null)
 
+,( 'circ.pickup_hold_stalling.hard', 'holds',
+  oils_i18n_gettext('circ.pickup_hold_stalling.hard',
+        'Pickup Library Hard stalling interval',
+        'coust','label'),
+  oils_i18n_gettext('circ.pickup_hold_stalling.hard',
+        'When set for the pickup library, this specifies that no items with a calculated proximity greater than 0 from the pickup library can be directly targeted for this time period if there are local available copies.  Example "3 days".',
+        'coust','description'),
+  'interval', null)
+
 ,( 'circ.hold_stalling_hard', 'holds',
     oils_i18n_gettext('circ.hold_stalling_hard',
         'Hard stalling interval',
index 4ddcafc..32f6439 100644 (file)
@@ -11,7 +11,17 @@ INSERT into config.org_unit_setting_type
 ( 'circ.pickup_hold_stalling.soft',
   'holds',
   'Pickup Library Soft stalling interval',
-  'When set for the pickup library, this specifies that only items scanned at the pickup library can be opportunistically captured for this time period.  Example "5 days".  This setting takes precedence over "Soft stalling interval" (circ.hold_stalling.soft).',
+  'When set for the pickup library, this specifies that for holds with a request time age smaller than this interval only items scanned at the pickup library can be opportunistically captured. Example "5 days". This setting takes precedence over "Soft stalling interval" (circ.hold_stalling.soft) when the interval is in force.',
+  'interval',
+  null
+);
+
+INSERT into config.org_unit_setting_type
+( name, grp, label, description, datatype, fm_class ) VALUES
+( 'circ.pickup_hold_stalling.hard',
+  'holds',
+  'Pickup Library Hard stalling interval',
+  'When set for the pickup library, this specifies that no items with a calculated proximity greater than 0 from the pickup library can be directly targeted for this time period if there are local available copies.  Example "3 days".',
   'interval',
   null
 );