File Coverage

blib/lib/Retry/Policy.pm
Criterion Covered Total %
statement 60 65 92.3
branch 31 44 70.4
condition 4 9 44.4
subroutine 12 12 100.0
pod 0 4 0.0
total 107 134 79.8


line stmt bran cond sub pod time code
1             package Retry::Policy;
2              
3 4     4   981402 use strict;
  4         10  
  4         143  
4 4     4   36 use warnings;
  4         7  
  4         217  
5              
6 4     4   69 use 5.030000;
  4         13  
7              
8              
9             our $VERSION = '0.02';
10              
11 4     4   24 use Time::HiRes qw(usleep);
  4         14  
  4         38  
12 4     4   2654 use Try::Tiny qw(try catch);
  4         9226  
  4         3663  
13              
14             sub new {
15 5     5 0 716838 my ($class, %args) = @_;
16              
17             my $self = bless {
18             max_attempts => defined $args{max_attempts} ? $args{max_attempts} : 5,
19             base_delay_ms => defined $args{base_delay_ms} ? $args{base_delay_ms} : 100,
20             max_delay_ms => defined $args{max_delay_ms} ? $args{max_delay_ms} : 10_000,
21             jitter => defined $args{jitter} ? $args{jitter} : 'full', # none|full
22             strategy => defined $args{strategy} ? $args{strategy} : 'exponential',
23             retry_on => $args{retry_on}, # optional coderef
24             on_retry => $args{on_retry}, # optional coderef
25 5 100       95 }, $class;
    100          
    100          
    50          
    50          
26              
27 5         24 _validate($self);
28 3         10 return $self;
29             }
30              
31             sub run {
32 2     2 0 36 my ($self, $code) = @_;
33 2 50       7 die "run() requires a coderef\n" if ref($code) ne 'CODE';
34              
35 2         1056 my $attempt = 0;
36 2         4 my $last_err;
37              
38 2         9 while ($attempt < $self->{max_attempts}) {
39 4         13 $attempt++;
40              
41 4         9 my ($ok, $result);
42             try {
43 4     4   232 $result = $code->($attempt);
44 1         9 $ok = 1;
45             }
46             catch {
47 3     3   67 $last_err = $_;
48 3         13 $ok = 0;
49 4         60 };
50              
51 4 100       78 return $result if $ok;
52              
53 3 50       9 last if $attempt >= $self->{max_attempts};
54 3 100       15 last if !$self->should_retry($last_err, $attempt);
55              
56 2         7 my $delay_ms = $self->delay_ms($attempt);
57              
58 2 50       7 if (ref($self->{on_retry}) eq 'CODE') {
59 0         0 $self->{on_retry}->(
60             attempt => $attempt,
61             error => "$last_err",
62             delay_ms => $delay_ms,
63             );
64             }
65              
66 2         8208 usleep($delay_ms * 1000);
67             }
68              
69 1         9 die $last_err;
70             }
71              
72             sub should_retry {
73 3     3 0 11 my ($self, $err, $attempt) = @_;
74              
75 3 100       9 if (ref($self->{retry_on}) eq 'CODE') {
76 1 50       4 return $self->{retry_on}->($err, $attempt) ? 1 : 0;
77             }
78              
79 2         32 return 1; # default: retry on any exception
80             }
81              
82             sub delay_ms {
83 7     7 0 19 my ($self, $attempt) = @_;
84              
85 7         17 my $base = $self->{base_delay_ms};
86 7         11 my $max = $self->{max_delay_ms};
87              
88 7         11 my $raw;
89 7 50       16 if ($self->{strategy} eq 'exponential') {
90 7         19 $raw = $base * (2 ** ($attempt - 1));
91             } else {
92 0         0 die "Unsupported strategy: $self->{strategy}\n";
93             }
94              
95 7 100       19 $raw = $max if $raw > $max;
96              
97 7 50       17 if ($self->{jitter} eq 'none') {
98 7         33 return int($raw);
99             }
100 0 0       0 if ($self->{jitter} eq 'full') {
101 0         0 return int(rand($raw + 1));
102             }
103              
104 0         0 die "Unsupported jitter: $self->{jitter}\n";
105             }
106              
107             sub _validate {
108 5     5   12 my ($self) = @_;
109              
110 5         22 for my $k (qw(max_attempts base_delay_ms max_delay_ms)) {
111             die "$k must be a positive integer\n"
112 15 50 33     184 if !defined($self->{$k}) || $self->{$k} !~ /^\d+$/ || $self->{$k} <= 0;
      33        
113             }
114              
115             die "max_delay_ms must be >= base_delay_ms\n"
116 5 100       40 if $self->{max_delay_ms} < $self->{base_delay_ms};
117              
118             die "jitter must be 'none' or 'full'\n"
119 4 100 66     31 if $self->{jitter} ne 'none' && $self->{jitter} ne 'full';
120              
121             die "strategy must be 'exponential'\n"
122 3 50       13 if $self->{strategy} ne 'exponential';
123              
124 3         7 for my $k (qw(retry_on on_retry)) {
125 6 100       16 next if !defined $self->{$k};
126 1 50       5 die "$k must be a coderef\n" if ref($self->{$k}) ne 'CODE';
127             }
128              
129 3         6 return 1;
130             }
131              
132             1;
133              
134             __END__