├── .circleci
└── config.yml
├── .codeclimate.yml
├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── LICENSE
├── README.md
├── dev-resources
└── logback.xml
├── project.clj
├── script
└── ci-test
├── src
└── lockjaw
│ ├── core.clj
│ ├── mock.clj
│ ├── operation.clj
│ ├── protocol.clj
│ └── util.clj
└── test
└── lockjaw
├── core_test.clj
├── operation_test.clj
├── test_system.clj
└── util_test.clj
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 | jobs:
3 | build:
4 | docker:
5 | - image: circleci/clojure:openjdk-11-lein-2.9.1
6 | - image: postgres:9.6
7 | environment:
8 | POSTGRES_USER: nomnom
9 | POSTGRES_PASSWORD: password
10 | POSTGRES_DB: nomnom_test
11 |
12 |
13 | working_directory: ~/repo
14 | environment:
15 | LEIN_ROOT: "true"
16 | JVM_OPTS: -Xmx2000m
17 | CIRCLE: "yep"
18 | PG_USER: nomnom
19 | PG_PASSWORD: password
20 | PG_DB: nomnom_test
21 | PG_HOST: localhost
22 |
23 | steps:
24 | - checkout
25 |
26 | - restore_cache:
27 | keys:
28 | - v1-dependencies-{{ checksum "project.clj" }}
29 | - v1-dependencies-
30 |
31 | - run: lein deps
32 |
33 | - save_cache:
34 | paths:
35 | - ~/.m2
36 | key: v1-dependencies-{{ checksum "project.clj" }}
37 |
38 | - run:
39 | name: run tests
40 | command: |
41 | ./script/ci-test
42 |
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | plugins:
3 | kibit:
4 | enabled: true
5 | fixme:
6 | enabled: true
7 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on: push
3 |
4 |
5 | concurrency:
6 | group: ci-${{ github.head_ref }}
7 | cancel-in-progress: true
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | container:
13 | image: clojure:openjdk-11-lein-2.9.6
14 |
15 | services:
16 | postgres:
17 | image: "postgres:13"
18 | env:
19 | POSTGRES_USER: test
20 | POSTGRES_PASSWORD: password
21 | POSTGRES_DB: test
22 | ports:
23 | - 5432:5432
24 | options: >-
25 | --health-cmd pg_isready
26 | --health-interval 10s
27 | --health-timeout 5s
28 | --health-retries 5
29 |
30 | steps:
31 | - uses: actions/checkout@v2
32 |
33 | - name: Cache deps
34 | uses: actions/cache@v2
35 | with:
36 | path: /root/.m2
37 | key: v1-deps-${{ hashFiles('project.clj') }}
38 | restore-keys: |
39 | v1-deps-${{ hashFiles('project.clj') }}
40 |
41 | - name: Install dependencies
42 | run: lein deps
43 |
44 | - name: Run lockjaw tests
45 | id: tests
46 | run: lein test 2>&1
47 |
48 | env:
49 | POSTGRES_USER: test
50 | POSTGRES_PASSWORD: password
51 | POSTGRES_HOST: postgres
52 | POSTGRES_PORT: 5432
53 | POSTGRES_DB: test
54 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /classes
3 | /checkouts
4 | profiles.clj
5 | pom.xml
6 | pom.xml.asc
7 | *.jar
8 | *.class
9 | /.lein-*
10 | /.nrepl-port
11 | .hgignore
12 | .hg/
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright 2018 NomNom Insights
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # lockjaw
2 |
3 |
4 | [](https://clojars.org/nomnom/lockjaw)
5 |
6 | [](https://circleci.com/gh/nomnom-insights/nomnom.lockjaw)
7 |
8 |
9 |
10 | Locks, backed by Postgres and Component
11 |
12 | ## Intro
13 |
14 | Lockjaw is a simple [Component](https://github.com/stuartsierra/component) which uses [Postgres' advisory locks](https://www.postgresql.org/docs/9.6/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS) which are managed by Postgrs itself and are really lightweight.
15 | They are not meant to be used for row level locking, but for implementing concurrency control primitives in applications.
16 |
17 | Intended usage is to ensure that at any given time, only one instance of *something* is doing the work, usually it's for ensuring that only one scheduler at a time is queueing up jobs for periodical processing. See [how it integrates with Eternity](https://github.com/nomnom-insights/nomnom.eternity#with-lock-eternitymiddlewarewith-lock)
18 |
19 | ### Locking advice
20 |
21 | Ideally, you do not do much work while holding the lock. E.g. ensuring that your application pushed only 1 job to a queue or call an endpoint once. Relying on performing long running tasks while holding the lock is not advised. Use it as a coordination mechanism, not the business logic.
22 |
23 | ## How to use it?
24 |
25 | The usage boils down to:
26 |
27 | - creating a lock instance with a lock name `(def lock-component (lockjaw.core/create {:name "delete-account"}))`
28 | - `(lockjaw.protocol/acquire! lock-component)` - acquire the lock
29 | - `(lockjaw.protocol/release! lock-component)` - release the lock (usually not needed, but just in case)
30 |
31 | Internally we use a lock ID which is an integer. The lock is established **per connection** (session), meaning the following is true:
32 |
33 | - if the current connection holds the lock for given ID, acquiring it again will still hold the lock and return true
34 | - if the current connection doesn't hold the lock for given ID, attempting to acquire it will return false, *unless the lock was released in the meantime*
35 | - if the connection is stopped, in a clean way (system/component stop) or jvm process exits (crash, or restart) - **the lock is released**
36 |
37 | It's important that you use the right connection type, especially when using tools like pgBouncer, as they might mess with the advisory locks and when they are (not) acquired. [See here for more details](https://electron0zero.xyz/blog/til-connection-pooling-and-pgbouncer).
38 |
39 | ### Lock IDs
40 |
41 | On Postgres level lock ids are just integers, and Lockjaw makes it easier to create them - we create an id out of a provided `name` configuration option.
42 | We use the CRC algorithm for ensuring that given string always produces same integer. Inspired by [Zencoder's Locker library](https://github.com/zencoder/locker/blob/master/lib/locker/advisory.rb#L97-L101).
43 |
44 |
45 | ### "Dynamic" locks
46 |
47 | While you can set the lock name while creating the component, you might need to dynamically create locks to ensure that only single user account is being processed at a given time. In that case you cannot create a component for each user ID, so you will need to use the following functions to acquire per-ID locks:
48 |
49 |
50 | - create your component (see above)
51 | - use `(lockjaw.protocol/acquire-by-name! lock (str "user:" (-> user :id)))` to hold a lock for given user id
52 | - use `(lockjaw.protocol/acquire-by-name! lock (str "user:" (-> user :id)))` to release it
53 |
54 | Same caveats apply as to 'default' locks: do not hold them for too long, and use them as a coordination mechanism instead.
55 |
56 | ## Usage
57 |
58 |
59 | ```clojure
60 | (require [lockjaw.core
61 | [com.stuartsierra.component :as component]
62 | [utility-belt.sql.component.db-pool :as db-pool]
63 | lockjaw.protocol :as lock])
64 |
65 | (def a-lock
66 | (.start
67 | (component/using
68 | ;; unique id, per service, in 99% of the cases service name is ok
69 | (lockjaw.core/create {:name "some-service" })
70 | [:db-conn]))) ;; assumes a hikari-cp pool is here, can be any other JDBC Postgres driver though!
71 |
72 | ;; explicitly:
73 | (if (lock/acquire! a-lock)
74 | (try
75 | (log/info "doing some work, exclusively")
76 | (do-work)
77 | (finally ;; release when done
78 | (lock/release! a-lock)))
79 | (log/warn "someone else is doing work"))
80 |
81 | ;;; and with a simple macro:
82 |
83 | (lock/with-lock a-lock
84 | (do-some-work))
85 |
86 | ;; if the lock is NOT acquired, it will return right away with :lockjaw.operation/no-lock keyword
87 |
88 |
89 | ;;; 'dynamic' locking
90 |
91 | (lock/with-named-lock! (:a-lock component) "delete-account:1"
92 | (do-delete component {:account-id 1 }))
93 | ```
94 |
95 |
96 |
97 | ## Mock component
98 |
99 | Lockjaw ships with a mock component, which doesn't depend on Postgrs and will always
100 | acquire the lock. It can also be configured to never acquire it:
101 |
102 | ```clojure
103 |
104 | (let [always-lock (lockjaw.mock/create {:always-acquire true})
105 | never-lock (lockjaw.mock/create {:always-acquire false})]
106 | (.start always-lock)
107 | (.start never-lock)
108 | (lock/acquire! always-lock) ;; => true
109 | (lock/acquire! never-lock) ;; => false
110 |
111 | ;; you can also pass the name lock name:
112 | (lock/acquire-by-name! always-lock "who?") ;; => true
113 | (.stop always-lock)
114 | (.stop never-lock))
115 | ```
116 |
117 | # Testing
118 |
119 | Just run:
120 |
121 | `lein test`
122 |
123 | > Ensure no other database connections are currently holding advisory locks when running tests.
124 |
125 | # Change log
126 |
127 | - 2022-02-08 - 0.3.1 - adds "acquired?" functions to check if a lock was already acquired. Updates dependencies (next.jdbc, logback-classic, tools.logging)
128 | - 2021-12-08 - 0.3.0 - "dynamic" locks, updated dependencies
129 | - 2021-11-09 - 0.2.1-SNAPSHOT, updates dependencies, includes `next.jdbc`, allows passing lock-name when asking for lock.
130 | - *unreleased* - 0.2.0-SNAPSHOT, switches to `next.jdbc`
131 | - 2019-10-24 - 0.1.2, Initial public offering
132 |
133 | # Authors
134 |
135 | In alphabetical order
136 |
137 | - [Afonso Tsukamoto](https://github.com/AfonsoTsukamoto)
138 | - [Łukasz Korecki](https://github.com/lukaszkorecki)
139 | - [Marketa Adamova](https://github.com/MarketaAdamova)
140 |
--------------------------------------------------------------------------------
/dev-resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | date=%date level=%level thread=%thread ns=%logger message=%msg%n
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject nomnom/lockjaw "0.3.1"
2 | :description "Postgres Advisory Locks as a Component"
3 | :url "https://github.com/nomnom-insights/nomnom.lockjaw"
4 | :license {:name "MIT License"
5 | :url "https://opensource.org/licenses/MIT"
6 | :year 2018
7 | :key "mit"}
8 | :deploy-repositories {"clojars" {:sign-releases false
9 | :username :env/clojars_username
10 | :password :env/clojars_password}}
11 |
12 | :dependencies [[org.clojure/clojure "1.10.3"]
13 | [com.github.seancorfield/next.jdbc "1.2.761"]
14 | [com.stuartsierra/component "1.0.0"]]
15 | :profiles {:dev
16 | {:resource-paths ["dev-resources"]
17 | :dependencies [[ch.qos.logback/logback-classic "1.2.10"]
18 | ;; pulls in all the PG bits and a connection pool
19 | ;; component
20 | [nomnom/utility-belt.sql "1.1.0"]
21 | [org.clojure/tools.logging "1.2.4"]]}})
22 |
--------------------------------------------------------------------------------
/script/ci-test:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
4 | chmod +x ./cc-test-reporter
5 | ./cc-test-reporter before-build
6 |
7 |
8 | LEIN_FAST_TRAMPOLINE=1 lein trampoline cloverage \
9 | --lcov \
10 | --no-text \
11 | --no-html \
12 | --no-summary \
13 | -o $PWD
14 | testRes=$?
15 | mkdir -p coverage
16 | mv lcov.info coverage/lcov.info
17 | ./cc-test-reporter after-build --coverage-input-type lcov --exit-code $testRes
18 |
--------------------------------------------------------------------------------
/src/lockjaw/core.clj:
--------------------------------------------------------------------------------
1 | (ns lockjaw.core
2 | (:require
3 | [clojure.tools.logging :as log]
4 | [com.stuartsierra.component :as component]
5 | [lockjaw.operation :as operation]
6 | [lockjaw.protocol :as lockjaw]
7 | [lockjaw.util :as util]))
8 |
9 |
10 | (defrecord Lockjaw
11 | [name lock-id db-conn]
12 | component/Lifecycle
13 | (start
14 | [this]
15 | (let [lock-id (util/name-to-id name)]
16 | (log/infof "name=%s status=starting lock-id=%s" name lock-id)
17 | (assoc this :lock-id lock-id)))
18 | (stop
19 | [this]
20 | (log/warnf "name=%s status=stopping lock-id=%s cleaning all locks!" name lock-id)
21 | (operation/release-all-locks! db-conn)
22 | (assoc this :lock-id nil))
23 | lockjaw/Lockjaw
24 | (acquire! [_]
25 | (operation/acquire-lock db-conn lock-id))
26 | (acquire-by-name! [_ lock-name]
27 | (let [lock-id (util/name-to-id lock-name)]
28 | (operation/acquire-lock db-conn lock-id)))
29 | (acquired? [_]
30 | (operation/lock-acquired? db-conn lock-id))
31 | (acquired-by-name? [_ lock-name]
32 | (let [lock-id (util/name-to-id lock-name)]
33 | (operation/lock-acquired? db-conn lock-id)))
34 | (release! [_]
35 | (operation/release-lock db-conn lock-id))
36 | (release-by-name! [_ lock-name]
37 | (let [lock-id (util/name-to-id lock-name)]
38 | (operation/release-lock db-conn lock-id)))
39 | (release-all! [_]
40 | (operation/release-all-locks! db-conn)))
41 |
42 |
43 | (defn create
44 | [{:keys [name] :as args}]
45 | {:pre [(and (string? name) (not (.isEmpty ^String name)))]}
46 | (map->Lockjaw args))
47 |
--------------------------------------------------------------------------------
/src/lockjaw/mock.clj:
--------------------------------------------------------------------------------
1 | (ns lockjaw.mock
2 | (:require
3 | [lockjaw.protocol]))
4 |
5 |
6 | (defrecord LockjawMock
7 | [always-acquire?]
8 | lockjaw.protocol/Lockjaw
9 | (acquire! [_]
10 | always-acquire?)
11 | (release! [_]
12 | always-acquire?)
13 | (acquire-by-name! [_ _]
14 | always-acquire?)
15 | (release-by-name! [_ _]
16 | always-acquire?))
17 |
18 |
19 | (defn create
20 | "Creates a mock, which by default always returns true on acquring the lock.
21 | use (create {:always-acquire false}) to make it always fail to acquire"
22 | [options]
23 | (->LockjawMock (get options :always-acquire true)))
24 |
--------------------------------------------------------------------------------
/src/lockjaw/operation.clj:
--------------------------------------------------------------------------------
1 | (ns lockjaw.operation
2 | (:refer-clojure :exclude [key])
3 | (:require
4 | [next.jdbc :as jdbc]))
5 |
6 |
7 | (def acquire-lock-query "SELECT pg_try_advisory_lock(?)")
8 |
9 | (def release-lock-query "SELECT pg_advisory_unlock(?)")
10 |
11 | (def release-all-locks-query "SELECT pg_advisory_unlock_all()")
12 |
13 | (def find-lock-for-id-query "SELECT objid, mode from pg_locks WHERE locktype = 'advisory' and objid = ?")
14 |
15 | (def find-all-locks "SELECT * from pg_locks WHERE locktype = 'advisory'")
16 |
17 |
18 | ;; The registry ensures that:
19 | ;; - we keep a track of how many times given lock has been acquired
20 | ;; - we can use it to release all locks on the JVM shutdown
21 | ;; - we can inspect if a lock is actually held in given instance
22 |
23 | (def no-lock ::no-lock)
24 |
25 |
26 | (def registry
27 | (atom {}))
28 |
29 |
30 | (defn- inc-key
31 | [key reg]
32 | (update reg key #(inc (or % 0))))
33 |
34 |
35 | (defn- dec-key
36 | [key reg]
37 | (update reg key #(dec (or % 0))))
38 |
39 |
40 | (defn acquire-lock
41 | [db-conn lock-id]
42 | (let [res (-> (jdbc/execute-one! db-conn [acquire-lock-query lock-id])
43 | :pg_try_advisory_lock
44 | true?)]
45 | (when res
46 | (swap! registry (partial inc-key lock-id)))
47 | res))
48 |
49 |
50 | (defn release-lock
51 | [db-conn lock-id]
52 | (let [res (-> (jdbc/execute-one! db-conn [release-lock-query lock-id])
53 | :pg_advisory_unlock
54 | true?)]
55 | (when res
56 | (swap! registry (partial dec-key lock-id)))
57 | res))
58 |
59 |
60 | (defn lock-acquired?
61 | [db-conn lock-id]
62 | (boolean (jdbc/execute-one! db-conn [find-lock-for-id-query lock-id])))
63 |
64 |
65 | (defn release-all-locks!
66 | "Releases all locks hold by this connection, regardless of how many were acquired"
67 | [db-conn]
68 | (jdbc/execute-one! db-conn [release-all-locks-query])
69 | (reset! registry {}))
70 |
71 |
72 | (defn all-locks
73 | [db-conn]
74 | (jdbc/execute! db-conn [find-all-locks]))
75 |
--------------------------------------------------------------------------------
/src/lockjaw/protocol.clj:
--------------------------------------------------------------------------------
1 | (ns lockjaw.protocol)
2 |
3 |
4 | (defprotocol Lockjaw
5 | (acquire! [this]
6 | "Tries to get a lock for given component ID")
7 | (acquire-by-name! [this lock-name]
8 | "Converts passed name to ID and tries to aquire a lock for it")
9 | (acquired? [this]
10 | "Checks if lock was already acquired")
11 | (acquired-by-name? [this lock-name]
12 | "Converts passed name to ID and checks if lock was already acquired")
13 | (release! [this]
14 | "Rleases the component lock")
15 | (release-by-name! [this lock-name]
16 | "Converts passed name to ID and releases it")
17 | (release-all! [this]
18 | "Releases all acquired locks"))
19 |
20 |
21 | (defmacro with-lock
22 | "Run the code if a lock is obtained"
23 | [a-lock & body]
24 | `(if (acquire! ~a-lock)
25 | (do
26 | ~@body)
27 | :lockjaw.operation/no-lock))
28 |
29 |
30 | (defmacro with-lock!
31 | "Like *with-lock* but release it after use"
32 | [a-lock & body]
33 | `(try
34 | (if (acquire! ~a-lock)
35 | (do
36 | ~@body)
37 | :lockjaw.operation/no-lock)
38 | (finally
39 | (release! ~a-lock))))
40 |
41 |
42 | (defmacro with-named-lock
43 | "Run the code if a lock with the passed name is obtained"
44 | [a-lock lock-name & body]
45 | `(if (acquire-by-name! ~a-lock ~lock-name)
46 | (do
47 | ~@body)
48 | :lockjaw.operation/no-lock))
49 |
50 |
51 | (defmacro with-named-lock!
52 | "Like *with-lock* but release it after use"
53 | [a-lock lock-name & body]
54 | `(try
55 | (if (acquire-by-name! ~a-lock ~lock-name)
56 | (do
57 | ~@body)
58 | :lockjaw.operation/no-lock)
59 | (finally
60 | (release-by-name! ~a-lock ~lock-name))))
61 |
--------------------------------------------------------------------------------
/src/lockjaw/util.clj:
--------------------------------------------------------------------------------
1 | (ns lockjaw.util
2 | (:import
3 | (java.util.zip
4 | CRC32)))
5 |
6 |
7 | (defn name-to-id
8 | "Converts a string to an int"
9 | [name]
10 | (let [bytes (.getBytes ^String name "UTF-8")
11 | crc (new CRC32)]
12 | (.update ^CRC32 crc bytes)
13 | (.getValue ^CRC32 crc)))
14 |
--------------------------------------------------------------------------------
/test/lockjaw/core_test.clj:
--------------------------------------------------------------------------------
1 | (ns lockjaw.core-test
2 | (:require
3 | [clojure.test :refer [deftest is testing use-fixtures]]
4 | [com.stuartsierra.component :as component]
5 | [lockjaw.core]
6 | [lockjaw.protocol :as lock]
7 | [lockjaw.test-system :as ts]))
8 |
9 |
10 | (def sys (atom nil))
11 |
12 |
13 | (use-fixtures :each (fn [test-fn]
14 | (ts/start! sys
15 | {:lock-1 (component/using
16 | (lockjaw.core/create {:name "lock-1"})
17 | [:db-conn])
18 | :lock-2 (component/using
19 | (lockjaw.core/create {:name "lock-1"})
20 | {:db-conn :db-conn-2})})
21 | (test-fn)
22 | (ts/stop! sys)))
23 |
24 |
25 | (deftest component-usage
26 | (testing "it generates a int lock id"
27 | (is (number? (:lock-id (:lock-1 @sys))))
28 | (is (number? (:lock-id (:lock-2 @sys))))
29 | (is (=
30 | (:lock-id (:lock-2 @sys))
31 | (:lock-id (:lock-1 @sys))))
32 | (is (= 3379800295
33 | (:lock-id (:lock-1 @sys)))))
34 | (testing "lock-1 gets a lock, lock-2 doesnt"
35 | (is (lock/acquire! (:lock-1 @sys)))
36 | (is (false? (lock/acquire! (:lock-2 @sys))))
37 | (is (lock/release! (:lock-1 @sys))))
38 | (testing "gets and releases locks by name"
39 | (is (lock/acquire-by-name! (:lock-1 @sys) "foo"))
40 | (is (false? (lock/acquire-by-name! (:lock-2 @sys) "foo")))
41 | (is (lock/release-by-name! (:lock-1 @sys) "foo"))
42 | (is (false? (lock/release-by-name! (:lock-2 @sys) "foo"))))
43 | (testing "checks if lock is acquired"
44 | (is (lock/acquire! (:lock-1 @sys)))
45 | (is (lock/acquired? (:lock-1 @sys))))
46 | (testing "checks if lock acquired by name"
47 | (is (lock/acquire-by-name! (:lock-1 @sys) "alock"))
48 | (is (lock/acquired-by-name? (:lock-1 @sys) "alock"))
49 | (is (false? (lock/acquired-by-name? (:lock-1 @sys) "no lock")))))
50 |
51 |
52 | (deftest handy-macros
53 | (testing "nice macro ensures lock clean up"
54 | (let [fut (future
55 | (lock/with-lock (:lock-1 @sys)
56 | (Thread/sleep 50)
57 | ::done))]
58 | (Thread/sleep 10)
59 | (is (= :lockjaw.operation/no-lock
60 | (lock/with-lock! (:lock-2 @sys)
61 | ::invalid)))
62 | (is (= ::done
63 | @fut))))
64 | (testing "nice macro with name ensures lock clean up too"
65 | (let [lock-name "a-nice-lock"
66 | fut (future
67 | (lock/with-named-lock (:lock-1 @sys) lock-name
68 | (Thread/sleep 50)
69 | ::done))]
70 | (Thread/sleep 10)
71 | (is (= :lockjaw.operation/no-lock
72 | (lock/with-named-lock! (:lock-2 @sys) lock-name
73 | ::invalid)))
74 | (is (= ::done
75 | @fut)))))
76 |
--------------------------------------------------------------------------------
/test/lockjaw/operation_test.clj:
--------------------------------------------------------------------------------
1 | (ns lockjaw.operation-test
2 | (:require
3 | [clojure.test :refer [deftest is testing use-fixtures]]
4 | [lockjaw.operation :as operation]
5 | [lockjaw.test-system :as test-system]))
6 |
7 |
8 | (def system (atom nil))
9 |
10 |
11 | (use-fixtures :each (fn [test-fn]
12 | (test-system/start! system {})
13 | (test-fn)
14 | (test-system/stop! system)))
15 |
16 |
17 | (deftest lock-operations
18 | (testing "one conn acquires lock, other doesnt"
19 | (is (operation/acquire-lock (:db-conn @system) 13))
20 | (is (= 1
21 | (count (operation/all-locks (:db-conn @system)))))
22 | (is (= 1
23 | (get @operation/registry 13)))
24 | (is (not (operation/acquire-lock (:db-conn-2 @system) 13))))
25 | (testing "releases lock, nobody hs it"
26 | (is (operation/release-lock (:db-conn @system) 13))
27 | (is (nil? (seq (operation/all-locks (:db-conn @system)))))))
28 |
29 |
30 | (deftest continous-acquiring-and-relesing
31 | (testing "it can continously acquire the lock and it's ok"
32 | (is (operation/acquire-lock (:db-conn @system) 27))
33 | (is (operation/acquire-lock (:db-conn @system) 27))
34 | (is (operation/acquire-lock (:db-conn @system) 27))
35 | (is (operation/acquire-lock (:db-conn @system) 27))
36 | (is (= 4
37 | (get @operation/registry 27)))
38 | (is (= [27]
39 | (map :pg_locks/objid (operation/all-locks (:db-conn @system)))))
40 | (operation/release-lock (:db-conn @system) 27)
41 | (is (= 3
42 | (get @operation/registry 27)))
43 | (is (= [27]
44 | (map :pg_locks/objid (operation/all-locks (:db-conn @system)))))
45 | (operation/release-all-locks! (:db-conn @system))
46 | (is (= nil
47 | (get @operation/registry 27)))
48 | (is (= []
49 | (map :pg_locks/objid (operation/all-locks (:db-conn @system)))))))
50 |
--------------------------------------------------------------------------------
/test/lockjaw/test_system.clj:
--------------------------------------------------------------------------------
1 | (ns lockjaw.test-system
2 | (:require
3 | [com.stuartsierra.component :as component]
4 | [utility-belt.sql.component.connection-pool :as cp]))
5 |
6 |
7 | (def db-spec
8 | {:pool-name "test"
9 | :adapter "postgresql"
10 | :username (or (System/getenv "POSTGRES_USER") "nomnom")
11 | :password (or (System/getenv "POSTGRES_PASSWORD") "password")
12 | :server-name (or (System/getenv "POSTGRES_HOST") "127.0.0.1")
13 | :port-number (Integer/parseInt (or (System/getenv "POSTGRES_PORT") "5432"))
14 | :maximum-pool-size 2
15 | :database-name (or (System/getenv "POSTGRES_DB") "nomnom_test")})
16 |
17 |
18 | (defn create
19 | [extra]
20 | (component/map->SystemMap
21 | (merge extra
22 | {:db-conn (cp/create db-spec)
23 | :db-conn-2 (cp/create db-spec)})))
24 |
25 |
26 | (defn start!
27 | [sysatom extra]
28 | (reset! sysatom (component/start (create extra))))
29 |
30 |
31 | (defn stop!
32 | [sysatom]
33 | (swap! sysatom component/stop))
34 |
--------------------------------------------------------------------------------
/test/lockjaw/util_test.clj:
--------------------------------------------------------------------------------
1 | (ns lockjaw.util-test
2 | (:require
3 | [clojure.test :refer [deftest is testing]]
4 | [lockjaw.util :as util]))
5 |
6 |
7 | (def sample-names
8 | ["optimus-prime"
9 | "megatron"
10 | "skywarp"
11 | "lazerbeak"
12 | "unicron"])
13 |
14 |
15 | (deftest ensures-all-names-from-id-are-unique
16 | (testing "name to id is always the same"
17 | (is (= 3092707704
18 | (util/name-to-id "sunstreak")))
19 | (is (= 3903148020
20 | (util/name-to-id "blitz")))
21 | (is (= 3076615343
22 | (util/name-to-id "blitzwing"))))
23 | (testing "generated ids are always unique"
24 | (is (= (count sample-names)
25 | (count
26 | (set
27 | (map util/name-to-id sample-names)))))))
28 |
--------------------------------------------------------------------------------