@@ -299,6 +299,138 @@ def test_local_data_grows_in_tiered_cloud(self):
299299 f"expected near { replication * self .local_target_bytes } bytes"
300300 )
301301
302+ @cluster (num_nodes = 4 )
303+ def test_capacity_pressure_reclaims_tiered_cloud (self ):
304+ """
305+ Cluster-wide disk pressure (the disk_space_manager capacity path)
306+ reclaims a tiered_cloud partition's local log, not just the
307+ retention.local.target path.
308+
309+ The topic sets no local target and effectively infinite retention, so
310+ only the capacity path can bound it. On a build that excludes
311+ tiered_cloud partitions from disk_space_manager the local log grows
312+ unbounded and this fails; with them admitted, the eviction policy pins
313+ _cloud_gc_offset and ctp_stm trims to it, so the footprint converges
314+ near the capacity target.
315+ """
316+ assert self .redpanda is not None
317+
318+ # Node-wide log-data target, small enough that the produced topic must
319+ # be trimmed to meet it. disk_reservation_percent=0 and percent=100 so
320+ # the bytes target is the effective one.
321+ capacity_target = 16 * 1024 * 1024
322+ self .redpanda .set_cluster_config (
323+ {
324+ "retention_local_target_capacity_bytes" : capacity_target ,
325+ "retention_local_target_capacity_percent" : 100 ,
326+ "disk_reservation_percent" : 0 ,
327+ }
328+ )
329+
330+ # No retention.local.target.bytes: the local-target path stays inert
331+ # (only the 1-day default applies, and nothing is that old), so only
332+ # the capacity path can bound this topic. retention.bytes is
333+ # effectively unlimited, mirroring the field scenario that filled the
334+ # disk.
335+ rpk = RpkTool (self .redpanda )
336+ rpk .create_topic (
337+ topic = self .topic_name ,
338+ partitions = 1 ,
339+ replicas = 3 ,
340+ config = {
341+ TopicSpec .PROPERTY_STORAGE_MODE : (TopicSpec .STORAGE_MODE_TIERED_CLOUD ),
342+ "cleanup.policy" : TopicSpec .CLEANUP_DELETE ,
343+ "retention.bytes" : str (self .retention_bytes ),
344+ "segment.bytes" : str (self .segment_size ),
345+ },
346+ )
347+ self ._wait_for_partition_info ()
348+
349+ # Produce well past the capacity target so an untrimmed build grows far
350+ # beyond it.
351+ produce_bytes = 64 * 1024 * 1024
352+ self ._produce (produce_bytes )
353+ self .wait_until_reconciled (topic = self .topic_name , partition = 0 )
354+
355+ # The target is per node; the metric sums this topic's 3 replicas and
356+ # excludes internal logs (topic-filtered). Allow 3x the target plus
357+ # per-replica headroom for the never-evicted active segment and
358+ # trim-cycle latency. Still far below the ~3x produce an untrimmed
359+ # build retains.
360+ replication = 3
361+ ceiling = replication * (capacity_target + 4 * self .segment_size )
362+ # The topic has no local retention target and a lenient retention.bytes,
363+ # so ctp_stm's own retention never trims at this volume; only a
364+ # disk_space_manager capacity pin can drive the reclaim. Staying below
365+ # the ceiling therefore proves the capacity path engaged for a
366+ # tiered_cloud partition.
367+ self ._wait_local_below (ceiling_bytes = ceiling , timeout_sec = 180 )
368+
369+ @cluster (num_nodes = 4 )
370+ def test_idle_partition_reclaims_on_pin (self ):
371+ """
372+ A tiered_cloud partition that has gone idle still reclaims when disk
373+ pressure sets _cloud_gc_offset afterwards.
374+
375+ With no produce the LRO is quiescent, so nothing signals ctp_stm's
376+ truncate loop. It wakes on its bounded poll interval, re-reads the
377+ offset disk_space_manager set, and trims to it, so an idle partition
378+ reclaims without further produce.
379+ """
380+ assert self .redpanda is not None
381+
382+ capacity_target = 16 * 1024 * 1024
383+ replication = 3
384+ ceiling = replication * (capacity_target + 4 * self .segment_size )
385+
386+ # Capacity path effectively off: percent=100 with no bytes target lets
387+ # the backlog accumulate without pressure, so nothing trims until the
388+ # bytes target is set below.
389+ self .redpanda .set_cluster_config (
390+ {
391+ "retention_local_target_capacity_percent" : 100 ,
392+ "disk_reservation_percent" : 0 ,
393+ }
394+ )
395+
396+ rpk = RpkTool (self .redpanda )
397+ rpk .create_topic (
398+ topic = self .topic_name ,
399+ partitions = 1 ,
400+ replicas = 3 ,
401+ config = {
402+ TopicSpec .PROPERTY_STORAGE_MODE : (TopicSpec .STORAGE_MODE_TIERED_CLOUD ),
403+ "cleanup.policy" : TopicSpec .CLEANUP_DELETE ,
404+ "retention.bytes" : str (self .retention_bytes ),
405+ "segment.bytes" : str (self .segment_size ),
406+ },
407+ )
408+ self ._wait_for_partition_info ()
409+
410+ # Fill the local log, then let reconciliation catch up so the LRO is
411+ # quiescent (nothing to drive the truncate loop). Nothing trims yet
412+ # (no pressure, lenient retention.bytes), so the footprint holds near
413+ # the produced size.
414+ self ._produce (64 * 1024 * 1024 )
415+ self .wait_until_reconciled (topic = self .topic_name , partition = 0 )
416+
417+ # Confirm the backlog is present and untrimmed before applying pressure,
418+ # so the reclaim assertion below cannot pass trivially.
419+ wait_until (
420+ lambda : self ._local_partition_bytes () > ceiling ,
421+ timeout_sec = 60 ,
422+ backoff_sec = 2 ,
423+ err_msg = "backlog never reached the expected size before pressure" ,
424+ )
425+
426+ # Apply pressure: disk_space_manager sets _cloud_gc_offset on its next
427+ # cycle. The LRO is quiescent, so the truncate loop only observes it on
428+ # its next bounded poll, then trims to it.
429+ self .redpanda .set_cluster_config (
430+ {"retention_local_target_capacity_bytes" : capacity_target }
431+ )
432+ self ._wait_local_below (ceiling_bytes = ceiling , timeout_sec = 120 )
433+
302434 @cluster (num_nodes = 4 )
303435 def test_compact_topic_evicts_via_l1_floor (self ):
304436 """
0 commit comments