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).
- Overview
- Request lifecycle
- Upload-pack caching (
UploadPackHandler) - Repo cache (
RepoCache) - Pack cache (
PackCache/PackCacheCleaner) - LFS caching (
LFSCacheManager) - Clone-bundle support (
CloneBundleManager) - Locking (
git_cdn/lock/) - HTTP client resilience (
ClientSessionWithRetry) - Configuration reference
- Observability
- Deployment topology
- Test suite as a spec
- Observations & potential improvements
- Appendix: file/module map
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 itshaves/wantsand 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
@endumlFour 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.
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.
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.
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:
find_gitpath(path)(util.py:36) canonicalizes the path to<repo>.gitregardless 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.gitin the URL already). ReturnsNonefor anything unrecognized.check_path()rejects path traversal (../, leading/).clone.bundleGET 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).redirect_browsers()(git_cdn.py:126) — ifUser-Agentdoesn't look like git or aiohttp (i.e. a real browser hit a.gitURL), issues a 308 redirect straight to upstream instead of proxying — avoids surprising a human clicking a repo link.check_git_url()(git_cdn.py:92, gated byFIX_GIT_URL, defaultTrue) — if the URL is missing a.gitsuffix right beforeinfo/refsorinfo/lfs, 308-redirects to the.git-suffixed URL. This saves a round-trip to the far upstream server for that one common client mistake. (POSTgit-upload-pack/git-receive-packpaths don't need this —find_gitpathalready normalizes those silently, no redirect needed.)check_auth()(git_cdn.py:84, gated byENFORCE_AUTH, defaultFalse) — if enabled and noAuthorizationheader present, returns 401 immediately (forces the client to re-send with creds) instead of relying on upstream to reject it.- Git protocol version is read from the
Git-Protocolheader (get_protocol_version, case-insensitive key match), defaulting to1. - Dispatch:
POST .../git-upload-pack→handle_upload_pack(the smart caching path, §3)POST/PUT .../git-receive-pack→proxify(push, pure dumb proxy)GETmatchingGITLFS_OBJECT_RE(a 64-hex-char sha256 object id) →lfs_manager.get_from_cachedirectly, 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, andGET .../info/lfs/objects/batch) →proxify(dumb proxy; the LFS batch response gets special JSON rewriting insidestream_response, see §6)
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.
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-
fetchcommand (ls-refs,object-info, or an empty/unknown command) — proxied directly to upstream viaproxify_with_data, no caching attempted at all. Onlyfetchcommands get the smart treatment. - Protocol v1, or v2
fetch— before doing anything local, git_cdn re-validates auth and repo existence by issuing aGET .../info/refs?service=git-upload-packto upstream, forwarding the client'sAuthorizationheader (ClientSessionWithRetry, no retry range configured so any non-2xx is returned immediately — a comment notes this replaces an olderHEAD-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), aStreamResponse(HTTP 200,Content-Type: application/x-git-upload-pack-result) isprepare()d — meaning the HTTP status line and headers are already flushed to the client at this point — and anUploadPackHandler(§3) is created and run against the already-prepared writer. - After the handler runs,
X-GitCDN-Cache-Status: HIT|MISSis set on the response, based onpcache_hit or rcache_hit(used both by tests and by the Prometheus accounting instats()).
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.
upload_pack.py implements UploadPackHandler, the core of the "smart" phase. One instance is
created per upload-pack request (git_cdn.py:488).
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:
havesmust be empty anddonemust beTrue— i.e. only full clones, not incremental fetches, benefit from the pack cache (fetches always go straight to_execute).- v1 only: must advertise
side-bandorside-band-64k(i.e. must want progress/multiplexed output — plain non-sideband clients bypass caching). - Partial clones (
filtercapability) are never cached. - More than one
want(multi-branch clone) is only cached ifPACK_CACHE_MULTI=true. - Shallow (
depth) clones are only cached ifPACK_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(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)
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:
- Read-lock check (cheap, shared, allows concurrent readers) — cache hit, done.
- 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. - Final read-lock re-check to serve what was just computed.
- 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(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()runsgit cat-file --batch-checkagainst all requestedwantsand checks for"missing"in the output (aFileNotFoundErrorhere — 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 anX-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.
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 inasyncio.shield()— a client disconnect (task cancellation) does not abort the cache write; the code will wait up to 10 minutes in thefinallyblock for the shielded write to finish so the cache still gets populated for the next client even if this one vanished. If not caching, onlyGIT_PROCESS_WAIT_TIMEOUT(default 2s) is allowed before the now-useless process is killed. write_inputtoleratesBrokenPipeErroron stdin — this happens legitimately whengit-upload-packdecides 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 withintimeout; 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
ERRpkt-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 runninggit clone.
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.
Runs inside a backoff(BACKOFF_START=0.5, BACKOFF_COUNT=5) retry loop (delays ≈0.5, 1, 2, 4, 8s):
- 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. - If the target directory already exists (leftover from a previous failed attempt),
rm -rfit first. git clone --bare <url-with-creds> <directory>directly from upstream.- Loop until
returncode == 0or retries exhausted; if still failing after all retries, raisesHTTPInternalServerError(reason=stderr).
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():
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.
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.
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.
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.
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.
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.
- 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 sharedclean.lockfile's mtime, and runs the actual scan/delete (clean_task) in a single-workerThreadPoolExecutorso 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.
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.
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.
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.
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.gzipsibling — the manager can serve either the raw or gzip-compressed variant depending on the current client'sAccept-Encoding, independent of what the original downloading client asked for. - Download (
download()): streams from upstream to a.part(or.part.gzip) temp file; externalgunzipandsha256sumsubprocesses 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 withHTTPNotFound. - 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_cachetakes 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.
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):
- A
HEADrequest toCDN_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 thex-goog-hashheader) and size (Content-Length). - 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_hitsfast path. - Otherwise, under an exclusive lock, stream-download the bundle from Google (via
BUNDLE_PROXYif set) directly to both the client and the on-disk cache file simultaneously (stream_and_md5sum), computing an MD5 as it goes. - 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).
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.
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 asasyncio.Futures) — multiple coroutines in the same worker process asking for the same file's lock share oneFLockobject and never need to touchfcntlat 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); onBlockingIOError(another process holds it), a blockingflock()call is dispatched to a dedicated single-workerThreadPoolExecutorso 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 thanCancelledErrorbefore 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 inupdate(), and the offlineclean_cache.pyscript both use lock-file/dir mtime as a proxy for "last actually used" time. - A
manager.remove_lock()call cleans up the in-memoryFLockonce 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).
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.
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-suppliedretry_onrange (every call site in this codebase usesrange(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 everyREQUEST_RESET_SESSION_ON_FAILURE-th consecutive failure (default 3) recreates the entire sharedClientSession(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 likeproxify_with_datatranslate that into an HTTP 502 to the client). metric_upstream_responses_totalis 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.
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.
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.
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_CONTEXTis set. - UDP JSON (
LOGGING_SERVER=host:portset): a customDatagramHandlersubclass serializes each log record (merged with structlog contextvars/threadlocal state) as JSON over UDP, meant to feed Vector (vector.dev, seedocker-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 flaggedtruncated: truerather 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.
- Docker images:
Dockerfile(Alpine,apt-installed git, setsPROMETHEUS_ENABLED=trueand a defaultPROMETHEUS_MULTIPROC_DIRin the image) andDockerfile-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/shmto avoid disk I/O for gunicorn's internal heartbeat files). Both entrypoints rungunicorn git_cdn.app:app -c config.py --bind :8000. - Gunicorn hosts N
aiohttp.worker.GunicornWebWorker(orGunicornUVLoopWebWorker) 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_DIRis required to get correct metrics across these worker processes. - systemd (
deploy/gitcdn.service): wraps a plaindocker run,Restart=always, kills/removes any pre-existinggitcdncontainer before starting, mounts the working directory as a volume,PartOf=docker.serviceso 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'ssplit_clientsdirective, weighting traffic between two upstream blocks on different ports — this is how rolling upgrades are done without a dedicated orchestrator like Kubernetes. docker-compose.ymlis dev-only tooling: spins up a local Vector instance on UDP port 3465 matchingLOGGING_SERVERfor 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 ifLOGGING_SERVERis left unset.)- Local dev:
make dev(poetry-managed venv, pinned poetry/python versions via.poetry-version/.python-version), thenmake run(gunicorn -c config.py git_cdn.app:app), reading required env vars from a git-ignoredtosourcefile (seetosource.example).
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.
These are things noticed while reading the code — candidates to look into, not prescribed fixes.
-
MAX_CONNECTIONSper-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 defaultMAX_CONNECTIONS=10and, say, 8 gunicorn workers, the intended per-worker share is10/8 = 1.25, butmax(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 settingMAX_CONNECTIONS=10would reasonably expect. The division only has any effect onceMAX_CONNECTIONS > 10 * GUNICORN_WORKER_NB. This looks like a copy-paste artifact (floor value10reused from the default instead of1). Worth confirming with the maintainers and likely worth a one-line fix + a regression test. -
CloneBundleManagerhas its own separate, non-worker-dividedMAX_CONNECTIONSpool.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. -
Repo mirrors (
git/), LFS objects (lfs/), and clone bundles (bundles/) have no automatic eviction in the running server. Onlypack_cache/is auto-managed (PackCacheCleaner, wired into the live request path). The other three only shrink if an operator runs the separatecache_handler/clean_cache.pyCLI 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 forgit//lfs/bundlesis 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. -
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), theStreamResponseisprepare()d — sending the 200 status and headers — beforeUploadPackHandler.run()is awaited. If that later fails all the way down toRepoCache.clone()/fetch()exhausting their backoff retries (raisingHTTPInternalServerError), 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-protocolERRpkt-line, not an HTTP status — see §3), but the specificclone()/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 exhaustRepoCache's 5-retry backoff mid-clone. -
git_cdn_upstream_responses_totalcounts retry attempts, not distinct requests.client_session.py:40increments 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 toREQUEST_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). -
Dead code:
git_cdn/lock/aio_lock.pycontains an unusedAsyncIOLockManagerclass and a commented-outmanager = AsyncIOLockManager()line — looks like an abandoned earlier approach (pureasyncio.Lock(), presumably before the cross-processfcntlrequirement was addressed). Safe to delete if confirmed unused elsewhere. -
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 outrepo_cache_received_byteswithout any visible failure. -
README config-var documentation is incomplete —
PACK_CACHE_MULTI,PACK_CACHE_DEPTH,PACK_CACHE_CHUNK_SIZE,REQUEST_RESET_SESSION_ON_FAILURE,ENFORCE_AUTH, andFIX_GIT_URLare 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. -
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.
-
Dockerfile(the source-built-git variant, formerlyDockerfile-slim) was actually broken, and has been patched during this review — confirmed by building it locally end-to-end:- It never installed the
gitpackage before runninggit clone https://github.com/git/gitto build git from source —git: not found. Fixed by addinggitto theapt-get installlist (only needed transiently, to fetch the source). - It cloned git's unpinned
masterbranch. Git upstream has been incrementally adopting Rust since Git 2.49 (optional at first); the version currently atmasterdefaults 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 causedpip installto fail compilingujson'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 inrequirements.txt/poetry.lock. Fixed by pinning topython:3.12-slim(matching the project's own.python-version) — confirmed via a dry-runpip installthat 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: nogit fetchto GitHub, only acat-filecheck; ~4s → ~0.7s). A defaultgit 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-branchfor CI. Repeating with--single-branchdid populatepack_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.
- It never installed the
| 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) |