logo
0
0
Login
实现update checker

update-checker

A lightweight HTTP service (written in Rust) that fetches and caches the latest matching GitHub Release for multiple repositories. It implements a stale‑while‑revalidate (SWR) caching strategy, flexible filtering (substring / regex / tag regex / latest), optional inclusion of prereleases & drafts, rate‑limit protection, ETag conditional requests, retry with exponential backoff, simple API key authentication, and Prometheus metrics.

Motivation: Client applications (especially anonymous ones) easily hit GitHub's unauthenticated rate limits when polling for updates. Centralizing update lookups through a small cache layer greatly reduces upstream traffic while keeping clients fast & reliable.


Features

  • Multiple repositories, each with its own filter & cache policy
  • SWR caching: fresh TTL + stale window + background refresh
  • Filter strategies: latest, substring, regex, tagregex (force tag only)
  • Optional inclusion of prerelease / draft releases
  • Semantic version comparison fallback when multiple candidates match
  • ETag support (If-None-Match → 304 handling without re-downloading JSON body)
  • GitHub rate limit protection (return cached data when remaining quota is low)
  • Exponential backoff retries for transient upstream errors (network / 429 / 5xx)
  • Per‑repository metrics & basic histograms
  • Simple API key auth via X-Api-Key header (optional)
  • Config file with environment variable expansion ($VAR / ${VAR})
  • JSON responses, Prometheus metrics endpoint, health endpoint

Quick Start

1. Build

cargo build --release

(or cargo run during development)

2. Prepare Configuration

Copy the example configuration:

cp config.example.toml config.toml

Export any referenced environment variables (e.g. GitHub token):

export GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxx

(Optional) Set an API key:

# In [server] api_key = "change_me_secret"

3. Run

./target/release/update-checker

Or point to a custom config path:

APP_CONFIG=/path/to/my.toml ./target/release/update-checker

4. Query

curl -H 'X-Api-Key: change_me_secret' http://localhost:8080/v1/repos curl -H 'X-Api-Key: change_me_secret' http://localhost:8080/v1/updates/example-latest/latest curl http://localhost:8080/healthz

Configuration

See config.example.toml (Chinese annotated). Key sections:

SectionFieldDescription
serverbind / portListen address & port
servergithub_tokenPersonal token to raise GitHub rate limits (env placeholder supported)
serverapi_keyStatic API key; if set, protected endpoints require X-Api-Key
cachedefault_ttlFresh lifetime (e.g. 30m, 1h, 45s)
cachedefault_stale_while_revalidateAdditional stale window enabling SWR
cachemax_concurrent_refreshSemaphore limit for simultaneous upstream fetches
repos[]nameLocal alias exposed via API
repos[]repoowner/project
repos[]include_prereleaseAllow prerelease selection
repos[]include_draftAllow draft selection
repos[].filtertypelatest / substring / regex / tagregex
repos[].filterpatternRequired for non-latest types
repos[].filtermatch_targettag or title (ignored for tagregex)
repos[].cachettlOverride default TTL
repos[].cachestale_while_revalidateOverride default SWR window

Environment variable expansion errors are fatal (missing variables referenced in the file).

Filter Semantics

  • latest: first acceptable release from GitHub's chronological list (after applying prerelease/draft flags)
  • substring: pattern must be a substring of the selected match_target
  • regex: regex applied to chosen match_target (tag or title)
  • tagregex: regex forced on tag regardless of match_target
  • When multiple acceptable releases match, semantic version ordering (if tags parse) is used to pick the greatest; otherwise first match wins.

Caching & SWR

Timeline for one repo:

  1. Fresh (within TTL) → immediate cache hit.
  2. TTL expired but within stale_while_revalidate → return stale response (cache.stale=true) and trigger async refresh (once).
  3. Beyond stale window → synchronous fetch. On failure, if old cache exists it is returned with stale=true & upstream_status=error.
  4. Rate limit low → no active refresh; only cached content returned with upstream_status=rate_limited.

API Summary

Protected endpoints (require X-Api-Key if configured): /v1/repos, /v1/updates/{name}/latest, /v1/status, /v1/metrics.

MethodPathDescription
GET/healthzLiveness probe
GET/v1/reposList configured repositories (alias + owner/project)
GET/v1/updates/{name}/latestLatest matching release with cache metadata
GET/v1/statusPer‑repo operational status (last fetch, counts, cache state)
GET/v1/metricsPrometheus metrics text exposition

/v1/updates/{name}/latest Response Fields

{ "name": "alias", "repo": "owner/project", "version": "v1.2.3", // same as tag "tag": "v1.2.3", "title": "v1.2.3", "published_at": "RFC3339", "body_markdown": "...", "body_excerpt": "...", // first line truncated to 120 chars "url": "https://github.com/...", "assets": [ {"name": "bin", "size": 123, "download_url": "...", "content_type": "...", "updated_at": "..."} ], "cache": { "fetched_at": "...", "expires_at": "...", "swr_expiry": "...", "stale": false }, "upstream_status": "ok | rate_limited | error" }

Error Mapping

HTTPbody.errorMeaning
401unauthorizedMissing / wrong API key
404unknown_repoAlias not configured
404no_matching_releaseRepo exists but nothing matched filters
502upstream_not_foundUpstream reported 404 (repo or releases)
503upstream_unavailableUpstream unavailable & no cache fallback

When cache exists but refresh failed: 200 with upstream_status=error and cache.stale=true.


Metrics (Prometheus)

Collected per repository:

  • update_checker_cache_hits_total{repo}
  • update_checker_cache_stale_returns_total{repo}
  • update_checker_fetch_total{repo,result="ok|error"}
  • update_checker_last_fetch_duration_ms{repo}
  • update_checker_last_fetch_timestamp_seconds{repo}
  • Histogram buckets: update_checker_fetch_duration_seconds_bucket{repo,le="0.1|0.3|1|3|10|+Inf"} + ..._count

Use these to derive success ratios, error rates, and latency percentiles (approx via buckets).


Rate Limit Protection

If X-RateLimit-Remaining from GitHub <= internal threshold (currently hardcoded 2), the service stores reset time & switches that repo into a "cache only" mode until reset. Requests still succeed using cached content, marked with upstream_status=rate_limited. If no cache exists, a 503 upstream_unavailable is returned.


Logging

Uses tracing + tracing-subscriber. By default emits plain text (JSON mode can be enabled in future). Typical fields: repo, event=fetch, result=ok|error, ms, etag_hit / etag_miss.

Set RUST_LOG for filtering, e.g.:

RUST_LOG=info,update-checker=debug ./target/release/update-checker

Internals (High Level)

  • DashMap for concurrent in-memory caches & metrics
  • Per repo async mutex (refresh lock) + atomic flag for SWR background refresh deduplication
  • Global semaphore limits concurrent upstream fetches
  • ETag caching to minimize payload size when unchanged
  • Histogram buckets maintained via atomic counters (no sum presently)

Roadmap / Potential Enhancements

  • JSON structured logging output
  • Add histogram sum & more detailed latency metrics
  • Expose remaining GitHub rate limit in /v1/status
  • Pluggable cache backends (Redis, persistent store)
  • GitHub Webhook ingestion to proactively refresh
  • Asset checksum / signature metadata
  • Multi-tenant namespaces

See todo.md for implementation status (Chinese) and prd.md for the original product requirements document.


Development

Run tests (add when available):

cargo test

Format & lint:

cargo fmt --all cargo clippy --all-targets -- -D warnings

Contribution

Contributions welcome!

  1. Fork & branch (feat/xyz)
  2. Add tests / docs
  3. Run formatter & clippy
  4. Open PR with a concise description

Please update README.md / api.md for externally visible changes.


Security

Do not commit real GitHub tokens. If you discover a security issue (e.g., authentication bypass or data confusion), please open a private issue or contact the maintainer directly before public disclosure.


License

(Choose a license, e.g. MIT / Apache-2.0. Add the license file and update this section.)


FAQ

Q: Why not just query GitHub directly from clients?
Anonymous rate limits are low; consolidating reduces quota pressure and adds resilience when GitHub is briefly unavailable.

Q: Does it cache assets?
No, only metadata & asset URLs.

Q: Can I force a refresh?
Currently no explicit endpoint; forcing happens automatically once TTL + SWR expire, or synchronously if fully expired.

Q: Draft releases not appearing?
Set include_draft = true for that repo (ensure the token has access if the repo is private – private repos not yet supported if token scope insufficient).


Status

Early stage (v0.1.0). Interfaces may evolve; pin versions when packaging.


Happy caching! 🚀