Skip to content

[BUG] Stale video thumbnail failure markers prevent later retries #1997

Description

@ResonantSpiral

Cosmic-files version:

Installed: 1.5.0~1786455995~24.04~5bdfffa
Apt candidate inspected: 1.6.0~1787090492~24.04~3c9a78a

The installed package was used for the behavioral tests. The relevant negative
cache logic remains present in the inspected 1.6.0 candidate source.

Issue/Bug description:

When an external thumbnail command fails, COSMIC Files writes a versioned
failure marker. For an unchanged source URI, modification time, and size, that
marker prevents later thumbnail attempts in the same COSMIC Files version.

This can leave a valid video showing the generic icon after a transient
thumbnailer failure, even when the exact thumbnail command now succeeds.
Reopening the folder does not recover it. Moving only the matching failure
marker aside and reopening the folder causes COSMIC Files to generate and show
the thumbnail normally.

The freedesktop Thumbnail Managing Standard intentionally defines
application-and-version-specific failure markers to avoid repeatedly processing
known unsupported, broken, or prohibitively expensive files. This report does
not propose removing that mechanism. The issue is that COSMIC Files gives any
unsuccessful external process attempt the same persistent treatment, including
a temporary failure that succeeds on a later identical invocation.

No private media, filenames, tag values, or local identifiers are included
with this report. Failure-marker PNGs should not be attached because their
metadata contains the source URI.

Steps to reproduce:

This is a recovery/retry reproduction for an existing failure marker. The
original transient process failure was not captured, so this report does not
claim a deterministic trigger for that first failure.

  1. Find a readable video that shows a generic icon and has a current-version
    failure marker but no successful thumbnail cache entry.

  2. Confirm that the same thumbnailer entry point and registered size now exit
    successfully. Direct output to /dev/null to avoid retaining a private
    probe frame:

    FILE=/path/to/affected-video.mp4
    cosmic-player --thumbnail /dev/null --size 512 "$FILE"
    echo "$?"
  3. Reopen the directory in COSMIC Files. The generic icon remains.

  4. Calculate and inspect the matching negative-cache entry:

    uri=$(gio info "$FILE" | sed -n 's/^uri: //p' | head -n1)
    key=$(printf '%s' "$uri" | md5sum | cut -d' ' -f1)
    marker="$HOME/.cache/thumbnails/fail/cosmic-files-1.5.0/$key.png"
    test -f "$marker" && echo 'failure marker present'
  5. Back up only that marker outside the active thumbnail cache, then reopen the
    directory.

  6. COSMIC Files now generates a valid thumbnail and displays it in place of the
    generic icon.

Expected behavior:

A transient external-thumbnailer failure should be retried after a bounded
interval, on an explicit refresh, or in a later COSMIC Files session. A valid
unchanged file should not remain suppressed for the lifetime of that version's
failure marker.

Persistent negative caching can remain useful for deterministic unsupported or
invalid files, provided it does not make temporary process or environment
failures effectively permanent.

Other notes:

Redacted local evidence

  • The inspected folder contained both successful video thumbnails and generic
    video icons.
  • A filename-free sweep exercised the exact registered thumbnail command
    against all 74 current-version failure markers in the folder, including one
    source that also had a successful cache entry. Thumbnail output was directed
    to /dev/null, so no private frames were retained.
  • 67 of 74 commands exited 0. Only seven remained repeatable failures: four
    invalid-framerate cases, two structurally empty or near-zero-duration inputs
    reported generically as missing-plugin failures, and one 1 ms extreme-rate
    input that timed out.
  • For one affected H.264/AAC MP4, the exact --size 512 command exited 0.
    After only its 1x1 failure marker was moved to a backup and the folder was
    reloaded, COSMIC Files produced a valid 512-pixel PNG and the thumbnail became
    visible.
  • The source media was not modified. The marker remains preserved locally for
    rollback and inspection.

Media-metadata classification

A filename-free metadata matrix compared cache state with container, codec,
profile, pixel format, resolution, stream counts, duration, start time,
framerate fields, permissions, and metadata tag keys (never values).

Common successful and failed entries substantially overlapped: H.264/AAC,
HEVC/AAC, VP9/Opus, MP4 and WebM containers, common resolutions, ordinary
one-video/one-audio layouts, file modes, and common tag-key sets all appeared
on both sides. No ordinary metadata field separated the 67 successful retries
from existing failure markers.

The four repeatable invalid-framerate cases were different. GStreamer reported
Frame rate: 0/1 for each, while FFmpeg reported a nonzero average rate and
nonzero packet durations. The video library used by COSMIC Player rejects a
zero framerate after reading the negotiated appsink caps. COSMIC Player then
maps that underlying open error to the generic text missing required plugin
in thumbnail mode:

This confirms that some media can fail deterministically because of negotiated
stream properties. It does not account for the much larger class of unchanged
files whose identical command now succeeds. Those stale markers require a
separate retry/revalidation path even if deterministic media failures continue
to be cached.

Privacy-safe controls

Locally generated H.264/AAC MP4 and VP9/Opus WebM fixtures both generated
thumbnails successfully. Five rounds of four concurrent commands against those
fixtures also passed 20 of 20 attempts. The installed source limits thumbnail
generation to four workers. Concurrency at that limit or codec choice alone was
therefore not sufficient to reproduce the original transient failures, and
this report does not assign that initial cause:

  • cosmic-files/src/tab.rs

    Lines 85 to 88 in 5bdfffa

    // Thumbnail generation semaphore - limits parallel thumbnail workers
    // Uses 4 workers for balanced throughput and memory usage
    pub static THUMB_SEMAPHORE: LazyLock<tokio::sync::Semaphore> =
    LazyLock::new(|| tokio::sync::Semaphore::const_new(num_cpus::get().min(4)));

Source-level path

The installed source:

  1. Returns CachedThumbnail::Failed whenever a valid failure marker exists:
    pub fn get_cached_thumbnail(&self) -> CachedThumbnail {
    // If the file is already a thumbnail, just use it so we don't generate
    // cached thumbnails of thumbnails.
    if let (Some(cache_base_dir), Ok(metadata)) = (
    THUMBNAIL_CACHE_BASE_DIR.as_ref(),
    std::fs::metadata(&self.file_path),
    ) && metadata.is_file()
    && self.file_path.starts_with(cache_base_dir)
    {
    return CachedThumbnail::Valid((self.file_path.clone(), None));
    }
    // Use cached thumbnail if it is valid.
    if self.is_thumbnail_valid(&self.thumbnail_path) {
    return CachedThumbnail::Valid((
    self.thumbnail_path.clone(),
    Some(self.thumbnail_size),
    ));
    }
    // Check if there is a fail marker from an earlier failure.
    if self.is_thumbnail_valid(&self.thumbnail_fail_marker_path) {
    return CachedThumbnail::Failed;
    }
    CachedThumbnail::RequiresUpdate(self.thumbnail_size)
    }
  2. Validates both successful thumbnails and failure markers using source URI,
    modification time, and size, without an age or retry condition:
    fn is_thumbnail_valid(&self, thumbnail_path: &Path) -> bool {
    let thumbnail_file = match File::open(thumbnail_path) {
    Ok(file) => file,
    Err(_) => return false,
    };
    let decoder = png::Decoder::new(BufReader::new(thumbnail_file));
    let reader = match decoder.read_info() {
    Ok(reader) => reader,
    Err(err) => {
    log::warn!(
    "failed to decode {} as PNG: {}",
    thumbnail_path.display(),
    err
    );
    return false;
    }
    };
    let texts = &reader.info().uncompressed_latin1_text;
    // Thumb::URI is required and must match.
    let thumb_uri = texts
    .iter()
    .find(|&text| text.keyword == "Thumb::URI")
    .map(|t| &t.text);
    if let Some(thumb_uri) = thumb_uri {
    if *thumb_uri != self.file_uri {
    return false;
    }
    } else {
    return false;
    }
    let metadata = match std::fs::metadata(&self.file_path) {
    Ok(m) => m,
    Err(err) => {
    log::warn!(
    "failed to get metatdata of {}: {}",
    self.file_path.display(),
    err
    );
    return false;
    }
    };
    // Thumb::MTime is required and must match.
    let thumb_mtime = texts
    .iter()
    .find(|&text| text.keyword == "Thumb::MTime")
    .map(|t| &t.text);
    if let Some(thumb_mtime) = thumb_mtime {
    let modified = match metadata.modified() {
    Ok(m) => m,
    Err(err) => {
    log::warn!(
    "failed to get modified from metatdata of {}, {}",
    self.file_path.display(),
    err
    );
    return false;
    }
    };
    let mtime = modified
    .duration_since(UNIX_EPOCH)
    .unwrap_or_default()
    .as_secs()
    .to_string();
    if *thumb_mtime != mtime {
    return false;
    }
    } else {
    return false;
    }
    // Thumb::Size isn't required, but it should be verified if present.
    let thumb_size = texts
    .iter()
    .find(|&text| text.keyword == "Thumb::Size")
    .map(|t| &t.text);
    if let Some(thumb_size) = thumb_size {
    let size = metadata.len();
    if *thumb_size != size.to_string() {
    return false;
    }
    }
    true
  3. Creates a failure marker whenever thumbnail generation returns no image for
    a MIME type with a registered thumbnailer:

    cosmic-files/src/tab.rs

    Lines 2189 to 2203 in 5bdfffa

    // If we weren't able to create a thumbnail, but we should have
    // been able to, create a fail marker so that it isn't tried the
    // next time.
    if let Ok(cacher) = thumbnail_cacher
    && tried_supported_file
    && let Err(err) = cacher.create_fail_marker()
    {
    log::warn!(
    "failed to create thumbnail fail marker for {}: {}",
    path.display(),
    err
    );
    }
    Self::NotImage
  4. Treats any nonzero external-thumbnailer status as a failed generation:

    cosmic-files/src/tab.rs

    Lines 2243 to 2293 in 5bdfffa

    let Some(mut command) = thumbnailer.command(path, file.path(), thumbnail_size) else {
    continue;
    };
    match command.status() {
    Ok(status) => {
    if status.success() {
    match image::ImageReader::open(file.path())
    .and_then(ImageReader::with_guessed_format)
    {
    Ok(reader) => match reader.decode().map(DynamicImage::into_rgba8) {
    Ok(image) => {
    return Some((
    Self::Image(
    widget::image::Handle::from_rgba(
    image.width(),
    image.height(),
    image.into_raw(),
    ),
    None,
    ),
    file,
    ));
    }
    Err(err) => {
    log::warn!("failed to decode {}: {}", path.display(), err);
    }
    },
    Err(err) => {
    log::warn!("failed to read {}: {}", path.display(), err);
    }
    }
    } else {
    log::warn!(
    "failed to run {:?} for {}: {}",
    thumbnailer,
    path.display(),
    status
    );
    }
    }
    Err(err) => {
    log::warn!(
    "failed to run {thumbnailer:?} for {}: {}",
    path.display(),
    err
    );
    }
    }
    }
    None

The failure-marker mechanism comes from the freedesktop
Thumbnail Managing Standard,
which describes preserving failures to avoid repeatedly retrying files that are
unknown, broken, or too expensive to thumbnail. It does not define how an
application should distinguish a durable content failure from a temporary
external-process or environment failure.

The inspected 1.6.0 candidate retains the same behavior:

  • // Use cached thumbnail if it is valid.
    if self.is_thumbnail_valid(&self.thumbnail_path) {
    return CachedThumbnail::Valid((
    self.thumbnail_path.clone(),
    Some(self.thumbnail_size),
    ));
    }
    // Check if there is a fail marker from an earlier failure.
    if self.is_thumbnail_valid(&self.thumbnail_fail_marker_path) {
    return CachedThumbnail::Failed;
    }
    CachedThumbnail::RequiresUpdate(self.thumbnail_size)
    }
  • cosmic-files/src/tab.rs

    Lines 2189 to 2203 in 3c9a78a

    // If we weren't able to create a thumbnail, but we should have
    // been able to, create a fail marker so that it isn't tried the
    // next time.
    if let Ok(cacher) = thumbnail_cacher
    && tried_supported_file
    && let Err(err) = cacher.create_fail_marker()
    {
    log::warn!(
    "failed to create thumbnail fail marker for {}: {}",
    path.display(),
    err
    );
    }
    Self::NotImage

The same failure-marker path is also present in upstream master at
28546795b0f4ff65d0c96d98a503b1dfec0d8e8e (2026-08-20), inspected but not
behaviorally tested:

  • if self.is_thumbnail_valid(&self.thumbnail_path) {
    return CachedThumbnail::Valid((
    self.thumbnail_path.clone(),
    Some(self.thumbnail_size),
    ));
    }
    // Check if there is a fail marker from an earlier failure.
    if self.is_thumbnail_valid(&self.thumbnail_fail_marker_path) {
    return CachedThumbnail::Failed;
  • cosmic-files/src/tab.rs

    Lines 2192 to 2200 in 2854679

    // been able to, create a fail marker so that it isn't tried the
    // next time.
    if let Ok(cacher) = thumbnail_cacher
    && tried_supported_file
    && let Err(err) = cacher.create_fail_marker()
    {
    log::warn!(
    "failed to create thumbnail fail marker for {}: {}",
    path.display(),

Possible fix directions

  • Keep the first external-process failure session-local, and persist a failure
    marker only after a bounded repeated failure.
  • Give failure markers a bounded retry age while retaining URI/mtime/size
    validation.
  • Let an explicit refresh retry negative-cache entries.
  • Distinguish deterministic decode/unsupported-media failures from temporary
    process, resource, or environment failures before writing a persistent
    marker.

Related reports such as
#501 and
#1950 concern thumbnail
resource usage and process concurrency. The installed four-worker limit and
the 20-of-20 bounded concurrency control distinguish this report from those
issues. Neither report covers successful retries being suppressed by the
negative cache.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions