Skip to content

Commit 95fd1d1

Browse files
authored
geoip: managed databases need to be closed when updated (#178)
1 parent ad29262 commit 95fd1d1

File tree

6 files changed

+111
-6
lines changed

6 files changed

+111
-6
lines changed

src/main/java/co/elastic/logstash/filters/elasticintegration/geoip/ConstantIpDatabaseHolder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public boolean isValid() {
1919
}
2020

2121
@Override
22-
public IpDatabase getDatabase() {
22+
public IpDatabaseAdapter getDatabase() {
2323
return this.ipDatabase;
2424
}
2525

src/main/java/co/elastic/logstash/filters/elasticintegration/geoip/IpDatabaseAdapter.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public class IpDatabaseAdapter implements IpDatabase {
2727
private final Reader databaseReader;
2828
private final String databaseType;
2929

30+
private volatile boolean isReaderClosed = false;
31+
3032
public IpDatabaseAdapter(final Reader databaseReader) {
3133
this.databaseReader = databaseReader;
3234
this.databaseType = databaseReader.getMetadata().getDatabaseType();
@@ -56,6 +58,12 @@ public void close() throws IOException {
5658
public void closeReader() throws IOException {
5759
LOGGER.debug("Closing the database adapter");
5860
this.databaseReader.close();
61+
this.isReaderClosed = true;
62+
}
63+
64+
// visible for test
65+
boolean isReaderClosed() {
66+
return this.isReaderClosed;
5967
}
6068

6169
public static IpDatabaseAdapter defaultForPath(final Path database) throws IOException {

src/main/java/co/elastic/logstash/filters/elasticintegration/geoip/IpDatabaseHolder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
interface IpDatabaseHolder {
66
boolean isValid();
77

8-
IpDatabase getDatabase();
8+
IpDatabaseAdapter getDatabase();
99

1010
String getTypeIdentifier();
1111

src/main/java/co/elastic/logstash/filters/elasticintegration/geoip/IpDatabaseProvider.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,25 @@ public class IpDatabaseProvider implements org.elasticsearch.ingest.geoip.IpData
3434

3535
@Override
3636
public Boolean isValid(String databaseIdentifierFileName) {
37-
final IpDatabaseHolder holder = databaseMap.get(databaseIdentifierFileName);
37+
final IpDatabaseHolder holder = getDatabaseHolder(databaseIdentifierFileName);
3838
return Objects.nonNull(holder) && holder.isValid();
3939
}
4040

4141
@Override
4242
public IpDatabase getDatabase(String databaseIdentifierFileName) {
43-
final IpDatabaseHolder holder = databaseMap.get(databaseIdentifierFileName);
43+
final IpDatabaseHolder holder = getDatabaseHolder(databaseIdentifierFileName);
4444
if (Objects.isNull(holder)) {
4545
return null;
4646
} else {
4747
return holder.getDatabase();
4848
}
4949
}
5050

51+
// visible for test
52+
IpDatabaseHolder getDatabaseHolder(String databaseIdentifierFileName) {
53+
return databaseMap.get(databaseIdentifierFileName);
54+
}
55+
5156
@Override
5257
public void close() throws IOException {
5358
databaseMap.forEach((name, holder) -> {

src/main/java/co/elastic/logstash/filters/elasticintegration/geoip/ManagedIpDatabaseHolder.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import org.apache.logging.log4j.LogManager;
44
import org.apache.logging.log4j.Logger;
5+
import org.elasticsearch.core.IOUtils;
56
import org.elasticsearch.ingest.geoip.IpDatabase;
67

78
import java.io.Closeable;
@@ -30,7 +31,14 @@ public class ManagedIpDatabaseHolder implements IpDatabaseHolder, Closeable {
3031
}
3132

3233
public ManagedIpDatabaseHolder(final String databaseTypeIdentifier) {
34+
this(databaseTypeIdentifier, null);
35+
}
36+
37+
ManagedIpDatabaseHolder(final String databaseTypeIdentifier, final Path databaseLocation) {
3338
this.databaseTypeIdentifier = databaseTypeIdentifier;
39+
if (Objects.nonNull(databaseLocation)) {
40+
setDatabasePath(databaseLocation.toAbsolutePath().toString());
41+
}
3442
}
3543

3644
@Override
@@ -39,7 +47,7 @@ public boolean isValid() {
3947
}
4048

4149
@Override
42-
public IpDatabase getDatabase() {
50+
public IpDatabaseAdapter getDatabase() {
4351
return withLock(readLock, () -> this.currentDatabase);
4452
}
4553

@@ -54,12 +62,18 @@ public String info() {
5462
}
5563

5664
public void setDatabasePath(final String newDatabasePath) {
57-
withLock(writeLock, () -> {
65+
final IpDatabaseAdapter previousDatabase = withLock(writeLock, () -> {
66+
final IpDatabaseAdapter localPreviousDatabase = this.currentDatabase;
5867
this.currentDatabase = Optional.ofNullable(newDatabasePath)
5968
.map(Paths::get)
6069
.map(this::loadDatabase)
6170
.orElse(null);
71+
return localPreviousDatabase;
6272
});
73+
74+
if (previousDatabase != null) {
75+
IOUtils.closeWhileHandlingException(previousDatabase::closeReader);
76+
}
6377
}
6478

6579
@Override
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package co.elastic.logstash.filters.elasticintegration.geoip;
2+
3+
import co.elastic.logstash.filters.elasticintegration.util.ResourcesUtil;
4+
import org.apache.commons.io.FilenameUtils;
5+
import org.hamcrest.Matchers;
6+
import org.junit.jupiter.api.Test;
7+
8+
import java.nio.file.Path;
9+
10+
import static org.hamcrest.MatcherAssert.assertThat;
11+
import static org.junit.jupiter.api.Assertions.*;
12+
13+
14+
class ManagedIpDatabaseHolderTest {
15+
static final Path GEOLITE2_ASN_MMDB_PATH;
16+
static final Path GEOLITE2_CITY_MMDB_PATH;
17+
18+
static final String GEOLITE2_ASN_TYPE = "GeoLite2-ASN.mmdb";
19+
static final String GEOLITE2_CITY_TYPE = "GeoLite2-City.mmdb";
20+
21+
static {
22+
try {
23+
final Path databases = ResourcesUtil.getResourcePath(ManagedIpDatabaseHolderTest.class, "databases").orElseThrow();
24+
GEOLITE2_ASN_MMDB_PATH = databases.resolve(GEOLITE2_ASN_TYPE).toRealPath();
25+
GEOLITE2_CITY_MMDB_PATH = databases.resolve(GEOLITE2_CITY_TYPE).toRealPath();
26+
} catch (Exception e) {
27+
throw new RuntimeException(e);
28+
}
29+
}
30+
31+
IpDatabaseProvider buildIpDatabaseProvider(final Path... paths) {
32+
final IpDatabaseProvider.Builder builder = new IpDatabaseProvider.Builder();
33+
for (Path path : paths) {
34+
final String databaseFileName = path.getFileName().toString();
35+
final String databaseTypeIdentifier = FilenameUtils.removeExtension(databaseFileName);
36+
@SuppressWarnings("resource") final ManagedIpDatabaseHolder dbh = new ManagedIpDatabaseHolder(databaseTypeIdentifier, path);
37+
builder.setDatabaseHolder(databaseFileName, dbh);
38+
}
39+
return builder.build();
40+
}
41+
42+
@Test
43+
void releaseOnReload() throws Exception {
44+
try (IpDatabaseProvider ipDatabaseProvider = buildIpDatabaseProvider(GEOLITE2_CITY_MMDB_PATH)) {
45+
final IpDatabaseHolder cityIpDatabaseHolder = ipDatabaseProvider.getDatabaseHolder(GEOLITE2_CITY_TYPE);
46+
assert cityIpDatabaseHolder instanceof ManagedIpDatabaseHolder;
47+
48+
final IpDatabaseAdapter first = cityIpDatabaseHolder.getDatabase();
49+
assertFalse(first.isReaderClosed());
50+
51+
// update the holder's path. It's okay to be the same underlying file.
52+
((ManagedIpDatabaseHolder) cityIpDatabaseHolder).setDatabasePath(GEOLITE2_CITY_MMDB_PATH.toString());
53+
54+
// get the database again, and make sure we have a new open one, and that the old one has been closed
55+
final IpDatabaseAdapter second = cityIpDatabaseHolder.getDatabase();
56+
assertNotEquals(first, second);
57+
assertTrue(first.isReaderClosed());
58+
assertFalse(second.isReaderClosed());
59+
}
60+
}
61+
62+
@Test
63+
void rejectTypeChangeOnReload() throws Exception {
64+
try (IpDatabaseProvider ipDatabaseProvider = buildIpDatabaseProvider(GEOLITE2_CITY_MMDB_PATH)) {
65+
final IpDatabaseHolder cityIpDatabaseHolder = ipDatabaseProvider.getDatabaseHolder(GEOLITE2_CITY_TYPE);
66+
assert cityIpDatabaseHolder instanceof ManagedIpDatabaseHolder;
67+
68+
final IpDatabaseAdapter first = cityIpDatabaseHolder.getDatabase();
69+
assertFalse(first.isReaderClosed());
70+
71+
// update the holder's path to the wrong type
72+
IllegalStateException illegalStateException = assertThrows(IllegalStateException.class, () -> {
73+
((ManagedIpDatabaseHolder) cityIpDatabaseHolder).setDatabasePath(GEOLITE2_ASN_MMDB_PATH.toString());
74+
});
75+
assertThat(illegalStateException.getMessage(), Matchers.containsString("Incompatible database type `GeoLite2-ASN` (expected `GeoLite2-City`)"));
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)