7
7
package org .elasticsearch .xpack .searchablesnapshots .cache .common ;
8
8
9
9
import org .apache .lucene .store .AlreadyClosedException ;
10
+ import org .apache .lucene .util .Constants ;
11
+ import org .apache .lucene .util .LuceneTestCase ;
10
12
import org .apache .lucene .util .SetOnce ;
11
13
import org .elasticsearch .common .UUIDs ;
14
+ import org .elasticsearch .common .filesystem .FileSystemNatives ;
12
15
import org .elasticsearch .common .util .concurrent .DeterministicTaskQueue ;
13
16
import org .elasticsearch .core .PathUtils ;
14
17
import org .elasticsearch .core .PathUtilsForTesting ;
20
23
import org .hamcrest .Matcher ;
21
24
22
25
import java .io .IOException ;
26
+ import java .nio .ByteBuffer ;
23
27
import java .nio .channels .FileChannel ;
24
28
import java .nio .file .FileSystem ;
25
29
import java .nio .file .Files ;
26
30
import java .nio .file .Path ;
27
31
import java .util .ArrayList ;
32
+ import java .util .Arrays ;
28
33
import java .util .HashSet ;
29
34
import java .util .Iterator ;
30
35
import java .util .List ;
31
36
import java .util .Locale ;
32
37
import java .util .Objects ;
38
+ import java .util .OptionalLong ;
33
39
import java .util .Set ;
34
40
import java .util .SortedSet ;
35
41
import java .util .concurrent .ExecutionException ;
38
44
import static org .elasticsearch .xpack .searchablesnapshots .cache .common .TestUtils .randomPopulateAndReads ;
39
45
import static org .hamcrest .Matchers .containsString ;
40
46
import static org .hamcrest .Matchers .equalTo ;
47
+ import static org .hamcrest .Matchers .greaterThanOrEqualTo ;
41
48
import static org .hamcrest .Matchers .hasSize ;
42
49
import static org .hamcrest .Matchers .instanceOf ;
43
50
import static org .hamcrest .Matchers .is ;
51
+ import static org .hamcrest .Matchers .lessThan ;
44
52
import static org .hamcrest .Matchers .notNullValue ;
45
53
import static org .hamcrest .Matchers .nullValue ;
46
54
import static org .hamcrest .Matchers .sameInstance ;
47
55
56
+ @ LuceneTestCase .SuppressFileSystems ("DisableFsyncFS" ) // required by {@link testCacheFileCreatedAsSparseFile()}
48
57
public class CacheFileTests extends ESTestCase {
49
58
50
59
private static final CacheFile .ModificationListener NOOP = new CacheFile .ModificationListener () {
@@ -57,7 +66,7 @@ public void onCacheFileDelete(CacheFile cacheFile) {}
57
66
58
67
private static final CacheKey CACHE_KEY = new CacheKey ("_snap_uuid" , "_snap_index" , new ShardId ("_name" , "_uuid" , 0 ), "_filename" );
59
68
60
- public void testGetCacheKey () throws Exception {
69
+ public void testGetCacheKey () {
61
70
final Path file = createTempDir ().resolve ("file.new" );
62
71
final CacheKey cacheKey = new CacheKey (
63
72
UUIDs .randomBase64UUID (random ()),
@@ -380,6 +389,54 @@ public void testFSyncFailure() throws Exception {
380
389
}
381
390
}
382
391
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
+
383
440
static class TestEvictionListener implements EvictionListener {
384
441
385
442
private final SetOnce <CacheFile > evicted = new SetOnce <>();
@@ -440,4 +497,24 @@ private static FSyncTrackingFileSystemProvider setupFSyncCountingFileSystem() {
440
497
PathUtilsForTesting .installMock (provider .getFileSystem (null ));
441
498
return provider ;
442
499
}
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
+ }
443
520
}
0 commit comments