Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improvements to event notifications & docs #2138

Merged
merged 3 commits into from
Apr 21, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/event-notification.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ with exponential backoff for up to 12 hours.

Due to retries, the event notification endpoint _should be_ idempotent.

Event notifications are _not guaranteed to be ordered_ by event
creation order, especially when retries occur.
Initial event notifications for each event are _guaranteed to be in
order_ of event creation, but retries might be out-of-order.

Event notification failures are logged. You can also inspect the
`outbox` db table for the retry state of notifications.
Expand All @@ -25,9 +25,9 @@ The body of the HTTP PUT request will be a JSON object that contains:

- `"event/type"`: the type of the event, a string
- `"event/actor"`: who caused this event
- `"event/id"`: unique event id
- `"event/time"`: when the event occured
- `"application/id"`: the id of the application
- `"event/application"`: the state of the application, in the same format as the `/api/applications/:id/raw` endpoint returns (see Swagger docs)

Other keys may also be present depending on the event type.

5 changes: 3 additions & 2 deletions resources/sql/queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -485,9 +485,10 @@ FROM application_event
ORDER BY id DESC
LIMIT 1;

-- :name add-application-event! :insert
-- :name add-application-event! :? :1
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about using :returning-execute here. This doesn't seem to be correct.

https://www.hugsql.org/#retrieving-last-inserted-id-or-record

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had some weird sql problems and was iterating stuff, this got left over from that I guess

INSERT INTO application_event (appId, eventData)
VALUES (:application, :eventdata::jsonb);
VALUES (:application, :eventdata::jsonb)
RETURNING id, eventData::TEXT;

-- :name upsert-api-key! :insert
INSERT INTO api_key (apiKey, comment, permittedRoles)
Expand Down
13 changes: 6 additions & 7 deletions src/clj/rems/api/services/command.clj
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,10 @@
(applications/get-application-internal app-id))
result (commands/handle-command cmd app command-injections)]
(when-not (:errors result)
(doseq [event (:events result)]
(events/add-event! event))
(doseq [cmd2 (run-process-managers (:events result))]
(let [result (command! cmd2)]
(when (:errors result)
(log/error "process manager command failed"
(pr-str {:cmd cmd2 :result result :parent-cmd cmd}))))))
(let [events-from-db (mapv events/add-event! (:events result))]
(doseq [cmd2 (run-process-managers events-from-db)]
(let [result (command! cmd2)]
(when (:errors result)
(log/error "process manager command failed"
(pr-str {:cmd cmd2 :result result :parent-cmd cmd})))))))
result))
9 changes: 5 additions & 4 deletions src/clj/rems/db/events.clj
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
(defn get-latest-event []
(fix-event-from-db (db/get-latest-application-event {})))

(defn add-event! [event]
(db/add-application-event! {:application (:application/id event)
:eventdata (event->json event)})
nil)
(defn add-event!
"Add event to database. Returns the event as it went into the db."
[event]
(fix-event-from-db (db/add-application-event! {:application (:application/id event)
:eventdata (event->json event)})))
7 changes: 6 additions & 1 deletion src/clj/rems/event_notification.clj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
(def ^:private default-timeout 60)

(defn- notify! [target body]
(log/info "Sending event notification for event" (select-keys body [:application/id :event/type :event/time])
(log/info "Sending event notification for event" (select-keys body [:event/id :application/id :event/type :event/time])
"to" (:url target))
(try
(let [timeout-ms (* 1000 (get target :timeout default-timeout))
Expand All @@ -35,6 +35,11 @@
"failed: exception")))

(defn process-outbox! []
;; TODO if we want to guarantee event ordering, we need fetch all
;; outbox entries here, and pick the one with the lowest outbox id
;; or event id, and do nothing if it isn't due yet.
;;
;; This can be done per target url or globally.
(doseq [entry (outbox/get-due-entries :event-notification)]
(if-let [error (notify! (get-in entry [:outbox/event-notification :target])
(get-in entry [:outbox/event-notification :body]))]
Expand Down
38 changes: 37 additions & 1 deletion test/clj/rems/test_event_notification.clj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
[rems.config]
[rems.api.services.command :as command]
[rems.api.testing :refer [api-fixture api-call]]
[rems.db.events]
[rems.db.test-data :as test-data]
[rems.event-notification :as event-notification]
[rems.json :as json]
Expand Down Expand Up @@ -75,7 +76,8 @@
app-id (:application-id (command/command! {:type :application.command/create
:actor applicant
:time (time/date-time 2001)
:catalogue-item-ids [cat-id]}))]
:catalogue-item-ids [cat-id]}))
event-id (:event/id (first (rems.db.events/get-application-events app-id)))]
(testing "no notifications before outbox is processed"
(is (empty? (stub/recorded-requests server))))
(event-notification/process-outbox!)
Expand All @@ -88,6 +90,7 @@
(set (map :path notifications))))
(is (= {:application/external-id "2001/1"
:application/id app-id
:event/id event-id
:event/time "2001-01-01T00:00:00.000Z"
:workflow/type "workflow/default"
:application/resources [{:resource/ext-id ext-id
Expand All @@ -114,3 +117,36 @@
(is (= "/all" (:path req)))
(is (= "application.event/draft-saved"
(:event/type (:data req))))))))))

(deftest test-event-notification-ordering
(with-open [server (stub/start! {"/" {:status 200}})]
(with-redefs [rems.config/env (assoc rems.config/env
:event-notification-targets [{:url (str (:uri server) "/")}])]
(let [get-notifications #(doall
(for [r (stub/recorded-requests server)]
(-> r
:body
(get "content")
json/parse-string
(select-keys [:event/id :event/time]))))
form-id (test-data/create-form! {:form/title "notifications"
:form/fields [{:field/type :text
:field/id "field-1"
:field/title {:en "text field"}
:field/optional false}]})
cat-id (test-data/create-catalogue-item! {:form-id form-id})
applicant "alice"
t (time/date-time 2010)
app-id (:application-id (command/command! {:type :application.command/create
:actor applicant
:time t
:catalogue-item-ids [cat-id]}))]
(dotimes [i 100]
(command/command! {:type :application.command/save-draft
:actor applicant
:application-id app-id
:time (time/plus t (time/seconds i))
:field-values [{:form form-id :field "field-1" :value (str i)}]}))
(event-notification/process-outbox!)
(let [notifications (get-notifications)]
(is (apply < (mapv :event/id notifications))))))))