Skip to content

Instantly share code, notes, and snippets.

@tstarck
Created July 18, 2026 13:27
Show Gist options
  • Select an option

  • Save tstarck/9e6ee776475d52b3719543249dd15820 to your computer and use it in GitHub Desktop.

Select an option

Save tstarck/9e6ee776475d52b3719543249dd15820 to your computer and use it in GitHub Desktop.

git_cdn Architecture

This document describes what the git_cdn/ project (Renault's grouperenault/git_cdn, MIT licensed) actually does today, based on a full read of the source under git_cdn/git_cdn/. It is meant as a standalone reference for understanding, running, and eventually improving the existing Python implementation — not as a spec for a rewrite.

All file references are relative to git_cdn/ (e.g. git_cdn/upload_pack.py means git_cdn/git_cdn/upload_pack.py).

Table of contents

  1. Overview
  2. Request lifecycle
  3. Upload-pack caching (UploadPackHandler)
  4. Repo cache (RepoCache)
  5. Pack cache (PackCache / PackCacheCleaner)
  6. LFS caching (LFSCacheManager)
  7. Clone-bundle support (CloneBundleManager)
  8. Locking (git_cdn/lock/)
  9. HTTP client resilience (ClientSessionWithRetry)
  10. Configuration reference
  11. Observability
  12. Deployment topology
  13. Test suite as a spec
  14. Observations & potential improvements
  15. Appendix: file/module map

1. Overview

git_cdn is a git+http(s) caching proxy placed between developers/CI workers and a central git server (GitLab, tested; anything BasicAuth-over-http(s) should work). It's an on-demand mirror: it never mirrors on a cron/webhook basis, only when a client actually asks for something — so it's always guaranteed as fresh as upstream, unlike gitlab geo-replication or periodic mirrors.

It is fully stateless for horizontal scaling: all state lives on local disk under $WORKING_DIRECTORY (git mirrors, pack cache, LFS objects, clone bundles), so any request can be served by any git_cdn instance/worker as long as it has (or can build) the relevant cache entry. Authentication is never stored locally — every request's BasicAuth credentials are forwarded to upstream to re-validate on every request.

The git+http protocol has two phases, and git_cdn treats them very differently:

  • GET .../info/refs — the client asks "what refs do you have?". git_cdn is a pure dumb proxy here: no caching, no interference, so the client always sees the true upstream state.
  • POST .../git-upload-pack — the client sends its haves/wants and expects a pack file back. This is the smart caching phase: git_cdn tries to resolve the request from a local bare-repo mirror (and a cache of previously-computed pack files) before ever talking to upstream.

POST/PUT .../git-receive-pack (push) is always a dumb proxy — pushes are just forwarded, with no local smarts, which is a deliberate simplicity/safety choice (README).

High-level component view (from doc/services.md):

@startuml
!theme cerulean-outline

node gitcdn {
    agent "upload-pack" as gup
    agent "clone-bundle" as gcbd
    agent lfs as glfs
    agent proxify as gp
}

database "clone-bundle" as cbd
database "repo-cache" as rc
database "pack-cache" as pc
database lfs
cloud upstream
cloud google

gcbd -- cbd
gup -- pc
glfs -- lfs
pc .. rc: upload pack

gp ~~ upstream
rc ~~ upstream
lfs ~~ upstream

cbd ~~ google
@enduml

Four independent on-disk caches live under $WORKING_DIRECTORY:

Directory Managed by Purpose
git/ RepoCache Bare git mirror per upstream repo
pack_cache/ PackCache / PackCacheCleaner Cached, ready-to-send git-upload-pack output
lfs/ LFSCacheManager Cached Git LFS objects
bundles/ CloneBundleManager Cached AOSP clone.bundle files

Correction to the README: the README's "Technical features" section claims "Git protocol v1 only. v2 can be implemented... (not yet done)". This is stale — protocol v2 is fully implemented (upload_pack_input_parser_v2.py) and tested end-to-end, including in test_integ.py's real-clone tests (parametrized over v1/v2). Both protocol versions are handled by GitCDN.handle_upload_pack based on the Git-Protocol: version=N request header.


2. Request lifecycle

Bootstrap (app.py)

app.py wires up Sentry (if SENTRY_DSN set), builds a bare aiohttp web.Application(), and passes it to GitCDN(upstream, app) (git_cdn.py), which registers all routes as a side effect of construction. The module-level app object is only built if GITSERVER_UPSTREAM and WORKING_DIRECTORY are both set, so the module can be imported for tests without side effects. helpers.netrc_from_env = lambda: None disables aiohttp's automatic .netrc lookup — git_cdn manages credentials itself via the Authorization header.

Routing (GitCDN.__init__, git_cdn.py:157-210)

Three routes are registered:

  • GET / → liveness check (handle_liveness, returns "live")
  • GET /metrics → Prometheus/OpenMetrics endpoint (serve_metrics)
  • * /{path:.+}routing_handler (everything else, all methods, manual routing inside)

On startup (on_startup hook), a shared aiohttp ClientSession is created (get_session()) and a single LFSCacheManager instance is built, bound to that session. On shutdown, both the proxy session and the clone-bundle module's session are closed.

Per-request flow (routing_handler_routing_handler)

routing_handler clears structlog contextvars and binds a fresh per-request context (uuid, eventloop type, start_time), tracks an in-process parallel_request counter (used only for debug logging, not a real concurrency limiter), and — in a finally — always calls self.stats(response) for Prometheus/logging bookkeeping, whether the request succeeded or raised.

_routing_handler does manual routing (not aiohttp's router) because of git's unusual URL conventions:

  1. find_gitpath(path) (util.py:36) canonicalizes the path to <repo>.git regardless of which git-http endpoint suffix was used (info/refs, git-upload-pack, git-receive-pack, clone.bundle, LFS object URLs, with or without a literal .git in the URL already). Returns None for anything unrecognized. check_path() rejects path traversal (../, leading /).
  2. clone.bundle GET is special-cased first (git_cdn.py:296-301) — no auth enforcement, no browser redirect — because bundles are meant to be fetched unauthenticated (see §7).
  3. redirect_browsers() (git_cdn.py:126) — if User-Agent doesn't look like git or aiohttp (i.e. a real browser hit a .git URL), issues a 308 redirect straight to upstream instead of proxying — avoids surprising a human clicking a repo link.
  4. check_git_url() (git_cdn.py:92, gated by FIX_GIT_URL, default True) — if the URL is missing a .git suffix right before info/refs or info/lfs, 308-redirects to the .git-suffixed URL. This saves a round-trip to the far upstream server for that one common client mistake. (POST git-upload-pack/git-receive-pack paths don't need this — find_gitpath already normalizes those silently, no redirect needed.)
  5. check_auth() (git_cdn.py:84, gated by ENFORCE_AUTH, default False) — if enabled and no Authorization header present, returns 401 immediately (forces the client to re-send with creds) instead of relying on upstream to reject it.
  6. Git protocol version is read from the Git-Protocol header (get_protocol_version, case-insensitive key match), defaulting to 1.
  7. Dispatch:
    • POST .../git-upload-packhandle_upload_pack (the smart caching path, §3)
    • POST/PUT .../git-receive-packproxify (push, pure dumb proxy)
    • GET matching GITLFS_OBJECT_RE (a 64-hex-char sha256 object id) → lfs_manager.get_from_cache directly, skipping the auth check — the code comment argues a 64-byte object id is unguessable without already having repo access, so the extra round-trip isn't worth it.
    • everything else (notably GET .../info/refs, and GET .../info/lfs/objects/batch) → proxify (dumb proxy; the LFS batch response gets special JSON rewriting inside stream_response, see §6)

Dumb proxy (proxify / proxify_with_data, git_cdn.py:358-417)

Builds upstream_url = self.upstream + path, strips hop-by-hop/encoding headers (Host, Transfer-Encoding, Content-Length, Content-Encoding), and issues the request via ClientSessionWithRetry (§9) with retry_on=range(500,600)any 5xx from upstream is transparently retried; 4xx (auth failures, 404s, etc.) are relayed as-is with no retry. The request body is streamed (an aiohttp StreamReader, not fully buffered) unless it's already fully buffered/empty. On success, the response is streamed back chunk-by-chunk (stream_response/writer.write) — with a special case for info/lfs/objects/batch responses (handle_lfs_response, see §6). A ClientConnectionError becomes an HTTP 502; any status ≥400 from upstream is read fully and relayed with the same status/body/headers.

Smart phase (handle_upload_pack, git_cdn.py:422-503)

The whole request body is read into memory (await request.content.read() — unlike the dumb proxy path, this is not streamed; want/have negotiation payloads are normally small, but this is a real difference worth remembering for a reimplementation).

  • Protocol v2, non-fetch command (ls-refs, object-info, or an empty/unknown command) — proxied directly to upstream via proxify_with_data, no caching attempted at all. Only fetch commands get the smart treatment.
  • Protocol v1, or v2 fetch — before doing anything local, git_cdn re-validates auth and repo existence by issuing a GET .../info/refs?service=git-upload-pack to upstream, forwarding the client's Authorization header (ClientSessionWithRetry, no retry range configured so any non-2xx is returned immediately — a comment notes this replaces an older HEAD-based check GitLab no longer supports). Non-200 here is relayed to the client directly, refusing to run anything local. This is deliberate: since git_cdn holds no session state, it can't assume a previous phase's auth still applies — auth is re-checked on the request that would trigger real (possibly expensive) local work.
  • On success, credentials are extracted (get_url_creds_from_auth, decodes+url-quotes the Basic auth pair for safe embedding in a git remote URL), a StreamResponse (HTTP 200, Content-Type: application/x-git-upload-pack-result) is prepare()d — meaning the HTTP status line and headers are already flushed to the client at this point — and an UploadPackHandler (§3) is created and run against the already-prepared writer.
  • After the handler runs, X-GitCDN-Cache-Status: HIT|MISS is set on the response, based on pcache_hit or rcache_hit (used both by tests and by the Prometheus accounting in stats()).

Because the response is prepared before the potentially-fallible local work happens, any unrecoverable error surfacing after that point (e.g. a repo-cache clone that exhausts all retries) can no longer be reported as a clean HTTP error status — see §14.


3. Upload-pack caching (UploadPackHandler)

upload_pack.py implements UploadPackHandler, the core of the "smart" phase. One instance is created per upload-pack request (git_cdn.py:488).

Parsing & cacheability

UploadPackInputParser (v1, upload_pack_input_parser.py) and UploadPackInputParserV2 (v2, upload_pack_input_parser_v2.py) parse the client's pkt-line payload (via PacketLineParser, §5) into wants/haves/capabilities/depth/done/etc., and — critically — compute a stable SHA-256 hash over the sorted caps/haves/wants/depth-lines/done-flag. This hash is invariant to irrelevant details (client git version/agent string, ordering) but changes whenever the actual object graph being requested changes; it's the pack-cache key. Any parse failure sets parse_error=True and assigns a random UUID as the hash (so malformed input is never cached and never crashes the parser — see the v1/v2 fuzz test files in §13).

can_be_cached() (identical logic in both parsers) governs whether the pack-cache path is used at all:

  • haves must be empty and done must be True — i.e. only full clones, not incremental fetches, benefit from the pack cache (fetches always go straight to _execute).
  • v1 only: must advertise side-band or side-band-64k (i.e. must want progress/multiplexed output — plain non-sideband clients bypass caching).
  • Partial clones (filter capability) are never cached.
  • More than one want (multi-branch clone) is only cached if PACK_CACHE_MULTI=true.
  • Shallow (depth) clones are only cached if PACK_CACHE_DEPTH=true.

So by default, only single-branch, non-shallow, non-partial, full clones get pack-cached — everything else (fetches, multi-branch, shallow, filtered clones) always executes locally via _execute() without ever touching the pack cache.

run() control flow

run(parsed_input):
  if parse_error:        write ERR pkt-line, return
  if no wants:            return (empty response)
  if can_be_cached():     _run_with_cache(parsed_input)
  else:                   _execute(parsed_input)

_run_with_cache (read-lock / write-lock / re-check dance)

async with pcache.read_lock():
    if pcache.exists(): pcache_hit=True; send_pack(); return   # fast HIT path

async with pcache.write_lock():
    if not pcache.exists():        # re-check: someone may have raced us to the write lock
        _execute(parsed_input)     # populates rcache if needed, runs git-upload-pack, fills pcache

async with pcache.read_lock():
    if pcache.exists():
        send_pack()
        cache_cleaner.clean()      # rate-limited background eviction, see §5
        return

# only reachable if _execute failed without setting upload_pack_status=="error" in context
# (should not happen; may indicate the just-written cache entry was evicted before re-read)
raise RuntimeError("Run with cache failed")

This is effectively the "retry states" the README alludes to, expressed as lock-guarded re-checks rather than an explicit numbered state machine:

  1. Read-lock check (cheap, shared, allows concurrent readers) — cache hit, done.
  2. Write-lock (exclusive) re-check-then-populate — protects against N concurrent identical clone requests all computing the same pack redundantly; only the first to get the write lock actually runs _execute, the rest fall through and find it already cached.
  3. Final read-lock re-check to serve what was just computed.
  4. If that's still missing and it's not an already-logged upload-pack error, it's an unexpected condition — surfaced as a 500-class exception (but by now, remember, HTTP headers are already flushed as 200 — see §14).

_execute → ensure local mirror has the wants → run git-upload-pack

_execute(parsed_input):
    self.rcache = RepoCache(path, auth, upstream)     # fresh, cheap object, not persistent state
    _ensure_input_wants_in_rcache(parsed_input.wants)
    _upload_pack(parsed_input)

_ensure_input_wants_in_rcache:

  • If the local mirror doesn't exist on disk at all → rcache.update() (full clone+fetch, §4).
  • Else, under a read lock on the repo cache, _missing_want() runs git cat-file --batch-check against all requested wants and checks for "missing" in the output (a FileNotFoundError here — e.g. the directory vanished mid-check because another request just nuked-and-recloned it — is conservatively treated as "missing", forcing an update). If anything is missing → rcache.update() (fetches all refs); if everything is already present → rcache_hit = True (this is itself an X-GitCDN-Cache-Status: HIT, even though no pack-cache file exists yet for this exact want-set — the mirror already had the data, no upstream network round-trip was needed).

_upload_pack then acquires a read lock on the repo cache (so it can't run concurrently with an in-progress mirror update) and, if a global semaphore (sema, bounded by MAX_GIT_UPLOAD_PACK divided across gunicorn workers) is configured, waits on it before actually spawning the subprocess — this bounds how many git-upload-pack processes run concurrently per worker, since each one can be CPU/IO-heavy.

_do_upload_pack — running the subprocess

Spawns git-upload-pack --stateless-rpc <rcache.directory> with GIT_PROTOCOL=version=N, piping stdin/stdout/stderr. asyncio.gather(write_input(...), <stdout consumer>) runs writing-to-stdin and reading-from-stdout concurrently — necessary to avoid the classic subprocess-pipe deadlock (git can start writing to stdout before it's finished reading all of stdin, especially for large want/have lists).

  • If pack-caching is active, the stdout consumer is PackCache.cache_pack (§5), and it's wrapped in asyncio.shield()a client disconnect (task cancellation) does not abort the cache write; the code will wait up to 10 minutes in the finally block for the shielded write to finish so the cache still gets populated for the next client even if this one vanished. If not caching, only GIT_PROCESS_WAIT_TIMEOUT (default 2s) is allowed before the now-useless process is killed.
  • write_input tolerates BrokenPipeError on stdin — this happens legitimately when git-upload-pack decides very early that it can't satisfy the request (e.g. "not our ref") and closes its stdin before the full input has been written.
  • Process lifecycle is managed by ensure_proc_terminated() (util.py:147): wait for natural exit within timeout; if not, SIGTERM and wait up to 30s; if still not, SIGKILL and wait up to 30s; if still not, just log an error (process is abandoned/zombie).
  • On non-zero return code, stderr is read and relayed to the client as a git protocol ERR pkt-line (_write_pack_error) — at the git-wire-protocol level, not as an HTTP error status, since the HTTP response was already started as 200. This is how errors like "not our ref" or "the remote end hung up unexpectedly" reach the git client — the git client's own error handling (exit code 128, specific stderr text) then surfaces to the human running git clone.

4. Repo cache (RepoCache)

repo_cache.py. One RepoCache per repo path is a cheap, disposable object (created fresh on every _execute() call) that just knows the on-disk paths and upstream URL — it holds no persistent in-memory state across requests; all persistence is the bare git directory itself.

On-disk layout: $WORKING_DIRECTORY/git/<repo-path>.git (a real bare git repository, i.e. a git mirror clone), with a sibling lock file <repo-path>.git.lock.

clone()

Runs inside a backoff(BACKOFF_START=0.5, BACKOFF_COUNT=5) retry loop (delays ≈0.5, 1, 2, 4, 8s):

  1. Bundle-assisted fast path: if an AOSP clone-bundle file already exists on disk for this repo's canonical basename (see §7), take a shared lock on the bundle and git clone --bare <bundle_file> <directory>. If that fails, delete the (apparently corrupt) bundle file and fall through to a normal clone on this or the next retry iteration.
  2. If the target directory already exists (leftover from a previous failed attempt), rm -rf it first.
  3. git clone --bare <url-with-creds> <directory> directly from upstream.
  4. Loop until returncode == 0 or retries exhausted; if still failing after all retries, raises HTTPInternalServerError(reason=stderr).

fetch()

git fetch --progress --prune --force --tags <url> +refs/*:refs/remotes/origin/* — fetches everything (all refs, including MRs, plus tags, pruning stale ones), also under the same backoff retry loop. On success, bumps the directory's mtime (utime()).

update() — race-protected refresh

update():
    prev_mtime = self.mtime()
    async with write_lock():
        if not exists():                 clone(); fetch()
        elif prev_mtime == self.mtime():  fetch()   # nobody else updated while we waited
        # else: someone else already updated it while we waited for the lock — skip

The prev_mtime-vs-current-mtime comparison, taken after acquiring the exclusive write lock, is what prevents a thundering herd: if N concurrent requests all discover the mirror is missing the same ref and all call update(), only the first one to get the write lock actually performs the (expensive) git fetch; by the time the others get the lock, the mtime has already moved, so they short-circuit and trust the update that already happened.

Subprocess/utility details

run_git() centralizes subprocess execution, credential redaction in logs (auth string replaced with a truncated+<XX> marker before logging stdout/stderr), and converts an upstream "HTTP Basic: Access denied" stderr message into HTTPUnauthorized. On task cancellation (client disconnect) run_git still awaits communicate() a second time to let the subprocess finish — explicitly to avoid orphaning a process that's still holding the write lock. parse_git_output() scrapes git fetch's human-readable "Receiving objects... done" progress line via regex to feed the repo_cache_received_bytes metric — a somewhat fragile implementation-detail dependency on git's exact stderr wording (see §14). cat_file() runs git cat-file --batch-check --no-buffer to answer "do we have these objects" in a single subprocess round-trip.


5. Pack cache (PackCache / PackCacheCleaner)

pack_cache.py. Content-addressed by the parsed-input hash (§3): $WORKING_DIRECTORY/pack_cache/<hash[:2]>/<hash> — sharded into 256 subdirectories by the hash's first two hex characters to keep any single directory from growing too large.

Validity / atomicity

exists() doesn't just check the file is present — it seeks to the last 4 bytes and checks they equal b"0000" (a git pkt-line flush packet), i.e. a cache entry is only considered valid if it ends with a proper flush marker. This is what makes the cache self-healing against truncated writes: cache_pack() writes to the file directly (not via a temp-file-then-rename), so if the upstream git-upload-pack process errors out mid-stream, the write is aborted (end_with_error=True), whatever partial bytes were captured are flushed straight to the stream_writer (so the current client still gets what was produced, e.g. an early error response) and the partial file is deleted — it's never left behind as a false cache hit for the next client.

Writing (cache_pack)

Wraps the raw subprocess stdout in PacketLineChunkParser (§ below), so the cache write is pkt-line-aware, not a raw byte copy — see the sideband-stripping behavior described in §"Pkt-line parsing" below.

Reading (send_pack)

Streams the cached file to the response writer in PACK_CACHE_CHUNK_SIZE-byte chunks (default 1 MiB), bumping mtime again on every full read (os.utime) so the file's LRU "last used" clock resets on cache hits, not just writes. Tolerates ConnectionResetError (client disconnected mid-send) as a normal early exit rather than an error.

Eviction (PackCacheCleaner)

  • Budget: (PACK_CACHE_SIZE_GB * 1024 - 512) MiB — i.e. the configured size in GB, converted to MiB, minus a fixed 512 MiB safety margin, so eviction kicks in slightly before hitting the literal configured limit.
  • clean() is called opportunistically after every pack-cache hit is served (_run_with_cache), but self-throttles to once per minute by checking the shared clean.lock file's mtime, and runs the actual scan/delete (clean_task) in a single-worker ThreadPoolExecutor so it never blocks the event loop.
  • Eviction is strict LRU by file mtime: scans every file under pack_cache/, sorts by mtime, and deletes the oldest files until back under budget, each under its own per-file lock (so a file currently being read/written isn't evicted out from under an in-flight request).
  • Metrics: pack_cache_used_bytes (current total), pack_cache_evicted_bytes (per file deleted).

Note: PackCacheCleaner only manages pack_cache/. It is wired into the live request path and runs automatically. The other three caches (git/, lfs/, bundles/) have no automatic eviction in the running server at all — see §14 and §15's cache_handler/clean_cache.py entry.

Pkt-line parsing (packet_line.py)

PacketLineParser parses a fully-buffered pkt-line byte string into an iterator of payloads (or FLUSH_PKT/DELIM_PKT/RESPONSE_END_PKT markers for the special zero/one/two-length headers) — used for the small, fully-buffered upload-pack request payloads (§3's input parsers).

PacketLineChunkParser is the streaming counterpart, used only for the (potentially huge) upload-pack response stream while writing to the pack cache. It has one deliberate, notable behavior: git's sideband channel 2 carries human-readable progress text ("Receiving objects..." etc.) multiplexed into the pack stream. The chunk parser drops every sideband-2 packet after the first, and replaces the first one with a synthetic "git-cdn, using cached pack\n" message — so cached pack files never store (now-irrelevant, upstream-specific) progress text, and a client on a cache-miss run gets a clear signal that git_cdn intervened. Any parse anomaly (invalid header, invalid length, or the stream ending without the expected trailing flush packet) raises ParseError, feeding directly into the cache-write abort logic above.


6. LFS caching (LFSCacheManager)

lfs_cache_manager.py. One LFSCacheManager is created once at app startup, bound to the upstream URL and the shared HTTP session; its base_url is refreshed per-request (set_base_url(request.url.origin())) so it works correctly behind different hostnames/reverse proxies.

Batch API rewriting

A GET .../info/lfs/objects/batch request is proxied normally (dumb proxy path), but stream_response special-cases this exact path suffix and routes it through handle_lfs_response: the (possibly gzip-encoded) upstream JSON response is fully buffered, decompressed if needed, and passed to hook_lfs_batch(), which rewrites every object's download/upload/verify action href from the upstream base URL to git-cdn's own base URL — so the LFS client is told to fetch objects from git-cdn, transparently. The (possibly re-gzipped) modified JSON is then streamed back with a corrected Content-Length.

Object download & cache (LFSCacheFile)

Requests for GET .../gitlab-lfs/objects/<sha256> (matched by GITLFS_OBJECT_RE, bypassing the auth check — see §2) are served by LFSCacheManager.get_from_cache, following the same read-lock → write-lock → re-check → populate idiom as the pack cache:

  • On-disk path: $WORKING_DIRECTORY/lfs/<url-dirname>/<hash[:2]>/<hash>, with an optional separately-cached .gzip sibling — the manager can serve either the raw or gzip-compressed variant depending on the current client's Accept-Encoding, independent of what the original downloading client asked for.
  • Download (download()): streams from upstream to a .part (or .part.gzip) temp file; external gunzip and sha256sum subprocesses are used instead of Python stdlib equivalents, explicitly for performance/memory reasons on potentially huge files (code comment). Checksum is verified against the sha256 embedded in the object URL itself; on mismatch the temp file(s) are deleted and the caller retries (up to 3 attempts total) before giving up with HTTPNotFound.
  • The download is asyncio.shield()ed — same rationale as the pack cache: a disconnecting client shouldn't abort a download that could satisfy the next request too.
  • A concurrent-download race is handled the same way as everywhere else: get_from_cache takes a write lock before actually downloading, so a second simultaneous request for the same object waits on that lock rather than triggering a duplicate download.

7. Clone-bundle support (CloneBundleManager)

clone_bundle_manager.py. Purpose: AOSP (Android) repositories are huge; Google publishes pre-built clone.bundle files (geo-replicated) that repo (the AOSP meta-tool) downloads by default and uses to bootstrap a local clone before doing an incremental git fetch — the README claims this cuts bootstrap time from ~3.5 hours to ~25 minutes for some repos.

Flow (handle_clone_bundle):

  1. A HEAD request to CDN_BUNDLE_URL.format(bundle_name) (default a Google Cloud Storage URL, template configurable, empty disables the feature entirely) discovers whether the bundle exists and reads its MD5 (from the x-goog-hash header) and size (Content-Length).
  2. Under a shared lock: if a same-size cached copy already exists on disk, serve it directly (streaming, recomputing the MD5 on the fly for verification — see below); this is the cache_hits fast path.
  3. Otherwise, under an exclusive lock, stream-download the bundle from Google (via BUNDLE_PROXY if set) directly to both the client and the on-disk cache file simultaneously (stream_and_md5sum), computing an MD5 as it goes.
  4. Checksum is verified only after the full transfer completes — deliberately: computing it up-front would mean either buffering the whole (huge) file or being vulnerable to a slow-loris style DoS. If the MD5 mismatches at the end, the cache file is deleted (so the next request re-downloads), but the current client has already received the (possibly corrupt) data — acceptable because repo/git bundles carry their own internal SHA1 checksums and object integrity checks, so a corrupt transfer is caught by the client anyway.

Bundle identity & security notes (from the README, corroborated by code): bundles are identified purely by the basename of the repo path (AOSP canonical naming, with / replaced by _) — meaning bundles are unauthenticated and shared across any repos that happen to have the same canonical basename, and can be downloaded from git_cdn by anyone without credentials (this is intentional: AOSP bundles are public Google-hosted data anyway; setting CDN_BUNDLE_URL="" disables the whole feature if this sharing behavior is undesirable).


8. Locking (git_cdn/lock/)

Needed because git_cdn runs as multiple OS processes (gunicorn spawns GUNICORN_WORKER worker processes, each its own asyncio event loop) all sharing the same WORKING_DIRECTORY on disk — so both intra-process (many coroutines in one worker) and inter-process (separate workers, possibly separate machines with shared/NFS storage) contention need to be handled.

aio_lock.py — the async lock actually used everywhere

FLock (one instance per absolute file path, cached in a process-global LockManager) combines two mechanisms:

  • Intra-process: an in-memory state machine (S.IDLE/ACQUIRING_EX/ACQUIRING_SH/ACQUIRED_EX/ACQUIRED_SH) with FIFO waiter queues (ex_waiters/sh_waiters, implemented as asyncio.Futures) — multiple coroutines in the same worker process asking for the same file's lock share one FLock object and never need to touch fcntl at all to coordinate with each other.
  • Inter-process: a real fcntl.flock() call. A non-blocking attempt is tried first (fast path, no thread hop); on BlockingIOError (another process holds it), a blocking flock() call is dispatched to a dedicated single-worker ThreadPoolExecutor so the event loop isn't blocked while waiting on a lock another process holds.
  • Writer preference: pending exclusive (write) waiters block new shared (read) grants, to avoid write-starvation — explicitly documented as only effective within one process, not across processes.
  • Cancellation safety: if the coroutine awaiting acquire() is cancelled (e.g. client disconnected) after the lock was actually already granted (a narrow race), it immediately releases it rather than leaking a held lock; __aexit__ logs a full traceback for any exception other than CancelledError before releasing.
  • On final release (last holder in this process), the lock file's mtime is bumped — this is deliberately relied upon elsewhere: RepoCache's race-protection in update(), and the offline clean_cache.py script both use lock-file/dir mtime as a proxy for "last actually used" time.
  • A manager.remove_lock() call cleans up the in-memory FLock once nobody holds/wants it, so the per-path registry doesn't grow unboundedly over the process lifetime.

Note: an unused, dead alternative (AsyncIOLockManager, using plain asyncio.Lock()) is left in the file with its instantiation commented out — apparently an earlier, intra-process-only approach that was superseded (see §14).

file_lock.py — synchronous counterpart

A plain blocking fcntl.flock wrapper, explicitly documented as not for use on the main event loop — only used from the PackCacheCleaner's background executor thread and from the standalone cache_handler/clean_cache.py CLI script.


9. HTTP client resilience (ClientSessionWithRetry)

client_session.py. An async context manager wrapping a single upstream HTTP call with retry/backoff, used for every upstream call in the app (dumb proxy, the upload-pack auth/existence pre-check, LFS object downloads).

  • Retries with backoff(0.1, REQUEST_MAX_RETRIES) (default 10 retries, delays doubling from 0.1s — a fairly long tail, over 50s if fully exhausted) whenever the response status falls in the caller-supplied retry_on range (every call site in this codebase uses range(500, 600)any 5xx is retried transparently; 4xx and other statuses are returned immediately, no retry, treated as legitimate application-level responses to relay to the client as-is).
  • On aiohttp.ClientConnectionError (as opposed to an HTTP error status), retries too, and every REQUEST_RESET_SESSION_ON_FAILURE-th consecutive failure (default 3) recreates the entire shared ClientSession (get_session(reset=True)) — the assumption being that a broken connection might mean the whole connection pool/keep-alive state is stuck, not just one socket. Exhausting all retries re-raises the connection error (callers like proxify_with_data translate that into an HTTP 502 to the client).
  • metric_upstream_responses_total is incremented on every attempt, including retries — so under retry storms this metric over-counts relative to "one request in, one delegated-to-upstream request out" (see §14).
  • __aexit__ always closes the last response object, regardless of how many retries happened.

This is a distinct retry mechanism from RepoCache's own backoff for git clone/git fetch (§4) — the two are not unified; ClientSessionWithRetry operates purely at the HTTP-request level, while RepoCache's retries wrap whole git subprocess invocations.


10. Configuration reference

Everything is environment-variable driven — no config files, no CLI flags for the running app itself (only the offline clean_cache.py script takes CLI args). This table is a superset of the README's own config section — several of these are real, tested env vars not documented there.

Variable Default Where used Purpose
GITSERVER_UPSTREAM (required) app.py, gitcdn.py Upstream git+http(s) base URL
WORKING_DIRECTORY /tmp/workdir util.py (WORKDIR) Root of all on-disk caches
GUNICORN_WORKER cpu_count() config.py Number of gunicorn worker processes
GUNICORN_WORKER_CLASS aiohttp.worker.GunicornWebWorker config.py e.g. swap for the uvloop variant
GUNICORN_KEEPALIVE 2 config.py Gunicorn keep-alive seconds
LOGGING_SERVER (unset → console) git_cdn.py, log.py host:port UDP JSON log sink (vector.dev)
LOGGING_CONTEXT (unset) git_cdn.py Include contextvars fields in console logs
SENTRY_DSN (unset) app.py, clean_cache.py Enable Sentry crash reporting
SENTRY_ENV dev app.py Sentry environment tag
INSTANCE_NAME git-cdn log.py Instance name tag in UDP JSON logs
PACK_CACHE_SIZE_GB 20 pack_cache.py Pack cache size budget before eviction
PACK_CACHE_MULTI false upload_pack_input_parser(_v2).py Also cache multi-branch clones
PACK_CACHE_DEPTH false upload_pack_input_parser(_v2).py Also cache shallow clones
PACK_CACHE_CHUNK_SIZE 1048576 (1MiB) pack_cache.py Read/write chunk size for cache files
https_proxy (unset) picked up via aiohttp trust_env=True Proxy for main upstream traffic
BUNDLE_PROXY (unset) clone_bundle_manager.py Proxy specifically for AOSP bundle fetches
CDN_BUNDLE_URL Google AOSP URL template clone_bundle_manager.py Empty disables clone-bundle feature
CHUNK_SIZE 32768 clone_bundle_manager.py, upload_pack.py Streaming chunk size (bundles, non-cached upload-pack)
REQUEST_MAX_RETRIES 10 client_session.py Max retries for upstream HTTP calls
REQUEST_RESET_SESSION_ON_FAILURE 3 client_session.py Consecutive conn-error threshold to reset session
BACKOFF_START 0.5 repo_cache.py Initial git clone/fetch backoff delay
BACKOFF_COUNT 5 repo_cache.py Number of git clone/fetch backoff retries
GIT_PROGRESS_OPTION --progress repo_cache.py Option passed to git clone/fetch
MAX_CONNECTIONS 10 git_cdn.py, clone_bundle_manager.py Max upstream TCP connections (see §14 for a bug here)
MAX_GIT_UPLOAD_PACK cpu_count() git_cdn.py Max concurrent git-upload-pack subprocesses (per worker)
GIT_SSL_NO_VERIFY (unset → verify) git_cdn.py, clone_bundle_manager.py Disable upstream SSL verification (staging only!)
GIT_PROCESS_WAIT_TIMEOUT 2 util.py Seconds to wait post-SIGTERM before SIGKILL
ENFORCE_AUTH False git_cdn.py Reject requests with no Authorization header outright
FIX_GIT_URL True git_cdn.py Auto-redirect to append missing .git suffix
PROMETHEUS_ENABLED (unset/false) metrics.py, config.py Enable /metrics + multiproc setup
PROMETHEUS_MULTIPROC_DIR (unset) metrics.py, util.py Shared dir for cross-worker metric aggregation (wiped on start)
POD_NAME socket.gethostname() metrics.py Hostname label on gauges
PORT 8000 app.py Port for direct (non-gunicorn) run

config.py (top-level, loaded via gunicorn -c config.py) additionally sets a 3600s hard worker timeout and 300s graceful timeout (clones can be slow), routes gunicorn's own access log to /dev/null (structlog covers this instead), and works around a known gunicorn/asyncio bug (asyncio.set_child_watcher(FastChildWatcher()), benoitc/gunicorn#3333) that otherwise spams spurious "Unknown child process" errors when reaping finished git subprocesses.


11. Observability

Prometheus metrics (metrics.py)

All under the git_cdn namespace. Gated behind PROMETHEUS_ENABLED; if PROMETHEUS_MULTIPROC_DIR is set, metrics aggregate across gunicorn worker processes via a file-based collector (wiped clean on every gunicorn startup, config.py's on_starting hook). Supports both classic Prometheus text exposition and OpenMetrics (content-negotiated via Accept: application/openmetrics-text).

Metric Type Meaning
git_cdn_requests_total Counter Every HTTP request served
git_cdn_response_status_total{status} Counter Requests by response status code
git_cdn_request_time_seconds Summary End-to-end request duration
git_cdn_total_bytes_sent Summary All response bytes sent
git_cdn_upstream_responses_total Counter Every attempt (incl. retries) at an upstream HTTP call
git_cdn_cache_hit_bytes_sent Summary Bytes sent when X-GitCDN-Cache-Status: HIT
git_cdn_cache_miss_bytes_sent Summary Bytes sent when X-GitCDN-Cache-Status: MISS
git_cdn_nocache_bytes_sent Summary Bytes sent for responses with no cache-status header at all
git_cdn_stats_write_seconds Summary Time spent in the metrics-bookkeeping function itself
git_cdn_workdir_filesystem_avail_bytes{gitcdn_hostname} Gauge Free space on WORKDIR's filesystem
git_cdn_workdir_filesystem_size_bytes{gitcdn_hostname} Gauge Total size of WORKDIR's filesystem
git_cdn_pack_cache_evicted_bytes Summary Per-eviction size
git_cdn_pack_sent_bytes{cache_status} Counter Bytes served directly from the pack cache file, labeled hit/miss
git_cdn_pack_cache_used_bytes{gitcdn_hostname} Gauge Current total pack-cache disk usage
git_cdn_repo_cache_received_bytes Summary Bytes received from upstream while updating repo mirrors

A useful accounting identity, exercised directly in test_metrics.py: total_bytes_sent == nocache_bytes_sent + cache_hit_bytes_sent + cache_miss_bytes_sent.

Logging (log.py)

Built on structlog layered over stdlib logging, in one of two mutually-exclusive modes:

  • Console (default): human-readable, split across stdout (below ERROR) / stderr (ERROR+), optionally including bound contextvars fields if LOGGING_CONTEXT is set.
  • UDP JSON (LOGGING_SERVER=host:port set): a custom DatagramHandler subclass serializes each log record (merged with structlog contextvars/threadlocal state) as JSON over UDP, meant to feed Vector (vector.dev, see docker-compose.yml/vector/vector.toml.example). Blocks at startup (up to 120s) until the log host's DNS resolves, to avoid silently dropping the first burst of logs. Oversized messages (>60000 bytes, staying under UDP's ~64KB limit) are truncated to 10000 chars and flagged truncated: true rather than being dropped or split.

structlog.contextvars threads per-request context (uuid, event loop type, start time, request path, handler type, protocol version, cache hit/miss, CI-related headers, exception info) through every log call for that request — per the README, upload_pack.py uses this most exhaustively; other modules use structlog but less thoroughly. hide_auth_on_headers() redacts the Authorization header value before it's ever logged (keeps first 10 + last char, masks the middle). Sentry integration hooks into the same structlog pipeline so crash reports carry the same contextual fields as the structured logs.


12. Deployment topology

  • Docker images: Dockerfile (Alpine, apt-installed git, sets PROMETHEUS_ENABLED=true and a default PROMETHEUS_MULTIPROC_DIR in the image) and Dockerfile-slim (Debian slim, builds git from source for a newer version than the distro provides, doesn't default-enable Prometheus, adds --worker-tmp-dir /dev/shm to avoid disk I/O for gunicorn's internal heartbeat files). Both entrypoints run gunicorn git_cdn.app:app -c config.py --bind :8000.
  • Gunicorn hosts N aiohttp.worker.GunicornWebWorker (or GunicornUVLoopWebWorker) processes — this is the horizontal-scaling mechanism on a single host; combined with running multiple git_cdn instances behind nginx/a load balancer for scaling across hosts. PROMETHEUS_MULTIPROC_DIR is required to get correct metrics across these worker processes.
  • systemd (deploy/gitcdn.service): wraps a plain docker run, Restart=always, kills/removes any pre-existing gitcdn container before starting, mounts the working directory as a volume, PartOf=docker.service so it restarts alongside Docker itself.
  • nginx (deploy/nginx.conf): TLS termination (BasicAuth is only safe over TLS!), very long timeouts (3600s, matching gunicorn's own worker timeout — clones can be slow), proxy_request_buffering off (streams request bodies through, important for large pushes/LFS uploads), client_max_body_size 200G. Notably implements canary/A-B deployment via nginx's split_clients directive, weighting traffic between two upstream blocks on different ports — this is how rolling upgrades are done without a dedicated orchestrator like Kubernetes.
  • docker-compose.yml is dev-only tooling: spins up a local Vector instance on UDP port 3465 matching LOGGING_SERVER for local structured-log testing. (Its pinned image is pulled from Renault's internal artifactory registry, so it may not be pullable outside their network — logging just falls back to console mode if LOGGING_SERVER is left unset.)
  • Local dev: make dev (poetry-managed venv, pinned poetry/python versions via .poetry-version/.python-version), then make run (gunicorn -c config.py git_cdn.app:app), reading required env vars from a git-ignored tosource file (see tosource.example).

13. Test suite as a spec

The test suite (git_cdn/tests/) is arguably the most precise behavioral spec available — more precise than the README, and directly executable. Summary of what each file locks down:

Test file What it guarantees
test_packet_line.py pkt-line parsing (v1 buffered + streaming chunk parser) is correct on well-formed input and raises cleanly on malformed input; includes a benchmark
test_upload_pack_input.py v1 input parser: want/have/caps extraction, stable cache-key hash invariant to client version, graceful parse_error on malformed input, fuzzed against a large corpus of real captured client inputs
test_upload_pack_input_v2.py v2 input parser: same, plus an extensive battery of protocol-v2-specific malformed-input edge cases (out-of-order pkts, missing command=, duplicate command=, unexpected delim/response-end pkts, missing terminating flush) — must never crash
test_pack_cache.py cache write/read round-trip is byte-exact; aborted/truncated writes never leave a valid cache entry; corruption is detected; upload-pack errors are never cached; PackCacheCleaner LRU eviction behavior
test_lock.py FLock acquire/release ordering (ex-then-sh, sh-then-sh), cancellation safety at multiple race windows, and a multi-process "monkey" stress test (lock_monkey.py) hammering one lock file
test_run_git.py task cancellation during a git subprocess call does not kill the subprocess
test_ensure_process_terminated.py SIGTERM→SIGKILL escalation logic and timing
test_find_directory.py find_gitpath() correctly normalizes all supported URL suffix forms
test_auth_quote.py BasicAuth header decoding + URL-safe quoting of user/password
test_utils.py remove_git_credentials() redacts secrets from logged command args
test_connexion.py ClientSessionWithRetry: recovers from transient connection errors and 5xx; exhausts retries and surfaces 502/last-status; non-retryable upstream errors (4xx, etc.) fail fast (1 try)
test_lfs_cache_manager.py LFS batch URL rewriting; gzip-aware download/cache; checksum-triggered 404; cache hit never touches upstream; concurrent-download coordination
test_clone_bundle_manager.py bundle download+cache with MD5/size verification; cache hit skips re-download; corrupted cache self-heals (possibly after serving the corrupt copy once)
test_metrics.py Prometheus counters/gauges/summaries update correctly, incl. the total == nocache+hit+miss identity; OpenMetrics content negotiation
test_upload_pack.py UploadPackHandler core logic: dedup of duplicate wants; large unknown-want sets triggering git's own early-exit ("not our ref"); fetch-needed path; shallow/depth clones (incl. a truncated-input edge case producing "the remote end hung up unexpectedly"); malformed input; empty (flush-only) input; _missing_want/_ensure_input_wants_in_rcache logic
test_integ.py real end-to-end integration against a live git server (gitlab.com by default, or UNDER_TEST_APP for a deployed instance): auth redirect flow, protocol v1 and v2 clones, nonexistent-branch failure, .git-suffix auto-fix, shallow/partial clones, full LFS clone+checkout, optional push tests (PUSH_TESTS env var), parallel-clone stress at increasing concurrency, clone-bundle end-to-end, non-git User-Agent redirect

Collectively, these tests encode the invariants that matter most for correctness: pkt-line parsing must never crash on malformed input; cache writes must be atomic and self-healing; locking must be safe under cancellation and cross-process contention; retry/backoff must distinguish transient from application errors; and metric accounting must be exact.


14. Observations & potential improvements

These are things noticed while reading the code — candidates to look into, not prescribed fixes.

  1. MAX_CONNECTIONS per-worker division has a floor bug that likely defeats its own purpose. git_cdn.py:158-160:

    MAX_CONNECTIONS = int(max(10, int(os.getenv("MAX_CONNECTIONS", "10")) / GUNICORN_WORKER_NB))

    The floor is 10, not 1. Compare to the very next block, MAX_SEMAPHORE, which correctly floors at 1. With the default MAX_CONNECTIONS=10 and, say, 8 gunicorn workers, the intended per-worker share is 10/8 = 1.25, but max(10, 1.25) evaluates to 10 — so each of the 8 workers actually gets a pool of 10 connections, for a real system-wide total of 80, not the "10" an operator setting MAX_CONNECTIONS=10 would reasonably expect. The division only has any effect once MAX_CONNECTIONS > 10 * GUNICORN_WORKER_NB. This looks like a copy-paste artifact (floor value 10 reused from the default instead of 1). Worth confirming with the maintainers and likely worth a one-line fix + a regression test.

  2. CloneBundleManager has its own separate, non-worker-divided MAX_CONNECTIONS pool. clone_bundle_manager.py:21: MAX_CONNECTIONS = int(os.getenv("MAX_CONNECTIONS", "10")) reuses the same env var name as the main proxy pool (git_cdn.py) but applies no per-worker division at all — so it's a second, independently-sized connection pool to a different upstream (Google Cloud Storage) sharing a config name with the first, which is a bit confusing and easy to misconfigure believing one setting controls all outbound connections.

  3. Repo mirrors (git/), LFS objects (lfs/), and clone bundles (bundles/) have no automatic eviction in the running server. Only pack_cache/ is auto-managed (PackCacheCleaner, wired into the live request path). The other three only shrink if an operator runs the separate cache_handler/clean_cache.py CLI script manually or via an external cron — it's not invoked anywhere in the app's own lifecycle. On a long-lived deployment mirroring many/large repos, disk usage for git//lfs/bundles is effectively unbounded unless someone has set up that cron job. Worth checking whether this is documented /scheduled in the actual production deployment, and considering whether it should be built into the running server (even as an opt-in periodic task) rather than left as an entirely separate operational step.

  4. A response can start streaming (HTTP 200, headers flushed) before the local repo mirror is guaranteed to be buildable. In handle_upload_pack (git_cdn.py:478-496), the StreamResponse is prepare()d — sending the 200 status and headers — before UploadPackHandler.run() is awaited. If that later fails all the way down to RepoCache.clone()/fetch() exhausting their backoff retries (raising HTTPInternalServerError), there's no way to actually send a 500 anymore; the client instead sees an abruptly-closed/incomplete stream. This is presumably intentional (the smart-caching phase's own errors are meant to surface via a git-protocol ERR pkt-line, not an HTTP status — see §3), but the specific clone()/fetch() exhausted-retries path raises an HTTP exception type rather than writing a pkt-line error, so in that one case the client just sees a broken connection rather than a clean git-level error message. Worth a targeted look (and maybe a test) at exactly what a client experiences when upstream is down long enough to exhaust RepoCache's 5-retry backoff mid-clone.

  5. git_cdn_upstream_responses_total counts retry attempts, not distinct requests. client_session.py:40 increments this metric inside the retry loop, once per HTTP attempt — so under sustained upstream 5xx errors, one client-facing request can inflate this counter by up to REQUEST_MAX_RETRIES (10 by default). The README describes it as "total requests delegated to the upstream without caching," which reads as one-per-request. Minor, but could mislead someone tuning alerts off this metric during an upstream outage (it will spike much harder than actual client traffic).

  6. Dead code: git_cdn/lock/aio_lock.py contains an unused AsyncIOLockManager class and a commented-out manager = AsyncIOLockManager() line — looks like an abandoned earlier approach (pure asyncio.Lock(), presumably before the cross-process fcntl requirement was addressed). Safe to delete if confirmed unused elsewhere.

  7. parse_git_output()'s upstream-received-bytes metric depends on scraping git's human-readable stderr text (repo_cache.py:38-62, regex matching "Receiving objects: ..., done."). This is inherently fragile against git version/locale changes (the README's own "Git version sensitive" section acknowledges similar issues elsewhere) — a silent regex-match failure just skips the metric (logged at debug level) rather than erroring, so a future git version change could silently zero out repo_cache_received_bytes without any visible failure.

  8. README config-var documentation is incompletePACK_CACHE_MULTI, PACK_CACHE_DEPTH, PACK_CACHE_CHUNK_SIZE, REQUEST_RESET_SESSION_ON_FAILURE, ENFORCE_AUTH, and FIX_GIT_URL are all real, tested, behavior-changing env vars absent from the README's configuration table (§10 above lists the full set). Low-risk, high-value fix.

  9. The stale "Git protocol v1 only" claim in the README's "Technical features" section (see §1) should be corrected — it may currently discourage people from relying on v2 behavior that is, in fact, implemented and tested.

  10. Dockerfile (the source-built-git variant, formerly Dockerfile-slim) was actually broken, and has been patched during this review — confirmed by building it locally end-to-end:

    • It never installed the git package before running git clone https://github.com/git/git to build git from source — git: not found. Fixed by adding git to the apt-get install list (only needed transiently, to fetch the source).
    • It cloned git's unpinned master branch. Git upstream has been incrementally adopting Rust since Git 2.49 (optional at first); the version currently at master defaults to requiring a Rust toolchain (cargo) to build, which this image doesn't install — cargo: not found. Git 3.0 will make this mandatory outright. Fixed by installing git as a distro package instead of building from source at all — Debian trixie (this image's base either way) already ships git 2.47.3, recent enough for git_cdn's needs (protocol v2, partial clone/uploadpack.allowfilter) without building anything. This sidesteps the Rust dependency question entirely rather than chasing it (adding a Rust toolchain, or pinning to an older git tag that will itself eventually cross the same threshold as git's Rust adoption progresses).
    • Separately, the base image tag was python:slim (a rolling tag, currently resolving to Python 3.14) rather than a pinned version — this caused pip install to fail compiling ujson's C extension (gcc: No such file or directory), because PyPI has no prebuilt wheel yet for that Python ABI for the pinned dependency versions in requirements.txt/poetry.lock. Fixed by pinning to python:3.12-slim (matching the project's own .python-version) — confirmed via a dry-run pip install that this version has prebuilt wheels for all C-extension dependencies (aiohttp, uvloop, ujson), so no compiler is needed at all. Both tags share the identical Debian trixie base, so this doesn't affect the git version.
    • Verified end-to-end after these fixes: built the image, ran it as a container with GITSERVER_UPSTREAM=https://github.com/, and cloned a small public repo (octocat/Hello-World) through it twice — the second clone reused the local repo mirror (rcache_hit: no git fetch to GitHub, only a cat-file check; ~4s → ~0.7s). A default git clone (no --single-branch) requested 3 branch tips and so was not pack-cached (can_be_cached() requires a single want by default — exactly as documented in §3), confirming in practice why the README recommends --single-branch for CI. Repeating with --single-branch did populate pack_cache/ on the first clone and stayed fast on the second, this time via the pack cache too. Solid, real-world confirmation that the caching design works as documented.

15. Appendix: file/module map

Path Responsibility
app.py aiohttp app bootstrap, Sentry init, module-level app/main() entrypoints
git_cdn.py GitCDN — manual routing, auth/redirect checks, dumb proxy, upload-pack dispatch, Prometheus/log request bookkeeping
upload_pack.py UploadPackHandler — the smart caching phase's core control flow
upload_pack_input_parser.py Protocol v1 want/have/caps parser + cache-key hash + can_be_cached()
upload_pack_input_parser_v2.py Protocol v2 (command-based) equivalent
packet_line.py git pkt-line wire format: buffered (PacketLineParser) + streaming (PacketLineChunkParser) parsers
repo_cache.py RepoCache — bare-mirror clone/fetch/update, subprocess + backoff-retry management
pack_cache.py PackCache (content-addressed cache file) + PackCacheCleaner (LRU eviction)
lfs_cache_manager.py LFSCacheManager/LFSCacheFile — LFS batch URL rewriting + object cache
clone_bundle_manager.py CloneBundleManager — AOSP clone.bundle proxy/cache with checksum verification
client_session.py ClientSessionWithRetry — retry/backoff/session-reset wrapper for upstream HTTP calls
lock/aio_lock.py Async, cross-process-safe shared/exclusive file lock (FLock/Lock/LockManager)
lock/file_lock.py Synchronous fcntl lock wrapper for non-event-loop contexts
cache_handler/common.py Cache-scanning helpers (GitRepo/LfsFile/BundleFile prune models) shared by the offline cleaner
cache_handler/clean_cache.py Standalone CLI: scans/deletes old git/LFS/bundle cache entries down to a disk-free threshold (not auto-invoked by the server)
metrics.py Prometheus metric definitions + /metrics endpoint (classic + OpenMetrics)
log.py structlog configuration: console renderer, UDP-JSON-to-vector.dev handler, Sentry hooks
util.py Shared helpers: path validation, URL/credential handling, subprocess lifecycle (ensure_proc_terminated), backoff generator
config.py (top-level) Gunicorn configuration (workers, timeouts, child-watcher workaround, Prometheus multiproc hook)
gitcdn.py (top-level) Dev-only direct-run entrypoint (python gitcdn.py)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment