@@ -24,16 +24,18 @@ defmodule Hyper.Img.Db.Gc do
2424 **Publish contract:** write a layer's file to the medium before inserting its
2525 `blobs` row (ideally write-temp then atomic rename); the grace period is
2626 insurance, not a substitute.
27+
28+ The delete step itself lives in `Hyper.Img.Db.Gc.Prune`.
2729 """
2830
2931 use GenServer
3032 use OpenTelemetryDecorator
3133 require Logger
32- import Ecto.Query
3334
3435 alias Hyper.Cfg.Gc , as: Config
3536 alias Hyper.Cluster.Routing
36- alias Hyper.Img.Db . { Blob , ImageLayer , Repo }
37+ alias Hyper.Img.Db.Blob
38+ alias Hyper.Img.Db.Gc.Prune
3739 alias Hyper.Img.Db.Gc.Sweep
3840 alias Hyper.Node.Layer.Repo , as: LayerRepo
3941
@@ -116,8 +118,6 @@ defmodule Hyper.Img.Db.Gc do
116118 # role, monitor noise, etc.) rather than crashing an in-flight sweep.
117119 def handle_info ( _msg , state ) , do: { :noreply , state }
118120
119- ## Internals
120-
121121 @ spec acquire ( t ( ) ) :: t ( )
122122 defp acquire ( state ) do
123123 case Horde.Registry . register ( Routing . name ( ) , @ singleton_key , nil ) do
@@ -136,10 +136,13 @@ defmodule Hyper.Img.Db.Gc do
136136 @ decorate with_span ( "Hyper.Img.Db.Gc.scan_one_batch" )
137137 defp scan_one_batch ( % __MODULE__ { sweep: sweep } = state ) do
138138 limit = state . config . batch_size
139- batch = with_low_priority ( state , fn -> Blob . present_after ( sweep . cursor , limit ) end )
140- { sweep , missing } = Sweep . absorb ( sweep , batch , & presence / 1 )
141139
142- { pruned , pruned_bytes , dangling } = maybe_prune ( state , missing )
140+ batch =
141+ Prune . with_low_priority ( state . config , fn -> Blob . present_after ( sweep . cursor , limit ) end )
142+
143+ { sweep , missing } = Sweep . absorb ( sweep , batch , & Prune . presence / 1 )
144+
145+ { pruned , pruned_bytes , dangling } = Prune . execute ( state . config , missing )
143146 sweep = Sweep . record_prune ( sweep , pruned , pruned_bytes , dangling )
144147
145148 if Sweep . continue? ( batch , limit ) do
@@ -156,120 +159,4 @@ defmodule Hyper.Img.Db.Gc do
156159 % { state | sweep: nil , last_sweep: sweep }
157160 end
158161 end
159-
160- # Re-check the medium is still mounted before acting on a page's "missing"
161- # set. If the whole mount vanished mid-page, every probe read as gone - skip
162- # the deletions rather than wipe a page of live rows.
163- @ spec maybe_prune ( t ( ) , [ Sweep . blob ( ) ] ) ::
164- { non_neg_integer ( ) , non_neg_integer ( ) , non_neg_integer ( ) }
165- defp maybe_prune ( _state , [ ] ) , do: { 0 , 0 , 0 }
166-
167- defp maybe_prune ( state , missing ) do
168- case LayerRepo . test_system ( ) do
169- :ok ->
170- prune_missing ( state , missing )
171-
172- { :error , reason } ->
173- Logger . warning (
174- "layer gc: medium became unavailable before pruning (#{ inspect ( reason ) } ); " <>
175- "skipping #{ length ( missing ) } deletion(s) this page"
176- )
177-
178- { 0 , 0 , 0 }
179- end
180- end
181-
182- # Prune the missing blobs that no image references; report the rest as dangling.
183- @ spec prune_missing ( t ( ) , [ Sweep . blob ( ) ] ) ::
184- { non_neg_integer ( ) , non_neg_integer ( ) , non_neg_integer ( ) }
185- defp prune_missing ( state , missing ) do
186- ids = Enum . map ( missing , fn { id , _size } -> id end )
187- referenced = referenced_ids ( state , ids )
188-
189- { dangling , prunable } =
190- Enum . split_with ( missing , fn { id , _size } -> MapSet . member? ( referenced , id ) end )
191-
192- report_dangling ( dangling )
193-
194- cutoff =
195- DateTime . add ( DateTime . utc_now ( ) , - Unit.Time . as_ms ( state . config . grace_period ) , :millisecond )
196-
197- prunable_ids = Enum . map ( prunable , fn { id , _size } -> id end )
198- { pruned , pruned_bytes } = prune_rows ( state , prunable_ids , cutoff )
199-
200- { pruned , pruned_bytes , length ( dangling ) }
201- end
202-
203- @ spec prune_rows ( t ( ) , [ String . t ( ) ] , DateTime . t ( ) ) :: { non_neg_integer ( ) , non_neg_integer ( ) }
204- defp prune_rows ( _state , [ ] , _cutoff ) , do: { 0 , 0 }
205-
206- defp prune_rows ( state , ids , cutoff ) do
207- # `RETURNING size` gives the exact deleted set's count and bytes. The grace
208- # cutoff protects freshly-published rows. NOT EXISTS double-guards the FK:
209- # even if a reference appeared since we snapshotted `referenced`, that row is
210- # skipped rather than raising.
211- query =
212- from b in Blob ,
213- as: :b ,
214- where:
215- b . id in ^ ids and b . state == :present and b . inserted_at < ^ cutoff and
216- not exists ( from il in ImageLayer , where: il . blob_id == parent_as ( :b ) . id ) ,
217- select: b . size
218-
219- { count , sizes } = with_low_priority ( state , fn -> Repo . delete_all ( query ) end )
220- { count , Enum . sum ( sizes ) }
221- end
222-
223- # Report dangling blobs (file gone, still referenced = data loss) once per page,
224- # aggregated, so a broken mount cannot flood the logs one line per blob.
225- @ spec report_dangling ( [ Sweep . blob ( ) ] ) :: :ok
226- defp report_dangling ( [ ] ) , do: :ok
227-
228- defp report_dangling ( dangling ) do
229- count = length ( dangling )
230- bytes = dangling |> Enum . map ( fn { _id , size } -> size end ) |> Enum . sum ( )
231- sample = dangling |> Enum . take ( 10 ) |> Enum . map ( fn { id , _size } -> id end )
232-
233- Logger . error (
234- "layer gc: #{ count } blob(s) missing from medium but still referenced by an image " <>
235- "(#{ bytes } bytes total); leaving in place. sample=#{ inspect ( sample ) } "
236- )
237- end
238-
239- @ spec referenced_ids ( t ( ) , [ String . t ( ) ] ) :: MapSet . t ( String . t ( ) )
240- defp referenced_ids ( state , ids ) do
241- query = from il in ImageLayer , where: il . blob_id in ^ ids , distinct: true , select: il . blob_id
242- state |> with_low_priority ( fn -> Repo . all ( query ) end ) |> MapSet . new ( )
243- end
244-
245- # Run a DB operation at low priority: in a transaction with a capped per-statement
246- # timeout, so it can never pin a backend and yields under contention.
247- @ spec with_low_priority ( t ( ) , ( -> result ) ) :: result when result: var
248- defp with_low_priority ( state , fun ) do
249- timeout = Unit.Time . as_ms ( state . config . timeout )
250-
251- { :ok , result } =
252- Repo . transaction ( fn ->
253- _ =
254- Repo . query! ( "SELECT set_config('statement_timeout', $1, true)" , [
255- Integer . to_string ( timeout )
256- ] )
257-
258- fun . ( )
259- end )
260-
261- result
262- end
263-
264- # Shared-medium presence probe injected into the pure Sweep core. Distinguishes
265- # a genuine absence (`:enoent` -> prunable) from an I/O error (`:unknown` ->
266- # never pruned), so a transient NFS hiccup can never drive a delete.
267- @ spec presence ( String . t ( ) ) :: Sweep . presence ( )
268- defp presence ( id ) do
269- case LayerRepo . find_layer ( id ) do
270- { :ok , _path } -> :present
271- { :error , :enoent } -> :missing
272- { :error , _posix } -> :unknown
273- end
274- end
275162end
0 commit comments