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 24 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 .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ jobs:
distribution: 'adopt'
java-version: '11'

- name: Clone Submodules
run: make jedis

- name: Install clojure cli
uses: DeLaGuardo/setup-clojure@master
with:
Expand Down
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
168 changes: 159 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,151 @@ com.moclojer/rq {:mvn/version "0.x.x"}

> see the versions distributed on clojars

## example
## building from source

We build Jedis ourselves to enable building queue functions directly using reflection. This approach ensures full compatibility with our library's features.

### prerequisites

- Make sure you have Java JDK (version X.X or higher) installed
- Ensure you have Make installed on your system

### build steps

1. Clone the repository: `git clone [repository URL]`
2. Navigate to the project directory: `cd clj-rq`
3. Run the build command: `make jedis`

After running `make jedis`, the library will be built and ready to be linked with your project. Linking in this context means that the built Jedis library will be properly referenced and used by clj-rq when you include it in your project.
J0sueTM marked this conversation as resolved.
Show resolved Hide resolved

## how clj-rq works under the hood

The `clj-rq` library leverages the `->wrap-method` macro to dynamically generate queue functions by wrapping methods from the Jedis library. This approach ensures that the library is always up-to-date with the latest changes in Jedis, providing enhanced security and compatibility.

The `->wrap-method` macro is defined in `src/com/moclojer/internal/reflection.clj` and is used in `src/com/moclojer/rq/queue.clj` to generate the queue functions. By using reflection, the library can dynamically adapt to changes in the Jedis API, ensuring that the generated functions are always in sync with the underlying Jedis methods.

This dynamic generation process is a key differentiator of the `clj-rq` library, making it more secure and future-proof compared to other libraries that rely on static function definitions.
J0sueTM marked this conversation as resolved.
Show resolved Hide resolved

## functions

This section outlines the key functions available in the clj-rq library, covering both queue and pub/sub operations. For detailed descriptions and examples of each function, please refer to the specific subsections below.

### queue

The `clj-rq` library provides a set of queue functions that are dynamically generated by wrapping methods from the Jedis library. These functions are defined in `src/com/moclojer/rq/queue.clj` and include:

- `push!`: Adds elements to the queue.
- `pop!`: Removes and returns elements from the queue.
- `bpop!`: Blocks until an element is available to pop from the queue.
- `index`: Retrieves an element at a specific index in the queue.
- `range`: Retrieves a range of elements from the queue.
- `set!`: Sets the value of an element at a specific index in the queue.
- `len`: Returns the length of the queue.
- `rem!`: Removes elements from the queue.
- `insert!`: Inserts an element into the queue at a specific position.
- `trim!`: Trims the queue to a specified range.

#### common options

All these functions share common options, such as specifying the queue name and handling encoding/decoding of messages. The options are passed as arguments to the functions and allow for flexible configuration.

#### examples

- **push!**: This function adds an element to the queue. It supports options for specifying the direction (left or right) and encoding the message before pushing it to the queue.

> [!WARNING]
> The element or elements to be pushed into a queue has to be passed inside a sequentiable (a vector for example).

```clojure
(rq-queue/push! client "my-queue" ["message"] {:direction :left})
```

- **pop!**: This function removes and returns an element from the queue. It supports options for specifying the direction (left or right) and decoding the message after popping it from the queue.

```clojure
(rq-queue/pop! client "my-queue" {:direction :right})
```

- **bpop!**: This function blocks until an element is available to pop from the queue. It is useful in scenarios where you need to wait for new messages to arrive.

```clojure
(rq-queue/bpop! client "my-queue" {:timeout 5})
```

- **index**: This function retrieves an element at a specific index in the queue. It supports options for decoding the retrieved message.

```clojure
(rq-queue/index client "my-queue" 0)
```

- **range**: This function retrieves a range of elements from the queue. It supports options for decoding the retrieved messages.

```clojure
(rq-queue/range client "my-queue" 0 -1)
```

- **set!**: This function sets the value of an element at a specific index in the queue. It supports options for encoding the message before setting it.

```clojure
(rq-queue/set! client "my-queue" 0 "new-message")
```

- **len**: This function returns the length of the queue. It is useful for monitoring the size of the queue.

```clojure
(rq-queue/len client "my-queue")
```

- **rem!**: This function removes elements from the queue based on a specified pattern. It supports options for specifying the number of elements to remove.

```clojure
(rq-queue/rem! client "my-queue" "message" {:count 2})
```

- **insert!**: This function inserts an element into the queue at a specific position. It supports options for encoding the message before inserting it.

```clojure
(rq-queue/insert! client "my-queue" "pivot-message" "new-message" {:position :before})
```

- **trim!**: This function trims the queue to a specified range. It is useful for maintaining the size of the queue within certain limits.

```clojure
(rq-queue/trim! client "my-queue" 0 10)
```

### pubsub

The `clj-rq` library provides a set of pub/sub functions that facilitate message publishing and subscription in a Redis-backed system. These functions are defined in `src/com/moclojer/rq/pubsub.clj` and include:

- `publish!`: Publishes a message to a specified channel.
- `group-handlers-by-channel`: Groups message handlers by their associated channels.
- `create-listener`: Creates a listener that processes messages from subscribed channels.
- `unarquive-channel!`: Unarchives a channel, making it active again.
- `pack-workers-channels`: Packs worker channels into a format suitable for processing.
- `subscribe!`: Subscribes to one or more channels and processes incoming messages.

#### examples

- **publish!**: This function publishes a message to a specified channel. It is used to send messages to subscribers listening on that channel.

```clojure
(rq-pubsub/publish! client "my-channel" "Hello, World!")
```

- **subscribe!**: This function subscribes to one or more channels and processes incoming messages using the provided handlers.

```clojure
(rq-pubsub/subscribe! client ["my-channel"] handlers)
```

- **unarquive-channel!**: This function unarchives a channel, making it active again. It is useful for reactivating channels that were previously archived.

```clojure
(rq-pubsub/unarquive-channel! client "my-channel")
```
J0sueTM marked this conversation as resolved.
Show resolved Hide resolved

## complete example

```clojure
(ns rq.example
Expand All @@ -34,9 +178,12 @@ com.moclojer/rq {:mvn/version "0.x.x"}
(def *redis-pool* (rq/create-client "redis://localhost:6379/0"))

;; queue
(queue/push! *redis-pool* "my-queue" {:now (java.time.LocalDateTime/now)
:foo "bar"})
(println :size (queue/llen *redis-pool* "my-queue"))
(queue/push! *redis-pool* "my-queue"
;; has to be an array of the elements to push
[{:now (java.time.LocalDateTime/now)
:foo "bar"}])

(println :size (queue/len *redis-pool* "my-queue"))
(prn :popped (queue/pop! *redis-pool* "my-queue"))

;; pub/sub
Expand All @@ -50,9 +197,10 @@ com.moclojer/rq {:mvn/version "0.x.x"}

(pubsub/subscribe! *redis-pool* my-workers)
(pubsub/publish! *redis-pool* "my-channel" "hello world")
(pubsub/publish! *redis-pool* "my-other-channel" {:my "moclojer team"
:data "app.moclojer.com"
:hello "maybe you'll like this website"})
(pubsub/publish! *redis-pool* "my-other-channel"
{:my "moclojer team"
:data "app.moclojer.com"
:hello "maybe you'll like this website"})

(rq/close-client *redis-pool*)
```
Expand Down Expand Up @@ -86,6 +234,8 @@ sequenceDiagram
User->>Client: close-client client
Client-->>Logger: log closing client
Client-->>User: confirm client closure
```
```

---

Read more about the project [here](docs/README.md).
Made with 💜 by [moclojer](https://moclojer.com).
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"}
org.clojure/tools.logging {:mvn/version "1.3.0"}
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

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"}
103 changes: 103 additions & 0 deletions src/com/moclojer/internal/reflection.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
(ns com.moclojer.internal.reflection
(:require
[camel-snake-kebab.core :as csk]
[clojure.string :as str]
[com.moclojer.rq.adapters :as adapters]))

(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 underload-methods
"Given a list of overloaded `methods`, returns each one's parameter
list that matches given its `paramlist`."
[paramlist methods]
(reduce
(fn [underloaded-methods {:keys [name parameters]}]
(let [allowed-params (get paramlist name)
param-names (map :name parameters)]
(if (and (= (count parameters) (count allowed-params))
(every? #(some #{%} param-names) allowed-params))
(assoc underloaded-methods name parameters)
underloaded-methods)))
{} methods))
J0sueTM marked this conversation as resolved.
Show resolved Hide resolved

(defn get-klazz-methods
[klazz allowmap]
(let [allowlist (set (keys allowmap))
paramlist (reduce-kv
(fn [acc name method]
(assoc acc name (second method)))
{} allowmap)]
(->> (.getMethods klazz)
(map unpack-method)
(filter #(contains? allowlist (:name %)))
(underload-methods paramlist))))
J0sueTM marked this conversation as resolved.
Show resolved Hide resolved

(defmacro ->wrap-method
"Wraps given jedis `method` and its respective `parameters` into a
common function for this library, which includes, besides the wrapped
function itself, options like key pattern and encoding/decoding."
[method parameters allowmap]
(let [wrapped-method (clojure.string/replace method #"[`0-9]" "")
base-doc (str "Wraps redis.clients.jedis.JedisPooled." wrapped-method)
param-syms (map #(-> % :name symbol) parameters)
[doc _ enc dec] (get allowmap method ["" nil :none :none])]
`(defn ~(symbol method)
~(str base-doc \newline doc)

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

(let [~{:keys ['pattern 'encoding 'decoding]
:or {'pattern :rq
'encoding enc
'decoding dec}} ~'options

~'result ~(->> (reduce
(fn [acc par]
(->> (cond
(= par 'key)
`(com.moclojer.rq.adapters/pack-pattern
~'pattern ~par)

(some #{'value 'string
'args 'pivot} [par])
`(com.moclojer.rq.adapters/encode
~'encoding ~par)

:else par)
(conj acc)))
[]
param-syms)
(into [(symbol (str "." wrapped-method)) '@client])
(seq))]
(try
(com.moclojer.rq.adapters/decode ~'decoding ~'result)
(catch ~'Exception ~'e
(.printStackTrace ~'e)
~'result))))))
J0sueTM marked this conversation as resolved.
Show resolved Hide resolved

(comment
(get-klazz-methods
redis.clients.jedis.JedisPooled
{"rpop" ["hello" ["key" "count"] :edn-array :none]})

(require '[clojure.pprint :refer [pprint]])
(let [allowmap {"linsert" ["Inserts a message into a queue in reference to given pivot"
["key" "where" "pivot" "value"] :edn :none]}
[method parameters] (first
(get-klazz-methods
redis.clients.jedis.JedisPooled
allowmap))]
(pprint
(macroexpand-1 `(->wrap-method ~method ~parameters ~allowmap))))

;;
)
J0sueTM marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading