[GH-ISSUE #2846] feat: Persist cached Newt version until next successful fetch (stale-while-revalidate) #13062

Closed
opened 2026-05-13 18:36:43 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @strausmann on GitHub (Apr 13, 2026).
Original GitHub issue: https://github.com/fosrl/pangolin/issues/2846

Problem

The `newtUpdateAvailable` field in the Sites API response intermittently shows `false` for sites running outdated Newt versions. This happens because the cached latest Newt version expires after 1 hour (TTL), and if the subsequent GitHub API fetch fails (timeout, network issue), the cache returns `null` — causing all sites to default to `newtUpdateAvailable: false`.

Current Behavior

In `server/routers/site/listSites.ts`:

  1. `getLatestNewtVersion()` checks cache → if miss, fetches `https://api.github.com/repos/fosrl/newt/tags`
  2. Cache TTL is 3600s (1 hour)
  3. Fetch timeout is 1.5 seconds
  4. If fetch fails after cache expiry → returns `null` → all sites show `newtUpdateAvailable: false`

Real-World Impact

In our self-hosted setup (Hetzner Cloud), the GitHub API fetch intermittently times out (1.5s is tight for DNS resolution + TLS handshake + response). This leads to:

  • Sites on Newt 1.10.1 showing `newtUpdateAvailable: false` on one API call
  • Same sites correctly showing `true` on the next call (when GitHub responds in time)
  • Dashboard showing inconsistent update indicators

Observed Example

Site Newt Version newtUpdateAvailable Expected
Ollama 1.10.1 true true
Docker3 1.10.3 false true
GitLab 1.10.3 false true

All three are outdated, but only Ollama shows the correct indicator (likely queried when GitHub was reachable within the timeout).

Proposed Solution

Stale-while-revalidate pattern: Keep the cached version until a new fetch succeeds, rather than discarding it after TTL.

Suggested Implementation

```typescript
async function getLatestNewtVersion(): Promise<string | null> {
try {
const cachedVersion = await cache.get("latestNewtVersion");

    // Check if we should attempt a refresh (TTL expired)
    const lastFetchTime = await cache.get<number>("latestNewtVersionFetchTime");
    const shouldRefresh = !lastFetchTime || (Date.now() - lastFetchTime > 3600 * 1000);
    
    if (cachedVersion && !shouldRefresh) {
        return cachedVersion;
    }

    // Attempt fetch, but don't discard cached value on failure
    try {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), 3000); // slightly longer timeout

        const response = await fetch(
            "https://api.github.com/repos/fosrl/newt/tags",
            { signal: controller.signal }
        );
        clearTimeout(timeoutId);

        if (response.ok) {
            let tags = await response.json();
            tags = tags.filter((v: any) => !v.name.includes("rc"));
            if (tags.length > 0) {
                const latestVersion = tags[0].name;
                // No TTL — persists until next successful fetch
                await cache.set("latestNewtVersion", latestVersion);
                await cache.set("latestNewtVersionFetchTime", Date.now());
                return latestVersion;
            }
        }
    } catch (fetchError) {
        logger.warn("Failed to refresh Newt version, using cached value");
    }

    // Return stale cached value if fetch failed
    return cachedVersion || null;
} catch (error) {
    return null;
}

}
```

Key Changes

  1. Cache has no TTL — value persists until explicitly replaced by a successful fetch
  2. Separate fetch timestamp — controls when to attempt a refresh (still every hour)
  3. Stale value returned on failure — if fetch fails, the last known version is used instead of `null`
  4. Slightly longer timeout — 3s instead of 1.5s gives more headroom for self-hosted instances with higher latency

Environment

  • Pangolin 1.17.0 (Enterprise Edition)
  • Self-hosted on Hetzner Cloud (Germany)
  • 10 sites, mix of Newt 1.10.x and 1.11.0
Originally created by @strausmann on GitHub (Apr 13, 2026). Original GitHub issue: https://github.com/fosrl/pangolin/issues/2846 ## Problem The \`newtUpdateAvailable\` field in the Sites API response intermittently shows \`false\` for sites running outdated Newt versions. This happens because the cached latest Newt version expires after 1 hour (TTL), and if the subsequent GitHub API fetch fails (timeout, network issue), the cache returns \`null\` — causing all sites to default to \`newtUpdateAvailable: false\`. ### Current Behavior In \`server/routers/site/listSites.ts\`: 1. \`getLatestNewtVersion()\` checks cache → if miss, fetches \`https://api.github.com/repos/fosrl/newt/tags\` 2. Cache TTL is 3600s (1 hour) 3. Fetch timeout is 1.5 seconds 4. **If fetch fails after cache expiry → returns \`null\` → all sites show \`newtUpdateAvailable: false\`** ### Real-World Impact In our self-hosted setup (Hetzner Cloud), the GitHub API fetch intermittently times out (1.5s is tight for DNS resolution + TLS handshake + response). This leads to: - Sites on Newt 1.10.1 showing \`newtUpdateAvailable: false\` on one API call - Same sites correctly showing \`true\` on the next call (when GitHub responds in time) - Dashboard showing inconsistent update indicators ### Observed Example | Site | Newt Version | newtUpdateAvailable | Expected | |------|-------------|--------------------:|----------| | Ollama | 1.10.1 | true | true | | Docker3 | 1.10.3 | **false** | true | | GitLab | 1.10.3 | **false** | true | All three are outdated, but only Ollama shows the correct indicator (likely queried when GitHub was reachable within the timeout). ## Proposed Solution **Stale-while-revalidate pattern**: Keep the cached version until a new fetch succeeds, rather than discarding it after TTL. ### Suggested Implementation \`\`\`typescript async function getLatestNewtVersion(): Promise<string | null> { try { const cachedVersion = await cache.get<string>("latestNewtVersion"); // Check if we should attempt a refresh (TTL expired) const lastFetchTime = await cache.get<number>("latestNewtVersionFetchTime"); const shouldRefresh = !lastFetchTime || (Date.now() - lastFetchTime > 3600 * 1000); if (cachedVersion && !shouldRefresh) { return cachedVersion; } // Attempt fetch, but don't discard cached value on failure try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 3000); // slightly longer timeout const response = await fetch( "https://api.github.com/repos/fosrl/newt/tags", { signal: controller.signal } ); clearTimeout(timeoutId); if (response.ok) { let tags = await response.json(); tags = tags.filter((v: any) => !v.name.includes("rc")); if (tags.length > 0) { const latestVersion = tags[0].name; // No TTL — persists until next successful fetch await cache.set("latestNewtVersion", latestVersion); await cache.set("latestNewtVersionFetchTime", Date.now()); return latestVersion; } } } catch (fetchError) { logger.warn("Failed to refresh Newt version, using cached value"); } // Return stale cached value if fetch failed return cachedVersion || null; } catch (error) { return null; } } \`\`\` ### Key Changes 1. **Cache has no TTL** — value persists until explicitly replaced by a successful fetch 2. **Separate fetch timestamp** — controls when to attempt a refresh (still every hour) 3. **Stale value returned on failure** — if fetch fails, the last known version is used instead of \`null\` 4. **Slightly longer timeout** — 3s instead of 1.5s gives more headroom for self-hosted instances with higher latency ## Environment - Pangolin 1.17.0 (Enterprise Edition) - Self-hosted on Hetzner Cloud (Germany) - 10 sites, mix of Newt 1.10.x and 1.11.0
Author
Owner

@strausmann commented on GitHub (Apr 13, 2026):

Closing this issue — feature requests should be submitted as GitHub Discussions per project guidelines. Re-creating as a discussion in the feature-requests category.

<!-- gh-comment-id:4237822389 --> @strausmann commented on GitHub (Apr 13, 2026): Closing this issue — feature requests should be submitted as GitHub Discussions per project guidelines. Re-creating as a discussion in the feature-requests category.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/pangolin#13062