Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
title: Filestore metadata API no longer returns HTTP 500 when a file is deleted concurrently with a metadata read; the response now matches the not-found case (null entry).
type: fixed # added, changed, fixed, deprecated, removed, dependency_update, security, other
authors:
- name: Eric Pugh
links: []
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changelog entries in this directory typically include a link (PR and/or JIRA) under links: rather than an empty list. Please add an appropriate link entry so the change is traceable from the release notes.

Suggested change
links: []
links:
- https://github.com/apache/solr/pull/4367

Copilot uses AI. Check for mistakes.
13 changes: 10 additions & 3 deletions solr/core/src/java/org/apache/solr/filestore/ClusterFileStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.solr.api.JerseyResource;
Expand Down Expand Up @@ -228,13 +229,14 @@ public static FileStoreDirectoryListingResponse getMetadata(
String parentPath = path.substring(0, path.lastIndexOf('/'));
List<FileStore.FileDetails> l = fileStore.list(parentPath, s -> s.equals(fileName));

dirListingResponse.files =
Collections.singletonMap(path, l.isEmpty() ? null : convertToResponse(l.get(0)));
FileStoreEntryMetadata entry = l.isEmpty() ? null : convertToResponse(l.get(0));
dirListingResponse.files = Collections.singletonMap(path, entry);
break;
case DIRECTORY:
final var directoryContents =
fileStore.list(path, null).stream()
.map(details -> convertToResponse(details))
.filter(Objects::nonNull)
.collect(Collectors.toList());
dirListingResponse.files = Map.of(path, directoryContents);
break;
Expand All @@ -254,7 +256,12 @@ private static FileStoreEntryMetadata convertToResponse(FileStore.FileDetails de
return entryMetadata;
}

entryMetadata.size = details.size();
long size = details.size();
if (size < 0) {
// File was deleted concurrently between listing and reading its attributes.
return null;
}
entryMetadata.size = size;
entryMetadata.timestamp = details.getTimeStamp();
Comment on lines +264 to 265
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

convertToResponse treats size < 0 as a concurrent-delete race, but getTimeStamp() can also now return null on the same race if the file is deleted after size() succeeds. To keep the response consistent with NOFILE semantics, consider treating a null timestamp as a vanished entry as well (return null from convertToResponse).

Suggested change
entryMetadata.size = size;
entryMetadata.timestamp = details.getTimeStamp();
final var timestamp = details.getTimeStamp();
if (timestamp == null) {
// File was deleted concurrently between reading its size and timestamp.
return null;
}
entryMetadata.size = size;
entryMetadata.timestamp = timestamp;

Copilot uses AI. Check for mistakes.
if (details.getMetaData() != null) {
details.getMetaData().toMap(entryMetadata.unknownProperties());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.nio.channels.SeekableByteChannel;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
Expand Down Expand Up @@ -288,6 +289,9 @@ public MetaData getMetaData() {
public Date getTimeStamp() {
try {
return new Date(Files.getLastModifiedTime(realPath()).toMillis());
} catch (NoSuchFileException e) {
// File was deleted concurrently between listing and reading its attributes.
return null;
} catch (IOException e) {
throw new SolrException(
SERVER_ERROR, "Failed to retrieve the last modified time for: " + realPath(), e);
Expand All @@ -303,6 +307,9 @@ public boolean isDir() {
public long size() {
try {
return Files.size(realPath());
} catch (NoSuchFileException e) {
// File was deleted concurrently between listing and reading its attributes.
return -1;
} catch (IOException e) {
throw new SolrException(
SERVER_ERROR, "Failed to retrieve the file size for: " + realPath(), e);
Expand Down
Loading