summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.pre-commit-config.yaml5
-rw-r--r--ocicl.csv4
-rwxr-xr-xrun-tests.sh12
-rw-r--r--src/rw-lock.lisp136
-rw-r--r--test/all.lisp28
-rw-r--r--test/rw-lock.lisp79
-rw-r--r--ukkoclot.asd4
7 files changed, 267 insertions, 1 deletions
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index f30fd61..e2f455c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -9,6 +9,11 @@ repos:
9 entry: ocicl lint 9 entry: ocicl lint
10 pass_filenames: true 10 pass_filenames: true
11 files: \.(lisp|asd)$ 11 files: \.(lisp|asd)$
12 - id: run-testsuite
13 name: run-testsuite
14 language: system
15 entry: ./run-tests.sh
16 pass_filenames: false
12- repo: https://github.com/fsfe/reuse-tool 17- repo: https://github.com/fsfe/reuse-tool
13 rev: v6.1.2 18 rev: v6.1.2
14 hooks: 19 hooks:
diff --git a/ocicl.csv b/ocicl.csv
index ecc65f6..44ae424 100644
--- a/ocicl.csv
+++ b/ocicl.csv
@@ -36,6 +36,7 @@ fast-http, ghcr.io/ocicl/fast-http@sha256:44e98b5239c0ded4921dc0b04ae272cff00583
36fast-http-test, ghcr.io/ocicl/fast-http@sha256:44e98b5239c0ded4921dc0b04ae272cff00583d93797a657543db782979eb50c, fast-http-20240503-2232fc9/fast-http-test.asd 36fast-http-test, ghcr.io/ocicl/fast-http@sha256:44e98b5239c0ded4921dc0b04ae272cff00583d93797a657543db782979eb50c, fast-http-20240503-2232fc9/fast-http-test.asd
37fast-io, ghcr.io/ocicl/fast-io@sha256:7ba35a0eddf00b3c25656b3c5a93e95460f6cb8f807afbc04ef4f50076d95aff, fast-io-20240503-a4c5ad6/fast-io.asd 37fast-io, ghcr.io/ocicl/fast-io@sha256:7ba35a0eddf00b3c25656b3c5a93e95460f6cb8f807afbc04ef4f50076d95aff, fast-io-20240503-a4c5ad6/fast-io.asd
38fast-io-test, ghcr.io/ocicl/fast-io@sha256:7ba35a0eddf00b3c25656b3c5a93e95460f6cb8f807afbc04ef4f50076d95aff, fast-io-20240503-a4c5ad6/fast-io-test.asd 38fast-io-test, ghcr.io/ocicl/fast-io@sha256:7ba35a0eddf00b3c25656b3c5a93e95460f6cb8f807afbc04ef4f50076d95aff, fast-io-20240503-a4c5ad6/fast-io-test.asd
39fiveam, ghcr.io/ocicl/fiveam@sha256:e283043dc8d31e8df2b214465cf5a06ea07bdbea10e600a40e17aaf9518226ac, fiveam-20240928-e43d6c8/fiveam.asd
39flexi-streams, ghcr.io/ocicl/flexi-streams@sha256:95005b06a8a95c82780c75ead53810bcb273d07fcf5fe76b527f1856df9923f2, flexi-streams-20240503-4951d57/flexi-streams.asd 40flexi-streams, ghcr.io/ocicl/flexi-streams@sha256:95005b06a8a95c82780c75ead53810bcb273d07fcf5fe76b527f1856df9923f2, flexi-streams-20240503-4951d57/flexi-streams.asd
40flexi-streams-test, ghcr.io/ocicl/flexi-streams@sha256:95005b06a8a95c82780c75ead53810bcb273d07fcf5fe76b527f1856df9923f2, flexi-streams-20240503-4951d57/flexi-streams-test.asd 41flexi-streams-test, ghcr.io/ocicl/flexi-streams@sha256:95005b06a8a95c82780c75ead53810bcb273d07fcf5fe76b527f1856df9923f2, flexi-streams-20240503-4951d57/flexi-streams-test.asd
41float-features, ghcr.io/ocicl/float-features@sha256:64922efd92a08670cf8292ea56dfff29c577d36d8a7f7576cea0c031805c52d2, float-features-20250813-cd5dab0/float-features.asd 42float-features, ghcr.io/ocicl/float-features@sha256:64922efd92a08670cf8292ea56dfff29c577d36d8a7f7576cea0c031805c52d2, float-features-20250813-cd5dab0/float-features.asd
@@ -60,6 +61,7 @@ log4cl-examples, ghcr.io/ocicl/log4cl@sha256:bd33f6e5ad346892e852492e7101bc18d26
60log4cl.log4slime, ghcr.io/ocicl/log4cl@sha256:bd33f6e5ad346892e852492e7101bc18d263f9ef2ba6192178568d1b20337112, log4cl-20240503-fe3da51/log4cl.log4slime.asd 61log4cl.log4slime, ghcr.io/ocicl/log4cl@sha256:bd33f6e5ad346892e852492e7101bc18d263f9ef2ba6192178568d1b20337112, log4cl-20240503-fe3da51/log4cl.log4slime.asd
61log4cl.log4sly, ghcr.io/ocicl/log4cl@sha256:bd33f6e5ad346892e852492e7101bc18d263f9ef2ba6192178568d1b20337112, log4cl-20240503-fe3da51/log4cl.log4sly.asd 62log4cl.log4sly, ghcr.io/ocicl/log4cl@sha256:bd33f6e5ad346892e852492e7101bc18d263f9ef2ba6192178568d1b20337112, log4cl-20240503-fe3da51/log4cl.log4sly.asd
62multilang-documentation-utils, ghcr.io/ocicl/documentation-utils@sha256:b2a1b3f3bcd1a738af85ae2b0168d408c177661eab6d6bbebb254e394d983f54, documentation-utils-20230511-98630dd/multilang-documentation-utils.asd 63multilang-documentation-utils, ghcr.io/ocicl/documentation-utils@sha256:b2a1b3f3bcd1a738af85ae2b0168d408c177661eab6d6bbebb254e394d983f54, documentation-utils-20230511-98630dd/multilang-documentation-utils.asd
64net.didierverna.asdf-flv, ghcr.io/ocicl/net.didierverna.asdf-flv@sha256:e5cd087d41cf24302a92d9bac426a925f8868b554be2414c0f012c961e3ddb8c, asdf-flv-20240503-3f1de41/net.didierverna.asdf-flv.asd
63parse-declarations-1.0, ghcr.io/ocicl/parse-declarations-1.0@sha256:9305fa9624205b16fd1fc51cb1cc4282a18e50eee9ac75b66a14f6f2c5df9518, parse-declarations-20240503-549aebb/parse-declarations-1.0.asd 65parse-declarations-1.0, ghcr.io/ocicl/parse-declarations-1.0@sha256:9305fa9624205b16fd1fc51cb1cc4282a18e50eee9ac75b66a14f6f2c5df9518, parse-declarations-20240503-549aebb/parse-declarations-1.0.asd
64parse-number, ghcr.io/ocicl/parse-number@sha256:11f3f9f512871e1f96cea6b8260aa3610e34e8fd7272bbcdb210fd3c5dd8025c, parse-number-20240503-cb9e487/parse-number.asd 66parse-number, ghcr.io/ocicl/parse-number@sha256:11f3f9f512871e1f96cea6b8260aa3610e34e8fd7272bbcdb210fd3c5dd8025c, parse-number-20240503-cb9e487/parse-number.asd
65proc-parse, ghcr.io/ocicl/proc-parse@sha256:0880f9acb35eb9efbe1f77a981e36e84e8f29a22b14f680c7686eb381fa87bd6, proc-parse-20240503-3afe2b7/proc-parse.asd 67proc-parse, ghcr.io/ocicl/proc-parse@sha256:0880f9acb35eb9efbe1f77a981e36e84e8f29a22b14f680c7686eb381fa87bd6, proc-parse-20240503-3afe2b7/proc-parse.asd
@@ -89,6 +91,8 @@ trivia.ppcre, ghcr.io/ocicl/trivia@sha256:51db74757bc31ceaf3a643914a8ffa87825799
89trivia.quasiquote, ghcr.io/ocicl/trivia@sha256:51db74757bc31ceaf3a643914a8ffa87825799097dea4912650e6ac62bed5209, trivia-20240904-4383dd8/trivia.quasiquote.asd 91trivia.quasiquote, ghcr.io/ocicl/trivia@sha256:51db74757bc31ceaf3a643914a8ffa87825799097dea4912650e6ac62bed5209, trivia-20240904-4383dd8/trivia.quasiquote.asd
90trivia.test, ghcr.io/ocicl/trivia@sha256:51db74757bc31ceaf3a643914a8ffa87825799097dea4912650e6ac62bed5209, trivia-20240904-4383dd8/trivia.test.asd 92trivia.test, ghcr.io/ocicl/trivia@sha256:51db74757bc31ceaf3a643914a8ffa87825799097dea4912650e6ac62bed5209, trivia-20240904-4383dd8/trivia.test.asd
91trivia.trivial, ghcr.io/ocicl/trivia@sha256:51db74757bc31ceaf3a643914a8ffa87825799097dea4912650e6ac62bed5209, trivia-20240904-4383dd8/trivia.trivial.asd 93trivia.trivial, ghcr.io/ocicl/trivia@sha256:51db74757bc31ceaf3a643914a8ffa87825799097dea4912650e6ac62bed5209, trivia-20240904-4383dd8/trivia.trivial.asd
94trivial-backtrace, ghcr.io/ocicl/trivial-backtrace@sha256:89a3138695bb767c50001402fad60dacfc1e867e4e3d3579cc6f636d3e29a51c, trivial-backtrace-20240503-7f90b4a/trivial-backtrace.asd
95trivial-backtrace-test, ghcr.io/ocicl/trivial-backtrace@sha256:89a3138695bb767c50001402fad60dacfc1e867e4e3d3579cc6f636d3e29a51c, trivial-backtrace-20240503-7f90b4a/trivial-backtrace-test.asd
92trivial-cltl2, ghcr.io/ocicl/trivial-cltl2@sha256:69470b4f6469314799074b9d944050dc4bcb67d5cbb2ba32c7a8899bb492f04e, trivial-cltl2-20230809-2ada872/trivial-cltl2.asd 96trivial-cltl2, ghcr.io/ocicl/trivial-cltl2@sha256:69470b4f6469314799074b9d944050dc4bcb67d5cbb2ba32c7a8899bb492f04e, trivial-cltl2-20230809-2ada872/trivial-cltl2.asd
93trivial-features, ghcr.io/ocicl/trivial-features@sha256:e278e24d39060fc7d3715531b16ddd9ab7f93f22ade53e436c0b86dbae3aa065, trivial-features-1.0/trivial-features.asd 97trivial-features, ghcr.io/ocicl/trivial-features@sha256:e278e24d39060fc7d3715531b16ddd9ab7f93f22ade53e436c0b86dbae3aa065, trivial-features-1.0/trivial-features.asd
94trivial-features-tests, ghcr.io/ocicl/trivial-features@sha256:e278e24d39060fc7d3715531b16ddd9ab7f93f22ade53e436c0b86dbae3aa065, trivial-features-1.0/trivial-features-tests.asd 98trivial-features-tests, ghcr.io/ocicl/trivial-features@sha256:e278e24d39060fc7d3715531b16ddd9ab7f93f22ade53e436c0b86dbae3aa065, trivial-features-1.0/trivial-features-tests.asd
diff --git a/run-tests.sh b/run-tests.sh
new file mode 100755
index 0000000..f8338b3
--- /dev/null
+++ b/run-tests.sh
@@ -0,0 +1,12 @@
1#!/bin/sh
2# SPDX-License-Identifier: EUPL-1.2
3# SPDX-FileCopyrightText: 2025 Uko Kokņevičs <perkontevs@gmail.com>
4
5set -eu
6
7exec sbcl \
8 --disable-ldb --lose-on-corruption \
9 --noinform --noprint --non-interactive \
10 --eval '(asdf:load-system :ukkoclot/test/all)' \
11 --eval '(setf ukkoclot/test/all:*should-quit* t)' \
12 --eval '(asdf:test-system :ukkoclot)'
diff --git a/src/rw-lock.lisp b/src/rw-lock.lisp
new file mode 100644
index 0000000..b32d3d0
--- /dev/null
+++ b/src/rw-lock.lisp
@@ -0,0 +1,136 @@
1;; SPDX-License-Identifier: EUPL-1.2
2;; SPDX-FileCopyrightText: 2025 Uko Kokņevičs <perkontevs@gmail.com>
3(defpackage :ukkoclot/src/rw-lock
4 (:documentation "Implementation of a shared/read-write lock.")
5 (:use :c2cl :bt2 :iterate)
6 (:import-from :alexandria :with-gensyms)
7 (:import-from :com.dieggsy.f-string :enable-f-strings)
8 (:import-from :sb-sys
9 :allow-with-interrupts :with-local-interrupts :without-interrupts)
10 (:export
11 #:rw-lock
12 #:rw-lock-p
13 #:make-rw-lock
14 #:acquire-read-lock
15 #:release-read-lock
16 #:with-read-lock
17 #:acquire-write-lock
18 #:release-write-lock
19 #:with-write-lock))
20(in-package :ukkoclot/src/rw-lock)
21
22(eval-when (:compile-toplevel :load-toplevel :execute)
23 (enable-f-strings))
24
25;; TODO: Get rid of broadcasting for writers, instead have separate condvars
26;; TODO: Make it a bit more fair, 50% chance of waking up writers or readers
27;; TODO: Use atomic-integer in best-case for read locks to decrease contention
28(defstruct (rw-lock (:constructor make-rw-lock%))
29 (lock nil :type lock :read-only t)
30 (unlocked nil :type condition-variable :read-only t)
31 (readers 0 :type integer)
32 (has-writer nil :type boolean))
33
34(defvar *counter-lock* (make-lock :name "rw-lock::*counter-lock*"))
35
36(defvar *counter* 0)
37
38(defun gen-name ()
39 (format nil "Read-Write Lock ~A"
40 (with-lock-held (*counter-lock*)
41 (incf *counter*))))
42
43(defun make-rw-lock (&key (name (gen-name)))
44 (check-type name string)
45 (make-rw-lock%
46 :lock (make-lock :name #f"{name}'s internal lock")
47 :unlocked (make-condition-variable :name #f"{name}'s internal unlocked")))
48
49(defun acquire-read-lock (rw-lock &key (wait t))
50 ;; TODO: timeout
51 (check-type rw-lock rw-lock)
52 (with-slots (lock unlocked readers has-writer) rw-lock
53 (let ((lock-acquired))
54 (without-interrupts
55 (unwind-protect
56 (when (setf lock-acquired
57 (allow-with-interrupts
58 (acquire-lock lock :wait wait)))
59 (with-local-interrupts
60 (when (if wait
61 (iter
62 (unless has-writer
63 (return t))
64 (condition-wait unlocked lock))
65 (not has-writer))
66 (incf readers)
67 t)))
68 (when lock-acquired
69 (release-lock lock)))))))
70
71(defun release-read-lock (rw-lock)
72 (check-type rw-lock rw-lock)
73 (with-slots (lock unlocked readers has-writer) rw-lock
74 (with-lock-held (lock)
75 (decf readers)
76 (when (zerop readers)
77 (condition-broadcast unlocked))))
78 rw-lock)
79
80(defmacro with-read-lock ((rw-lock) &body body)
81 ;; TODO: timeout
82 (with-gensyms (lock-acquired lock-value)
83 `(let ((,lock-acquired)
84 (,lock-value ,rw-lock))
85 (without-interrupts
86 (unwind-protect
87 (when (setf ,lock-acquired
88 (allow-with-interrupts
89 (acquire-read-lock ,lock-value)))
90 (with-local-interrupts ,@body))
91 (when ,lock-acquired
92 (release-read-lock ,lock-value)))))))
93
94(defun acquire-write-lock (rw-lock &key (wait t))
95 ;; TODO: timeout
96 (check-type rw-lock rw-lock)
97 (with-slots (lock unlocked readers has-writer) rw-lock
98 (let ((lock-acquired))
99 (without-interrupts
100 (unwind-protect
101 (when (setf lock-acquired
102 (allow-with-interrupts
103 (acquire-lock lock :wait wait)))
104 (with-local-interrupts
105 (when (if wait
106 (iter
107 (unless (or (> readers 0) has-writer)
108 (return t))
109 (condition-wait unlocked lock))
110 (and (zerop readers) (not has-writer)))
111 (setf has-writer t)
112 t)))
113 (when lock-acquired
114 (release-lock lock)))))))
115
116(defun release-write-lock (rw-lock)
117 (check-type rw-lock rw-lock)
118 (with-slots (lock unlocked readers has-writer) rw-lock
119 (with-lock-held (lock)
120 (setf has-writer nil)
121 (condition-broadcast unlocked)))
122 rw-lock)
123
124(defmacro with-write-lock ((rw-lock) &body body)
125 ;; TODO: timeout
126 (with-gensyms (lock-acquired lock-value)
127 `(let ((,lock-acquired)
128 (,lock-value ,rw-lock))
129 (without-interrupts
130 (unwind-protect
131 (when (setf ,lock-acquired
132 (allow-with-interrupts
133 (acquire-write-lock ,lock-value)))
134 (with-local-interrupts ,@body))
135 (when ,lock-acquired
136 (release-write-lock ,lock-value)))))))
diff --git a/test/all.lisp b/test/all.lisp
new file mode 100644
index 0000000..b0a731b
--- /dev/null
+++ b/test/all.lisp
@@ -0,0 +1,28 @@
1;; SPDX-License-Identifier: EUPL-1.2
2;; SPDX-FileCopyrightText: 2025 Uko Kokņevičs <perkontevs@gmail.com>
3(defpackage :ukkoclot/test/all
4 (:documentation "Main test runner package.")
5 (:use :c2cl :fiveam
6 :ukkoclot/test/rw-lock)
7 (:import-from :bt2 :with-timeout)
8 (:import-from :uiop)
9 (:export #:*should-quit* #:run-tests))
10(in-package :ukkoclot/test/all)
11
12(defvar *should-quit* nil
13 "Bind as true if `run-tests' should exit the process with 0 or 1.
14
15Useful for CI.")
16
17;; TODO: Maybe I should signal on failure instead :thinking:
18(defun run-tests ()
19 "Run all tests"
20 (with-timeout (100)
21 (let ((status (run-all-tests)))
22 (when *should-quit*
23 (format t "~2&BTW Ignore the fatal ERROR condition below")
24 (cond
25 (status (format t "~&!GREAT SUCCESS!~2%")
26 (uiop:quit 0))
27 (t (format t "~&!!! TESTS R FUCKED MATE !!!~2%")
28 (uiop:quit 1)))))))
diff --git a/test/rw-lock.lisp b/test/rw-lock.lisp
new file mode 100644
index 0000000..4460398
--- /dev/null
+++ b/test/rw-lock.lisp
@@ -0,0 +1,79 @@
1;; SPDX-License-Identifier: EUPL-1.2
2;; SPDX-FileCopyrightText: 2025 Uko Kokņevičs <perkontevs@gmail.com>
3(defpackage :ukkoclot/test/rw-lock
4 (:documentation "Test-suite for :ukkoclot/src/rw-lock.")
5 (:use :c2cl :fiveam :ukkoclot/src/rw-lock)
6 (:import-from :bt2 :with-timeout))
7(in-package :ukkoclot/test/rw-lock)
8
9(def-suite :ukkoclot/rw-lock)
10(in-suite :ukkoclot/rw-lock)
11
12(test rw-lock.typep
13 (is (typep (make-rw-lock) 'rw-lock)))
14
15(test rw-lock-p
16 (is (rw-lock-p (make-rw-lock))))
17
18(test acquire-read-lock.no-contention
19 (with-timeout (5)
20 (let ((lock (make-rw-lock)))
21 (is-true (acquire-read-lock lock :wait t))
22 (is (rw-lock-p (release-read-lock lock)))
23 (is-true (acquire-read-lock lock :wait nil))
24 (is (rw-lock-p (release-read-lock lock))))))
25
26(test acquire-read-lock.multiple
27 (with-timeout (5)
28 (let ((lock (make-rw-lock)))
29 (is-true (acquire-read-lock lock :wait t))
30 (is-true (acquire-read-lock lock :wait nil))
31 (is-true (acquire-read-lock lock :wait t))
32 (is (rw-lock-p (release-read-lock lock)))
33 (is (rw-lock-p (release-read-lock lock)))
34 (is (rw-lock-p (release-read-lock lock))))))
35
36(test acquire-write-lock.no-contention
37 (let ((lock (make-rw-lock)))
38 (is-true (acquire-write-lock lock :wait nil))
39 (is (rw-lock-p (release-write-lock lock)))
40 (is-true (acquire-write-lock lock :wait nil))
41 (is (rw-lock-p (release-write-lock lock)))))
42
43(test acquire-write-lock.contention
44 (let ((lock (make-rw-lock)))
45 (is-true (acquire-write-lock lock :wait nil))
46 (is-false (acquire-write-lock lock :wait nil))
47 (is (rw-lock-p (release-write-lock lock)))))
48
49(test acquire-read&write-lock.contention
50 (let ((lock (make-rw-lock)))
51 (is-true (acquire-read-lock lock :wait nil))
52 (is-true (acquire-read-lock lock :wait nil))
53 (is-false (acquire-write-lock lock :wait nil))
54 (is (rw-lock-p (release-read-lock lock)))
55 (is (rw-lock-p (release-read-lock lock)))
56 (is-true (acquire-write-lock lock :wait nil))
57 (is-false (acquire-read-lock lock :wait nil))
58 (is (rw-lock-p (release-write-lock lock)))))
59
60(test with-read-lock.simple
61 (let ((lock (make-rw-lock)))
62 (with-read-lock (lock)
63 (is-true (acquire-read-lock lock :wait nil))
64 (is (rw-lock-p (release-read-lock lock)))
65 (is-false (acquire-write-lock lock :wait nil)))
66 (is-true (acquire-read-lock lock :wait nil))
67 (is (rw-lock-p (release-read-lock lock)))
68 (is-true (acquire-write-lock lock :wait nil))
69 (is (rw-lock-p (release-write-lock lock)))))
70
71(test with-write-lock.simple
72 (let ((lock (make-rw-lock)))
73 (with-write-lock (lock)
74 (is-false (acquire-read-lock lock :wait nil))
75 (is-false (acquire-write-lock lock :wait nil)))
76 (is-true (acquire-read-lock lock :wait nil))
77 (is (rw-lock-p (release-read-lock lock)))
78 (is-true (acquire-write-lock lock :wait nil))
79 (is (rw-lock-p (release-write-lock lock)))))
diff --git a/ukkoclot.asd b/ukkoclot.asd
index fd7f14e..339a208 100644
--- a/ukkoclot.asd
+++ b/ukkoclot.asd
@@ -11,11 +11,13 @@
11 :description "ukkoclot: Ukko's shitty telegram bot" 11 :description "ukkoclot: Ukko's shitty telegram bot"
12 :long-description #.(uiop:read-file-string (uiop:subpathname *load-pathname* "README.md")) 12 :long-description #.(uiop:read-file-string (uiop:subpathname *load-pathname* "README.md"))
13 :depends-on (:ukkoclot/src/main) 13 :depends-on (:ukkoclot/src/main)
14 ;; TODO: :in-order-to ((test-op (test-op ukkoclot-test))) 14 :in-order-to ((test-op (load-op :ukkoclot/test/all)))
15 :perform (test-op (o c) (symbol-call :ukkoclot/test/all :run-tests)))
15 16
16(register-system-packages :ukkoclot/src/config '(:conf)) 17(register-system-packages :ukkoclot/src/config '(:conf))
17(register-system-packages :ukkoclot/src/main '(:ukkoclot)) 18(register-system-packages :ukkoclot/src/main '(:ukkoclot))
18 19
20(register-system-packages :bordeaux-threads '(:bt2))
19(register-system-packages :closer-mop '(:c2cl)) 21(register-system-packages :closer-mop '(:c2cl))
20(register-system-packages :dexador '(:dex)) 22(register-system-packages :dexador '(:dex))
21(register-system-packages :log4cl '(:log)) 23(register-system-packages :log4cl '(:log))