| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package Algorithm::History::Levels; |
|
2
|
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
our $DATE = '2017-06-14'; # DATE |
|
4
|
|
|
|
|
|
|
our $VERSION = '0.001'; # VERSION |
|
5
|
|
|
|
|
|
|
|
|
6
|
1
|
|
|
1
|
|
48750
|
use 5.010001; |
|
|
1
|
|
|
|
|
4
|
|
|
7
|
1
|
|
|
1
|
|
5
|
use strict; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
22
|
|
|
8
|
1
|
|
|
1
|
|
4
|
use warnings; |
|
|
1
|
|
|
|
|
3
|
|
|
|
1
|
|
|
|
|
24
|
|
|
9
|
|
|
|
|
|
|
|
|
10
|
1
|
|
|
1
|
|
4
|
use Exporter qw(import); |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
757
|
|
|
11
|
|
|
|
|
|
|
our @EXPORT_OK = qw(group_histories_into_levels); |
|
12
|
|
|
|
|
|
|
|
|
13
|
|
|
|
|
|
|
our %SPEC; |
|
14
|
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
sub _pick_history { |
|
16
|
81
|
|
|
81
|
|
144
|
my ($histories, $min_time, $max_time) = @_; |
|
17
|
81
|
|
|
|
|
112
|
for my $i (0..$#{$histories}) { |
|
|
81
|
|
|
|
|
155
|
|
|
18
|
|
|
|
|
|
|
#say "D:$histories->[$i][1] between $min_time & $max_time?"; |
|
19
|
265
|
100
|
100
|
|
|
927
|
if ($histories->[$i][1] >= $min_time && |
|
20
|
|
|
|
|
|
|
$histories->[$i][1] <= $max_time) { |
|
21
|
54
|
|
|
|
|
122
|
return splice(@$histories, $i, 1); |
|
22
|
|
|
|
|
|
|
} |
|
23
|
|
|
|
|
|
|
} |
|
24
|
27
|
|
|
|
|
46
|
undef; |
|
25
|
|
|
|
|
|
|
} |
|
26
|
|
|
|
|
|
|
|
|
27
|
|
|
|
|
|
|
$SPEC{group_histories_into_levels} = { |
|
28
|
|
|
|
|
|
|
v => 1.1, |
|
29
|
|
|
|
|
|
|
summary => 'Group histories into levels', |
|
30
|
|
|
|
|
|
|
description => <<'_', |
|
31
|
|
|
|
|
|
|
|
|
32
|
|
|
|
|
|
|
This routine can group a single, linear histories into levels. This is be better |
|
33
|
|
|
|
|
|
|
explained by an example. Suppose you produce daily database backups. Your backup |
|
34
|
|
|
|
|
|
|
files are named: |
|
35
|
|
|
|
|
|
|
|
|
36
|
|
|
|
|
|
|
mydb.2017-06-13.sql.gz |
|
37
|
|
|
|
|
|
|
mydb.2017-06-12.sql.gz |
|
38
|
|
|
|
|
|
|
mydb.2017-06-11.sql.gz |
|
39
|
|
|
|
|
|
|
mydb.2017-06-10.sql.gz |
|
40
|
|
|
|
|
|
|
mydb.2017-06-09.sql.gz |
|
41
|
|
|
|
|
|
|
... |
|
42
|
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
After a while, your backups grow into tens and then hundreds of dump files. You |
|
44
|
|
|
|
|
|
|
typically want to keep certain number of backups only, for example: 7 daily |
|
45
|
|
|
|
|
|
|
backups, 4 weekly backups, 6 monthly backups (so you practically have 6 months |
|
46
|
|
|
|
|
|
|
of history but do not need to store 6*30 = 180 dumps, only 7 + 4 + 6 = 17). This |
|
47
|
|
|
|
|
|
|
is the routine you can use to select which files to keep and which to discard. |
|
48
|
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
You provide the list of histories either in the form of Unix timestamps: |
|
50
|
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
[1497286800, 1497200400, 1497114000, ...] |
|
52
|
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
or in the form of `[name, timestamp]` pairs, e.g.: |
|
54
|
|
|
|
|
|
|
|
|
55
|
|
|
|
|
|
|
[ |
|
56
|
|
|
|
|
|
|
['mydb.2017-06-13.sql.gz', 1497286800], |
|
57
|
|
|
|
|
|
|
['mydb.2017-06-12.sql.gz', 1497200400], |
|
58
|
|
|
|
|
|
|
['mydb.2017-06-11.sql.gz', 1497114000], |
|
59
|
|
|
|
|
|
|
... |
|
60
|
|
|
|
|
|
|
] |
|
61
|
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
Duplicates of timestamps are allowed, but duplicates of names are not allowed. |
|
63
|
|
|
|
|
|
|
If list of timestamps are given, the name is assumed to be the timestamp itself |
|
64
|
|
|
|
|
|
|
and there must not be duplicates. |
|
65
|
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
|
Then, you specify the levels with a list of `[period, num-in-this-level]` pairs. |
|
67
|
|
|
|
|
|
|
For example, 7 daily + 4 weekly + 6 monthly can be specified using: |
|
68
|
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
[ |
|
70
|
|
|
|
|
|
|
[86400, 7], |
|
71
|
|
|
|
|
|
|
[7*86400, 4], |
|
72
|
|
|
|
|
|
|
[30*86400, 6], |
|
73
|
|
|
|
|
|
|
] |
|
74
|
|
|
|
|
|
|
|
|
75
|
|
|
|
|
|
|
Subsequent level must have greater period than its previous. |
|
76
|
|
|
|
|
|
|
|
|
77
|
|
|
|
|
|
|
This routine will return a hash. The `levels` key will contain the history |
|
78
|
|
|
|
|
|
|
names, grouped into levels. The `discard` key will contain list of history names |
|
79
|
|
|
|
|
|
|
to discard: |
|
80
|
|
|
|
|
|
|
|
|
81
|
|
|
|
|
|
|
{ |
|
82
|
|
|
|
|
|
|
levels => [ |
|
83
|
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
# histories for the first level |
|
85
|
|
|
|
|
|
|
['mydb.2017-06-13.sql.gz', |
|
86
|
|
|
|
|
|
|
'mydb.2017-06-12.sql.gz', |
|
87
|
|
|
|
|
|
|
'mydb.2017-06-11.sql.gz', |
|
88
|
|
|
|
|
|
|
'mydb.2017-06-10.sql.gz', |
|
89
|
|
|
|
|
|
|
'mydb.2017-06-09.sql.gz', |
|
90
|
|
|
|
|
|
|
'mydb.2017-06-08.sql.gz', |
|
91
|
|
|
|
|
|
|
'mydb.2017-06-07.sql.gz'], |
|
92
|
|
|
|
|
|
|
|
|
93
|
|
|
|
|
|
|
# histories for the second level |
|
94
|
|
|
|
|
|
|
['mydb.2017-06-06.sql.gz', |
|
95
|
|
|
|
|
|
|
'mydb.2017-05-30.sql.gz', |
|
96
|
|
|
|
|
|
|
'mydb.2017-05-23.sql.gz', |
|
97
|
|
|
|
|
|
|
'mydb.2017-05-16.sql.gz'], |
|
98
|
|
|
|
|
|
|
|
|
99
|
|
|
|
|
|
|
# histories for the third level |
|
100
|
|
|
|
|
|
|
['mydb.2017-06-05.sql.gz', |
|
101
|
|
|
|
|
|
|
'mydb.2017-05-06.sql.gz', |
|
102
|
|
|
|
|
|
|
'mydb.2017-04-06.sql.gz', |
|
103
|
|
|
|
|
|
|
...], |
|
104
|
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
discard => [ |
|
106
|
|
|
|
|
|
|
'mydb.2017-06-04.sql.gz', |
|
107
|
|
|
|
|
|
|
'mydb.2017-06-03.sql.gz', |
|
108
|
|
|
|
|
|
|
... |
|
109
|
|
|
|
|
|
|
], |
|
110
|
|
|
|
|
|
|
} |
|
111
|
|
|
|
|
|
|
|
|
112
|
|
|
|
|
|
|
_ |
|
113
|
|
|
|
|
|
|
args => { |
|
114
|
|
|
|
|
|
|
histories => { |
|
115
|
|
|
|
|
|
|
schema => ['array*', { |
|
116
|
|
|
|
|
|
|
of=>['any*', { |
|
117
|
|
|
|
|
|
|
of=>[ |
|
118
|
|
|
|
|
|
|
'int*', |
|
119
|
|
|
|
|
|
|
['array*', elems=>['str*', 'float*']], |
|
120
|
|
|
|
|
|
|
], |
|
121
|
|
|
|
|
|
|
}], |
|
122
|
|
|
|
|
|
|
}], |
|
123
|
|
|
|
|
|
|
req => 1, |
|
124
|
|
|
|
|
|
|
}, |
|
125
|
|
|
|
|
|
|
levels => { |
|
126
|
|
|
|
|
|
|
schema => ['array*', { |
|
127
|
|
|
|
|
|
|
of => ['array*', elems => ['float*', 'posint*']], |
|
128
|
|
|
|
|
|
|
min_len => 1, |
|
129
|
|
|
|
|
|
|
}], |
|
130
|
|
|
|
|
|
|
req => 1, |
|
131
|
|
|
|
|
|
|
}, |
|
132
|
|
|
|
|
|
|
now => { |
|
133
|
|
|
|
|
|
|
schema => 'int*', |
|
134
|
|
|
|
|
|
|
}, |
|
135
|
|
|
|
|
|
|
discard_old_histories => { |
|
136
|
|
|
|
|
|
|
schema => ['bool*'], |
|
137
|
|
|
|
|
|
|
default => 0, |
|
138
|
|
|
|
|
|
|
}, |
|
139
|
|
|
|
|
|
|
discard_young_histories => { |
|
140
|
|
|
|
|
|
|
schema => ['bool*'], |
|
141
|
|
|
|
|
|
|
default => 0, |
|
142
|
|
|
|
|
|
|
}, |
|
143
|
|
|
|
|
|
|
}, |
|
144
|
|
|
|
|
|
|
result_naked => 1, |
|
145
|
|
|
|
|
|
|
}; |
|
146
|
|
|
|
|
|
|
sub group_histories_into_levels { |
|
147
|
13
|
|
|
13
|
1
|
7317
|
require Array::Sample::Partition; |
|
148
|
|
|
|
|
|
|
|
|
149
|
13
|
|
|
|
|
269
|
my %args = @_; |
|
150
|
|
|
|
|
|
|
|
|
151
|
13
|
|
33
|
|
|
69
|
my $now = $args{now} // time(); |
|
152
|
|
|
|
|
|
|
|
|
153
|
13
|
100
|
|
|
|
47
|
my $histories0 = $args{histories} or die "Please specify histories"; |
|
154
|
12
|
|
|
|
|
20
|
my @histories; |
|
155
|
|
|
|
|
|
|
{ |
|
156
|
12
|
|
|
|
|
21
|
my %seen; |
|
|
12
|
|
|
|
|
17
|
|
|
157
|
12
|
|
|
|
|
31
|
for my $h (@$histories0) { |
|
158
|
103
|
|
|
|
|
154
|
my ($name, $time); |
|
159
|
103
|
100
|
|
|
|
210
|
if (ref $h eq 'ARRAY') { |
|
160
|
84
|
|
|
|
|
152
|
($name, $time) = @$h; |
|
161
|
|
|
|
|
|
|
} else { |
|
162
|
19
|
|
|
|
|
35
|
$name = $h; |
|
163
|
19
|
|
|
|
|
27
|
$time = $h; |
|
164
|
|
|
|
|
|
|
} |
|
165
|
103
|
100
|
|
|
|
278
|
$seen{$name}++ and die "Duplicate history name '$name'"; |
|
166
|
101
|
|
|
|
|
236
|
push @histories, [$name, $time]; |
|
167
|
|
|
|
|
|
|
} |
|
168
|
|
|
|
|
|
|
} |
|
169
|
|
|
|
|
|
|
|
|
170
|
10
|
100
|
|
|
|
35
|
my $levels = $args{levels} or die "Please specify levels"; |
|
171
|
9
|
100
|
|
|
|
32
|
@$levels > 0 or die "Please specify at least one level"; |
|
172
|
8
|
|
|
|
|
13
|
my $i = 0; |
|
173
|
8
|
|
|
|
|
15
|
my $min_period; |
|
174
|
8
|
|
|
|
|
15
|
for my $l (@$levels) { |
|
175
|
20
|
50
|
|
|
|
46
|
ref($l) eq 'ARRAY' or die "Level #$i: not an array"; |
|
176
|
20
|
100
|
|
|
|
52
|
@$l == 2 or die "Level #$i: not a 2-element array"; |
|
177
|
19
|
50
|
|
|
|
41
|
$l->[0] > 0 or die "Level #$i: period must be a positive number"; |
|
178
|
19
|
50
|
|
|
|
40
|
$l->[1] >= 1 or die "Level #$i: number of items must be at least 1"; |
|
179
|
19
|
100
|
|
|
|
44
|
if (defined $min_period) { |
|
180
|
12
|
100
|
|
|
|
37
|
$l->[0] > $min_period or die "Level #$i: period must be larger than previous ($min_period)"; |
|
181
|
|
|
|
|
|
|
} |
|
182
|
18
|
|
|
|
|
27
|
$min_period = $l->[0]; |
|
183
|
18
|
|
|
|
|
32
|
$i++; |
|
184
|
|
|
|
|
|
|
} |
|
185
|
|
|
|
|
|
|
|
|
186
|
|
|
|
|
|
|
# first, we sort the histories by timestamp (newer first) |
|
187
|
6
|
|
|
|
|
28
|
@histories = sort { $b->[1] <=> $a->[1] } @histories; |
|
|
207
|
|
|
|
|
296
|
|
|
188
|
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
my $res = { |
|
190
|
6
|
|
|
|
|
14
|
levels => [ map {[]} @$levels], |
|
|
17
|
|
|
|
|
43
|
|
|
191
|
|
|
|
|
|
|
discard => [], |
|
192
|
|
|
|
|
|
|
}; |
|
193
|
|
|
|
|
|
|
|
|
194
|
|
|
|
|
|
|
LEVEL: |
|
195
|
6
|
|
|
|
|
14
|
for my $l (0..$#{$levels}) { |
|
|
6
|
|
|
|
|
16
|
|
|
196
|
17
|
|
|
|
|
27
|
my ($period, $num_per_level) = @{ $levels->[$l] }; |
|
|
17
|
|
|
|
|
38
|
|
|
197
|
|
|
|
|
|
|
|
|
198
|
|
|
|
|
|
|
# first, fill the level with histories that fit the time frame for each |
|
199
|
|
|
|
|
|
|
# level's slot |
|
200
|
17
|
|
|
|
|
38
|
for my $slot (0..$num_per_level-1) { |
|
201
|
81
|
|
|
|
|
135
|
my $min_time = $now-($slot+1)*$period; |
|
202
|
81
|
|
|
|
|
113
|
my $max_time = $now-($slot )*$period; |
|
203
|
81
|
100
|
|
|
|
166
|
if ($l > 0) { |
|
204
|
39
|
|
|
|
|
60
|
my ($lower_period, $lower_num_per_level) = @{ $levels->[$l-1] }; |
|
|
39
|
|
|
|
|
66
|
|
|
205
|
39
|
|
|
|
|
59
|
$min_time -= $lower_num_per_level*$lower_period; |
|
206
|
39
|
|
|
|
|
55
|
$max_time -= $lower_num_per_level*$lower_period; |
|
207
|
|
|
|
|
|
|
} |
|
208
|
81
|
|
|
|
|
159
|
my $h = _pick_history(\@histories, $min_time, $max_time); |
|
209
|
81
|
100
|
|
|
|
178
|
push @{ $res->{levels}[$l] }, $h if $h; |
|
|
54
|
|
|
|
|
120
|
|
|
210
|
|
|
|
|
|
|
} |
|
211
|
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
# if the level is not fully filled yet, fill it with young or old |
|
213
|
|
|
|
|
|
|
# histories |
|
214
|
17
|
|
|
|
|
25
|
my $num_filled = @{ $res->{levels}[$l] }; |
|
|
17
|
|
|
|
|
32
|
|
|
215
|
|
|
|
|
|
|
#say "D:level=$l, num_filled=$num_filled"; |
|
216
|
17
|
100
|
|
|
|
54
|
unless ($num_filled >= $num_per_level) { |
|
217
|
11
|
|
|
|
|
25
|
my @filler = @histories; |
|
218
|
11
|
100
|
100
|
|
|
43
|
if ($args{discard_young_histories} // 0) { |
|
219
|
4
|
|
|
|
|
6
|
my $time = $now-$num_per_level*$period; |
|
220
|
4
|
50
|
|
|
|
12
|
if ($l > 0) { |
|
221
|
|
|
|
|
|
|
my ($lower_period, $lower_num_per_level) = |
|
222
|
4
|
|
|
|
|
7
|
@{ $levels->[$l-1] }; |
|
|
4
|
|
|
|
|
7
|
|
|
223
|
4
|
|
|
|
|
9
|
$time -= $lower_num_per_level*$lower_period; |
|
224
|
|
|
|
|
|
|
} |
|
225
|
4
|
|
|
|
|
7
|
@filler = grep { $_->[1] <= $time } |
|
|
30
|
|
|
|
|
55
|
|
|
226
|
|
|
|
|
|
|
@filler; |
|
227
|
|
|
|
|
|
|
} |
|
228
|
11
|
100
|
100
|
|
|
37
|
if ($args{discard_old_histories} // 0) { |
|
229
|
4
|
|
|
|
|
8
|
my $time = $now-$num_per_level*$period; |
|
230
|
4
|
50
|
|
|
|
10
|
if ($l > 0) { |
|
231
|
|
|
|
|
|
|
my ($lower_period, $lower_num_per_level) = |
|
232
|
4
|
|
|
|
|
5
|
@{ $levels->[$l-1] }; |
|
|
4
|
|
|
|
|
9
|
|
|
233
|
4
|
|
|
|
|
7
|
$time -= $lower_num_per_level*$lower_period; |
|
234
|
|
|
|
|
|
|
} |
|
235
|
4
|
|
|
|
|
7
|
@filler = grep { $_->[1] >= $time } |
|
|
16
|
|
|
|
|
30
|
|
|
236
|
|
|
|
|
|
|
@filler; |
|
237
|
|
|
|
|
|
|
} |
|
238
|
11
|
|
|
|
|
39
|
my @sample = Array::Sample::Partition::sample_partition( |
|
239
|
|
|
|
|
|
|
\@filler, $num_per_level - $num_filled); |
|
240
|
|
|
|
|
|
|
$res->{levels}[$l] = [ |
|
241
|
31
|
|
|
|
|
56
|
sort { $b->[1] <=> $a->[1] } |
|
242
|
11
|
|
|
|
|
190
|
(@{ $res->{levels}[$l] }, @sample), |
|
|
11
|
|
|
|
|
29
|
|
|
243
|
|
|
|
|
|
|
]; |
|
244
|
11
|
|
|
|
|
30
|
for my $i (reverse 0..$#histories) { |
|
245
|
73
|
|
|
|
|
126
|
for my $j (0..$#sample) { |
|
246
|
88
|
100
|
|
|
|
226
|
if ($histories[$i] eq $sample[$j]) { |
|
247
|
17
|
|
|
|
|
26
|
splice @histories, $i, 1; |
|
248
|
17
|
|
|
|
|
33
|
last; |
|
249
|
|
|
|
|
|
|
} |
|
250
|
|
|
|
|
|
|
} |
|
251
|
|
|
|
|
|
|
} |
|
252
|
|
|
|
|
|
|
} |
|
253
|
|
|
|
|
|
|
|
|
254
|
|
|
|
|
|
|
# only return names |
|
255
|
17
|
|
|
|
|
32
|
$res->{levels}[$l] = [ map {$_->[0]} @{ $res->{levels}[$l] } ]; |
|
|
71
|
|
|
|
|
196
|
|
|
|
17
|
|
|
|
|
37
|
|
|
256
|
|
|
|
|
|
|
} |
|
257
|
|
|
|
|
|
|
|
|
258
|
6
|
|
|
|
|
15
|
push @{ $res->{discard} }, $_->[0] for @histories; |
|
|
26
|
|
|
|
|
55
|
|
|
259
|
|
|
|
|
|
|
|
|
260
|
6
|
|
|
|
|
55
|
END: |
|
261
|
|
|
|
|
|
|
$res; |
|
262
|
|
|
|
|
|
|
} |
|
263
|
|
|
|
|
|
|
|
|
264
|
|
|
|
|
|
|
1; |
|
265
|
|
|
|
|
|
|
# ABSTRACT: Group histories into levels |
|
266
|
|
|
|
|
|
|
|
|
267
|
|
|
|
|
|
|
__END__ |