diff --git a/.github/workflows/jepsen-test.yml b/.github/workflows/jepsen-test.yml new file mode 100644 index 0000000..0ffc3c6 --- /dev/null +++ b/.github/workflows/jepsen-test.yml @@ -0,0 +1,28 @@ +on: [ push ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-jepsen-test + +name: Jepsen Test +permissions: + contents: read +jobs: + test: + runs-on: ubuntu-latest + env: + RUN_JEPSEN: ${{ secrets.RUN_JEPSEN }} + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + - name: Install Leiningen + run: | + curl -L https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein > ~/lein + chmod +x ~/lein + ~/lein version + - name: Run Jepsen tests + if: env.RUN_JEPSEN == 'true' + working-directory: jepsen + run: ~/lein test diff --git a/.gitignore b/.gitignore index be60a6a..5c6ff6c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,9 @@ go.work # Built binary -elastickv +/elastickv + +# Clojure/Leiningen build artifacts +jepsen/target/ +jepsen/.lein-* +jepsen/.nrepl-port diff --git a/README.md b/README.md index 2bdca12..5eee88e 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,21 @@ quit ### Development +### Running Jepsen tests + +Jepsen tests live in `jepsen/`. Install Leiningen and run tests locally: + +```bash +curl -L https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein > ~/lein +chmod +x ~/lein +(cd jepsen && ~/lein test) +``` + +These Jepsen tests execute concurrent read and write operations while a nemesis +injects random network partitions. Jepsen's linearizability checker verifies the +history. + + ### Setup pre-commit hooks ```bash diff --git a/jepsen/project.clj b/jepsen/project.clj new file mode 100644 index 0000000..337fb2b --- /dev/null +++ b/jepsen/project.clj @@ -0,0 +1,8 @@ +(defproject elastickv-jepsen "0.1.0-SNAPSHOT" + :description "Jepsen tests for Elastickv" + :repositories [["clojars" {:url "https://repo.clojars.org"}]] + :dependencies [[org.clojure/clojure "1.11.1"] + [jepsen "0.3.5"] + [redis.clients/jedis "5.1.0" :exclusions [org.slf4j/slf4j-api]] + [org.slf4j/slf4j-nop "2.0.9"]] + :main elastickv.jepsen-test) diff --git a/jepsen/src/elastickv/jepsen_test.clj b/jepsen/src/elastickv/jepsen_test.clj new file mode 100644 index 0000000..7e7c482 --- /dev/null +++ b/jepsen/src/elastickv/jepsen_test.clj @@ -0,0 +1,45 @@ +(ns elastickv.jepsen-test + (:gen-class) + (:require [jepsen + [core :as jepsen] + [cli :as cli] + [db :as db] + [client :as client] + [checker :as checker]] + [jepsen.checker.timeline :as timeline] + [jepsen.tests.linearizable-register :as register] + [jepsen.nemesis :as nemesis]) + (:import (redis.clients.jedis Jedis))) + +(defrecord RedisClient [port] + client/Client + (open! [this test node] + (assoc this :conn (Jedis. (name node) port))) + (close! [this test] + (.close (:conn this)) + this) + (setup! [this test]) + (teardown! [this test]) + (invoke! [this test op] + (let [conn (:conn this) + value (:value op)] + (case (:f op) + :write (do (.set conn "k" (pr-str value)) + (assoc op :type :ok)) + :read (let [v (.get conn "k")] + (assoc op :type :ok + :value (when v (read-string v)))) + (assoc op :type :fail :error :unknown-op))))) + +(defn elastickv-test [] + (register/test + {:name "elastickv-register" + :nodes ["n1" "n2" "n3"] + :db db/noop + :client (->RedisClient 63791) + :concurrency 5 + :nemesis (nemesis/partition-random-halves)})) + +(defn -main + [& args] + (cli/run! (cli/single-test-cmd {:test-fn elastickv-test}) args)) diff --git a/jepsen/test/elastickv/jepsen_test_test.clj b/jepsen/test/elastickv/jepsen_test_test.clj new file mode 100644 index 0000000..a2e6b6c --- /dev/null +++ b/jepsen/test/elastickv/jepsen_test_test.clj @@ -0,0 +1,6 @@ +(ns elastickv.jepsen-test-test + (:require [clojure.test :refer :all] + [elastickv.jepsen-test :as jt])) + +(deftest builds-test-spec + (is (map? (jt/elastickv-test))))