6
6
7
7
package org .elasticsearch .xpack .core .slm ;
8
8
9
+ import org .apache .logging .log4j .LogManager ;
10
+ import org .apache .logging .log4j .Logger ;
9
11
import org .elasticsearch .common .Nullable ;
10
12
import org .elasticsearch .common .ParseField ;
11
13
import org .elasticsearch .common .Strings ;
20
22
import org .elasticsearch .snapshots .SnapshotInfo ;
21
23
22
24
import java .io .IOException ;
25
+ import java .util .Comparator ;
23
26
import java .util .List ;
24
27
import java .util .Objects ;
28
+ import java .util .Set ;
29
+ import java .util .function .LongSupplier ;
25
30
import java .util .function .Predicate ;
31
+ import java .util .stream .Collectors ;
26
32
27
33
public class SnapshotRetentionConfiguration implements ToXContentObject , Writeable {
28
34
29
- public static final SnapshotRetentionConfiguration EMPTY = new SnapshotRetentionConfiguration (( TimeValue ) null );
35
+ public static final SnapshotRetentionConfiguration EMPTY = new SnapshotRetentionConfiguration (null , null , null );
30
36
31
37
private static final ParseField EXPIRE_AFTER = new ParseField ("expire_after" );
38
+ private static final ParseField MINIMUM_SNAPSHOT_COUNT = new ParseField ("min_count" );
39
+ private static final ParseField MAXIMUM_SNAPSHOT_COUNT = new ParseField ("max_count" );
40
+ private static final Logger logger = LogManager .getLogger (SnapshotRetentionConfiguration .class );
32
41
33
42
private static final ConstructingObjectParser <SnapshotRetentionConfiguration , Void > PARSER =
34
43
new ConstructingObjectParser <>("snapshot_retention" , true , a -> {
35
44
TimeValue expireAfter = a [0 ] == null ? null : TimeValue .parseTimeValue ((String ) a [0 ], EXPIRE_AFTER .getPreferredName ());
36
- return new SnapshotRetentionConfiguration (expireAfter );
45
+ Integer minCount = (Integer ) a [1 ];
46
+ Integer maxCount = (Integer ) a [2 ];
47
+ return new SnapshotRetentionConfiguration (expireAfter , minCount , maxCount );
37
48
});
38
49
39
50
static {
40
51
PARSER .declareString (ConstructingObjectParser .optionalConstructorArg (), EXPIRE_AFTER );
52
+ PARSER .declareInt (ConstructingObjectParser .optionalConstructorArg (), MINIMUM_SNAPSHOT_COUNT );
53
+ PARSER .declareInt (ConstructingObjectParser .optionalConstructorArg (), MAXIMUM_SNAPSHOT_COUNT );
41
54
}
42
55
43
- // TODO: add the rest of the configuration values
56
+ private final LongSupplier nowSupplier ;
44
57
private final TimeValue expireAfter ;
45
-
46
- public SnapshotRetentionConfiguration (@ Nullable TimeValue expireAfter ) {
47
- this .expireAfter = expireAfter ;
48
- }
58
+ private final Integer minimumSnapshotCount ;
59
+ private final Integer maximumSnapshotCount ;
49
60
50
61
SnapshotRetentionConfiguration (StreamInput in ) throws IOException {
62
+ nowSupplier = System ::currentTimeMillis ;
51
63
this .expireAfter = in .readOptionalTimeValue ();
64
+ this .minimumSnapshotCount = in .readOptionalVInt ();
65
+ this .maximumSnapshotCount = in .readOptionalVInt ();
66
+ }
67
+
68
+ public SnapshotRetentionConfiguration (@ Nullable TimeValue expireAfter ,
69
+ @ Nullable Integer minimumSnapshotCount ,
70
+ @ Nullable Integer maximumSnapshotCount ) {
71
+ this (System ::currentTimeMillis , expireAfter , minimumSnapshotCount , maximumSnapshotCount );
72
+ }
73
+
74
+ public SnapshotRetentionConfiguration (LongSupplier nowSupplier ,
75
+ @ Nullable TimeValue expireAfter ,
76
+ @ Nullable Integer minimumSnapshotCount ,
77
+ @ Nullable Integer maximumSnapshotCount ) {
78
+ this .nowSupplier = nowSupplier ;
79
+ this .expireAfter = expireAfter ;
80
+ this .minimumSnapshotCount = minimumSnapshotCount ;
81
+ this .maximumSnapshotCount = maximumSnapshotCount ;
82
+ if (this .minimumSnapshotCount != null && this .minimumSnapshotCount < 1 ) {
83
+ throw new IllegalArgumentException ("minimum snapshot count must be at least 1, but was: " + this .minimumSnapshotCount );
84
+ }
85
+ if (this .maximumSnapshotCount != null && this .maximumSnapshotCount < 1 ) {
86
+ throw new IllegalArgumentException ("maximum snapshot count must be at least 1, but was: " + this .maximumSnapshotCount );
87
+ }
88
+ if ((maximumSnapshotCount != null && minimumSnapshotCount != null ) && this .minimumSnapshotCount > this .maximumSnapshotCount ) {
89
+ throw new IllegalArgumentException ("minimum snapshot count " + this .minimumSnapshotCount +
90
+ " cannot be larger than maximum snapshot count " + this .maximumSnapshotCount );
91
+ }
52
92
}
53
93
54
94
public static SnapshotRetentionConfiguration parse (XContentParser parser , String name ) {
@@ -59,44 +99,129 @@ public TimeValue getExpireAfter() {
59
99
return this .expireAfter ;
60
100
}
61
101
102
+ public Integer getMinimumSnapshotCount () {
103
+ return this .minimumSnapshotCount ;
104
+ }
105
+
106
+ public Integer getMaximumSnapshotCount () {
107
+ return this .maximumSnapshotCount ;
108
+ }
109
+
62
110
/**
63
111
* Return a predicate by which a SnapshotInfo can be tested to see
64
112
* whether it should be deleted according to this retention policy.
65
113
* @param allSnapshots a list of all snapshot pertaining to this SLM policy and repository
66
114
*/
67
115
public Predicate <SnapshotInfo > getSnapshotDeletionPredicate (final List <SnapshotInfo > allSnapshots ) {
116
+ final int snapCount = allSnapshots .size ();
117
+ List <SnapshotInfo > sortedSnapshots = allSnapshots .stream ()
118
+ .sorted (Comparator .comparingLong (SnapshotInfo ::startTime ))
119
+ .collect (Collectors .toList ());
120
+
68
121
return si -> {
122
+ final String snapName = si .snapshotId ().getName ();
123
+
124
+ // First, enforce the maximum count, if the size is over the maximum number of
125
+ // snapshots, then allow the oldest N (where N is the number over the maximum snapshot
126
+ // count) snapshots to be eligible for deletion
127
+ if (this .maximumSnapshotCount != null ) {
128
+ if (allSnapshots .size () > this .maximumSnapshotCount ) {
129
+ int snapsToDelete = allSnapshots .size () - this .maximumSnapshotCount ;
130
+ boolean eligible = sortedSnapshots .stream ()
131
+ .limit (snapsToDelete )
132
+ .anyMatch (s -> s .equals (si ));
133
+
134
+ if (eligible ) {
135
+ logger .trace ("[{}]: ELIGIBLE as it is one of the {} oldest snapshots with " +
136
+ "{} total snapshots, over the limit of {} maximum snapshots" ,
137
+ snapName , snapsToDelete , snapCount , this .maximumSnapshotCount );
138
+ return true ;
139
+ } else {
140
+ logger .trace ("[{}]: INELIGIBLE as it is not one of the {} oldest snapshots with " +
141
+ "{} total snapshots, over the limit of {} maximum snapshots" ,
142
+ snapName , snapsToDelete , snapCount , this .maximumSnapshotCount );
143
+ return false ;
144
+ }
145
+ }
146
+ }
147
+
148
+ // Next check the minimum count, since that is a blanket requirement regardless of time,
149
+ // if we haven't hit the minimum then we need to keep the snapshot regardless of
150
+ // expiration time
151
+ if (this .minimumSnapshotCount != null ) {
152
+ if (allSnapshots .size () <= this .minimumSnapshotCount ) {
153
+ logger .trace ("[{}]: INELIGIBLE as there are {} snapshots and {} minimum snapshots needed" ,
154
+ snapName , snapCount , this .minimumSnapshotCount );
155
+ return false ;
156
+ }
157
+ }
158
+
159
+ // Finally, check the expiration time of the snapshot, if it is past, then it is
160
+ // eligible for deletion
69
161
if (this .expireAfter != null ) {
70
- TimeValue snapshotAge = new TimeValue (System .currentTimeMillis () - si .startTime ());
162
+ TimeValue snapshotAge = new TimeValue (nowSupplier .getAsLong () - si .startTime ());
163
+
164
+ if (this .minimumSnapshotCount != null ) {
165
+ int eligibleForExpiration = snapCount - minimumSnapshotCount ;
166
+
167
+ // Only the oldest N snapshots are actually eligible, since if we went below this we
168
+ // would fall below the configured minimum number of snapshots to keep
169
+ Set <SnapshotInfo > snapsEligibleForExpiration = sortedSnapshots .stream ()
170
+ .limit (eligibleForExpiration )
171
+ .collect (Collectors .toSet ());
172
+
173
+ if (snapsEligibleForExpiration .contains (si ) == false ) {
174
+ // This snapshot is *not* one of the N oldest snapshots, so even if it were
175
+ // old enough, the other snapshots would be deleted before it
176
+ logger .trace ("[{}]: INELIGIBLE as snapshot expiration would pass the " +
177
+ "minimum number of configured snapshots ({}) to keep, regardless of age" ,
178
+ snapName , this .minimumSnapshotCount );
179
+ return false ;
180
+ }
181
+ }
182
+
71
183
if (snapshotAge .compareTo (this .expireAfter ) > 0 ) {
184
+ logger .trace ("[{}]: ELIGIBLE as snapshot age of {} is older than {}" ,
185
+ snapName , snapshotAge .toHumanReadableString (3 ), this .expireAfter .toHumanReadableString (3 ));
72
186
return true ;
73
187
} else {
188
+ logger .trace ("[{}]: INELIGIBLE as snapshot age of {} is newer than {}" ,
189
+ snapName , snapshotAge .toHumanReadableString (3 ), this .expireAfter .toHumanReadableString (3 ));
74
190
return false ;
75
191
}
76
192
}
77
193
// If nothing matched, the snapshot is not eligible for deletion
194
+ logger .trace ("[{}]: INELIGIBLE as no retention predicates matched" , snapName );
78
195
return false ;
79
196
};
80
197
}
81
198
199
+ @ Override
200
+ public void writeTo (StreamOutput out ) throws IOException {
201
+ out .writeOptionalTimeValue (this .expireAfter );
202
+ out .writeOptionalVInt (this .minimumSnapshotCount );
203
+ out .writeOptionalVInt (this .maximumSnapshotCount );
204
+ }
205
+
82
206
@ Override
83
207
public XContentBuilder toXContent (XContentBuilder builder , Params params ) throws IOException {
84
208
builder .startObject ();
85
209
if (expireAfter != null ) {
86
210
builder .field (EXPIRE_AFTER .getPreferredName (), expireAfter .getStringRep ());
87
211
}
212
+ if (minimumSnapshotCount != null ) {
213
+ builder .field (MINIMUM_SNAPSHOT_COUNT .getPreferredName (), minimumSnapshotCount );
214
+ }
215
+ if (maximumSnapshotCount != null ) {
216
+ builder .field (MAXIMUM_SNAPSHOT_COUNT .getPreferredName (), maximumSnapshotCount );
217
+ }
88
218
builder .endObject ();
89
219
return builder ;
90
220
}
91
221
92
- @ Override
93
- public void writeTo (StreamOutput out ) throws IOException {
94
- out .writeOptionalTimeValue (this .expireAfter );
95
- }
96
-
97
222
@ Override
98
223
public int hashCode () {
99
- return Objects .hash (expireAfter );
224
+ return Objects .hash (expireAfter , minimumSnapshotCount , maximumSnapshotCount );
100
225
}
101
226
102
227
@ Override
@@ -108,7 +233,9 @@ public boolean equals(Object obj) {
108
233
return false ;
109
234
}
110
235
SnapshotRetentionConfiguration other = (SnapshotRetentionConfiguration ) obj ;
111
- return Objects .equals (this .expireAfter , other .expireAfter );
236
+ return Objects .equals (this .expireAfter , other .expireAfter ) &&
237
+ Objects .equals (minimumSnapshotCount , other .minimumSnapshotCount ) &&
238
+ Objects .equals (maximumSnapshotCount , other .maximumSnapshotCount );
112
239
}
113
240
114
241
@ Override
0 commit comments