{"record":{"id":"e8f847268b90de28","repo":"elastic/elasticsearch","slug":"expected-an-fsdirectory-but-got-after-unwrapp","errorCode":null,"errorMessage":"expected an FSDirectory but got [{}] after unwrapping [{}]","messagePattern":"expected an FSDirectory but got \\[(.+?)\\] after unwrapping \\[(.+?)\\]","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"libs/gpu-codec/src/main/java/org/elasticsearch/gpu/codec/MemorySegmentUtils.java","lineNumber":64,"sourceCode":"        @Override\n        default void close() {}\n    }\n\n    private MemorySegmentUtils() {}\n\n    /**\n     * Unwraps a {@link Directory} through any {@link FilterDirectory} layers to find the underlying {@link FSDirectory}.\n     * Elasticsearch wraps directories (e.g. {@code Store$StoreDirectory} extends {@code FilterDirectory}), so a direct\n     * cast to {@link FSDirectory} will fail at runtime.\n     *\n     * @throws IllegalArgumentException if the unwrapped directory is not an {@link FSDirectory}\n     */\n    static FSDirectory unwrapFSDirectory(Directory dir) {\n        Directory unwrapped = FilterDirectory.unwrap(dir);\n        if (unwrapped instanceof FSDirectory fsDir) {\n            return fsDir;\n        }\n        throw new IllegalArgumentException(\n            \"expected an FSDirectory but got [\" + unwrapped.getClass().getName() + \"] after unwrapping [\" + dir.getClass().getName() + \"]\"\n        );\n    }\n\n    /**\n     * Creates a file-backed MemorySegment, mapping the first {@param dataSize} bytes from {@param dataFile}, using the\n     * Java {@link FileChannel} API.\n     */\n    static MemorySegmentHolder createFileBackedMemorySegment(Path dataFile, long dataSize) throws IOException {\n        // Unwrap test-only filesystem layers so we get a real FileChannelImpl that supports Arena-based map.\n        Path unwrappedPath = Unwrappable.unwrapAll(dataFile);\n        Arena arena = null;\n        try {\n            arena = Arena.ofConfined();\n            try (FileChannel fc = FileChannel.open(unwrappedPath, Set.of(READ))) {\n                MemorySegment mapped = fc.map(FileChannel.MapMode.READ_ONLY, 0L, dataSize, arena);\n                return new FileBackedMemorySegmentHolder(mapped, arena, dataFile);\n            }","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/elastic/elasticsearch/blob/db6a809a667c081ca1dc7500389d26975573215f/libs/gpu-codec/src/main/java/org/elasticsearch/gpu/codec/MemorySegmentUtils.java#L46-L82","documentation":"Thrown by MemorySegmentUtils.unwrapFSDirectory() when the Lucene Directory, after unwrapping all FilterDirectory layers, is not an FSDirectory (filesystem-backed directory). The GPU codec needs a filesystem path to create file-backed MemorySegments for vector data when the data exceeds MMapDirectory's chunk size and must be manually mapped via FileChannel. RAMDirectory, ByteBuffersDirectory, or other non-filesystem directories cannot provide a real file path.","triggerScenarios":"Calling getContiguousMemorySegment or getContiguousPackedMemorySegment with a Directory that is not backed by the filesystem. This happens when: the vector data is too large for a single mmap chunk (exceeds MMapDirectory.DEFAULT_MAX_CHUNK_SIZE, default 1GB on 64-bit JVMs), AND the Directory is a test/mock directory (RAMDirectory, NIOFSDirectory in test mode, etc.). In production, Elasticsearch always uses FSDirectory via Store.","commonSituations":"Unit/integration tests using RAMDirectory or MockDirectoryWrapper with vector data that exceeds the mmap chunk size; custom Directory implementation that doesn't extend FSDirectory; test fixtures that wrap directories in non-standard ways that prevent unwrapping to FSDirectory.","solutions":["In tests, use FSDirectory (e.g. MMapDirectory or NIOFSDirectory) backed by a temp directory instead of RAMDirectory when testing the GPU codec with large vector segments.","Increase MMapDirectory's max chunk size so the data fits in a single mmap chunk, avoiding the fallback path entirely.","Ensure that test Directory wrappers properly extend FilterDirectory so unwrap() can find the underlying FSDirectory.","In production, verify that the index store configuration uses a filesystem-backed directory."],"exampleFix":"// before — test with RAMDirectory\nDirectory dir = new RAMDirectory();\n\n// after — test with FSDirectory\nPath tmp = Files.createTempDirectory(\"gpu-test\");\nDirectory dir = new MMapDirectory(tmp);","handlingStrategy":"validation","validationCode":"Directory unwrapped = FilterDirectory.unwrap(dir);\nif (!(unwrapped instanceof FSDirectory)) {\n    throw new IllegalStateException(\n        \"GPU codec requires FSDirectory for large vector segments, got: \" + unwrapped.getClass().getName());\n}","typeGuard":"static boolean isFSDirectory(Directory dir) {\n    return FilterDirectory.unwrap(dir) instanceof FSDirectory;\n}","tryCatchPattern":null,"preventionTips":["In tests, use MMapDirectory or NIOFSDirectory with a temp path instead of RAMDirectory for GPU codec tests.","Ensure production index stores use FSDirectory (default in Elasticsearch).","Increase MMapDirectory max chunk size to avoid the fallback path that requires FSDirectory."],"tags":["gpu-codec","memory-segment","directory","test-infrastructure","filesystem"],"backgroundTag":null,"analyzedSha":"db6a809a667c081ca1dc7500389d26975573215f","analyzedAt":"2026-08-12T01:39:14.192Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}