summaryrefslogtreecommitdiff
path: root/src/weepinbell/processor/facade.clj
blob: 1360b30821ff7a7e126a918436ae3f4163cd1251 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
(ns weepinbell.processor.facade
  (:require [clojure.spec.alpha :as s]
            [clojure.core.async :refer [go-loop <! put! chan <!!]]
            [taoensso.timbre :as timbre]
            [weepinbell.utils.digest :refer [digest]]
            [weepinbell.storage.spec]
            ;; pipeline functions:
            [weepinbell.processor.storage-checker :refer [check-storage]]
            [weepinbell.processor.verifier :refer [verify]]
            [weepinbell.processor.janitor :refer [clean]]
            [weepinbell.processor.wrapper :refer [wrap]]
            [weepinbell.processor.writer :refer [write]]))

(s/def ::wmc (partial instance? clojure.core.async.impl.channels.ManyToManyChannel))
(s/def ::callbacks
  (s/keys :req-un [::on-receive ::on-create ::on-delete ::on-update]))
(s/def ::storage (partial s/valid? :weepinbell.storage.spec/vtable))
(s/def ::base-url string?)
(s/def ::allowed-target-domains sequential?)
(s/def ::pipeline-concurrency int?)
(s/def ::config
  (s/keys :req-un [::base-url ::allowed-target-domains ::pipeline-concurrency]))
(s/def ::processor-meta
  (s/keys :req-un [::wmc ::callbacks ::storage ::config]))

(defn pipe
  "Takes variable number of functions and form a pipeline.
   Essentially a -> macro that returns a function, or a reversed comp."
  [& functions]
  (apply comp (reverse functions)))

(defn assemble-pipeline
  "Makes a proceesor pipeline. Returns a function that accepts webmention."
  [{:keys [callbacks storage config]}]
  (pipe
   ;; initializer function
   ;;; turns {:s ... :t ...} to pipeline map that the following functions
   ;;; pass around; calls :on-receive hook.
   (fn [wm]
     (let [{:keys [s t]} wm
           id (digest (str s t))]
       ((:on-receive callbacks) {:webmention {:id id :source s :target t}})
       {:id id
        :wm wm
        :ok? true}))
   ;; pipeline steps
   (partial check-storage storage)    ;; check if webmention exists
   verify                             ;; verify if target is mentioned
   (partial clean storage callbacks)  ;; delete old unverified webmention
   (partial wrap config)              ;; wrap metadata into webmention
   (partial write storage callbacks)));; write to storage

(defn exec-pipeline
  "Basically wraps (pipeline wm) call inside a try block.
   catches exceptions and errors so they don't paralyze pipelines."
  [pipeline wm]
  (try
    (pipeline wm)
    (catch Throwable e
      (timbre/error "unexpceted exception in pipeline: " (.getMessage e))
      (timbre/debug e))))

(defn dispatch-pipeline-loop
  "Start a loop that takes from wmc and do pipeline when data come in."
  [n wmc stopped pipeline]
  (go-loop [wm (<! wmc)]
    (let [{:keys [stop?]} wm]
      (if (or stop? (nil? wm))
        (do ;; handle stop signal
          (put! stopped true)
          (timbre/debug (str "#" n) "processor shuts down."))
        (do ;; process found webmention
          (timbre/info (str "#" n) "webmention found in queue, processing.")
          (exec-pipeline pipeline wm)
          (recur (<! wmc)))))))

(defn start-processor
  "Starts webmention processing pipelines. Returns a stopper function."
  [metadata]
  {:pre [(s/valid? ::processor-meta metadata)]
   :post [fn?]}
  ;; wmc is webmention queue; stopped is channel where processors 
  ;; put signals into, to notify that they're shut down
  (let [wmc (:wmc metadata), stopped (chan)
        concurrency (get-in metadata [:config :pipeline-concurrency])]
    ;; dispatch pipelines
    (doseq [n (range concurrency)
            :let [pipeline (assemble-pipeline metadata)]]
      (dispatch-pipeline-loop n wmc stopped pipeline))
    ;; return a function that stops the loop 
    (fn []
      (dotimes [_ concurrency] (put! wmc {:stop? true}))
      ;; after putting the stop signal, processor might not
      ;; stop immediately. wait for it to process what's left 
      ;; inside webmention queue and close it.
      (timbre/info "waiting for queue to clear, and processor to shut down")
      (dotimes [_ concurrency] (<!! stopped)))))