2323import com .maxmind .db .NodeCache ;
2424import com .maxmind .db .Reader ;
2525import com .maxmind .geoip2 .DatabaseReader ;
26- import org . elasticsearch . core . internal . io . IOUtils ;
26+ import com . maxmind . geoip2 . model . AbstractResponse ;
2727import org .elasticsearch .common .Booleans ;
2828import org .elasticsearch .common .SuppressForbidden ;
29+ import org .elasticsearch .common .cache .Cache ;
30+ import org .elasticsearch .common .cache .CacheBuilder ;
2931import org .elasticsearch .common .settings .Setting ;
32+ import org .elasticsearch .core .internal .io .IOUtils ;
3033import org .elasticsearch .ingest .Processor ;
3134import org .elasticsearch .plugins .IngestPlugin ;
3235import org .elasticsearch .plugins .Plugin ;
3336
3437import java .io .Closeable ;
3538import java .io .IOException ;
39+ import java .net .InetAddress ;
3640import java .nio .file .Files ;
3741import java .nio .file .Path ;
3842import java .nio .file .PathMatcher ;
4246import java .util .Iterator ;
4347import java .util .List ;
4448import java .util .Map ;
49+ import java .util .Objects ;
50+ import java .util .function .Function ;
4551import java .util .stream .Stream ;
4652
4753public class IngestGeoIpPlugin extends Plugin implements IngestPlugin , Closeable {
@@ -61,24 +67,18 @@ public Map<String, Processor.Factory> getProcessors(Processor.Parameters paramet
6167 throw new IllegalStateException ("getProcessors called twice for geoip plugin!!" );
6268 }
6369 Path geoIpConfigDirectory = parameters .env .configFile ().resolve ("ingest-geoip" );
64- NodeCache cache ;
6570 long cacheSize = CACHE_SIZE .get (parameters .env .settings ());
66- if (cacheSize > 0 ) {
67- cache = new GeoIpCache (cacheSize );
68- } else {
69- cache = NoCache .getInstance ();
70- }
7171 try {
72- databaseReaders = loadDatabaseReaders (geoIpConfigDirectory , cache );
72+ databaseReaders = loadDatabaseReaders (geoIpConfigDirectory );
7373 } catch (IOException e ) {
7474 throw new RuntimeException (e );
7575 }
76- return Collections .singletonMap (GeoIpProcessor .TYPE , new GeoIpProcessor .Factory (databaseReaders ));
76+ return Collections .singletonMap (GeoIpProcessor .TYPE , new GeoIpProcessor .Factory (databaseReaders , new GeoIpCache ( cacheSize ) ));
7777 }
7878
79- static Map <String , DatabaseReaderLazyLoader > loadDatabaseReaders (Path geoIpConfigDirectory , NodeCache cache ) throws IOException {
79+ static Map <String , DatabaseReaderLazyLoader > loadDatabaseReaders (Path geoIpConfigDirectory ) throws IOException {
8080 if (Files .exists (geoIpConfigDirectory ) == false && Files .isDirectory (geoIpConfigDirectory )) {
81- throw new IllegalStateException ("the geoip directory [" + geoIpConfigDirectory + "] containing databases doesn't exist" );
81+ throw new IllegalStateException ("the geoip directory [" + geoIpConfigDirectory + "] containing databases doesn't exist" );
8282 }
8383 boolean loadDatabaseOnHeap = Booleans .parseBoolean (System .getProperty ("es.geoip.load_db_on_heap" , "false" ));
8484 Map <String , DatabaseReaderLazyLoader > databaseReaders = new HashMap <>();
@@ -92,7 +92,7 @@ static Map<String, DatabaseReaderLazyLoader> loadDatabaseReaders(Path geoIpConfi
9292 String databaseFileName = databasePath .getFileName ().toString ();
9393 DatabaseReaderLazyLoader holder = new DatabaseReaderLazyLoader (databaseFileName ,
9494 () -> {
95- DatabaseReader .Builder builder = createDatabaseBuilder (databasePath ).withCache (cache );
95+ DatabaseReader .Builder builder = createDatabaseBuilder (databasePath ).withCache (NoCache . getInstance () );
9696 if (loadDatabaseOnHeap ) {
9797 builder .fileMode (Reader .FileMode .MEMORY );
9898 } else {
@@ -119,4 +119,75 @@ public void close() throws IOException {
119119 }
120120 }
121121
122+ /**
123+ * The in-memory cache for the geoip data. There should only be 1 instance of this class..
124+ * This cache differs from the maxmind's {@link NodeCache} such that this cache stores the deserialized Json objects to avoid the
125+ * cost of deserialization for each lookup (cached or not). This comes at slight expense of higher memory usage, but significant
126+ * reduction of CPU usage.
127+ */
128+ static class GeoIpCache {
129+ private final Cache <CacheKey , AbstractResponse > cache ;
130+
131+ //package private for testing
132+ GeoIpCache (long maxSize ) {
133+ if (maxSize < 0 ) {
134+ throw new IllegalArgumentException ("geoip max cache size must be 0 or greater" );
135+ }
136+ this .cache = CacheBuilder .<CacheKey , AbstractResponse >builder ().setMaximumWeight (maxSize ).build ();
137+ }
138+
139+ <T extends AbstractResponse > T putIfAbsent (InetAddress ip , Class <T > responseType ,
140+ Function <InetAddress , AbstractResponse > retrieveFunction ) {
141+
142+ //can't use cache.computeIfAbsent due to the elevated permissions for the jackson (run via the cache loader)
143+ CacheKey <T > cacheKey = new CacheKey <>(ip , responseType );
144+ //intentionally non-locking for simplicity...it's OK if we re-put the same key/value in the cache during a race condition.
145+ AbstractResponse response = cache .get (cacheKey );
146+ if (response == null ) {
147+ response = retrieveFunction .apply (ip );
148+ cache .put (cacheKey , response );
149+ }
150+ return responseType .cast (response );
151+ }
152+
153+ //only useful for testing
154+ <T extends AbstractResponse > T get (InetAddress ip , Class <T > responseType ) {
155+ CacheKey <T > cacheKey = new CacheKey <>(ip , responseType );
156+ return responseType .cast (cache .get (cacheKey ));
157+ }
158+
159+ /**
160+ * The key to use for the cache. Since this cache can span multiple geoip processors that all use different databases, the response
161+ * type is needed to be included in the cache key. For example, if we only used the IP address as the key the City and ASN the same
162+ * IP may be in both with different values and we need to cache both. The response type scopes the IP to the correct database
163+ * provides a means to safely cast the return objects.
164+ * @param <T> The AbstractResponse type used to scope the key and cast the result.
165+ */
166+ private static class CacheKey <T extends AbstractResponse > {
167+
168+ private final InetAddress ip ;
169+ private final Class <T > responseType ;
170+
171+ private CacheKey (InetAddress ip , Class <T > responseType ) {
172+ this .ip = ip ;
173+ this .responseType = responseType ;
174+ }
175+
176+ //generated
177+ @ Override
178+ public boolean equals (Object o ) {
179+ if (this == o ) return true ;
180+ if (o == null || getClass () != o .getClass ()) return false ;
181+ CacheKey <?> cacheKey = (CacheKey <?>) o ;
182+ return Objects .equals (ip , cacheKey .ip ) &&
183+ Objects .equals (responseType , cacheKey .responseType );
184+ }
185+
186+ //generated
187+ @ Override
188+ public int hashCode () {
189+ return Objects .hash (ip , responseType );
190+ }
191+ }
192+ }
122193}
0 commit comments