@@ -58,6 +58,7 @@ type Strategy struct {
5858 snapshotSpools sync.Map // keyed by upstream URL, values are *snapshotSpoolEntry
5959 coldSnapshotMu sync.Map // keyed by upstream URL, values are *coldSnapshotEntry
6060 deferredRestoreOnce sync.Map // keyed by upstream URL, ensures at most one deferred restore per repo
61+ metrics * gitMetrics
6162}
6263
6364func New (
@@ -108,6 +109,11 @@ func New(
108109 return nil , errors .Wrap (err , "failed to create scheduler" )
109110 }
110111
112+ m , err := newGitMetrics ()
113+ if err != nil {
114+ return nil , errors .Wrap (err , "create git metrics" )
115+ }
116+
111117 s := & Strategy {
112118 config : config ,
113119 cache : cache ,
@@ -117,47 +123,11 @@ func New(
117123 scheduler : scheduler .WithQueuePrefix ("git" ),
118124 spools : make (map [string ]* RepoSpools ),
119125 tokenManager : tokenManager ,
126+ metrics : m ,
120127 }
121128 s .config .ServerURL = strings .TrimRight (config .ServerURL , "/" )
122129
123- existing , err := s .cloneManager .DiscoverExisting (ctx )
124- if err != nil {
125- logger .WarnContext (ctx , "Failed to discover existing clones" , "error" , err )
126- }
127- for _ , repo := range existing {
128- logger .InfoContext (ctx , "Running startup fetch for existing repo" , "upstream" , repo .UpstreamURL ())
129-
130- preRefs , err := repo .GetLocalRefs (ctx )
131- if err != nil {
132- logger .WarnContext (ctx , "Failed to get pre-fetch refs for existing repo" , "upstream" , repo .UpstreamURL (),
133- "error" , err )
134- }
135-
136- start := time .Now ()
137- if err := repo .FetchLenient (ctx , gitclone .CloneTimeout ); err != nil {
138- logger .ErrorContext (ctx , "Startup fetch failed for existing repo" , "upstream" , repo .UpstreamURL (), "error" , err ,
139- "duration" , time .Since (start ))
140- continue
141- }
142- logger .InfoContext (ctx , "Startup fetch completed for existing repo" , "upstream" , repo .UpstreamURL (),
143- "duration" , time .Since (start ))
144-
145- postRefs , err := repo .GetLocalRefs (ctx )
146- if err != nil {
147- logger .WarnContext (ctx , "Failed to get post-fetch refs for existing repo" , "upstream" , repo .UpstreamURL (),
148- "error" , err )
149- } else {
150- maps .DeleteFunc (postRefs , func (k , v string ) bool { return preRefs [k ] == v })
151- logger .InfoContext (ctx , "Post-fetch changed refs for existing repo" , "upstream" , repo .UpstreamURL (), "refs" , postRefs )
152- }
153-
154- if s .config .SnapshotInterval > 0 {
155- s .scheduleSnapshotJobs (repo )
156- }
157- if s .config .RepackInterval > 0 {
158- s .scheduleRepackJobs (repo )
159- }
160- }
130+ s .warmExistingRepos (ctx )
161131
162132 s .proxy = & httputil.ReverseProxy {
163133 Director : func (req * http.Request ) {
@@ -198,6 +168,48 @@ func New(
198168
199169var _ strategy.Strategy = (* Strategy )(nil )
200170
171+ func (s * Strategy ) warmExistingRepos (ctx context.Context ) {
172+ logger := logging .FromContext (ctx )
173+ existing , err := s .cloneManager .DiscoverExisting (ctx )
174+ if err != nil {
175+ logger .WarnContext (ctx , "Failed to discover existing clones" , "error" , err )
176+ }
177+ for _ , repo := range existing {
178+ logger .InfoContext (ctx , "Running startup fetch for existing repo" , "upstream" , repo .UpstreamURL ())
179+
180+ preRefs , err := repo .GetLocalRefs (ctx )
181+ if err != nil {
182+ logger .WarnContext (ctx , "Failed to get pre-fetch refs for existing repo" , "upstream" , repo .UpstreamURL (),
183+ "error" , err )
184+ }
185+
186+ start := time .Now ()
187+ if err := repo .FetchLenient (ctx , gitclone .CloneTimeout ); err != nil {
188+ logger .ErrorContext (ctx , "Startup fetch failed for existing repo" , "upstream" , repo .UpstreamURL (), "error" , err ,
189+ "duration" , time .Since (start ))
190+ continue
191+ }
192+ logger .InfoContext (ctx , "Startup fetch completed for existing repo" , "upstream" , repo .UpstreamURL (),
193+ "duration" , time .Since (start ))
194+
195+ postRefs , err := repo .GetLocalRefs (ctx )
196+ if err != nil {
197+ logger .WarnContext (ctx , "Failed to get post-fetch refs for existing repo" , "upstream" , repo .UpstreamURL (),
198+ "error" , err )
199+ } else {
200+ maps .DeleteFunc (postRefs , func (k , v string ) bool { return preRefs [k ] == v })
201+ logger .InfoContext (ctx , "Post-fetch changed refs for existing repo" , "upstream" , repo .UpstreamURL (), "refs" , postRefs )
202+ }
203+
204+ if s .config .SnapshotInterval > 0 {
205+ s .scheduleSnapshotJobs (repo )
206+ }
207+ if s .config .RepackInterval > 0 {
208+ s .scheduleRepackJobs (repo )
209+ }
210+ }
211+ }
212+
201213// SetHTTPTransport overrides the HTTP transport used for upstream requests.
202214// This is intended for testing.
203215func (s * Strategy ) SetHTTPTransport (t http.RoundTripper ) {
@@ -217,11 +229,13 @@ func (s *Strategy) handleRequest(w http.ResponseWriter, r *http.Request) {
217229 logger .DebugContext (ctx , "Git request" , "method" , r .Method , "host" , host , "path" , pathValue )
218230
219231 if strings .HasSuffix (pathValue , "/snapshot.tar.zst" ) {
232+ s .metrics .recordRequest (ctx , "snapshot" )
220233 s .handleSnapshotRequest (w , r , host , pathValue )
221234 return
222235 }
223236
224237 if strings .HasSuffix (pathValue , "/snapshot.bundle" ) {
238+ s .metrics .recordRequest (ctx , "bundle" )
225239 s .handleBundleRequest (w , r , host , pathValue )
226240 return
227241 }
@@ -230,11 +244,14 @@ func (s *Strategy) handleRequest(w http.ResponseWriter, r *http.Request) {
230244 isReceivePack := service == "git-receive-pack" || strings .HasSuffix (pathValue , "/git-receive-pack" )
231245
232246 if isReceivePack {
247+ s .metrics .recordRequest (ctx , "receive-pack" )
233248 logger .DebugContext (ctx , "Forwarding write operation to upstream" )
234249 s .forwardToUpstream (w , r , host , pathValue )
235250 return
236251 }
237252
253+ s .metrics .recordRequest (ctx , "upload-pack" )
254+
238255 repoPath := ExtractRepoPath (pathValue )
239256 upstreamURL := "https://" + host + "/" + repoPath
240257
@@ -514,6 +531,7 @@ func (s *Strategy) startClone(ctx context.Context, repo *gitclone.Repository) {
514531
515532 logger .InfoContext (ctx , "Starting clone" , "upstream" , upstream , "path" , repo .Path ())
516533
534+ cloneStart := time .Now ()
517535 err := repo .Clone (ctx )
518536
519537 // Clean up spools regardless of clone success or failure, so that subsequent
@@ -523,11 +541,13 @@ func (s *Strategy) startClone(ctx context.Context, repo *gitclone.Repository) {
523541 }
524542
525543 if err != nil {
544+ s .metrics .recordOperation (ctx , "clone" , upstream , "error" , time .Since (cloneStart ))
526545 logger .ErrorContext (ctx , "Clone failed" , "upstream" , upstream , "error" , err )
527546 repo .ResetToEmpty ()
528547 return
529548 }
530549
550+ s .metrics .recordOperation (ctx , "clone" , upstream , "success" , time .Since (cloneStart ))
531551 logger .InfoContext (ctx , "Clone completed" , "upstream" , upstream , "path" , repo .Path ())
532552
533553 if s .config .SnapshotInterval > 0 {
@@ -601,9 +621,11 @@ func (s *Strategy) doFetch(ctx context.Context, repo *gitclone.Repository) error
601621
602622 start := time .Now ()
603623 if err := repo .Fetch (ctx ); err != nil {
624+ s .metrics .recordOperation (ctx , "fetch" , repo .UpstreamURL (), "error" , time .Since (start ))
604625 logger .ErrorContext (ctx , "Fetch failed" , "upstream" , repo .UpstreamURL (), "duration" , time .Since (start ), "error" , err )
605626 return errors .Errorf ("fetch failed: %w" , err )
606627 }
628+ s .metrics .recordOperation (ctx , "fetch" , repo .UpstreamURL (), "success" , time .Since (start ))
607629 logger .InfoContext (ctx , "Fetch completed" , "upstream" , repo .UpstreamURL (), "duration" , time .Since (start ))
608630 return nil
609631}
0 commit comments