0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-13 10:38:13 -05:00

Merge pull request #683 from penpot/niwinz/session-updater

Enhacements
This commit is contained in:
Andrés Moya 2021-02-22 14:53:10 +01:00 committed by GitHub
commit 4a61eba3b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 658 additions and 418 deletions

View file

@ -7,6 +7,7 @@
- Add major refactor of internal pubsub/redis code; improves scalability and performance [#640](https://github.com/penpot/penpot/pull/640)
- Add optional loki integration [#645](https://github.com/penpot/penpot/pull/645)
- Add emailcatcher and ldap test containers to devenv. [#506](https://github.com/penpot/penpot/pull/506)
- Add proper http session lifecycle handling.
- Add more presets for artboard [#654](https://github.com/penpot/penpot/pull/654)
- Bounce & Complaint handling [#635](https://github.com/penpot/penpot/pull/635)
- Disable groups interactions when holding "Ctrl" key (deep selection)
@ -21,6 +22,7 @@
- Fix corner cases on invitation/signup flows.
- Fix problem width handoff code generation [Taiga #1204](https://tree.taiga.io/project/penpot/issue/1204)
- Fix problem with indices refreshing on page changes [#646](https://github.com/penpot/penpot/issues/646)
- Fix infinite recursion on logout.
- Have language change notification written in the new language [Taiga #1205](https://tree.taiga.io/project/penpot/issue/1205)
- Properly handle errors on github, gitlab and ldap auth backends.
- Properly mark profile auth backend (on first register/ auth with 3rd party auth provider).

View file

@ -237,6 +237,6 @@
(try
(run-in-system system preset)
(catch Exception e
(log/errorf e "Unhandled exception."))
(log/errorf e "unhandled exception"))
(finally
(ig/halt! system)))))

View file

@ -5,7 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
;; Copyright (c) 2020-2021 UXBOX Labs SL
(ns app.config
"A configuration management."
@ -80,92 +80,78 @@
;; :initial-data-project-name "Penpot Oboarding"
})
(s/def ::http-server-port ::us/integer)
(s/def ::host ::us/string)
(s/def ::tenant ::us/string)
(s/def ::database-username (s/nilable ::us/string))
(s/def ::allow-demo-users ::us/boolean)
(s/def ::asserts-enabled ::us/boolean)
(s/def ::assets-path ::us/string)
(s/def ::database-password (s/nilable ::us/string))
(s/def ::database-uri ::us/string)
(s/def ::redis-uri ::us/string)
(s/def ::loggers-loki-uri ::us/string)
(s/def ::loggers-zmq-uri ::us/string)
(s/def ::storage-backend ::us/keyword)
(s/def ::storage-fs-directory ::us/string)
(s/def ::assets-path ::us/string)
(s/def ::storage-s3-region ::us/keyword)
(s/def ::storage-s3-bucket ::us/string)
(s/def ::media-uri ::us/string)
(s/def ::media-directory ::us/string)
(s/def ::asserts-enabled ::us/boolean)
(s/def ::feedback-enabled ::us/boolean)
(s/def ::feedback-destination ::us/string)
(s/def ::profile-complaint-max-age ::dt/duration)
(s/def ::profile-complaint-threshold ::us/integer)
(s/def ::profile-bounce-max-age ::dt/duration)
(s/def ::profile-bounce-threshold ::us/integer)
(s/def ::database-username (s/nilable ::us/string))
(s/def ::default-blob-version ::us/integer)
(s/def ::error-report-webhook ::us/string)
(s/def ::smtp-enabled ::us/boolean)
(s/def ::smtp-default-reply-to ::us/string)
(s/def ::smtp-default-from ::us/string)
(s/def ::smtp-host ::us/string)
(s/def ::smtp-port ::us/integer)
(s/def ::smtp-username (s/nilable ::us/string))
(s/def ::smtp-password (s/nilable ::us/string))
(s/def ::smtp-tls ::us/boolean)
(s/def ::smtp-ssl ::us/boolean)
(s/def ::allow-demo-users ::us/boolean)
(s/def ::registration-enabled ::us/boolean)
(s/def ::registration-domain-whitelist ::us/string)
(s/def ::public-uri ::us/string)
(s/def ::srepl-host ::us/string)
(s/def ::srepl-port ::us/integer)
(s/def ::rlimits-password ::us/integer)
(s/def ::rlimits-image ::us/integer)
(s/def ::google-client-id ::us/string)
(s/def ::google-client-secret ::us/string)
(s/def ::gitlab-client-id ::us/string)
(s/def ::gitlab-client-secret ::us/string)
(s/def ::gitlab-base-uri ::us/string)
(s/def ::feedback-destination ::us/string)
(s/def ::feedback-enabled ::us/boolean)
(s/def ::github-client-id ::us/string)
(s/def ::github-client-secret ::us/string)
(s/def ::ldap-host ::us/string)
(s/def ::ldap-port ::us/integer)
(s/def ::ldap-bind-dn ::us/string)
(s/def ::ldap-bind-password ::us/string)
(s/def ::ldap-ssl ::us/boolean)
(s/def ::ldap-starttls ::us/boolean)
(s/def ::ldap-base-dn ::us/string)
(s/def ::ldap-user-query ::us/string)
(s/def ::ldap-attrs-username ::us/string)
(s/def ::gitlab-base-uri ::us/string)
(s/def ::gitlab-client-id ::us/string)
(s/def ::gitlab-client-secret ::us/string)
(s/def ::google-client-id ::us/string)
(s/def ::google-client-secret ::us/string)
(s/def ::host ::us/string)
(s/def ::http-server-port ::us/integer)
(s/def ::http-session-cookie-name ::us/string)
(s/def ::http-session-idle-max-age ::dt/duration)
(s/def ::http-session-updater-batch-max-age ::dt/duration)
(s/def ::http-session-updater-batch-max-size ::us/integer)
(s/def ::initial-data-file ::us/string)
(s/def ::initial-data-project-name ::us/string)
(s/def ::ldap-attrs-email ::us/string)
(s/def ::ldap-attrs-fullname ::us/string)
(s/def ::ldap-attrs-photo ::us/string)
(s/def ::ldap-attrs-username ::us/string)
(s/def ::ldap-base-dn ::us/string)
(s/def ::ldap-bind-dn ::us/string)
(s/def ::ldap-bind-password ::us/string)
(s/def ::ldap-host ::us/string)
(s/def ::ldap-port ::us/integer)
(s/def ::ldap-ssl ::us/boolean)
(s/def ::ldap-starttls ::us/boolean)
(s/def ::ldap-user-query ::us/string)
(s/def ::loggers-loki-uri ::us/string)
(s/def ::loggers-zmq-uri ::us/string)
(s/def ::media-directory ::us/string)
(s/def ::media-uri ::us/string)
(s/def ::profile-bounce-max-age ::dt/duration)
(s/def ::profile-bounce-threshold ::us/integer)
(s/def ::profile-complaint-max-age ::dt/duration)
(s/def ::profile-complaint-threshold ::us/integer)
(s/def ::public-uri ::us/string)
(s/def ::redis-uri ::us/string)
(s/def ::registration-domain-whitelist ::us/string)
(s/def ::registration-enabled ::us/boolean)
(s/def ::rlimits-image ::us/integer)
(s/def ::rlimits-password ::us/integer)
(s/def ::smtp-default-from ::us/string)
(s/def ::smtp-default-reply-to ::us/string)
(s/def ::smtp-enabled ::us/boolean)
(s/def ::smtp-host ::us/string)
(s/def ::smtp-password (s/nilable ::us/string))
(s/def ::smtp-port ::us/integer)
(s/def ::smtp-ssl ::us/boolean)
(s/def ::smtp-tls ::us/boolean)
(s/def ::smtp-username (s/nilable ::us/string))
(s/def ::srepl-host ::us/string)
(s/def ::srepl-port ::us/integer)
(s/def ::storage-backend ::us/keyword)
(s/def ::storage-fs-directory ::us/string)
(s/def ::storage-s3-bucket ::us/string)
(s/def ::storage-s3-region ::us/keyword)
(s/def ::telemetry-enabled ::us/boolean)
(s/def ::telemetry-with-taiga ::us/boolean)
(s/def ::telemetry-uri ::us/string)
(s/def ::telemetry-server-enabled ::us/boolean)
(s/def ::telemetry-server-port ::us/integer)
(s/def ::initial-data-file ::us/string)
(s/def ::initial-data-project-name ::us/string)
(s/def ::default-blob-version ::us/integer)
(s/def ::telemetry-uri ::us/string)
(s/def ::telemetry-with-taiga ::us/boolean)
(s/def ::tenant ::us/string)
(s/def ::config
(s/keys :opt-un [::allow-demo-users
@ -185,6 +171,9 @@
::google-client-id
::google-client-secret
::http-server-port
::http-session-updater-batch-max-age
::http-session-updater-batch-max-size
::http-session-idle-max-age
::host
::ldap-attrs-username
::ldap-attrs-email

View file

@ -13,6 +13,7 @@
[app.common.geom.point :as gpt]
[app.common.spec :as us]
[app.db.sql :as sql]
[app.metrics :as mtx]
[app.util.json :as json]
[app.util.migrations :as mg]
[app.util.time :as dt]
@ -45,19 +46,21 @@
;; Initialization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare instrument-jdbc!)
(s/def ::uri ::us/not-empty-string)
(s/def ::name ::us/not-empty-string)
(s/def ::min-pool-size ::us/integer)
(s/def ::max-pool-size ::us/integer)
(s/def ::migrations map?)
(s/def ::metrics map?)
(defmethod ig/pre-init-spec ::pool [_]
(s/keys :req-un [::uri ::name ::min-pool-size ::max-pool-size ::migrations]))
(s/keys :req-un [::uri ::name ::min-pool-size ::max-pool-size ::migrations ::mtx/metrics]))
(defmethod ig/init-key ::pool
[_ {:keys [migrations] :as cfg}]
(log/debugf "initialize connection pool %s with uri %s" (:name cfg) (:uri cfg))
[_ {:keys [migrations metrics] :as cfg}]
(log/infof "initialize connection pool '%s' with uri '%s'" (:name cfg) (:uri cfg))
(instrument-jdbc! (:registry metrics))
(let [pool (create-pool cfg)]
(when (seq migrations)
(with-open [conn ^AutoCloseable (open pool)]
@ -70,12 +73,22 @@
[_ pool]
(.close ^HikariDataSource pool))
(defn- instrument-jdbc!
[registry]
(mtx/instrument-vars!
[#'next.jdbc/execute-one!
#'next.jdbc/execute!]
{:registry registry
:type :counter
:name "database_query_count"
:help "An absolute counter of database queries."}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; API & Impl
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def initsql
(str "SET statement_timeout = 60000;\n"
(str "SET statement_timeout = 120000;\n"
"SET idle_in_transaction_session_timeout = 120000;"))
(defn- create-datasource-config

View file

@ -42,7 +42,7 @@
(= mtype "SubscriptionConfirmation")
(let [surl (get body "SubscribeURL")
stopic (get body "TopicArn")]
(log/infof "Subscription received (topic=%s, url=%s)" stopic surl)
(log/infof "subscription received (topic=%s, url=%s)" stopic surl)
(http/send! {:uri surl :method :post :timeout 10000}))
(= mtype "Notification")
@ -52,7 +52,7 @@
(process-report cfg notification)))
:else
(log/warn (str "Unexpected data received.\n"
(log/warn (str "unexpected data received\n"
(pprint-report body))))
{:status 200 :body ""})))
@ -184,14 +184,14 @@
(defn- process-report
[cfg {:keys [type profile-id] :as report}]
(log/debug (str "Procesing report:\n" (pprint-report report)))
(log/debug (str "procesing report:\n" (pprint-report report)))
(cond
;; In this case we receive a bounce/complaint notification without
;; confirmed identity, we just emit a warning but do nothing about
;; it because this is not a normal case. All notifications should
;; come with profile identity.
(nil? profile-id)
(log/warn (str "A notification without identity recevied from AWS\n"
(log/warn (str "a notification without identity recevied from AWS\n"
(pprint-report report)))
(= "bounce" type)
@ -201,7 +201,7 @@
(register-complaint-for-profile cfg report)
:else
(log/warn (str "Unrecognized report received from AWS\n"
(log/warn (str "unrecognized report received from AWS\n"
(pprint-report report)))))

View file

@ -73,7 +73,7 @@
(let [edata (ex-data error)
cdata (get-error-context request error)]
(update-thread-context! cdata)
(log/errorf error "Internal error: assertion (id: %s)" (str (:id cdata)))
(log/errorf error "internal error: assertion (id: %s)" (str (:id cdata)))
{:status 500
:body {:type :server-error
:data (-> edata
@ -88,7 +88,7 @@
[error request]
(let [cdata (get-error-context request error)]
(update-thread-context! cdata)
(log/errorf error "Internal error: %s (id: %s)"
(log/errorf error "internal error: %s (id: %s)"
(ex-message error)
(str (:id cdata)))
{:status 500

View file

@ -9,11 +9,20 @@
(ns app.http.session
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.config :as cfg]
[app.db :as db]
[app.metrics :as mtx]
[app.util.async :as aa]
[app.util.log4j :refer [update-thread-context!]]
[app.util.time :as dt]
[app.worker :as wrk]
[buddy.core.codecs :as bc]
[buddy.core.nonce :as bn]
[clojure.core.async :as a]
[clojure.spec.alpha :as s]
[clojure.tools.logging :as log]
[integrant.core :as ig]))
;; --- IMPL
@ -42,8 +51,7 @@
(defn- retrieve
[{:keys [conn] :as cfg} token]
(when token
(-> (db/exec-one! conn ["select profile_id from http_session where id = ?" token])
(:profile-id))))
(db/exec-one! conn ["select id, profile_id from http_session where id = ?" token])))
(defn- retrieve-from-request
[{:keys [cookie-name] :as cfg} {:keys [cookies] :as request}]
@ -57,24 +65,33 @@
(defn- middleware
[cfg handler]
(fn [request]
(if-let [profile-id (retrieve-from-request cfg request)]
(do
(if-let [{:keys [id profile-id] :as session} (retrieve-from-request cfg request)]
(let [ech (::events-ch cfg)]
(a/>!! ech id)
(update-thread-context! {:profile-id profile-id})
(handler (assoc request :profile-id profile-id)))
(handler request))))
;; --- STATE INIT
;; --- STATE INIT: SESSION
(s/def ::cookie-name ::cfg/http-session-cookie-name)
(defmethod ig/pre-init-spec ::session [_]
(s/keys :req-un [::db/pool]))
(s/keys :req-un [::db/pool]
:opt-un [::cookie-name]))
(defmethod ig/prep-key ::session
[_ cfg]
(merge {:cookie-name "auth-token"} cfg))
(merge {:cookie-name "auth-token"
:buffer-size 64}
(d/without-nils cfg)))
(defmethod ig/init-key ::session
[_ {:keys [pool] :as cfg}]
(let [cfg (assoc cfg :conn pool)]
(let [events (a/chan (a/dropping-buffer (:buffer-size cfg)))
cfg (assoc cfg
:conn pool
::events-ch events)]
(-> cfg
(assoc :middleware #(middleware cfg %))
(assoc :create (fn [profile-id]
@ -89,3 +106,113 @@
:body ""
:cookies (cookies cfg {:value "" :max-age -1})))))))
(defmethod ig/halt-key! ::session
[_ data]
(a/close! (::events-ch data)))
;; --- STATE INIT: SESSION UPDATER
(declare batch-events)
(declare update-sessions)
(s/def ::session map?)
(s/def ::max-batch-age ::cfg/http-session-updater-batch-max-age)
(s/def ::max-batch-size ::cfg/http-session-updater-batch-max-size)
(defmethod ig/pre-init-spec ::updater [_]
(s/keys :req-un [::db/pool ::wrk/executor ::mtx/metrics ::session]
:opt-un [::max-batch-age
::max-batch-size]))
(defmethod ig/prep-key ::updater
[_ cfg]
(merge {:max-batch-age (dt/duration {:minutes 5})
:max-batch-size 200}
(d/without-nils cfg)))
(defmethod ig/init-key ::updater
[_ {:keys [session metrics] :as cfg}]
(log/infof "initialize session updater (max-batch-age=%s, max-batch-size=%s)"
(str (:max-batch-age cfg))
(str (:max-batch-size cfg)))
(let [input (batch-events cfg (::events-ch session))
mcnt (mtx/create
{:name "http_session_updater_count"
:help "A counter of session update batch events."
:registry (:registry metrics)
:type :counter})]
(a/go-loop []
(when-let [[reason batch] (a/<! input)]
(let [result (a/<! (update-sessions cfg batch))]
(mcnt :inc)
(if (ex/exception? result)
(log/error result "updater: unexpected error on update sessions")
(log/debugf "updater: updated %s sessions (reason: %s)." result (name reason)))
(recur))))))
(defn- timeout-chan
[cfg]
(a/timeout (inst-ms (:max-batch-age cfg))))
(defn- batch-events
[cfg in]
(let [out (a/chan)]
(a/go-loop [tch (timeout-chan cfg)
buf #{}]
(let [[val port] (a/alts! [tch in])]
(cond
(identical? port tch)
(if (empty? buf)
(recur (timeout-chan cfg) buf)
(do
(a/>! out [:timeout buf])
(recur (timeout-chan cfg) #{})))
(nil? val)
(a/close! out)
(identical? port in)
(let [buf (conj buf val)]
(if (>= (count buf) (:max-batch-size cfg))
(do
(a/>! out [:size buf])
(recur (timeout-chan cfg) #{}))
(recur tch buf))))))
out))
(defn- update-sessions
[{:keys [pool executor]} ids]
(aa/with-thread executor
(db/exec-one! pool ["update http_session set updated_at=now() where id = ANY(?)"
(into-array String ids)])
(count ids)))
;; --- STATE INIT: SESSION GC
(declare sql:delete-expired)
(s/def ::max-age ::dt/duration)
(defmethod ig/pre-init-spec ::gc-task [_]
(s/keys :req-un [::db/pool]
:opt-un [::max-age]))
(defmethod ig/prep-key ::gc-task
[_ cfg]
(merge {:max-age (dt/duration {:days 2})}
(d/without-nils cfg)))
(defmethod ig/init-key ::gc-task
[_ {:keys [pool max-age] :as cfg}]
(fn [_]
(db/with-atomic [conn pool]
(let [interval (db/interval max-age)
result (db/exec-one! conn [sql:delete-expired interval])
result (:next.jdbc/update-count result)]
(log/debugf "gc-task: removed %s rows from http-session table" result)
result))))
(def ^:private
sql:delete-expired
"delete from http_session
where updated_at < now() - ?::interval")

View file

@ -33,13 +33,13 @@
(defmethod ig/init-key ::reporter
[_ {:keys [receiver uri] :as cfg}]
(when uri
(log/info "Intializing loki reporter.")
(log/info "intializing loki reporter")
(let [output (a/chan (a/sliding-buffer 1024))]
(receiver :sub output)
(a/go-loop []
(let [msg (a/<! output)]
(if (nil? msg)
(log/info "Stoping error reporting loop.")
(log/info "stoping error reporting loop")
(do
(a/<! (handle-event cfg msg))
(recur)))))
@ -75,10 +75,10 @@
(if (= (:status response) 204)
true
(do
(log/errorf "Error on sending log to loki (try %s).\n%s" i (pr-str response))
(log/errorf "error on sending log to loki (try %s)\n%s" i (pr-str response))
false)))
(catch Exception e
(log/errorf e "Error on sending message to loki (try %s)." i)
(log/errorf e "error on sending message to loki (try %s)" i)
false)))
(defn- handle-event

View file

@ -43,14 +43,14 @@
(defmethod ig/init-key ::reporter
[_ {:keys [receiver] :as cfg}]
(log/info "Intializing mattermost error reporter.")
(log/info "intializing mattermost error reporter")
(let [output (a/chan (a/sliding-buffer 128)
(filter #(= (:level %) "error")))]
(receiver :sub output)
(a/go-loop []
(let [msg (a/<! output)]
(if (nil? msg)
(log/info "Stoping error reporting loop.")
(log/info "stoping error reporting loop")
(do
(a/<! (handle-event cfg msg))
(recur)))))
@ -75,10 +75,10 @@
:headers {"content-type" "application/json"}
:body (json/encode-str {:text text})})]
(when (not= (:status rsp) 200)
(log/errorf "Error on sending data to mattermost\n%s" (pr-str rsp))))
(log/errorf "error on sending data to mattermost\n%s" (pr-str rsp))))
(catch Exception e
(log/error e "Unexpected exception on error reporter."))))
(log/error e "unexpected exception on error reporter"))))
(defn- persist-on-database!
[{:keys [pool] :as cfg} {:keys [id] :as cdata}]
@ -116,7 +116,7 @@
(send-mattermost-notification! cfg cdata))
(persist-on-database! cfg cdata))
(catch Exception e
(log/error e "Unexpected exception on error reporter.")))))
(log/error e "unexpected exception on error reporter")))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Http Handler

View file

@ -34,7 +34,7 @@
(defmethod ig/init-key ::receiver
[_ {:keys [endpoint] :as cfg}]
(log/infof "Intializing ZMQ receiver on '%s'." endpoint)
(log/infof "intializing ZMQ receiver on '%s'" endpoint)
(let [buffer (a/chan 1)
output (a/chan 1 (comp (filter map?)
(map prepare)))

View file

@ -37,7 +37,15 @@
:max-pool-size 20}
:app.metrics/metrics
{}
{:definitions
{:profile-register
{:name "actions_profile_register_count"
:help "A global counter of user registrations."
:type :counter}
:profile-activation
{:name "actions_profile_activation_count"
:help "A global counter of profile activations"
:type :counter}}}
:app.migrations/all
{:main (ig/ref :app.migrations/migrations)
@ -69,7 +77,19 @@
:app.http.session/session
{:pool (ig/ref :app.db/pool)
:cookie-name "auth-token"}
:cookie-name (:http-session-cookie-name config)}
:app.http.session/gc-task
{:pool (ig/ref :app.db/pool)
:max-age (:http-session-idle-max-age config)}
:app.http.session/updater
{:pool (ig/ref :app.db/pool)
:metrics (ig/ref :app.metrics/metrics)
:executor (ig/ref :app.worker/executor)
:session (ig/ref :app.http.session/session)
:max-batch-age (:http-session-updater-batch-max-age config)
:max-batch-size (:http-session-updater-batch-max-size config)}
:app.http.awsns/handler
{:tokens (ig/ref :app.tokens/tokens)
@ -174,46 +194,61 @@
:app.worker/worker
{:executor (ig/ref :app.worker/executor)
:pool (ig/ref :app.db/pool)
:tasks (ig/ref :app.tasks/all)}
:tasks (ig/ref :app.tasks/registry)}
:app.worker/scheduler
{:executor (ig/ref :app.worker/executor)
:pool (ig/ref :app.db/pool)
:tasks (ig/ref :app.tasks/registry)
:schedule
[{:id "file-media-gc"
:cron #app/cron "0 0 0 */1 * ? *" ;; daily
:fn (ig/ref :app.tasks.file-media-gc/handler)}
:task :file-media-gc}
{:id "file-xlog-gc"
:cron #app/cron "0 0 */1 * * ?" ;; hourly
:fn (ig/ref :app.tasks.file-xlog-gc/handler)}
:task :file-xlog-gc}
{:id "storage-deleted-gc"
:cron #app/cron "0 0 1 */1 * ?" ;; daily (1 hour shift)
:fn (ig/ref :app.storage/gc-deleted-task)}
:task :storage-deleted-gc}
{:id "storage-touched-gc"
:cron #app/cron "0 0 2 */1 * ?" ;; daily (2 hour shift)
:fn (ig/ref :app.storage/gc-touched-task)}
:task :storage-touched-gc}
{:id "session-gc"
:cron #app/cron "0 0 3 */1 * ?" ;; daily (3 hour shift)
:task :session-gc}
{:id "storage-recheck"
:cron #app/cron "0 0 */1 * * ?" ;; hourly
:fn (ig/ref :app.storage/recheck-task)}
:task :storage-recheck}
{:id "tasks-gc"
:cron #app/cron "0 0 0 */1 * ?" ;; daily
:fn (ig/ref :app.tasks.tasks-gc/handler)}
:task :tasks-gc}
(when (:telemetry-enabled config)
{:id "telemetry"
:cron #app/cron "0 0 */6 * * ?" ;; every 6h
:uri (:telemetry-uri config)
:fn (ig/ref :app.tasks.telemetry/handler)})]}
:task :telemetry})]}
:app.tasks/all
{"sendmail" (ig/ref :app.tasks.sendmail/handler)
"delete-object" (ig/ref :app.tasks.delete-object/handler)
"delete-profile" (ig/ref :app.tasks.delete-profile/handler)}
:app.tasks/registry
{:metrics (ig/ref :app.metrics/metrics)
:tasks
{:sendmail (ig/ref :app.tasks.sendmail/handler)
:delete-object (ig/ref :app.tasks.delete-object/handler)
:delete-profile (ig/ref :app.tasks.delete-profile/handler)
:file-media-gc (ig/ref :app.tasks.file-media-gc/handler)
:file-xlog-gc (ig/ref :app.tasks.file-xlog-gc/handler)
:storage-deleted-gc (ig/ref :app.storage/gc-deleted-task)
:storage-touched-gc (ig/ref :app.storage/gc-touched-task)
:storage-recheck (ig/ref :app.storage/recheck-task)
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
:telemetry (ig/ref :app.tasks.telemetry/handler)
:session-gc (ig/ref :app.http.session/gc-task)}}
:app.tasks.sendmail/handler
{:host (:smtp-host config)
@ -335,7 +370,7 @@
(-> system-config
(ig/prep)
(ig/init))))
(log/infof "Welcome to penpot! Version: '%s'."
(log/infof "welcome to penpot (version: '%s')"
(:full cfg/version))))
(defn stop

View file

@ -10,17 +10,15 @@
(ns app.metrics
(:require
[app.common.exceptions :as ex]
[app.util.time :as dt]
[app.worker]
[clojure.spec.alpha :as s]
[clojure.tools.logging :as log]
[integrant.core :as ig]
[next.jdbc :as jdbc])
[integrant.core :as ig])
(:import
io.prometheus.client.CollectorRegistry
io.prometheus.client.Counter
io.prometheus.client.Gauge
io.prometheus.client.Summary
io.prometheus.client.Histogram
io.prometheus.client.exporter.common.TextFormat
io.prometheus.client.hotspot.DefaultExports
io.prometheus.client.jetty.JettyStatisticsCollector
@ -30,41 +28,12 @@
(declare instrument-vars!)
(declare instrument)
(declare create-registry)
(declare create)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Entry Point
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- instrument-jdbc!
[registry]
(instrument-vars!
[#'next.jdbc/execute-one!
#'next.jdbc/execute!]
{:registry registry
:type :counter
:name "database_query_counter"
:help "An absolute counter of database queries."}))
(defn- instrument-workers!
[registry]
(instrument-vars!
[#'app.worker/run-task]
{:registry registry
:type :summary
:name "worker_task_checkout_millis"
:help "Latency measured between scheduld_at and execution time."
:wrap (fn [rootf mobj]
(let [mdata (meta rootf)
origf (::original mdata rootf)]
(with-meta
(fn [tasks item]
(let [now (inst-ms (dt/now))
sat (inst-ms (:scheduled-at item))]
(mobj :observe (- now sat))
(origf tasks item)))
{::original origf})))}))
(defn- handler
[registry _request]
(let [samples (.metricFamilySamples ^CollectorRegistry registry)
@ -73,13 +42,24 @@
{:headers {"content-type" TextFormat/CONTENT_TYPE_004}
:body (.toString writer)}))
(s/def ::definitions
(s/map-of keyword? map?))
(defmethod ig/pre-init-spec ::metrics [_]
(s/keys :opt-un [::definitions]))
(defmethod ig/init-key ::metrics
[_ _cfg]
[_ {:keys [definitions] :as cfg}]
(log/infof "Initializing prometheus registry and instrumentation.")
(let [registry (create-registry)]
(instrument-workers! registry)
(instrument-jdbc! registry)
(let [registry (create-registry)
definitions (reduce-kv (fn [res k v]
(->> (assoc v :registry registry)
(create)
(assoc res k)))
{}
definitions)]
{:handler (partial handler registry)
:definitions definitions
:registry registry}))
(s/def ::handler fn?)
@ -87,7 +67,6 @@
(s/def ::metrics
(s/keys :req-un [::registry ::handler]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Implementation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -126,7 +105,7 @@
(invoke [_ cmd labels]
(.. ^Counter instance
(labels labels)
(labels (into-array String labels))
(inc))))))
(defn make-gauge
@ -150,19 +129,27 @@
:dec (.dec ^Gauge instance)))
(invoke [_ cmd labels]
(let [labels (into-array String [labels])]
(case cmd
:inc (.. ^Gauge instance (labels labels) (inc))
:dec (.. ^Gauge instance (labels labels) (dec)))))))
:dec (.. ^Gauge instance (labels labels) (dec))))))))
(def default-quantiles
[[0.75 0.02]
[0.99 0.001]])
(defn make-summary
[{:keys [name help registry reg labels max-age] :or {max-age 3600} :as props}]
[{:keys [name help registry reg labels max-age quantiles buckets]
:or {max-age 3600 buckets 6 quantiles default-quantiles} :as props}]
(let [registry (or registry reg)
instance (doto (Summary/build)
(.name name)
(.help help)
(.maxAgeSeconds max-age)
(.quantile 0.75 0.02)
(.quantile 0.99 0.001))
(.help help))
_ (when (seq quantiles)
(.maxAgeSeconds ^Summary instance max-age)
(.ageBuckets ^Summary instance buckets))
_ (doseq [[q e] quantiles]
(.quantile ^Summary instance q e))
_ (when (seq labels)
(.labelNames instance (into-array String labels)))
instance (.register instance registry)]
@ -176,7 +163,34 @@
(invoke [_ cmd val labels]
(.. ^Summary instance
(labels labels)
(labels (into-array String labels))
(observe val))))))
(def default-histogram-buckets
[1 5 10 25 50 75 100 250 500 750 1000 2500 5000 7500])
(defn make-histogram
[{:keys [name help registry reg labels buckets]
:or {buckets default-histogram-buckets}}]
(let [registry (or registry reg)
instance (doto (Histogram/build)
(.name name)
(.help help)
(.buckets (into-array Double/TYPE buckets)))
_ (when (seq labels)
(.labelNames instance (into-array String labels)))
instance (.register instance registry)]
(reify
clojure.lang.IDeref
(deref [_] instance)
clojure.lang.IFn
(invoke [_ cmd val]
(.observe ^Histogram instance val))
(invoke [_ cmd val labels]
(.. ^Histogram instance
(labels (into-array String labels))
(observe val))))))
(defn create
@ -184,7 +198,8 @@
(case type
:counter (make-counter props)
:gauge (make-gauge props)
:summary (make-summary props)))
:summary (make-summary props)
:histogram (make-histogram props)))
(defn wrap-counter
([rootf mobj]
@ -204,7 +219,6 @@
(assoc mdata ::original origf))))
([rootf mobj labels]
(let [mdata (meta rootf)
labels (into-array String labels)
origf (::original mdata rootf)]
(with-meta
(fn
@ -241,7 +255,6 @@
([rootf mobj labels]
(let [mdata (meta rootf)
labels (into-array String labels)
origf (::original mdata rootf)]
(with-meta
(fn
@ -284,6 +297,9 @@
(instance? Summary @obj)
((or wrap wrap-summary) f obj)
(instance? Histogram @obj)
((or wrap wrap-summary) f obj)
:else
(ex/raise :type :not-implemented))))

View file

@ -158,6 +158,8 @@
{:name "0048-mod-storage-tables"
:fn (mg/resource "app/migrations/sql/0048-mod-storage-tables.sql")}
{:name "0049-mod-http-session-table"
:fn (mg/resource "app/migrations/sql/0049-mod-http-session-table.sql")}
])

View file

@ -0,0 +1,6 @@
ALTER TABLE http_session
ADD COLUMN updated_at timestamptz NULL;
CREATE INDEX http_session__updated_at__idx
ON http_session (updated_at)
WHERE updated_at IS NOT NULL;

View file

@ -14,6 +14,7 @@
[app.db :as db]
[app.metrics :as mtx]
[app.util.async :as aa]
[app.util.time :as dt]
[app.util.transit :as t]
[clojure.core.async :as a]
[clojure.spec.alpha :as s]
@ -46,29 +47,32 @@
mtx-active-connections
(mtx/create
{:name "websocket_notifications_active_connections"
{:name "websocket_active_connections"
:registry (:registry metrics)
:type :gauge
:help "Active websocket connections on notifications service."})
:help "Active websocket connections."})
mtx-message-recv
mtx-messages
(mtx/create
{:name "websocket_notifications_message_recv_timing"
{:name "websocket_message_count"
:registry (:registry metrics)
:type :summary
:help "Message receive summary timing (ms)."})
:labels ["op"]
:type :counter
:help "Counter of processed messages."})
mtx-message-send
mtx-sessions
(mtx/create
{:name "websocket_notifications_message_send_timing"
{:name "websocket_session_timing"
:registry (:registry metrics)
:type :summary
:help "Message receive summary timing (ms)."})
:quantiles []
:help "Websocket session timing (seconds)."
:type :summary})
cfg (assoc cfg
:mtx-active-connections mtx-active-connections
:mtx-message-recv mtx-message-recv
:mtx-message-send mtx-message-send)]
:mtx-messages mtx-messages
:mtx-sessions mtx-sessions
)]
(-> #(handler cfg %)
(wrap-session)
(wrap-keyword-params)
@ -132,14 +136,15 @@
[{:keys [file-id team-id msgbus] :as cfg}]
(let [in (a/chan 32)
out (a/chan 32)
mtx-active-connections (:mtx-active-connections cfg)
mtx-message-send (:mtx-message-send cfg)
mtx-message-recv (:mtx-message-recv cfg)
mtx-aconn (:mtx-active-connections cfg)
mtx-messages (:mtx-messages cfg)
mtx-sessions (:mtx-sessions cfg)
created-at (dt/now)
ws-send (mtx/wrap-summary ws-send mtx-message-send)]
ws-send (mtx/wrap-counter ws-send mtx-messages ["send"])]
(letfn [(on-connect [conn]
(mtx-active-connections :inc)
(mtx-aconn :inc)
(let [sub (a/chan)
ws (WebSocket. conn in out sub nil cfg)]
@ -159,11 +164,14 @@
(a/close! sub))))
(on-error [_conn _e]
(mtx-aconn :dec)
(mtx-sessions :observe (/ (inst-ms (dt/duration-between created-at (dt/now))) 1000.0))
(a/close! out)
(a/close! in))
(on-close [_conn _status _reason]
(mtx-active-connections :dec)
(mtx-aconn :dec)
(mtx-sessions :observe (/ (inst-ms (dt/duration-between created-at (dt/now))) 1000.0))
(a/close! out)
(a/close! in))
@ -174,7 +182,7 @@
{:on-connect on-connect
:on-error on-error
:on-close on-close
:on-text (mtx/wrap-summary on-message mtx-message-recv)
:on-text (mtx/wrap-counter on-message mtx-messages ["recv"])
:on-bytes (constantly nil)})))
(declare handle-message)
@ -188,7 +196,7 @@
(aa/<? (start-loop! ws))
(aa/<? (handle-message ws {:type :disconnect}))
(catch Throwable err
(log/errorf err "Unexpected exception on websocket handler.")
(log/errorf err "unexpected exception on websocket handler")
(let [session (.getSession ^WebSocketAdapter conn)]
(when session
(.disconnect session)))))))

View file

@ -25,6 +25,11 @@
[_]
(ex/raise :type :not-found))
(defn- run-hook
[hook-fn response]
(ex/ignoring (hook-fn))
response)
(defn- rpc-query-handler
[methods {:keys [profile-id] :as request}]
(let [type (keyword (get-in request [:path-params :type]))
@ -50,7 +55,11 @@
result ((get methods type default-handler) data)
mdata (meta result)]
(cond->> {:status 200 :body result}
(fn? (:transform-response mdata)) ((:transform-response mdata) request))))
(fn? (:transform-response mdata))
((:transform-response mdata) request)
(fn? (:before-complete mdata))
(run-hook (:before-complete mdata)))))
(defn- wrap-with-metrics
[cfg f mdata]
@ -66,7 +75,7 @@
(ex/raise :type :internal
:code :rlimit-not-configured
:hint (str/fmt "%s rlimit not configured" key)))
(log/tracef "Adding rlimit to '%s' rpc handler." (::sv/name mdata))
(log/tracef "adding rlimit to '%s' rpc handler" (::sv/name mdata))
(fn [cfg params]
(rlm/execute rlinst (f cfg params))))
f))
@ -76,7 +85,7 @@
(let [f (wrap-with-rlimits cfg f mdata)
f (wrap-with-metrics cfg f mdata)
spec (or (::sv/spec mdata) (s/spec any?))]
(log/tracef "Registering '%s' command to rpc service." (::sv/name mdata))
(log/tracef "registering '%s' command to rpc service" (::sv/name mdata))
(fn [params]
(when (and (:auth mdata true) (not (uuid? (:profile-id params))))
(ex/raise :type :authentication
@ -96,7 +105,7 @@
{:name "rpc_query_timing"
:labels ["name"]
:registry (get-in cfg [:metrics :registry])
:type :summary
:type :histogram
:help "Timing of query services."})
cfg (assoc cfg ::mobj mobj)]
(->> (sv/scan-ns 'app.rpc.queries.projects
@ -115,7 +124,7 @@
{:name "rpc_mutation_timing"
:labels ["name"]
:registry (get-in cfg [:metrics :registry])
:type :summary
:type :histogram
:help "Timing of mutation services."})
cfg (assoc cfg ::mobj mobj)]
(->> (sv/scan-ns 'app.rpc.mutations.demo

View file

@ -30,7 +30,7 @@
(try
(ldap/connect params)
(catch Exception e
(log/errorf e "Cannot connect to LDAP %s:%s"
(log/errorf e "cannot connect to LDAP %s:%s"
(get-in params [:host :address])
(get-in params [:host :port])))))))

View file

@ -26,7 +26,6 @@
[app.util.time :as dt]
[buddy.hashers :as hashers]
[clojure.spec.alpha :as s]
[clojure.tools.logging :as log]
[cuerdas.core :as str]))
;; --- Helpers & Specs
@ -42,10 +41,12 @@
;; --- Mutation: Register Profile
(declare annotate-profile-register)
(declare check-profile-existence!)
(declare create-profile)
(declare create-profile-relations)
(declare email-domain-in-whitelist?)
(declare register-profile)
(s/def ::invitation-token ::us/not-empty-string)
(s/def ::register-profile
@ -63,9 +64,23 @@
:code :email-domain-is-not-allowed))
(db/with-atomic [conn pool]
(let [cfg (assoc cfg :conn conn)]
(register-profile cfg params))))
(defn- annotate-profile-register
"A helper for properly increase the profile-register metric once the
transaction is completed."
[metrics profile]
(fn []
(when (::created profile)
((get-in metrics [:definitions :profile-register]) :inc))))
(defn- register-profile
[{:keys [conn tokens session metrics] :as cfg} params]
(check-profile-existence! conn params)
(let [profile (->> (create-profile conn params)
(create-profile-relations conn))]
(create-profile-relations conn))
profile (assoc profile ::created true)]
(create-profile-initial-data conn profile)
(if-let [token (:invitation-token params)]
@ -77,10 +92,11 @@
claims (assoc claims
:member-id (:id profile)
:member-email (:email profile))
token (tokens :generate claims)]
(with-meta
{:invitation-token token}
{:transform-response ((:create session) (:id profile))}))
token (tokens :generate claims)
resp {:invitation-token token}]
(with-meta resp
{:transform-response ((:create session) (:id profile))
:before-complete (annotate-profile-register metrics profile)}))
;; If no token is provided, send a verification email
(let [vtoken (tokens :generate
@ -104,7 +120,8 @@
:name (:fullname profile)
:token vtoken
:extra-data ptoken})
profile)))))
(with-meta profile
{:before-complete (annotate-profile-register metrics profile)})))))
(defn email-domain-in-whitelist?
"Returns true if email's domain is in the given whitelist or if given
@ -142,8 +159,7 @@
[attempt password]
(try
(hashers/verify attempt password)
(catch Exception e
(log/warnf e "Error on verify password (only informative, nothing affected to user).")
(catch Exception _e
{:update false
:valid false})))
@ -269,10 +285,12 @@
(s/keys :req-un [::email ::fullname ::backend]))
(sv/defmethod ::login-or-register {:auth false}
[{:keys [pool] :as cfg} params]
[{:keys [pool metrics] :as cfg} params]
(db/with-atomic [conn pool]
(-> (assoc cfg :conn conn)
(login-or-register params))))
(let [profile (-> (assoc cfg :conn conn)
(login-or-register params))]
(with-meta profile
{:before-complete (annotate-profile-register metrics profile)}))))
(defn login-or-register
[{:keys [conn] :as cfg} {:keys [email backend] :as params}]
@ -294,7 +312,7 @@
(let [profile (->> (create-profile conn params)
(create-profile-relations conn))]
(create-profile-initial-data conn profile)
profile))]
(assoc profile ::created true)))]
(let [profile (profile/retrieve-profile-data-by-email conn email)
profile (if profile

View file

@ -40,8 +40,15 @@
{:id profile-id})
claims)
(defn- annotate-profile-activation
"A helper for properly increase the profile-activation metric once the
transaction is completed."
[metrics]
(fn []
((get-in metrics [:definitions :profile-activation]) :inc)))
(defmethod process-token :verify-email
[{:keys [conn session] :as cfg} _params {:keys [profile-id] :as claims}]
[{:keys [conn session metrics] :as cfg} _ {:keys [profile-id] :as claims}]
(let [profile (profile/retrieve-profile conn profile-id)
claims (assoc claims :profile profile)]
@ -56,7 +63,8 @@
{:id (:id profile)}))
(with-meta claims
{:transform-response ((:create session) profile-id)})))
{:transform-response ((:create session) profile-id)
:before-complete (annotate-profile-activation metrics)})))
(defmethod process-token :auth
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]

View file

@ -5,17 +5,19 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
;; Copyright (c) 2020-2021 UXBOX Labs SL
(ns app.tasks
(:require
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
;; [app.metrics :as mtx]
[app.metrics :as mtx]
[app.util.time :as dt]
[app.worker]
[clojure.spec.alpha :as s]
[clojure.tools.logging :as log]))
[clojure.tools.logging :as log]
[integrant.core :as ig]))
(s/def ::name ::us/string)
(s/def ::delay
@ -41,11 +43,68 @@
interval (db/interval duration)
props (db/tjson props)
id (uuid/next)]
(log/infof "Submit task '%s' to be executed in '%s'." name (str duration))
(log/debugf "submit task '%s' to be executed in '%s'" name (str duration))
(db/exec-one! conn [sql:insert-new-task id name props queue priority max-retries interval])
id))
;; (mtx/instrument-with-counter!
;; {:var #'submit!
;; :id "tasks__submit_counter"
;; :help "Absolute task submit counter."})
(defn- instrument!
[registry]
(mtx/instrument-vars!
[#'submit!]
{:registry registry
:type :counter
:labels ["name"]
:name "tasks_submit_counter"
:help "An absolute counter of task submissions."
:wrap (fn [rootf mobj]
(let [mdata (meta rootf)
origf (::original mdata rootf)]
(with-meta
(fn [conn params]
(let [tname (:name params)]
(mobj :inc [tname])
(origf conn params)))
{::original origf})))})
(mtx/instrument-vars!
[#'app.worker/run-task]
{:registry registry
:type :summary
:quantiles []
:name "tasks_checkout_timing"
:help "Latency measured between scheduld_at and execution time."
:wrap (fn [rootf mobj]
(let [mdata (meta rootf)
origf (::original mdata rootf)]
(with-meta
(fn [tasks item]
(let [now (inst-ms (dt/now))
sat (inst-ms (:scheduled-at item))]
(mobj :observe (- now sat))
(origf tasks item)))
{::original origf})))}))
;; --- STATE INIT: REGISTRY
(s/def ::tasks
(s/map-of keyword? fn?))
(defmethod ig/pre-init-spec ::registry [_]
(s/keys :req-un [::mtx/metrics ::tasks]))
(defmethod ig/init-key ::registry
[_ {:keys [metrics tasks]}]
(instrument! (:registry metrics))
(let [mobj (mtx/create
{:registry (:registry metrics)
:type :summary
:labels ["name"]
:quantiles []
:name "tasks_timing"
:help "Background task execution timing."})]
(reduce-kv (fn [res k v]
(let [tname (name k)]
(log/debugf "registring task '%s'" tname)
(assoc res tname (mtx/wrap-summary v mobj [tname]))))
{}
tasks)))

View file

@ -12,42 +12,32 @@
(:require
[app.common.spec :as us]
[app.db :as db]
[app.metrics :as mtx]
[clojure.spec.alpha :as s]
[clojure.tools.logging :as log]
[integrant.core :as ig]))
(declare handler)
(declare handle-deletion)
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::db/pool ::mtx/metrics]))
(s/keys :req-un [::db/pool]))
(defmethod ig/init-key ::handler
[_ {:keys [metrics] :as cfg}]
(let [handler #(handler cfg %)]
(->> {:registry (:registry metrics)
:type :summary
:name "task_delete_object_timing"
:help "delete object task timing"}
(mtx/instrument handler))))
[_ {:keys [pool] :as cfg}]
(fn [{:keys [props] :as task}]
(us/verify ::props props)
(db/with-atomic [conn pool]
(handle-deletion conn props))))
(s/def ::type ::us/keyword)
(s/def ::id ::us/uuid)
(s/def ::props (s/keys :req-un [::id ::type]))
(defn- handler
[{:keys [pool]} {:keys [props] :as task}]
(us/verify ::props props)
(db/with-atomic [conn pool]
(handle-deletion conn props)))
(defmulti handle-deletion
(fn [_ props] (:type props)))
(defmethod handle-deletion :default
[_conn {:keys [type]}]
(log/warnf "no handler found for %s" type))
(log/warnf "no handler found for '%s'" type))
(defmethod handle-deletion :file
[conn {:keys [id] :as props}]

View file

@ -13,27 +13,16 @@
[app.common.spec :as us]
[app.db :as db]
[app.db.sql :as sql]
[app.metrics :as mtx]
[clojure.spec.alpha :as s]
[clojure.tools.logging :as log]
[integrant.core :as ig]))
(declare delete-profile-data)
(declare handler)
;; --- INIT
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::db/pool ::mtx/metrics]))
(defmethod ig/init-key ::handler
[_ {:keys [metrics] :as cfg}]
(let [handler #(handler cfg %)]
(->> {:registry (:registry metrics)
:type :summary
:name "task_delete_profile_timing"
:help "delete profile task timing"}
(mtx/instrument handler))))
(s/keys :req-un [::db/pool]))
;; This task is responsible to permanently delete a profile with all
;; the dependent data. As step (1) we delete all owned teams of the
@ -48,8 +37,9 @@
(s/def ::profile-id ::us/uuid)
(s/def ::props (s/keys :req-un [::profile-id]))
(defn handler
[{:keys [pool]} {:keys [props] :as task}]
(defmethod ig/init-key ::handler
[_ {:keys [pool] :as cfg}]
(fn [{:keys [props] :as task}]
(us/verify ::props props)
(db/with-atomic [conn pool]
(let [id (:profile-id props)
@ -57,7 +47,7 @@
(if (or (:is-demo profile)
(:deleted-at profile))
(delete-profile-data conn id)
(log/warnf "Profile %s does not match constraints for deletion" id)))))
(log/warnf "profile '%s' does not match constraints for deletion" id))))))
;; --- IMPL
@ -80,7 +70,7 @@
(defn- delete-profile-data
[conn profile-id]
(log/infof "Proceding to delete all data related to profile id = %s" profile-id)
(log/debugf "proceding to delete all data related to profile '%s'" profile-id)
(delete-teams conn profile-id)
(delete-profile conn profile-id)
true)

View file

@ -5,7 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
;; Copyright (c) 2020-2021 UXBOX Labs SL
(ns app.tasks.file-media-gc
"A maintenance task that is responsible to purge the unused media
@ -14,33 +14,23 @@
(:require
[app.common.pages.migrations :as pmg]
[app.db :as db]
[app.metrics :as mtx]
[app.util.blob :as blob]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[clojure.tools.logging :as log]
[integrant.core :as ig]))
(declare handler)
(declare process-file)
(declare retrieve-candidates)
(s/def ::max-age ::dt/duration)
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::db/pool ::mtx/metrics ::max-age]))
(s/keys :req-un [::db/pool ::max-age]))
(defmethod ig/init-key ::handler
[_ {:keys [metrics] :as cfg}]
(let [handler #(handler cfg %)]
(->> {:registry (:registry metrics)
:type :summary
:name "task_file_media_gc_timing"
:help "file media garbage collection task timing"}
(mtx/instrument handler))))
(defn- handler
[{:keys [pool] :as cfg} _]
[_ {:keys [pool] :as cfg}]
(fn [_]
(db/with-atomic [conn pool]
(let [cfg (assoc cfg :conn conn)]
(loop [n 0]
@ -50,8 +40,8 @@
(run! (partial process-file cfg) files)
(recur (+ n (count files))))
(do
(log/infof "finalized with total of %s processed files" n)
{:processed n})))))))
(log/debugf "finalized with total of %s processed files" n)
{:processed n}))))))))
(def ^:private
sql:retrieve-candidates-chunk
@ -98,7 +88,7 @@
unused (->> (db/query conn :file-media-object {:file-id id})
(remove #(contains? used (:id %))))]
(log/infof "processing file: id='%s' age='%s' to-delete=%s" id age (count unused))
(log/debugf "processing file: id='%s' age='%s' to-delete=%s" id age (count unused))
;; Mark file as trimmed
(db/update! conn :file

View file

@ -5,45 +5,36 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
;; Copyright (c) 2020-2021 UXBOX Labs SL
(ns app.tasks.file-xlog-gc
"A maintenance task that performs a garbage collection of the file
change (transaction) log."
(:require
[app.db :as db]
[app.metrics :as mtx]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[clojure.tools.logging :as log]
[integrant.core :as ig]))
(declare handler)
(declare sql:delete-files-xlog)
(s/def ::max-age ::dt/duration)
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::db/pool ::mtx/metrics ::max-age]))
(s/keys :req-un [::db/pool ::max-age]))
(defmethod ig/init-key ::handler
[_ {:keys [metrics] :as cfg}]
(let [handler #(handler cfg %)]
(->> {:registry (:registry metrics)
:type :summary
:name "task_file_xlog_gc_timing"
:help "file changes garbage collection task timing"}
(mtx/instrument handler))))
[_ {:keys [pool max-age] :as cfg}]
(fn [_]
(db/with-atomic [conn pool]
(let [interval (db/interval max-age)
result (db/exec-one! conn [sql:delete-files-xlog interval])
result (:next.jdbc/update-count result)]
(log/debugf "removed %s rows from file-change table" result)
result))))
(def ^:private
sql:delete-files-xlog
"delete from file_change
where created_at < now() - ?::interval")
(defn- handler
[{:keys [pool max-age]} _]
(db/with-atomic [conn pool]
(let [interval (db/interval max-age)
result (db/exec-one! conn [sql:delete-files-xlog interval])
result (:next.jdbc/update-count result)]
(log/infof "removed %s rows from file_change table" result)
nil)))

View file

@ -10,13 +10,12 @@
(ns app.tasks.sendmail
(:require
[app.config :as cfg]
[app.metrics :as mtx]
[app.util.emails :as emails]
[clojure.spec.alpha :as s]
[clojure.tools.logging :as log]
[integrant.core :as ig]))
(declare handler)
(declare send-console!)
(s/def ::username ::cfg/smtp-username)
(s/def ::password ::cfg/smtp-password)
@ -29,7 +28,7 @@
(s/def ::enabled ::cfg/smtp-enabled)
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::enabled ::mtx/metrics]
(s/keys :req-un [::enabled]
:opt-un [::username
::password
::tls
@ -40,13 +39,11 @@
::default-reply-to]))
(defmethod ig/init-key ::handler
[_ {:keys [metrics] :as cfg}]
(let [handler #(handler cfg %)]
(->> {:registry (:registry metrics)
:type :summary
:name "task_sendmail_timing"
:help "sendmail task timing"}
(mtx/instrument handler))))
[_ cfg]
(fn [{:keys [props] :as task}]
(if (:enabled cfg)
(emails/send! cfg props)
(send-console! cfg props))))
(defn- send-console!
[cfg email]
@ -59,9 +56,3 @@
(println (.toString baos))
(println "******** end email "(:id email) "**********"))]
(log/info out))))
(defn handler
[cfg {:keys [props] :as task}]
(if (:enabled cfg)
(emails/send! cfg props)
(send-console! cfg props)))

View file

@ -5,46 +5,36 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
;; Copyright (c) 2020-2021 UXBOX Labs SL
(ns app.tasks.tasks-gc
"A maintenance task that performs a cleanup of already executed tasks
from the database table."
(:require
[app.db :as db]
[app.metrics :as mtx]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[clojure.tools.logging :as log]
[integrant.core :as ig]))
(declare handler)
(declare sql:delete-completed-tasks)
(s/def ::max-age ::dt/duration)
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::db/pool ::mtx/metrics ::max-age]))
(s/keys :req-un [::db/pool ::max-age]))
(defmethod ig/init-key ::handler
[_ {:keys [metrics] :as cfg}]
(let [handler #(handler cfg %)]
(->> {:registry (:registry metrics)
:type :summary
:name "task_tasks_gc_timing"
:help "tasks garbage collection task timing"}
(mtx/instrument handler))))
[_ {:keys [pool max-age] :as cfg}]
(fn [_]
(db/with-atomic [conn pool]
(let [interval (db/interval max-age)
result (db/exec-one! conn [sql:delete-completed-tasks interval])
result (:next.jdbc/update-count result)]
(log/debugf "removed %s rows from tasks-completed table" result)
result))))
(def ^:private
sql:delete-completed-tasks
"delete from task_completed
where scheduled_at < now() - ?::interval")
(defn- handler
[{:keys [pool max-age]} _]
(db/with-atomic [conn pool]
(let [interval (db/interval max-age)
result (db/exec-one! conn [sql:delete-completed-tasks interval])
result (:next.jdbc/update-count result)]
(log/infof "removed %s rows from tasks_completed table" result)
nil)))

View file

@ -88,7 +88,7 @@
(catch Exception e
;; We don't want notify user of a error, just log it for posible
;; future investigation.
(log/warn e (str "Unexpected error on telemetry:\n"
(log/warn e (str "unexpected error on telemetry:\n"
(when-let [edata (ex-data e)]
(str "ex-data: \n"
(with-out-str (pprint edata))))
@ -118,4 +118,4 @@
data
data])))
(catch Exception e
(log/errorf e "Error on procesing request."))))
(log/errorf e "error on procesing request"))))

View file

@ -10,6 +10,7 @@
(ns app.worker
"Async tasks abstraction (impl)."
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db]
@ -19,6 +20,7 @@
[clojure.core.async :as a]
[clojure.spec.alpha :as s]
[clojure.tools.logging :as log]
[cuerdas.core :as str]
[integrant.core :as ig]
[promesa.exec :as px])
(:import
@ -72,7 +74,7 @@
(s/def ::queue ::us/string)
(s/def ::parallelism ::us/integer)
(s/def ::batch-size ::us/integer)
(s/def ::tasks (s/map-of string? ::us/fn))
(s/def ::tasks (s/map-of string? fn?))
(s/def ::poll-interval ::dt/duration)
(defmethod ig/pre-init-spec ::worker [_]
@ -94,7 +96,7 @@
(defmethod ig/init-key ::worker
[_ {:keys [pool poll-interval name queue] :as cfg}]
(log/infof "Starting worker '%s' on queue '%s'." name queue)
(log/infof "starting worker '%s' on queue '%s'" name queue)
(let [cch (a/chan 1)
poll-ms (inst-ms poll-interval)]
(a/go-loop []
@ -103,30 +105,30 @@
;; Terminate the loop if close channel is closed or
;; event-loop-fn returns nil.
(or (= port cch) (nil? val))
(log/infof "Stop condition found. Shutdown worker: '%s'" name)
(log/infof "stop condition found; shutdown worker: '%s'" name)
(db/pool-closed? pool)
(do
(log/info "Worker eventloop is aborted because pool is closed.")
(log/info "worker eventloop is aborted because pool is closed")
(a/close! cch))
(and (instance? java.sql.SQLException val)
(contains? #{"08003" "08006" "08001" "08004"} (.getSQLState ^java.sql.SQLException val)))
(do
(log/error "Connection error, trying resume in some instants.")
(log/error "connection error, trying resume in some instants")
(a/<! (a/timeout poll-interval))
(recur))
(and (instance? java.sql.SQLException val)
(= "40001" (.getSQLState ^java.sql.SQLException val)))
(do
(log/debug "Serialization failure (retrying in some instants).")
(log/debug "serialization failure (retrying in some instants)")
(a/<! (a/timeout poll-ms))
(recur))
(instance? Exception val)
(do
(log/errorf val "Unexpected error ocurried on polling the database (will resume in some instants).")
(log/errorf val "unexpected error ocurried on polling the database (will resume in some instants)")
(a/<! (a/timeout poll-ms))
(recur))
@ -202,7 +204,7 @@
(let [task-fn (get tasks name)]
(if task-fn
(task-fn item)
(log/warn "no task handler found for" (pr-str name)))
(log/warnf "no task handler found for '%s'" (pr-str name)))
{:status :completed :task item}))
(defn get-error-context
@ -227,7 +229,7 @@
(let [cdata (get-error-context error item)]
(update-thread-context! cdata)
(log/errorf error "Unhandled exception on task (id: %s)" (:id cdata))
(log/errorf error "unhandled exception on task (id: '%s')" (:id cdata))
(if (>= (:retry-num item) (:max-retries item))
{:status :failed :task item :error error}
{:status :retry :task item :error error})))))
@ -235,12 +237,12 @@
(defn- run-task
[{:keys [tasks]} item]
(try
(log/debugf "Started task '%s/%s/%s'." (:name item) (:id item) (:retry-num item))
(log/debugf "started task '%s/%s/%s'" (:name item) (:id item) (:retry-num item))
(handle-task tasks item)
(catch Exception e
(handle-exception e item))
(finally
(log/debugf "Finished task '%s/%s/%s'." (:name item) (:id item) (:retry-num item)))))
(log/debugf "finished task '%s/%s/%s'" (:name item) (:id item) (:retry-num item)))))
(def sql:select-next-tasks
"select * from task as t
@ -289,21 +291,31 @@
(s/def ::id ::us/string)
(s/def ::cron dt/cron?)
(s/def ::props (s/nilable map?))
(s/def ::task keyword?)
(s/def ::scheduled-task-spec
(s/keys :req-un [::id ::cron ::fn]
(s/keys :req-un [::id ::cron ::task]
:opt-un [::props]))
(s/def ::schedule
(s/coll-of (s/nilable ::scheduled-task-spec)))
(s/def ::schedule (s/coll-of (s/nilable ::scheduled-task-spec)))
(defmethod ig/pre-init-spec ::scheduler [_]
(s/keys :req-un [::executor ::db/pool ::schedule]))
(s/keys :req-un [::executor ::db/pool ::schedule ::tasks]))
(defmethod ig/init-key ::scheduler
[_ {:keys [schedule] :as cfg}]
[_ {:keys [schedule tasks] :as cfg}]
(let [scheduler (Executors/newScheduledThreadPool (int 1))
schedule (filter some? schedule)
schedule (->> schedule
(filter some?)
(map (fn [{:keys [task] :as item}]
(let [f (get tasks (name task))]
(when-not f
(ex/raise :type :internal
:code :task-not-found
:hint (str/fmt "task %s not configured" task)))
(-> item
(dissoc :task)
(assoc :fn f))))))
cfg (assoc cfg
:scheduler scheduler
:schedule schedule)]
@ -330,7 +342,7 @@
(defn- synchronize-schedule-item
[conn {:keys [id cron]}]
(let [cron (str cron)]
(log/debugf "initialize scheduled task '%s' (cron: '%s')." id cron)
(log/infof "initialize scheduled task '%s' (cron: '%s')" id cron)
(db/exec-one! conn [sql:upsert-scheduled-task id cron cron])))
(defn- synchronize-schedule
@ -351,27 +363,16 @@
(letfn [(run-task [conn]
(try
(when (db/exec-one! conn [sql:lock-scheduled-task id])
(log/info "Executing scheduled task" id)
(log/debugf "executing scheduled task '%s'" id)
((:fn task) task))
(catch Exception e
(catch Throwable e
e)))
(handle-task* [conn]
(let [result (run-task conn)]
(if (instance? Throwable result)
(do
(log/warnf result "unhandled exception on scheduled task '%s'" id)
(db/insert! conn :scheduled-task-history
{:id (uuid/next)
:task-id id
:is-error true
:reason (exception->string result)}))
(db/insert! conn :scheduled-task-history
{:id (uuid/next)
:task-id id}))))
(handle-task []
(db/with-atomic [conn pool]
(handle-task* conn)))]
(let [result (run-task conn)]
(when (ex/exception? result)
(log/errorf result "unhandled exception on scheduled task '%s'" id)))))]
(try
(px/run! executor handle-task)

View file

@ -52,3 +52,7 @@
(defn ex-info?
[v]
(instance? #?(:clj clojure.lang.ExceptionInfo :cljs cljs.core.ExceptionInfo) v))
(defn exception?
[v]
(instance? #?(:clj java.lang.Throwable :cljs js/Error) v))

View file

@ -90,6 +90,7 @@
ptk/WatchEvent
(watch [_ state s]
(->> (rp/mutation :logout)
(rx/catch (constantly (rx/empty)))
(rx/ignore)))
ptk/EffectEvent