@@ -225,12 +225,24 @@ func ConvertTileLeafToRawLogEntry(leaf TileLeaf, index uint64) *ct.RawLogEntry {
225225 return rawEntry
226226}
227227
228+ // DefaultMaxPartialWait is the default maximum time to wait before forcefully
229+ // fetching a partial tile that has not yet grown into a full tile.
230+ const DefaultMaxPartialWait = 1 * time .Minute
231+
228232type StaticCTClient struct {
229233 url string
230234 httpClient * http.Client
231235 backoff backoff.Backoff
232236 userAgent string
233237 ctIndex uint64
238+
239+ // Deferred partial-tile state.
240+ // partialTileIndex holds the tile index of the currently tracked partial tile.
241+ // partialTileFirstSeen is when that partial tile was first observed; zero means
242+ // no partial tile is being tracked.
243+ partialTileIndex uint64
244+ partialTileFirstSeen time.Time
245+ maxPartialWait time.Duration
234246}
235247
236248func NewStaticCTClient (url string , httpClient * http.Client , userAgent string , startIndex uint64 ) * StaticCTClient {
@@ -243,8 +255,9 @@ func NewStaticCTClient(url string, httpClient *http.Client, userAgent string, st
243255 Factor : 1.3 ,
244256 Jitter : true ,
245257 },
246- userAgent : userAgent ,
247- ctIndex : startIndex ,
258+ userAgent : userAgent ,
259+ ctIndex : startIndex ,
260+ maxPartialWait : DefaultMaxPartialWait ,
248261 }
249262}
250263
@@ -296,22 +309,54 @@ func (s *StaticCTClient) fetchAndProcessTiles(ctx context.Context, foundCert fun
296309 endTile := currentTreeSize / TileSize
297310
298311 // Process full tiles
312+ fetchedFullTiles := false
299313 for tileIndex := startTile ; tileIndex < endTile ; tileIndex ++ {
300314 if err := s .processTile (ctx , tileIndex , 0 , foundCert , foundPrecert ); err != nil {
301315 return false , fmt .Errorf ("processing tile %d: %w" , tileIndex , err )
302316 }
317+
318+ fetchedFullTiles = true
319+ }
320+
321+ // When the current end tile has advanced past the tracked partial tile, that tile
322+ // has since become a full tile and been processed; reset tracking so we start
323+ // fresh for the new partial tile (if any).
324+ if endTile > s .partialTileIndex {
325+ s .partialTileFirstSeen = time.Time {}
303326 }
304327
305- // Process partial tile if exists
328+ // Process partial tiles.
306329 partialSize := currentTreeSize % TileSize
307330 if partialSize > 0 {
308- if err := s .processTile (ctx , endTile , partialSize , foundCert , foundPrecert ); err != nil {
309- log .Printf ("Warning: error processing partial tile %d: %s\n " , endTile , err )
310- // Don't return error for partial tiles as they might be incomplete
331+ switch {
332+ case s .partialTileFirstSeen .IsZero () || s .partialTileIndex != endTile :
333+ // First time we see this partial tile – start the deferral clock.
334+ s .partialTileIndex = endTile
335+ s .partialTileFirstSeen = time .Now ()
336+ log .Println ("Deferring fetch of partial tile" , endTile , "with size" , partialSize )
337+
338+ case time .Since (s .partialTileFirstSeen ) >= s .maxPartialWait :
339+ // The partial tile has been pending too long – fetch it now to prevent
340+ // extreme processing delays on slow-growing logs.
341+ log .Println ("Forcefully fetching partial tile" , endTile , "with size" , partialSize )
342+
343+ if err := s .processTile (ctx , endTile , partialSize , foundCert , foundPrecert ); err != nil {
344+ log .Printf ("Warning: error processing partial tile %d: %s\n " , endTile , err )
345+ }
346+
347+ // Reset tracking; the tile will be re-observed on the next poll if it
348+ // still hasn't grown into a full tile.
349+ s .partialTileFirstSeen = time.Time {}
350+
351+ default :
352+ // Still within the deferral window – skip.
311353 }
354+ } else {
355+ // currentTreeSize is an exact multiple of TileSize; no partial tile exists.
356+ s .partialTileFirstSeen = time.Time {}
312357 }
313358
314- return true , nil
359+ return fetchedFullTiles , nil
315360}
316361
317362// processTile processes a single tile from the tiled log.
0 commit comments