Hi, guys. Any recommendations and tips how to build dynamic plugin system for clojure system?
Do you want to alter functionality based on packages on the classpath? Or what exactly do you mean?
Yeah, depends what you mean. But multi methods is one common way to let downstream logic extend upstream logic (like extending the behavior of a lib)
you could use dependency injection with component/integrant or you could use requiring-resolve . There are other ways as well
The new integrant expand-key is good for making a system out of modules https://github.com/weavejester/integrant?tab=readme-ov-file#expanding
multi methods, protocols and programmatic loading of namespaces on the classpath are useful to extend an application with 'plugins'.
Another question is what is the trust model? Should plugins have all the system access of the host app?
They are trying to solve reverse problem - how engine will call plugin lifecycle methods. That’s quite trivial and can be solved even by naming convention. I need to provide core api to plugins with ability to build plugin without core and core deps (~ 200-300 libs) in final jar.
If you are happy to restart your app when you need to add a plugin then it is easy. When you start up the app you can have 200 jar files on your classpath
java -cp a.jar:b.jar:c.jar.... com.company.entrypoint
None of the jars need any shared code or shared awareness of each other.
You can then have an EDN called plugins.edn that points to functions
{:plugin-1 com.company.aws/aws-plugin
:plugin-2 comp.company.mongo/mongo-plugin}
In your code you can do
(defn get-plugins[]
(->> (read-string (slurp "plugins.ed"))
(map (fn[[k v]]
(require (namespace v))
[k (eval v))))
;;; (let[plugin (:plugin-2 (get-plugins))]
;;; (plugin {.....}))
You don’t even need the plugins.edn file - you can use Java APIs to iterate over files and auto discover the plugins on the classpath,
BTW if you are using Amazoninca and concerned about uberjar size - then don’t use that - there are other AWS libraries that are much smaller/light weight.BTW -when you use Ansible or rsync to uberjar a modified uberjar to the server - they only send small changes and not the whole thing (e.g. only send 1MB to update 500 MB jar file). If you are using Uberjars - then don’t bother with a remote/centralised m2 repo or docker repo - that’s just adding a whole lot of extra hops/upload/download/storage for little benefit. Your code is in Git repo already - you don’t need it in m2/docker repo too. Creating a large uberjar does a lot of disk IO. Linux can be much faster for than this than Windows, WSL2 or Mac. Fast SSD (e.g. NVME SSD) helps.
@rupert why are you talking about restart? - clojure.core/add-classpath can do it in runtime
True, that’s an option. I mention restarts since it feels more straightforward (fewer edge case and principle of least surprise) to use the standard classpath.
Can you do?
(let[fx (eval (read-string code))]
(fx abc))I can, but I'm not looking for "scripting". I want for example to move all cloud integrations into plugins/adapters - especially to get rid of cloud sdk deps from service core.
"plugins as source code" sure is an interesting idea though. Definitely simplifies some things
Def doable with add-libs
Does anyone know - how do datomic ions work?
They're essentially db functions that let you run arbitrary logic within a transaction
Adding or updating an ion will cycle the datomic process in ec2 fwiu
Resulting in a new class path
You could technically build your whole app on ions/datomic
Can ions bring extra deps?
Yeah, but you deploy a deps.edn type thing. It came out before add-lib was a thing I believe. Not sure if you can use add-lib there
Most dynamic runtime plugin solutions are a bunch of code that ultimately wraps a call to eval.
Sounds like you don’t mind restarting your application to add a plugin?
If so you can have multiple jar files/source folders of the class path (`java -cp` a.jar:b.jar:c.jar) and use dependency injection (eg integrant) to bind your code together at start up time (rather than compile time).
Yes, sure I played with clojure.core/add-classpath and it works fine, even with urls. I'm not a big fan of integrant. There few decisions to make - first how to make interface of core engine for plugins. I would love to use just namespace and do not use protocols or multimethods. But this makes build of plugin little bit tricky. Now I create for plugin build the "fake" namespace with same functions and with no deps and exclude it from uberjar
Ah interesting hack. But yeah there's gotta be a better way. I'd recommend pinging the #clojure channel about it. Might take a bit before enough eyeballs see this channel
Seems like you’re going quite far out of your way to avoid dependency injection. If you don’t like integrant there must be at least 10 other libraries that provide it in Clojure (eg component, clip etc). They don’t have to use multimethods/protocols - they can expose plain java functions/maps etc.
We have a clojure service (aka supabase for healthcare) and want to add dynamically loadable plugins - for example Kafka integration. Trust model should not to be too strict - or at least we want to have a set of trusted plugins.
Looking for something like eclipse or idea plugins system. Not sure how clojure ions work - but may be.