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

Refactoring layers and cache #3175

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft

Refactoring layers and cache #3175

wants to merge 10 commits into from

Conversation

Macroz
Copy link
Collaborator

@Macroz Macroz commented Aug 8, 2023

Goals:

  • Refactor all code to follow already agreed layered architecture (refactoring has been a slow ongoing process)
  • Fully populate service layer and do not bypass it in business code
  • Rename functions for clarity (e.g. get-full-internal-application instead of always just get-application)
  • Introduce rems.service.cache to cache "joined things". E.g. fully populated application with users, workflow, form etc. joined to the map.
  • Introduce rems.cache for basic caching things and use it throughout.
  • Cache metadata (catalogue items, workflow, resources, ...) completely (likely to be small)
  • Cache most recent applications only
  • Cache common complex queries as projections:
    • Which roles a user has? (also from applications)
    • Which applications a user is participating in?

NB:

  • Removes catalogue item localization cache (should not be needed)
  • Tests may use service/DB layer as they wish
  • Removes the event namespace (as it is really a part of the application)
    • User service should handle attributes, settings etc.
    • Application service should handle also events

Open issues:

  • Many TODO
  • Commented code
  • Management of application event projections (including new ones such as user roles and user applications)
  • Performance testing
  • Introduce back just enough caching (and cache invalidation)
  • ADR
  • Follow-up tasks
  • Should mounted state be in rems.system.xxx?
  • ...

Related issues:

Checklist for author

Remove items that aren't applicable, check items that are done.

Reviewability

  • Link to issue

Backwards compatibility

  • API is backwards compatible or completely new
  • Events are backwards compatible or have migrations
  • Config is backwards compatible
  • Feature appears correctly in PDF print

Documentation

  • Update changelog if necessary
  • Update docs/ (if applicable)
  • ADR for major architectural decisions or experiments (write a caching startegy ADR)
  • New config options in config-defaults.edn

Testing

  • Complex logic is unit tested
  • Valuable features are integration / browser / acceptance tested automatically

Follow-up

  • New tasks are created for pending or remaining tasks

Comment on lines +147 to 148
(mapv project-application-overview)
(filter-with-search query))))))
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We can change the order of these operations like in the next function (slightly improve perf)

(applications/delete-application! (:application/id event))
(applications/delete-application-and-reload-cache! (:application/id event))))))
(application/delete-application! (:application/id event))
#_(if (= "expirer-bot" (:event/actor event)) ; will reload in the end
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This can be removed. Cache invalidation (and reload) happens automatically.

Comment on lines -30 to +31
(->> (mapcat applications/get-my-applications user-ids)
(->> (mapcat application/get-full-personalized-applications-by-user user-ids)
(mapv :application/id)
distinct
(mapv applications/get-application)
(mapv application/get-full-internal-application)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We don't have to get the full internal twice (the previous get-my-applications returned overview?).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

  (->> (mapcat application/get-full-personalized-applications-by-user user-ids)
       (distinct-by :application/id))

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Does it need to be a full internal application?

Consider implementing get-full-internal-applications-by-user

Comment on lines +107 to +112
#_(log/info "Start indexing" (count app-ids) "applications...")
(doseq [app-id app-ids
:let [application (get-full-internal-application app-id)]
:when application]
(index-application! writer application))
#_(log/info "Finished indexing" (count app-ids) "applications")))
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Remove commented code

Comment on lines +118 to +124
(locking index-lock
(when-some [requests (seq @indexing-requests)]
(let [events (mapcat first requests)
get-full-internal-application (second (first requests))] ; NB: doesn't matter which get-full-internal-application is actually used
(index-events! events
get-full-internal-application)) ; TODO avoid by moving to system ns
(reset! indexing-requests nil))))
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

  • Locking not necessary with reset-vals!
  • get-full-internal-application could be static reference if we move the system ns

Comment on lines +127 to +129
(locking index-lock
(swap! indexing-requests conj [events get-full-internal-application])
nil)) ; process manager expects sequence of new events)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

  • Locking not necessary (see above)
  • sequence of new commands?

@@ -0,0 +1,76 @@
(ns rems.cache
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

TODO: Implement (use from service.cache)

;; - Schema in use
;; - response codes
;; - user info from headers
;; - access rights
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Who is what role? Coarse-grained access control, i.e. who can call what API?

;; - transforms between db and internal representation
;; - plain Clojure data
;; - Schema supported
;; - rems.service
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fine-grained role-based access control

;; - access rights
;; - rems.cache (NEW)
;; - collection of caching functionality
;; - centralized access to cache status, invalidation and statistics
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Config

;; rems.api -> rems.custom -> rems.cache -> rems.service -> rems.db -> pg
;; - cache complete joined representation
;; - items are customized later in code so everything cached can be shared
;;
Copy link
Collaborator Author

@Macroz Macroz Nov 9, 2023

Choose a reason for hiding this comment

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

OPTION 4
- rems.api -> rems.service -> rems.service.cache (rems.cache) -> rems.db (rems.cache) -> pg
- rems.service.cache joins stuff together and caches full representation
- rems.db can use low level rems.cache to cache db data
- rems.watcher to notify when something has changed, listeners will invalidate and/or recompute
- rems.service.cache may cache customized / personalized content and collections if necessary
- additional projections for user-roles, user-applications etc. performance critical data

;;; Cache

(defn reload-cache! []
;; TODO implement
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Remove?

:application.event/voted)
user-applications ; no user added

:application.event/copied-from
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Should this actually be copied-to if we want to add the new application id to the members of the (old) application?

Copy link
Collaborator Author

@Macroz Macroz Nov 10, 2023

Choose a reason for hiding this comment

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

{:event/type :application.event/copied-from
 :application/id new-app-id
 :application/copied-from (select-keys application [:application/id :application/external-id])}

So the app-id is the new application id. The members come from application which is from the command, i.e. the old application before this event (and with members).

(reduce (fn [user-applications userid]
(update user-applications userid app-id))
user-applications
(:application/members application))
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

; NB: applicant gets the copied application already from `created` event.

:application.event/reviewer-joined
(update user-applications actor conj-set app-id))))

(defn- user-roles-projection [user-roles event application]
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Could this name be user-application-roles-projection or so because these are only the roles that come from applications. Not workflow handlers, roles table etc.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Consider also just listening to application and updating based on its :application/user-roles data that is enriched.

Comment on lines +160 to +163
:application.event/applicant-changed
(-> user-roles
(update (:application/applicant application) disj-set :applicant)
(update (:application/applicant event) (conj-set :applicant)))
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Should make the old applicant a member?

Comment on lines +29 to +32
(defn get-users-with-role [role]
(set/union
(users/get-users-with-role role)
(applications/get-users-with-role role)))
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This should consider workflow handlers. Currently applications/get-users-with-role does not do it but could.

Copy link
Collaborator Author

@Macroz Macroz Nov 10, 2023

Choose a reason for hiding this comment

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

There isn't a test that requires it at the moment. This is only user to enrich super users into applications which could use the "roles" table directly. We can remove this function.

Comment on lines 76 to +79
(set/union (roles/get-roles (getx-user-id))
(organizations/get-all-organization-roles (getx-user-id))
(workflow/get-all-workflow-roles (getx-user-id))
(applications/get-all-application-roles (getx-user-id))))
(application/get-all-application-roles (getx-user-id))))
Copy link
Collaborator Author

@Macroz Macroz Nov 10, 2023

Choose a reason for hiding this comment

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

Let's refactor this to use rems.service.user/get-roles which would use rems.service.cache/get-roles and do this whole thing. User roles would be cached as a projection but the whole result should be cached too (it joins many tables).

Comment on lines +333 to +334
(when (empty? (:errors result)) ; nothing can't change when there's an error
(dependencies/notify-watchers! {:application/id (mapv :application/id (:events result))}))
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Is this double notification? Wouldn't the lower level functions be enough (add-event etc.)?

Comment on lines +352 to +353
(defn get-applications-with-user [userid]
(throw (RuntimeException. "get-applications-with-user not implemented")))
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Is this needed?

@Macroz Macroz mentioned this pull request Nov 22, 2023
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants