;; This Source Code Form is subject to the terms of the Mozilla Public ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; ;; Copyright (c) KALEIDOS INC (ns app.tasks.objects-gc "A maintenance task that performs a general purpose garbage collection of deleted or unreachable objects." (:require [app.common.data :as d] [app.common.logging :as l] [app.config :as cf] [app.db :as db] [app.media :as media] [app.storage :as sto] [app.util.time :as dt] [clojure.spec.alpha :as s] [integrant.core :as ig])) (declare ^:private delete-profiles!) (declare ^:private delete-teams!) (declare ^:private delete-fonts!) (declare ^:private delete-projects!) (declare ^:private delete-files!) (declare ^:private delete-orphan-teams!) (defmethod ig/pre-init-spec ::handler [_] (s/keys :req [::db/pool ::sto/storage])) (defmethod ig/prep-key ::handler [_ cfg] (assoc cfg ::min-age cf/deletion-delay)) (defmethod ig/init-key ::handler [_ {:keys [::db/pool ::sto/storage] :as cfg}] (fn [params] (db/with-atomic [conn pool] (let [min-age (or (:min-age params) (::min-age cfg)) _ (l/info :hint "gc started" :min-age (dt/format-duration min-age) :rollback? (boolean (:rollback? params))) storage (media/configure-assets-storage storage conn) cfg (-> cfg (assoc ::min-age (db/interval min-age)) (assoc ::conn conn) (assoc ::storage storage)) htotal (+ (delete-profiles! cfg) (delete-teams! cfg) (delete-projects! cfg) (delete-files! cfg) (delete-fonts! cfg)) stotal (delete-orphan-teams! cfg)] (l/info :hint "gc finished" :deleted htotal :orphans stotal :rollback? (boolean (:rollback? params))) (when (:rollback? params) (db/rollback! conn)) {:processed (+ stotal htotal) :orphans stotal})))) (def ^:private sql:get-profiles-chunk "select id, photo_id, created_at from profile where deleted_at is not null and deleted_at < now() - ?::interval and created_at < ? order by created_at desc limit 10 for update skip locked") (defn- delete-profiles! [{:keys [::conn ::min-age ::storage] :as cfg}] (letfn [(get-chunk [cursor] (let [rows (db/exec! conn [sql:get-profiles-chunk min-age cursor])] [(some->> rows peek :created-at) rows])) (process-profile [total {:keys [id photo-id]}] (l/debug :hint "permanently delete profile" :id (str id)) ;; Mark as deleted the storage object related with the ;; photo-id field. (some->> photo-id (sto/touch-object! storage)) ;; And finally, permanently delete the profile. (db/delete! conn :profile {:id id}) (inc total))] (->> (d/iteration get-chunk :vf second :kf first :initk (dt/now)) (reduce process-profile 0)))) (def ^:private sql:get-teams-chunk "select id, photo_id, created_at from team where deleted_at is not null and deleted_at < now() - ?::interval and created_at < ? order by created_at desc limit 10 for update skip locked") (defn- delete-teams! [{:keys [::conn ::min-age ::storage] :as cfg}] (letfn [(get-chunk [cursor] (let [rows (db/exec! conn [sql:get-teams-chunk min-age cursor])] [(some->> rows peek :created-at) rows])) (process-team [total {:keys [id photo-id]}] (l/debug :hint "permanently delete team" :id (str id)) ;; Mark as deleted the storage object related with the ;; photo-id field. (some->> photo-id (sto/touch-object! storage)) ;; And finally, permanently delete the team. (db/delete! conn :team {:id id}) (inc total))] (->> (d/iteration get-chunk :vf second :kf first :initk (dt/now)) (reduce process-team 0)))) (def ^:private sql:get-orphan-teams-chunk "select t.id, t.created_at from team as t left join team_profile_rel as tpr on (t.id = tpr.team_id) where tpr.profile_id is null and t.created_at < ? order by t.created_at desc limit 10 for update of t skip locked;") (defn- delete-orphan-teams! "Find all orphan teams (with no members and mark them for deletion (soft delete)." [{:keys [::conn] :as cfg}] (letfn [(get-chunk [cursor] (let [rows (db/exec! conn [sql:get-orphan-teams-chunk cursor])] [(some->> rows peek :created-at) rows])) (process-team [total {:keys [id]}] (let [result (db/update! conn :team {:deleted-at (dt/now)} {:id id :deleted-at nil} {::db/return-keys? false}) count (db/get-update-count result)] (when (pos? count) (l/debug :hint "mark team for deletion" :id (str id) )) (+ total count)))] (->> (d/iteration get-chunk :vf second :kf first :initk (dt/now)) (reduce process-team 0)))) (def ^:private sql:get-fonts-chunk "select id, created_at, woff1_file_id, woff2_file_id, otf_file_id, ttf_file_id from team_font_variant where deleted_at is not null and deleted_at < now() - ?::interval and created_at < ? order by created_at desc limit 10 for update skip locked") (defn- delete-fonts! [{:keys [::conn ::min-age ::storage] :as cfg}] (letfn [(get-chunk [cursor] (let [rows (db/exec! conn [sql:get-fonts-chunk min-age cursor])] [(some->> rows peek :created-at) rows])) (process-font [total {:keys [id] :as font}] (l/debug :hint "permanently delete font variant" :id (str id)) ;; Mark as deleted the all related storage objects (some->> (:woff1-file-id font) (sto/touch-object! storage)) (some->> (:woff2-file-id font) (sto/touch-object! storage)) (some->> (:otf-file-id font) (sto/touch-object! storage)) (some->> (:ttf-file-id font) (sto/touch-object! storage)) ;; And finally, permanently delete the team font variant (db/delete! conn :team-font-variant {:id id}) (inc total))] (->> (d/iteration get-chunk :vf second :kf first :initk (dt/now)) (reduce process-font 0)))) (def ^:private sql:get-projects-chunk "select id, created_at from project where deleted_at is not null and deleted_at < now() - ?::interval and created_at < ? order by created_at desc limit 10 for update skip locked") (defn- delete-projects! [{:keys [::conn ::min-age] :as cfg}] (letfn [(get-chunk [cursor] (let [rows (db/exec! conn [sql:get-projects-chunk min-age cursor])] [(some->> rows peek :created-at) rows])) (process-project [total {:keys [id]}] (l/debug :hint "permanently delete project" :id (str id)) ;; And finally, permanently delete the project. (db/delete! conn :project {:id id}) (inc total))] (->> (d/iteration get-chunk :vf second :kf first :initk (dt/now)) (reduce process-project 0)))) (def ^:private sql:get-files-chunk "select id, created_at from file where deleted_at is not null and deleted_at < now() - ?::interval and created_at < ? order by created_at desc limit 10 for update skip locked") (defn- delete-files! [{:keys [::conn ::min-age] :as cfg}] (letfn [(get-chunk [cursor] (let [rows (db/exec! conn [sql:get-files-chunk min-age cursor])] [(some->> rows peek :created-at) rows])) (process-file [total {:keys [id]}] (l/debug :hint "permanently delete file" :id (str id)) ;; And finally, permanently delete the file. (db/delete! conn :file {:id id}) (inc total))] (->> (d/iteration get-chunk :vf second :kf first :initk (dt/now)) (reduce process-file 0))))