4
4
import edu .umd .cs .findbugs .annotations .NonNull ;
5
5
import edu .umd .cs .findbugs .annotations .SuppressFBWarnings ;
6
6
import hudson .Extension ;
7
+ import hudson .ExtensionList ;
8
+ import hudson .ExtensionPoint ;
7
9
import hudson .Util ;
8
10
import hudson .model .Job ;
9
11
import hudson .model .PermalinkProjectAction .Permalink ;
14
16
import hudson .util .AtomicFileWriter ;
15
17
import java .io .File ;
16
18
import java .io .IOException ;
19
+ import java .io .Serializable ;
17
20
import java .nio .charset .StandardCharsets ;
18
21
import java .nio .file .Files ;
19
22
import java .util .HashMap ;
24
27
import java .util .logging .Logger ;
25
28
import java .util .stream .Stream ;
26
29
import org .kohsuke .accmod .Restricted ;
30
+ import org .kohsuke .accmod .restrictions .Beta ;
27
31
import org .kohsuke .accmod .restrictions .NoExternalUse ;
28
32
29
33
/**
63
67
*/
64
68
public abstract class PeepholePermalink extends Permalink implements Predicate <Run <?, ?>> {
65
69
66
- /**
67
- * JENKINS-22822: avoids rereading caches.
68
- * Top map keys are {@code builds} directories.
69
- * Inner maps are from permalink name to build number.
70
- * Synchronization is first on the outer map, then on the inner.
71
- */
72
- private static final Map <File , Map <String , Integer >> caches = new HashMap <>();
73
-
74
70
/**
75
71
* Checks if the given build satisfies the peep-hole criteria.
76
72
*
@@ -94,115 +90,216 @@ protected File getPermalinkFile(Job<?, ?> job) {
94
90
*/
95
91
@ Override
96
92
public Run <?, ?> resolve (Job <?, ?> job ) {
97
- Map <String , Integer > cache = cacheFor (job .getBuildDir ());
98
- int n ;
99
- synchronized (cache ) {
100
- n = cache .getOrDefault (getId (), 0 );
93
+ return ExtensionList .lookupFirst (Cache .class ).get (job , getId ()).resolve (this , job , getId ());
94
+ }
95
+
96
+ /**
97
+ * Start from the build 'b' and locate the build that matches the criteria going back in time
98
+ */
99
+ @ CheckForNull
100
+ private Run <?, ?> find (@ CheckForNull Run <?, ?> b ) {
101
+ while (b != null && !apply (b )) {
102
+ b = b .getPreviousBuild ();
101
103
}
102
- if (n == RESOLVES_TO_NONE ) {
103
- return null ;
104
+ return b ;
105
+ }
106
+
107
+ /**
108
+ * Remembers the value 'n' in the cache for future {@link #resolve(Job)}.
109
+ */
110
+ protected void updateCache (@ NonNull Job <?, ?> job , @ CheckForNull Run <?, ?> b ) {
111
+ ExtensionList .lookupFirst (Cache .class ).put (job , getId (), b != null ? new Cache .Some (b .getNumber ()) : Cache .NONE );
112
+ }
113
+
114
+ /**
115
+ * Persistable cache of peephole permalink targets.
116
+ */
117
+ @ Restricted (Beta .class )
118
+ public interface Cache extends ExtensionPoint {
119
+
120
+ /** Cacheable target of a permalink. */
121
+ sealed interface PermalinkTarget extends Serializable {
122
+
123
+ /**
124
+ * Implementation of {@link #resolve(Job)}.
125
+ * This may update the cache if it was missing or found to be invalid.
126
+ */
127
+ @ Restricted (NoExternalUse .class )
128
+ @ CheckForNull
129
+ Run <?, ?> resolve (@ NonNull PeepholePermalink pp , @ NonNull Job <?, ?> job , @ NonNull String id );
130
+
131
+ /**
132
+ * Partial implementation of {@link #resolve(PeepholePermalink, Job, String)} when searching.
133
+ * @param b if set, the newest build to even consider when searching
134
+ */
135
+ @ Restricted (NoExternalUse .class )
136
+ @ CheckForNull
137
+ default Run <?, ?> search (@ NonNull PeepholePermalink pp , @ NonNull Job <?, ?> job , @ NonNull String id , @ CheckForNull Run <?, ?> b ) {
138
+ if (b == null ) {
139
+ // no cache
140
+ b = job .getLastBuild ();
141
+ }
142
+ // start from the build 'b' and locate the build that matches the criteria going back in time
143
+ b = pp .find (b );
144
+ pp .updateCache (job , b );
145
+ return b ;
146
+ }
147
+
104
148
}
105
- Run <?, ?> b ;
106
- if (n > 0 ) {
107
- b = job .getBuildByNumber (n );
108
- if (b != null && apply (b )) {
109
- return b ; // found it (in the most efficient way possible)
149
+
150
+ /**
151
+ * The cache entry for this target is missing.
152
+ */
153
+ record Unknown () implements PermalinkTarget {
154
+ @ Override
155
+ public Run <?, ?> resolve (PeepholePermalink pp , Job <?, ?> job , String id ) {
156
+ return search (pp , job , id , null );
110
157
}
111
- } else {
112
- b = null ;
113
158
}
114
159
115
- // the cache is stale. start the search
116
- if (b == null ) {
117
- b = job .getNearestOldBuild (n );
160
+ Unknown UNKNOWN = new Unknown ();
161
+
162
+ /**
163
+ * The cache entry for this target is present.
164
+ */
165
+ sealed interface Known extends PermalinkTarget {}
166
+
167
+ /** There is known to be no matching build. */
168
+ record None () implements Known {
169
+ @ Override
170
+ public Run <?, ?> resolve (PeepholePermalink pp , Job <?, ?> job , String id ) {
171
+ return null ;
172
+ }
118
173
}
119
174
120
- if (b == null ) {
121
- // no cache
122
- b = job .getLastBuild ();
175
+ /** Singleton of {@link None}. */
176
+ None NONE = new None ();
177
+
178
+ /** A matching build, indicated by {@link Run#getNumber}. */
179
+ record Some (int number ) implements Known {
180
+ @ Override
181
+ public Run <?, ?> resolve (PeepholePermalink pp , Job <?, ?> job , String id ) {
182
+ Run <?, ?> b = job .getBuildByNumber (number );
183
+ if (b != null && pp .apply (b )) {
184
+ return b ; // found it (in the most efficient way possible)
185
+ }
186
+ // the cache is stale. start the search
187
+ if (b == null ) {
188
+ b = job .getNearestOldBuild (number );
189
+ }
190
+ return search (pp , job , id , b );
191
+ }
123
192
}
124
193
125
- // start from the build 'b' and locate the build that matches the criteria going back in time
126
- b = find (b );
194
+ /**
195
+ * Looks for any existing cache hit.
196
+ * @param id {@link #getId}
197
+ * @return {@link Some} or {@link #NONE} or {@link #UNKNOWN}
198
+ */
199
+ @ NonNull PermalinkTarget get (@ NonNull Job <?, ?> job , @ NonNull String id );
127
200
128
- updateCache (job , b );
129
- return b ;
201
+ /**
202
+ * Updates the cache.
203
+ * Note that this may be called not just when a build completes or is deleted
204
+ * (meaning that the logical value of the cache has changed),
205
+ * but also when {@link #resolve} has failed to find a cached value
206
+ * (or determined that a previously cached value is in fact invalid).
207
+ * @param id {@link #getId}
208
+ * @param target {@link Some} or {@link #NONE}
209
+ */
210
+ void put (@ NonNull Job <?, ?> job , @ NonNull String id , @ NonNull Known target );
130
211
}
131
212
132
213
/**
133
- * Start from the build 'b' and locate the build that matches the criteria going back in time
214
+ * Default cache based on a {@code permalinks} file in the build directory.
215
+ * There is one line per cached permalink, in the format {@code lastStableBuild 123}
216
+ * or (for a negative cache) {@code lastFailedBuild -1}.
134
217
*/
135
- private Run <?, ?> find (Run <?, ?> b ) {
136
- //noinspection StatementWithEmptyBody
137
- for ( ; b != null && !apply (b ); b = b .getPreviousBuild ())
138
- ;
139
- return b ;
140
- }
218
+ @ Restricted (NoExternalUse .class )
219
+ @ Extension (ordinal = -1000 )
220
+ public static final class DefaultCache implements Cache {
221
+
222
+ /**
223
+ * JENKINS-22822: avoids rereading caches.
224
+ * Top map keys are {@code builds} directories.
225
+ * Inner maps are from permalink name to target.
226
+ * Synchronization is first on the outer map, then on the inner.
227
+ */
228
+ private final Map <File , Map <String , Known >> caches = new HashMap <>();
141
229
142
- private static @ NonNull Map < String , Integer > cacheFor ( @ NonNull File buildDir ) {
143
- synchronized ( caches ) {
144
- Map < String , Integer > cache = caches . get ( buildDir );
145
- if (cache == null ) {
146
- cache = load ( buildDir );
147
- caches . put ( buildDir , cache ) ;
230
+ @ Override
231
+ public PermalinkTarget get ( Job <?, ?> job , String id ) {
232
+ var cache = cacheFor ( job . getBuildDir () );
233
+ synchronized (cache ) {
234
+ var cached = cache . get ( id );
235
+ return cached != null ? cached : UNKNOWN ;
148
236
}
149
- return cache ;
150
237
}
151
- }
152
238
153
- private static @ NonNull Map <String , Integer > load (@ NonNull File buildDir ) {
154
- Map <String , Integer > cache = new TreeMap <>();
155
- File storage = storageFor (buildDir );
156
- if (storage .isFile ()) {
157
- try (Stream <String > lines = Files .lines (storage .toPath (), StandardCharsets .UTF_8 )) {
158
- lines .forEach (line -> {
159
- int idx = line .indexOf (' ' );
160
- if (idx == -1 ) {
161
- return ;
162
- }
239
+ @ Override
240
+ public void put (Job <?, ?> job , String id , Known target ) {
241
+ File buildDir = job .getBuildDir ();
242
+ var cache = cacheFor (buildDir );
243
+ synchronized (cache ) {
244
+ cache .put (id , target );
245
+ File storage = storageFor (buildDir );
246
+ LOGGER .fine (() -> "saving to " + storage + ": " + cache );
247
+ try (AtomicFileWriter cw = new AtomicFileWriter (storage )) {
163
248
try {
164
- cache .put (line .substring (0 , idx ), Integer .parseInt (line .substring (idx + 1 )));
165
- } catch (NumberFormatException x ) {
166
- LOGGER .log (Level .WARNING , "failed to read " + storage , x );
249
+ for (var entry : cache .entrySet ()) {
250
+ cw .write (entry .getKey ());
251
+ cw .write (' ' );
252
+ cw .write (Integer .toString (entry .getValue () instanceof Cache .Some some ? some .number : -1 ));
253
+ cw .write ('\n' );
254
+ }
255
+ cw .commit ();
256
+ } finally {
257
+ cw .abort ();
167
258
}
168
- });
169
- } catch ( IOException x ) {
170
- LOGGER . log ( Level . WARNING , "failed to read " + storage , x );
259
+ } catch ( IOException x ) {
260
+ LOGGER . log ( Level . WARNING , "failed to update " + storage , x );
261
+ }
171
262
}
172
- LOGGER .fine (() -> "loading from " + storage + ": " + cache );
173
263
}
174
- return cache ;
175
- }
176
264
177
- static @ NonNull File storageFor (@ NonNull File buildDir ) {
178
- return new File (buildDir , "permalinks" );
179
- }
265
+ private @ NonNull Map <String , Known > cacheFor (@ NonNull File buildDir ) {
266
+ synchronized (caches ) {
267
+ var cache = caches .get (buildDir );
268
+ if (cache == null ) {
269
+ cache = load (buildDir );
270
+ caches .put (buildDir , cache );
271
+ }
272
+ return cache ;
273
+ }
274
+ }
180
275
181
- /**
182
- * Remembers the value 'n' in the cache for future {@link #resolve(Job)}.
183
- */
184
- protected void updateCache (@ NonNull Job <?, ?> job , @ CheckForNull Run <?, ?> b ) {
185
- File buildDir = job .getBuildDir ();
186
- Map <String , Integer > cache = cacheFor (buildDir );
187
- synchronized (cache ) {
188
- cache .put (getId (), b == null ? RESOLVES_TO_NONE : b .getNumber ());
276
+ private static @ NonNull Map <String , Known > load (@ NonNull File buildDir ) {
277
+ Map <String , Known > cache = new TreeMap <>();
189
278
File storage = storageFor (buildDir );
190
- LOGGER .fine (() -> "saving to " + storage + ": " + cache );
191
- try (AtomicFileWriter cw = new AtomicFileWriter (storage )) {
192
- try {
193
- for (Map .Entry <String , Integer > entry : cache .entrySet ()) {
194
- cw .write (entry .getKey ());
195
- cw .write (' ' );
196
- cw .write (Integer .toString (entry .getValue ()));
197
- cw .write ('\n' );
198
- }
199
- cw .commit ();
200
- } finally {
201
- cw .abort ();
279
+ if (storage .isFile ()) {
280
+ try (Stream <String > lines = Files .lines (storage .toPath (), StandardCharsets .UTF_8 )) {
281
+ lines .forEach (line -> {
282
+ int idx = line .indexOf (' ' );
283
+ if (idx == -1 ) {
284
+ return ;
285
+ }
286
+ try {
287
+ int number = Integer .parseInt (line .substring (idx + 1 ));
288
+ cache .put (line .substring (0 , idx ), number == -1 ? Cache .NONE : new Cache .Some (number ));
289
+ } catch (NumberFormatException x ) {
290
+ LOGGER .log (Level .WARNING , "failed to read " + storage , x );
291
+ }
292
+ });
293
+ } catch (IOException x ) {
294
+ LOGGER .log (Level .WARNING , "failed to read " + storage , x );
202
295
}
203
- } catch (IOException x ) {
204
- LOGGER .log (Level .WARNING , "failed to update " + storage , x );
296
+ LOGGER .fine (() -> "loading from " + storage + ": " + cache );
205
297
}
298
+ return cache ;
299
+ }
300
+
301
+ static @ NonNull File storageFor (@ NonNull File buildDir ) {
302
+ return new File (buildDir , "permalinks" );
206
303
}
207
304
}
208
305
@@ -380,7 +477,5 @@ public boolean apply(Run<?, ?> run) {
380
477
@ Restricted (NoExternalUse .class )
381
478
public static void initialized () {}
382
479
383
- private static final int RESOLVES_TO_NONE = -1 ;
384
-
385
480
private static final Logger LOGGER = Logger .getLogger (PeepholePermalink .class .getName ());
386
481
}
0 commit comments