Skip to content

Commit 4cf8e4b

Browse files
authored
Add Windows native method to retrieve the number of allocated bytes on disk for file (#79698)
In #79371 we fixed a bug where cache files were not created as sparse files on Windows platforms because the wrong options were used when creating the files for the first time. This bug got unnoticed as we were lacking a way to retrieve the exact number of bytes allocated for a given file on disk. This commit adds a FileSystemNatives.allocatedSizeInBytes(Path) method for that exact purpose (only implemented for Windows for now) and a test in CacheFileTests that would fail on Windows if the cache file is not sparse. Relates #79371
1 parent eb161a0 commit 4cf8e4b

File tree

4 files changed

+246
-1
lines changed

4 files changed

+246
-1
lines changed

server/src/main/java/org/elasticsearch/bootstrap/Bootstrap.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.elasticsearch.Version;
2121
import org.elasticsearch.cli.UserException;
2222
import org.elasticsearch.common.PidFile;
23+
import org.elasticsearch.common.filesystem.FileSystemNatives;
2324
import org.elasticsearch.common.inject.CreationException;
2425
import org.elasticsearch.common.logging.LogConfigurator;
2526
import org.elasticsearch.common.logging.Loggers;
@@ -150,6 +151,9 @@ public boolean handle(int code) {
150151

151152
// init lucene random seed. it will use /dev/urandom where available:
152153
StringHelper.randomId();
154+
155+
// init filesystem natives
156+
FileSystemNatives.init();
153157
}
154158

155159
static void initializeProbes() {
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.common.filesystem;
10+
11+
import org.apache.logging.log4j.LogManager;
12+
import org.apache.logging.log4j.Logger;
13+
import org.apache.lucene.util.Constants;
14+
15+
import java.nio.file.Path;
16+
import java.util.OptionalLong;
17+
18+
/**
19+
* This class provides utility methods for calling some native methods related to filesystems.
20+
*/
21+
public final class FileSystemNatives {
22+
23+
private static final Logger logger = LogManager.getLogger(FileSystemNatives.class);
24+
25+
@FunctionalInterface
26+
interface Provider {
27+
OptionalLong allocatedSizeInBytes(Path path);
28+
}
29+
30+
private static final Provider NOOP_FILE_SYSTEM_NATIVES_PROVIDER = path -> OptionalLong.empty();
31+
private static final Provider JNA_PROVIDER = loadJnaProvider();
32+
33+
private static Provider loadJnaProvider() {
34+
try {
35+
// load one of the main JNA classes to see if the classes are available. this does not ensure that all native
36+
// libraries are available, only the ones necessary by JNA to function
37+
Class.forName("com.sun.jna.Native");
38+
if (Constants.WINDOWS) {
39+
return WindowsFileSystemNatives.getInstance();
40+
}
41+
} catch (ClassNotFoundException e) {
42+
logger.warn("JNA not found. FileSystemNatives methods will be disabled.", e);
43+
} catch (LinkageError e) {
44+
logger.warn("unable to load JNA native support library, FileSystemNatives methods will be disabled.", e);
45+
}
46+
return NOOP_FILE_SYSTEM_NATIVES_PROVIDER;
47+
}
48+
49+
private FileSystemNatives() {}
50+
51+
public static void init() {
52+
assert JNA_PROVIDER != null;
53+
}
54+
55+
/**
56+
* Returns the number of allocated bytes on disk for a given file.
57+
*
58+
* @param path the path to the file
59+
* @return an {@link OptionalLong} that contains the number of allocated bytes on disk for the file. The optional is empty is the
60+
* allocated size of the file failed be retrieved using native methods
61+
*/
62+
public static OptionalLong allocatedSizeInBytes(Path path) {
63+
return JNA_PROVIDER.allocatedSizeInBytes(path);
64+
}
65+
66+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.common.filesystem;
10+
11+
import com.sun.jna.Native;
12+
import com.sun.jna.WString;
13+
import com.sun.jna.ptr.IntByReference;
14+
15+
import org.apache.logging.log4j.LogManager;
16+
import org.apache.logging.log4j.Logger;
17+
import org.apache.lucene.util.Constants;
18+
19+
import java.nio.file.Files;
20+
import java.nio.file.Path;
21+
import java.util.OptionalLong;
22+
23+
/**
24+
* {@link FileSystemNatives.Provider} implementation for Windows/Kernel32
25+
*/
26+
final class WindowsFileSystemNatives implements FileSystemNatives.Provider {
27+
28+
private static final Logger logger = LogManager.getLogger(WindowsFileSystemNatives.class);
29+
30+
private static final WindowsFileSystemNatives INSTANCE = new WindowsFileSystemNatives();
31+
32+
private static final int INVALID_FILE_SIZE = -1;
33+
private static final int NO_ERROR = 0;
34+
35+
private WindowsFileSystemNatives() {
36+
assert Constants.WINDOWS : Constants.OS_NAME;
37+
try {
38+
Native.register("kernel32");
39+
logger.debug("windows/Kernel32 library loaded");
40+
} catch (LinkageError e) {
41+
logger.warn("unable to link Windows/Kernel32 library. native methods and handlers will be disabled.", e);
42+
throw e;
43+
}
44+
}
45+
46+
static WindowsFileSystemNatives getInstance() {
47+
return INSTANCE;
48+
}
49+
50+
/**
51+
* Retrieves the actual number of bytes of disk storage used to store a specified file.
52+
*
53+
* https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getcompressedfilesizew
54+
*
55+
* @param lpFileName the path string
56+
* @param lpFileSizeHigh pointer to high-order DWORD for compressed file size (or null if not needed)
57+
* @return the low-order DWORD for compressed file siz
58+
*/
59+
private native int GetCompressedFileSizeW(WString lpFileName, IntByReference lpFileSizeHigh);
60+
61+
/**
62+
* Retrieves the actual number of bytes of disk storage used to store a specified file. If the file is located on a volume that supports
63+
* compression and the file is compressed, the value obtained is the compressed size of the specified file. If the file is located on a
64+
* volume that supports sparse files and the file is a sparse file, the value obtained is the sparse size of the specified file.
65+
*
66+
* This method uses Win32 DLL native method {@link #GetCompressedFileSizeW(WString, IntByReference)}.
67+
*
68+
* @param path the path to the file
69+
* @return an {@link OptionalLong} that contains the number of allocated bytes on disk for the file, or empty if the size is invalid
70+
*/
71+
public OptionalLong allocatedSizeInBytes(Path path) {
72+
assert Files.isRegularFile(path) : path;
73+
final WString fileName = new WString("\\\\?\\" + path);
74+
final IntByReference lpFileSizeHigh = new IntByReference();
75+
76+
final int lpFileSizeLow = GetCompressedFileSizeW(fileName, lpFileSizeHigh);
77+
if (lpFileSizeLow == INVALID_FILE_SIZE) {
78+
final int err = Native.getLastError();
79+
if (err != NO_ERROR) {
80+
logger.warn("error [{}] when executing native method GetCompressedFileSizeW for file [{}]", err, path);
81+
return OptionalLong.empty();
82+
}
83+
}
84+
85+
// convert lpFileSizeLow to unsigned long and combine with signed/shifted lpFileSizeHigh
86+
final long allocatedSize = (((long) lpFileSizeHigh.getValue()) << Integer.SIZE) | Integer.toUnsignedLong(lpFileSizeLow);
87+
if (logger.isTraceEnabled()) {
88+
logger.trace(
89+
"executing native method GetCompressedFileSizeW returned [high={}, low={}, allocated={}] for file [{}]",
90+
lpFileSizeHigh,
91+
lpFileSizeLow,
92+
allocatedSize,
93+
path
94+
);
95+
}
96+
return OptionalLong.of(allocatedSize);
97+
}
98+
}

x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/xpack/searchablesnapshots/cache/common/CacheFileTests.java

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
package org.elasticsearch.xpack.searchablesnapshots.cache.common;
88

99
import org.apache.lucene.store.AlreadyClosedException;
10+
import org.apache.lucene.util.Constants;
11+
import org.apache.lucene.util.LuceneTestCase;
1012
import org.apache.lucene.util.SetOnce;
1113
import org.elasticsearch.common.UUIDs;
14+
import org.elasticsearch.common.filesystem.FileSystemNatives;
1215
import org.elasticsearch.common.util.concurrent.DeterministicTaskQueue;
1316
import org.elasticsearch.core.PathUtils;
1417
import org.elasticsearch.core.PathUtilsForTesting;
@@ -20,16 +23,19 @@
2023
import org.hamcrest.Matcher;
2124

2225
import java.io.IOException;
26+
import java.nio.ByteBuffer;
2327
import java.nio.channels.FileChannel;
2428
import java.nio.file.FileSystem;
2529
import java.nio.file.Files;
2630
import java.nio.file.Path;
2731
import java.util.ArrayList;
32+
import java.util.Arrays;
2833
import java.util.HashSet;
2934
import java.util.Iterator;
3035
import java.util.List;
3136
import java.util.Locale;
3237
import java.util.Objects;
38+
import java.util.OptionalLong;
3339
import java.util.Set;
3440
import java.util.SortedSet;
3541
import java.util.concurrent.ExecutionException;
@@ -38,13 +44,16 @@
3844
import static org.elasticsearch.xpack.searchablesnapshots.cache.common.TestUtils.randomPopulateAndReads;
3945
import static org.hamcrest.Matchers.containsString;
4046
import static org.hamcrest.Matchers.equalTo;
47+
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
4148
import static org.hamcrest.Matchers.hasSize;
4249
import static org.hamcrest.Matchers.instanceOf;
4350
import static org.hamcrest.Matchers.is;
51+
import static org.hamcrest.Matchers.lessThan;
4452
import static org.hamcrest.Matchers.notNullValue;
4553
import static org.hamcrest.Matchers.nullValue;
4654
import static org.hamcrest.Matchers.sameInstance;
4755

56+
@LuceneTestCase.SuppressFileSystems("DisableFsyncFS") // required by {@link testCacheFileCreatedAsSparseFile()}
4857
public class CacheFileTests extends ESTestCase {
4958

5059
private static final CacheFile.ModificationListener NOOP = new CacheFile.ModificationListener() {
@@ -57,7 +66,7 @@ public void onCacheFileDelete(CacheFile cacheFile) {}
5766

5867
private static final CacheKey CACHE_KEY = new CacheKey("_snap_uuid", "_snap_index", new ShardId("_name", "_uuid", 0), "_filename");
5968

60-
public void testGetCacheKey() throws Exception {
69+
public void testGetCacheKey() {
6170
final Path file = createTempDir().resolve("file.new");
6271
final CacheKey cacheKey = new CacheKey(
6372
UUIDs.randomBase64UUID(random()),
@@ -380,6 +389,54 @@ public void testFSyncFailure() throws Exception {
380389
}
381390
}
382391

392+
public void testCacheFileCreatedAsSparseFile() throws Exception {
393+
assumeTrue("This test uses a native method implemented only for Windows", Constants.WINDOWS);
394+
final long oneMb = 1 << 20;
395+
396+
final Path file = createTempDir().resolve(UUIDs.randomBase64UUID(random()));
397+
final CacheFile cacheFile = new CacheFile(
398+
new CacheKey("_snap_uuid", "_snap_name", new ShardId("_name", "_uid", 0), "_filename"),
399+
oneMb,
400+
file,
401+
NOOP
402+
);
403+
assertFalse(Files.exists(file));
404+
405+
final TestEvictionListener listener = new TestEvictionListener();
406+
cacheFile.acquire(listener);
407+
try {
408+
final FileChannel fileChannel = cacheFile.getChannel();
409+
assertTrue(Files.exists(file));
410+
411+
OptionalLong sizeOnDisk = FileSystemNatives.allocatedSizeInBytes(file);
412+
assertTrue(sizeOnDisk.isPresent());
413+
assertThat(sizeOnDisk.getAsLong(), equalTo(0L));
414+
415+
// write 1 byte at the last position in the cache file.
416+
// For non sparse files, Windows would allocate the full file on disk in order to write a single byte at the end,
417+
// making the next assertion fails.
418+
fill(fileChannel, Math.toIntExact(cacheFile.getLength() - 1L), Math.toIntExact(cacheFile.getLength()));
419+
fileChannel.force(false);
420+
421+
sizeOnDisk = FileSystemNatives.allocatedSizeInBytes(file);
422+
assertTrue(sizeOnDisk.isPresent());
423+
assertThat("Cache file should be sparse and not fully allocated on disk", sizeOnDisk.getAsLong(), lessThan(oneMb));
424+
425+
fill(fileChannel, 0, Math.toIntExact(cacheFile.getLength()));
426+
fileChannel.force(false);
427+
428+
sizeOnDisk = FileSystemNatives.allocatedSizeInBytes(file);
429+
assertTrue(sizeOnDisk.isPresent());
430+
assertThat(
431+
"Cache file should be fully allocated on disk (maybe more given cluster/block size)",
432+
sizeOnDisk.getAsLong(),
433+
greaterThanOrEqualTo(oneMb)
434+
);
435+
} finally {
436+
cacheFile.release(listener);
437+
}
438+
}
439+
383440
static class TestEvictionListener implements EvictionListener {
384441

385442
private final SetOnce<CacheFile> evicted = new SetOnce<>();
@@ -440,4 +497,24 @@ private static FSyncTrackingFileSystemProvider setupFSyncCountingFileSystem() {
440497
PathUtilsForTesting.installMock(provider.getFileSystem(null));
441498
return provider;
442499
}
500+
501+
private static void fill(FileChannel fileChannel, int from, int to) {
502+
final byte[] buffer = new byte[Math.min(Math.max(0, to - from), 1024)];
503+
Arrays.fill(buffer, (byte) 0xff);
504+
assert fileChannel.isOpen();
505+
506+
try {
507+
int written = 0;
508+
int remaining = to - from;
509+
while (remaining > 0) {
510+
final int len = Math.min(remaining, buffer.length);
511+
fileChannel.write(ByteBuffer.wrap(buffer, 0, len), from + written);
512+
remaining -= len;
513+
written += len;
514+
}
515+
assert written == to - from;
516+
} catch (IOException e) {
517+
throw new AssertionError(e);
518+
}
519+
}
443520
}

0 commit comments

Comments
 (0)