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

Dynamically generating queue functions from Jedis' source code #18

Merged
merged 26 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1bfd4d8
chore: add modified jedis as dep
J0sueTM Aug 7, 2024
47ff1dc
feat: scratch method retrieval
J0sueTM Aug 7, 2024
bec5f04
refactor: mv dynamic loading to `reflection` file
J0sueTM Aug 7, 2024
810939a
feat: implement reflection
J0sueTM Aug 14, 2024
5716287
feat: statically define docs, encoding and decoding
J0sueTM Aug 27, 2024
bfeed35
fix: handle diff array types on enc/dec
J0sueTM Aug 27, 2024
9825da0
fix: rm trailing `vec`
J0sueTM Aug 27, 2024
63e6815
fix: underload by arity
J0sueTM Sep 2, 2024
3e17915
style: change ambiguous helper files name
J0sueTM Sep 2, 2024
030d0a9
chore: merge 'main' into feat/dynamic-queue-fns
J0sueTM Sep 2, 2024
721351d
fix: use param names instead of arity
J0sueTM Sep 3, 2024
04b11ad
feat: wrap gen fns, match old style
J0sueTM Sep 3, 2024
5311106
feat: clone jedis submodule on test workflow
J0sueTM Sep 4, 2024
d5f6982
fix: only clone jedis on test workflow
J0sueTM Sep 4, 2024
066bb9d
refactor: update pubsub to new queue fns
J0sueTM Sep 4, 2024
188fd35
revert: use Makefile on jedis build step
J0sueTM Sep 4, 2024
07eaf6e
doc: reflection fns
J0sueTM Sep 4, 2024
376e73c
doc: improve build steps on readme
J0sueTM Sep 4, 2024
c696ff2
refactor: mv restraint maps outside of lets
J0sueTM Sep 4, 2024
cfd6b2c
feat: check for valid prefix when unpacking
J0sueTM Sep 4, 2024
d206efa
refactor: group similar tests into t/are
J0sueTM Sep 4, 2024
ed9e222
doc: document changes and workflow in the README
J0sueTM Sep 5, 2024
519f203
style: warning on push! intricacy
J0sueTM Sep 5, 2024
88cb8c7
style: typo
J0sueTM Sep 5, 2024
977a72e
style: use `get` instead of `get-in`
J0sueTM Sep 5, 2024
206dd1e
style: typo getin -> get
J0sueTM Sep 5, 2024
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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "vendor/jedis"]
path = vendor/jedis
url = https://github.com/moclojer/jedis.git
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
jedis:
git submodule update --init --recursive --remote
cd vendor/jedis && make mvn-package-no-tests

all: jedis
J0sueTM marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ com.moclojer/rq {:mvn/version "0.x.x"}

> see the versions distributed on clojars

## building from source

We build jedis ourselves, in order to build the queue functions directly from reflection.

Simply run `make jedis`, and the library is be built and ready to be linked.

J0sueTM marked this conversation as resolved.
Show resolved Hide resolved
## example

```clojure
Expand Down
9 changes: 6 additions & 3 deletions deps.edn
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
{:paths ["src"]
{:paths ["src" "resources"]
:deps
{redis.clients/jedis {:mvn/version "5.1.2"}
{redis.clients/jedis {#_#_:mvn/version "5.1.2"
:local/root "vendor/jedis/target/jedis-5.2.0-SNAPSHOT.jar"}
Copy link
Member Author

Choose a reason for hiding this comment

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

You don't need to use jar in deps, you can use clj src from jedis, so you don't need to compile (generate the jar) to generate the build because it will take it from source

org.clojure/tools.logging {:mvn/version "1.3.0"}
ch.qos.logback/logback-classic {:mvn/version "1.5.6"}}
ch.qos.logback/logback-classic {:mvn/version "1.5.6"}
camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.3"}
org.clojure/data.json {:mvn/version "2.5.0"}}
J0sueTM marked this conversation as resolved.
Show resolved Hide resolved

:aliases
{;; clj -A:dev -m com.moclojer.rq
Expand Down
4 changes: 4 additions & 0 deletions resources/command-allowlist.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#{"lpush" "rpush" "lpop" "rpop" "brpop"
"blpop" "lrange" "lindex" "lset" "lrem"
"llen" "linsert" "ltrim" "rpoplpush"
"brpoplpush" "lmove"}
81 changes: 81 additions & 0 deletions src/com/moclojer/internal/reflection.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
(ns com.moclojer.internal.reflection
(:require
[camel-snake-kebab.core :as csk]
[clojure.string :as str]
[com.moclojer.rq.utils :as utils]))

(defn unpack-parameter
[parameter]
{:type (.. parameter getType getName)
:name (csk/->kebab-case (.getName parameter))})
J0sueTM marked this conversation as resolved.
Show resolved Hide resolved

(defn unpack-method
[method]
{:name (csk/->kebab-case (.getName method))
:parameters (map unpack-parameter (.getParameters method))})
J0sueTM marked this conversation as resolved.
Show resolved Hide resolved

(defn reduce-method-overloads
[methods]
(reduce
(fn [overloaded-methods {:keys [name parameters]}]
(let [overload-count (count
(filter
#(str/starts-with? (key %) name)
overloaded-methods))
cur-overload-id (when (> overload-count 0) overload-count)]
(assoc overloaded-methods (str name cur-overload-id) parameters)))
{} methods))

(defn get-klazz-methods
[klazz allowlist]
(->> (.getMethods klazz)
(map unpack-method)
(filter #(contains? allowlist (:name %)))
(reduce-method-overloads)))

(defmacro ->wrap-method
[method parameters]
(let [wrapped-method (clojure.string/replace method #"[`0-9]" "")
param-syms (map #(-> % :name symbol) parameters)]
`(defn ~(symbol method)
~(str "Wraps redis.clients.jedis.JedisPooled." wrapped-method)

~(-> (into ['client] param-syms)
(conj '& 'options))

(let [~{:keys ['pattern 'encoding 'decoding]
:or {'pattern :rq
'encoding :edn
'decoding :edn}} ~'options
~'result ~(seq
(into
[(symbol (str "." wrapped-method)) '@client]
(reduce
(fn [acc par]
(->> (cond
(= par 'key)
`(com.moclojer.rq.utils/pack-pattern
~'pattern ~par)

(= par 'value)
`(com.moclojer.rq.utils/encode
~'encoding ~par)

:else par)
(conj acc)))
[]
param-syms)))]
(try
(com.moclojer.rq.utils/decode ~'decoding ~'result)
(catch ~'Exception ~'e
(.printStackTrace ~'e)
~'result))))))

(comment
(let [[method parameters] (-> redis.clients.jedis.JedisPooled
(get-klazz-methods #{"lpush"})
first)]
(clojure.pprint/pprint
(macroexpand-1 `(->wrap-method ~method ~parameters))))
;;
)
6 changes: 2 additions & 4 deletions src/com/moclojer/rq/pubsub.clj
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
(do
(log/warn "published message, but didn't meet min consumers. archiving..."
debug-args)
(queue/push! client channel message :pattern :pending)))
(queue/lpush client channel message :pattern :pending)))

consumer-met?))

Expand Down Expand Up @@ -64,9 +64,7 @@
on each of them."
[client channel on-msg-fn]
(loop [message-count 0]
(if-let [?message (queue/pop! client channel
:direction :r
:pattern :pending)]
(if-let [?message (queue/rpop client channel 1 :pattern :pending)]
(do
(try
(on-msg-fn ?message)
Expand Down
83 changes: 13 additions & 70 deletions src/com/moclojer/rq/queue.clj
Original file line number Diff line number Diff line change
@@ -1,73 +1,16 @@
(ns com.moclojer.rq.queue
(:refer-clojure :exclude [pop!])
(:require
[clojure.edn :as edn]
[clojure.tools.logging :as log]
[com.moclojer.rq.utils :as utils]))

(defn push!
Copy link
Member Author

Choose a reason for hiding this comment

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

@J0sueTM I like to use the standard names used by the library we are wrapping, but the idea is for clj-rq to be simple and inclusive.

Even if someone doesn't know the names of Redis functions but knows about queues, they should be able to use clj-rq. In other words, it's important to keep push! and pop! calling their respective functions from jedis. These names are explicit about what is expected in execution.

Copy link
Member

Choose a reason for hiding this comment

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

@avelino Using macros puts a couple of limitations on ourselves. Of course what you're asking is feasible, but I don't think that parsing directions, and adding them into the function definition is within our time budget.
ourselves
It would involve changing the way we load the parameters from a given class, which could dismantle our macro. And for goods sake, writing that shivers my whole spine. But it's feasible nevertheless.

I'll scratch out a way to have the direction parsed when loading the parameters, as I'm currently implementing the static documentation, as layed out here.

Copy link
Member Author

Choose a reason for hiding this comment

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

it may be that you are complicating more than necessary, how would I do it?

I would keep the same logic of (defn push! [...]), but changing the calls we make directly to jedis to the functions built with the macro.

does it make sense?

Copy link
Contributor

Choose a reason for hiding this comment

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

@avelino not just about the directions, in some functions we have things like a position (alike before and after) and some extra opts to add. If we straight forward make it "static" on this way, it would be easier to test out, since the tests we need to write involves some data transformation to fit the Redis patterns. But how can we improve it to maintain as dynamic as possible, @J0sueTM ?

Copy link
Contributor

Choose a reason for hiding this comment

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

find out we can call specific opts while on code like this:
image

Copy link
Member

Choose a reason for hiding this comment

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

@avelino @Felipe-gsilva There are too many variables to keep updating dynamically. I still think that we can of course check the direction from l or r, and check the method parameters and parse them appropiately. It just will take a lot more time.

Copy link
Member Author

Choose a reason for hiding this comment

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

@J0sueTM I don't know if I understood the last comment - I imagine it's about @Felipe-gsilva's comment

It's clear my proposal to keep push! and pop! as they were before and have them use the macro implementation, right?

Copy link
Member

@J0sueTM J0sueTM Sep 2, 2024

Choose a reason for hiding this comment

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

@avelino Yes. My comment was regarding Felipe's coment. This conversation all happened before our meeting, when I finally understood what you meant with

keep push! and pop! as they were before and have them use the macro implementation

Copy link
Member Author

Choose a reason for hiding this comment

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

It's worth writing by sharing what we will do in a separate comment so it doesn't get lost here

#18 (comment)

Copy link
Member

Choose a reason for hiding this comment

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

@avelino give a look at the current implementation. Does it match with what you had in mind?

"Push a message into a queue.

Options:

- direction: Direction to push the message (:l or :r)
- pattern: Pattern for the queue name"
[client queue-name message & options]
(let [{:keys [direction pattern _at _in _retry _retry-delay]
:or {direction :l
pattern :rq}
:as opts} options
packed-queue-name (utils/pack-pattern pattern queue-name)
encoded-message (into-array [(pr-str message)])
pushed-count (if (= direction :l)
(.lpush @client packed-queue-name encoded-message)
(.rpush @client packed-queue-name encoded-message))]

(log/debug "pushed to queue"
{:client client
:queue-name packed-queue-name
:message message
:options opts
:pushed-count pushed-count})

pushed-count))

(defn pop!
"Pop a message from a queue.

Options:

- direction: Direction to pop the message (:l or :r)
- pattern: Pattern for the queue name"
[client queue-name & options]
(let [{:keys [direction pattern]
:or {direction :l
pattern :rq}
:as opts} options
packed-queue-name (utils/pack-pattern pattern queue-name)
message (if (= direction :l)
(.lpop @client packed-queue-name)
(.rpop @client packed-queue-name))]

(when message

(log/debug "popped from queue"
{:client client
:queue-name packed-queue-name
:options opts
:message message})

(edn/read-string message))))

(defn llen
"Get the size of a queue.

Parameters:
- client: Redis client
- queue-name: Name of the queue
- options: Optional parameters, including:
- pattern: Pattern for the queue name"
[client queue-name & options]
(let [{:keys [pattern]
:or {pattern :rq}} options]
(.llen @client (utils/pack-pattern pattern queue-name))))
[com.moclojer.internal.reflection :as reflection]))

(def allowlist
#{"lpush" "rpush" "lpop" "rpop" "brpop"
"blpop" "lrange" "lindex" "lset" "lrem"
"llen" "linsert" "ltrim" "rpoplpush"
"brpoplpush" "lmove"})

(def commands
(for [[method parameters] (reflection/get-klazz-methods
redis.clients.jedis.JedisPooled
allowlist)]
(eval `(reflection/->wrap-method ~method ~parameters))))
58 changes: 57 additions & 1 deletion src/com/moclojer/rq/utils.clj
J0sueTM marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
(ns com.moclojer.rq.utils)
(ns com.moclojer.rq.utils
J0sueTM marked this conversation as resolved.
Show resolved Hide resolved
(:require
[clojure.data.json :as json]
[clojure.edn :as edn]))

(defn- pattern->str
"Adapts given pattern keyword to a know internal pattern. Raises
Expand All @@ -21,3 +24,56 @@
(defn unpack-pattern
[pattern queue-name]
(subs queue-name (count (pattern->str pattern))))

(defn- keyword-enc->fn
[enc]
(let [fns {:none identity
:edn pr-str
:json json/write-str
:array #(into-array (map pr-str %))
:edn-array #(into-array (map pr-str %))
:json-array #(into-array (map json/write-str %))}]
(or (get fns enc)
(throw (ex-info (str "No encoding " (name enc))
{:cause :illegal-argument
:value enc
:expected (keys fns)})))))

(defn encode
[enc message]
((cond
(keyword? enc) (keyword-enc->fn enc)
(fn? enc) enc
:else (throw (ex-info
(str "`encoding` must be either keyword or function")
{:cause :illegal-argument
:value enc
:expected #{keyword? fn?}})))
message))

(defn- keyword-dec->fn
[dec]
(let [json-dec-fn #(json/read-str % :key-fn keyword)
fns {:none identity
:edn edn/read-string
:json json-dec-fn
:array vec
:edn-array #(vec (map edn/read-string %))
:json-array #(vec (map json-dec-fn %))}]
(or (get fns dec)
(throw (ex-info (str "No decoding " (name dec))
{:cause :illegal-argument
:value dec
:expected (keys fns)})))))

(defn decode
[dec message]
((cond
(keyword? dec) (keyword-dec->fn dec)
(fn? dec) dec
:else (throw (ex-info
(str "`decoding` must be either keyword or function")
{:cause :illegal-argument
:value dec
:expected #{keyword? fn?}})))
message))
23 changes: 16 additions & 7 deletions test/com/moclojer/rq/queue_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,26 @@
message (utils/gen-message)]

(t/testing "raw"
(rq-queue/push! client queue-name message)
(rq-queue/push! client queue-name (utils/gen-message))
(rq-queue/lpush client queue-name message)
(rq-queue/lpush client queue-name (utils/gen-message))
(t/is (= 2 (rq-queue/llen client queue-name)))
(t/is (= message (rq-queue/pop! client queue-name :direction :r))))
(t/is (= message (rq-queue/rpop client queue-name 1))))

(t/testing "direction"
(rq-queue/push! client queue-name message :direction :r)
(t/is (= message (rq-queue/pop! client queue-name :direction :r))))
(rq-queue/rpush client queue-name message)
(t/is (= message (rq-queue/rpop client queue-name))))

(t/testing "pattern"
(rq-queue/push! client queue-name message :pattern :pending)
(t/is (= message (rq-queue/pop! client queue-name :pattern :pending))))
(rq-queue/lpush client queue-name message :pattern :pending)
(t/is (= message (rq-queue/rpop client queue-name :pattern :pending))))

(rq/close-client client)))

(comment
(def my-client (rq/create-client "redis://localhost:6379"))

(.lpush @my-client)

(rq/close-client my-client)
;;
)
24 changes: 24 additions & 0 deletions test/com/moclojer/rq/utils_test.clj
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
(ns com.moclojer.rq.utils-test
(:require
[clojure.string :as str]
[clojure.test :as t]
[com.moclojer.rq.utils :as utils]))

Expand All @@ -13,3 +14,26 @@
(t/is "my-queue" (utils/unpack-pattern :rq "rq:my-queue"))
(t/is "my-queue" (utils/unpack-pattern :pubsub "rq:pubsub:my-queue"))
(t/is "my-queue" (utils/unpack-pattern :pending "rq:pubsub:pending:my-queue"))])

(t/deftest encode-test
(t/testing "keyword encoders"
[(t/is (= "hello world" (utils/encode :none "hello world")))
(t/is (= "{:hello? true}" (utils/encode :edn {:hello? true})))
(t/is (= "{\"hello?\":true}" (utils/encode :json {:hello? true})))
(t/is (= ["3" "true"] (vec (utils/encode :array [3 true]))))
(t/is (= ["{\"hello?\":true}"] (vec (utils/encode
:json-array
[{:hello? true}]))))])
(t/testing "function encoder"
(t/is (= "HELLO WORLD" (utils/encode str/upper-case "hello world")))))

(t/deftest decode-test
(t/testing "keyword decoders"
[(t/is (= "hello world" (utils/decode :none "hello world")))
(t/is (= {:hello? true} (utils/decode :edn "{:hello? true}")))
(t/is (= {:hello? true} (utils/decode :json "{\"hello?\":true}")))
(t/is (= ["3" "true"] (utils/decode :array (into-array ["3" "true"]))))
(t/is (= [3 true] (utils/decode :edn-array (into-array ["3" "true"]))))
(t/is (= [{:hello? true}] (utils/decode
:json-array
(into-array ["{\"hello?\":true}"]))))]))
1 change: 1 addition & 0 deletions vendor/jedis
Submodule jedis added at 288617