1
1
package life .qbic .projectmanagement .infrastructure .ontology ;
2
2
3
+ import static life .qbic .logging .service .LoggerFactory .logger ;
4
+
3
5
import com .fasterxml .jackson .core .JsonProcessingException ;
4
6
import com .fasterxml .jackson .databind .DeserializationFeature ;
5
7
import com .fasterxml .jackson .databind .JsonNode ;
15
17
import java .net .http .HttpResponse .BodyHandlers ;
16
18
import java .nio .charset .StandardCharsets ;
17
19
import java .time .Duration ;
20
+ import java .time .Instant ;
18
21
import java .util .ArrayList ;
22
+ import java .util .Comparator ;
19
23
import java .util .List ;
24
+ import java .util .Objects ;
20
25
import java .util .Optional ;
26
+ import java .util .stream .Collectors ;
27
+ import life .qbic .logging .api .Logger ;
21
28
import life .qbic .projectmanagement .application .ontology .LookupException ;
22
29
import life .qbic .projectmanagement .application .ontology .OntologyClass ;
23
30
import life .qbic .projectmanagement .application .ontology .TerminologySelect ;
35
42
@ Service
36
43
public class TIBTerminologyServiceIntegration implements TerminologySelect {
37
44
45
+ private static final Logger log = logger (TIBTerminologyServiceIntegration .class );
38
46
private static final int TIMEOUT_5_SECONDS = 5 ;
39
47
private static final HttpClient HTTP_CLIENT = httpClient (TIMEOUT_5_SECONDS );
40
48
@@ -54,6 +62,7 @@ public class TIBTerminologyServiceIntegration implements TerminologySelect {
54
62
55
63
private final URI selectEndpointAbsoluteUrl ;
56
64
private final URI searchEndpointAbsoluteUrl ;
65
+ private final RequestCache cache ;
57
66
58
67
@ Autowired
59
68
public TIBTerminologyServiceIntegration (
@@ -62,6 +71,7 @@ public TIBTerminologyServiceIntegration(
62
71
@ Value ("${terminology.service.tib.api.url}" ) String tibApiUrl ) {
63
72
this .selectEndpointAbsoluteUrl = URI .create (tibApiUrl ).resolve (selectEndpoint );
64
73
this .searchEndpointAbsoluteUrl = URI .create (tibApiUrl ).resolve (searchEndpoint );
74
+ this .cache = new RequestCache (1000 );
65
75
}
66
76
67
77
/**
@@ -174,9 +184,13 @@ public List<OntologyClass> query(String searchTerm, int offset, int limit)
174
184
@ Override
175
185
public Optional <OntologyClass > searchByCurie (String curie ) throws LookupException {
176
186
try {
177
- return searchByOboIdExact (curie ).map (TIBTerminologyServiceIntegration ::convert );
187
+ return searchByOboIdExact (curie ).map (this ::updateCache )
188
+ .map (TIBTerminologyServiceIntegration ::convert );
178
189
} catch (IOException e ) {
179
- throw wrapIO (e );
190
+ // this happens on network interrupts or if the remote service is down
191
+ // we try to recover from the cache
192
+ log .error ("Error searching by CURIE: " + curie , e );
193
+ return cache .findByCurie (curie ).map (TIBTerminologyServiceIntegration ::convert );
180
194
} catch (InterruptedException e ) {
181
195
Thread .currentThread ().interrupt ();
182
196
throw wrapInterrupted (e );
@@ -227,7 +241,7 @@ private List<TibTerm> fullSearch(String searchTerm, int offset, int limit)
227
241
+ "&ontology=" + createOntologyFilterQueryParameter ()))
228
242
.header ("Content-Type" , "application/json" ).GET ().build ();
229
243
var response = HTTP_CLIENT .send (termSelectQuery , BodyHandlers .ofString ());
230
- return parseResponse (response );
244
+ return parseResponse (response ). stream (). toList () ;
231
245
}
232
246
233
247
/**
@@ -257,7 +271,7 @@ private List<TibTerm> select(String searchTerm, int offset, int limit)
257
271
+ createOntologyFilterQueryParameter ()))
258
272
.header ("Content-Type" , "application/json" ).GET ().build ();
259
273
var response = HTTP_CLIENT .send (termSelectQuery , BodyHandlers .ofString ());
260
- return parseResponse (response );
274
+ return parseResponse (response ). stream (). toList () ;
261
275
}
262
276
263
277
/**
@@ -342,4 +356,133 @@ private List<TibTerm> parseResponse(HttpResponse<String> response) {
342
356
throw wrapProcessingException (e );
343
357
}
344
358
}
359
+
360
+ // adds a term to the cache
361
+ private TibTerm updateCache (TibTerm term ) {
362
+ cache .add (term );
363
+ return term ;
364
+ }
365
+
366
+ /**
367
+ * In-memory cache for {@link TibTerm} as failsafe for network interrupts.
368
+ *
369
+ * @since 1.9.0
370
+ */
371
+ static class RequestCache {
372
+
373
+ // Pretty random, we need to see what value actual makes sense
374
+ private static final int DEFAULT_CACHE_SIZE = 500 ;
375
+
376
+ private final List <TibTerm > cache = new ArrayList <>();
377
+ private final int limit ;
378
+ private List <CacheEntryStat > accessFrequency = new ArrayList <>();
379
+
380
+ RequestCache () {
381
+ limit = DEFAULT_CACHE_SIZE ;
382
+ }
383
+
384
+ RequestCache (int limit ) {
385
+ this .limit = limit ;
386
+ }
387
+
388
+ /**
389
+ * Adds a {@link TibTerm} to the in-memory cache.
390
+ * <p>
391
+ * If the cache max size is reached, the oldest entry will be replaced with the one passed to
392
+ * the function.
393
+ *
394
+ * @param term the term to store in the cache
395
+ * @since 1.9.0
396
+ */
397
+ void add (TibTerm term ) {
398
+ if (cache .contains (term )) {
399
+ return ;
400
+ }
401
+ if (cache .size () >= limit ) {
402
+ addByReplace (term );
403
+ return ;
404
+ }
405
+ cache .add (term );
406
+ addStats (new CacheEntryStat (term ));
407
+ }
408
+
409
+ // Puts the term with the time of caching into an own list for tracking
410
+ private void addStats (CacheEntryStat cacheEntryStat ) {
411
+ if (accessFrequency .contains (cacheEntryStat )) {
412
+ return ;
413
+ }
414
+ accessFrequency .add (cacheEntryStat );
415
+ }
416
+
417
+ // A special case of adding by looking for the oldest cache entry and replacing it with
418
+ // the provided one
419
+ private void addByReplace (TibTerm term ) {
420
+ // We want to be sure that the access statistic list is in natural order
421
+ ensureSorted ();
422
+ // We then remove the oldest cache entry
423
+ if (!cache .isEmpty ()) {
424
+ cache .set (0 , term );
425
+ addStats (new CacheEntryStat (term ));
426
+ }
427
+ }
428
+
429
+ // Ensures the natural order sorting by datetime, when the cache entry has been created
430
+ // Oldest entry will be the first element, newest the last element of the list
431
+ private void ensureSorted () {
432
+ accessFrequency = accessFrequency .stream ()
433
+ .sorted (Comparator .comparing (CacheEntryStat ::created , Instant ::compareTo ))
434
+ .collect (Collectors .toList ());
435
+ }
436
+
437
+ /**
438
+ * Searches for a matching {@link TibTerm} in the cache.
439
+ *
440
+ * @param curie the CURIE to search for
441
+ * @return the search result, {@link Optional#empty()} if no match was found
442
+ * @since 1.9.0
443
+ */
444
+ Optional <TibTerm > findByCurie (String curie ) {
445
+ return cache .stream ().filter (term -> term .oboId .equals (curie )).findFirst ();
446
+ }
447
+ }
448
+
449
+ /**
450
+ * A small container for when a cache entry has been created.
451
+ *
452
+ * @since 1.9.0
453
+ */
454
+ static class CacheEntryStat {
455
+
456
+ private final TibTerm term ;
457
+ private final Instant created ;
458
+
459
+ CacheEntryStat (TibTerm term ) {
460
+ this .term = term ;
461
+ created = Instant .now ();
462
+ }
463
+
464
+ /**
465
+ * When the cache entry has been created
466
+ *
467
+ * @return the instant of creation
468
+ * @since 1.9.0
469
+ */
470
+ Instant created () {
471
+ return created ;
472
+ }
473
+
474
+ @ Override
475
+ public boolean equals (Object o ) {
476
+ if (o == null || getClass () != o .getClass ()) {
477
+ return false ;
478
+ }
479
+ CacheEntryStat that = (CacheEntryStat ) o ;
480
+ return Objects .equals (term , that .term );
481
+ }
482
+
483
+ @ Override
484
+ public int hashCode () {
485
+ return Objects .hashCode (term );
486
+ }
487
+ }
345
488
}
0 commit comments