Skip to content

Commit 368b9a8

Browse files
committed
Use java.time Durations and Instants. Remove clj-time.
1 parent 2994ca5 commit 368b9a8

File tree

11 files changed

+100
-105
lines changed

11 files changed

+100
-105
lines changed

src/io/staticweb/rate_limit/limits.clj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@
2828
(get-quota [self req])
2929
(get-ttl [self req]))
3030

31-
(defrecord IpRateLimit [id quota ttl]
31+
(defrecord IpRateLimit [id quota ^java.time.Duration ttl]
3232
RateLimit
3333
(get-key [self req]
34-
(str (.getName (type self)) id "-" (:remote-addr req)))
34+
(str (.getName ^Class (type self)) id "-" (:remote-addr req)))
3535

3636
(get-quota [self req]
3737
quota)

src/io/staticweb/rate_limit/middleware.clj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
An optional :response-builder can be provided to override the
2121
default 429 response. The builder must be a (fn [quota retry-after]
2222
...) function where `quota` is the number of requests allowed by the
23-
limit and `retry-after` is a clj-time/Joda DateTime specifying when
23+
limit and `retry-after` is a java.time.Duration specifying when
2424
the rate-limit will be reset. The too-many-requests-response fn can
2525
be used as a helper in forming a proper 429 response."
2626
[handler {:keys [storage limit response-builder]}]
@@ -80,7 +80,7 @@
8080
limit in the storage (eg two IP-based limits on two different routes
8181
that should have independent counters).
8282
83-
Eg. (ip-rate-limit :my-limit 1000 (t/hours 1)) allows 1000 requests
83+
Eg. (ip-rate-limit :my-limit 1000 (java.time.Duration/ofHours 1)) allows 1000 requests
8484
per hour per IP-address.
8585
8686
Note: make sure that the incoming ring request has the
@@ -109,6 +109,6 @@
109109

110110
(defn add-retry-after-header
111111
"Add a Retry-After header to the provided response. The
112-
`retry-after` argument is expected to be a clj-time/Joda DateTime."
112+
`retry-after` argument is expected to be a java.time.Duration."
113113
[rsp retry-after]
114114
(responses/add-retry-after-header rsp retry-after))

src/io/staticweb/rate_limit/redis.clj

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
(ns io.staticweb.rate-limit.redis
22
(:require [io.staticweb.rate-limit.storage :as storage]
3-
[taoensso.carmine :as car]))
3+
[taoensso.carmine :as car])
4+
(:import [java.time Duration Instant]))
45

56
(set! *warn-on-reflection* true)
67

@@ -24,23 +25,23 @@
2425
(if-let [counter (car/wcar
2526
conn-opts
2627
(car/get redis-key))]
27-
(Integer. counter)
28+
(Integer/parseInt counter)
2829
0)))
2930

3031
(increment-count [self key ttl]
3132
(let [redis-key (generate-redis-key key)
32-
ttl-in-secs (-> ttl .toStandardDuration .getStandardSeconds)]
33+
ttl-in-secs (-> ^Duration ttl .getSeconds)]
3334
(car/wcar
3435
conn-opts
3536
(car/eval* ttl-incr-script 1 redis-key ttl-in-secs))))
3637

3738
(counter-expiry [self key]
38-
(let [now (t/now)
39+
(let [now (Instant/now)
3940
redis-key (generate-redis-key key)
4041
ttl (car/wcar conn-opts (car/ttl redis-key))]
4142
(if (neg? ttl)
4243
now
43-
(t/plus now (t/seconds ttl)))))
44+
(.plus now (Duration/ofSeconds ttl)))))
4445

4546
(clear-counters [self]
4647
(let [redis-key (generate-redis-key "*")]

src/io/staticweb/rate_limit/responses.clj

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
1-
(ns io.staticweb.rate-limit.responses
2-
(:require [clj-time.format :as f]))
1+
(ns io.staticweb.rate-limit.responses)
32

43
(set! *warn-on-reflection* true)
54

65
(def default-response
76
"The default 429 response."
8-
{:headers {"Content-Type" "application/json"}
9-
:body "{\"error\": \"Too Many Requests\"}"})
10-
11-
(def ^:private time-format (f/formatter "EEE, dd MMM yyyy HH:mm:ss"))
12-
13-
(defn- time->str
14-
[time]
15-
;; All HTTP timestamps MUST be in GMT and UTC == GMT in this case.
16-
(str (f/unparse time-format time) " GMT"))
7+
{:status 429
8+
:headers {"Content-Type" "application/json"}
9+
:body "{\"error\":\"rate-limit-exceeded\"}"})
1710

1811
(defn rate-limit-applied?
1912
[rsp]
@@ -26,10 +19,10 @@
2619
(assoc rsp ::rate-limit-applied quota-state))
2720

2821
(defn add-retry-after-header
29-
[rsp retry-after]
22+
[rsp ^java.time.Duration retry-after]
3023
(assoc-in rsp
3124
[:headers "Retry-After"]
32-
(time->str retry-after)))
25+
(str (.getSeconds retry-after))))
3326

3427
(defn too-many-requests-response
3528
([retry-after]

src/io/staticweb/rate_limit/storage.clj

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
(ns io.staticweb.rate-limit.storage
2-
(:require [clj-time.core :as t]))
2+
(:import [java.time Duration Instant]))
33

44
(set! *warn-on-reflection* true)
55

6+
(defn now ^Instant []
7+
(Instant/now))
68

79
(defprotocol Storage
810
"A protocol describing the interface for storage backends.
@@ -18,15 +20,15 @@
1820
will expire, ie when the rate limit is reset again."
1921

2022
(get-count [self key])
21-
(increment-count [self key ttl])
23+
(increment-count [self key ^Duration ttl])
2224
(counter-expiry [self key])
2325
(clear-counters [self]))
2426

2527
;;; LocalStorage implementation
2628
(defn- expired-keys
27-
[m now]
29+
[m ^Instant now]
2830
(->> (:timeouts m)
29-
(filter (fn [[k v]] (t/before? v now)))
31+
(filter (fn [[k ^Instant v]] (.isAfter v now)))
3032
(map first)))
3133

3234
(defn- remove-key
@@ -37,21 +39,21 @@
3739

3840
(defn- remove-expired-keys
3941
[state]
40-
(doseq [k (expired-keys @state (t/now))]
42+
(doseq [k (expired-keys @state (now))]
4143
(swap! state remove-key k)))
4244

4345
(defn- increment-key
4446
"Increment the counter in the state map.
4547
4648
If the counter didn't exist already, we also record the time when
4749
the counter expires."
48-
[state key ttl]
50+
[state key ^Duration ttl]
4951
(if (get-in state [:counters key])
5052
(update-in state [:counters key] inc)
5153
(->
5254
state
5355
(assoc-in [:counters key] 1)
54-
(assoc-in [:timeouts key] (t/plus (t/now) ttl)))))
56+
(assoc-in [:timeouts key] (.plus (now) ttl)))))
5557

5658
(defrecord LocalStorage [state]
5759
Storage
@@ -66,7 +68,7 @@
6668
(counter-expiry [self key]
6769
(if-let [timeout (get-in @state [:timeouts key])]
6870
timeout
69-
(t/now)))
71+
(now)))
7072

7173
(clear-counters [self]
7274
(reset! state {})))

test/io/staticweb/rate_limit/acceptance_test.clj

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
compojure.core
44
io.staticweb.rate-limit.middleware
55
io.staticweb.rate-limit.test-utils)
6-
(:require [io.staticweb.rate-limit.storage :as s]
7-
[ring.mock.request :as mock]))
6+
(:require [io.staticweb.rate-limit.redis :as redis]
7+
[io.staticweb.rate-limit.storage :as s]
8+
[ring.mock.request :as mock])
9+
(:import [java.time Duration Instant]))
810

911
(def default-response-handler
1012
(constantly default-response))
@@ -13,7 +15,7 @@
1315
"A collection of functions used to instantiate the various storage
1416
backends."
1517
[s/local-storage
16-
#(s/redis-storage {:spec {:host "localhost" :port 6379}})])
18+
#(redis/redis-storage {:spec {:host "localhost" :port 6379}})])
1719

1820
(defn create-storage
1921
[factory-fn]
@@ -24,7 +26,7 @@
2426
(testing "wrap-rate-limit"
2527
(doseq [storage-factory-fn storage-factory-fns]
2628
(let [storage (create-storage storage-factory-fn)
27-
limit (ip-rate-limit :test 1 (t/seconds 1))
29+
limit (ip-rate-limit :test 1 (Duration/ofSeconds 1))
2830
response-builder (fn [& args]
2931
(apply too-many-requests-response
3032
{:headers
@@ -82,7 +84,7 @@
8284
(testing "single wrap-stacking-rate-limit instance"
8385
(doseq [storage-factory-fn storage-factory-fns]
8486
(let [storage (create-storage storage-factory-fn)
85-
limit (ip-rate-limit :test 1 (t/seconds 1))
87+
limit (ip-rate-limit :test 1 (Duration/ofSeconds 1))
8688
response-builder (partial too-many-requests-response
8789
{:headers
8890
{"Content-Type" "text/plain"}
@@ -153,10 +155,10 @@
153155
(testing "multiple wrap-stacking-rate-limit instance"
154156
(doseq [storage-factory-fn storage-factory-fns]
155157
(let [storage (create-storage storage-factory-fn)
156-
first-limit (ip-rate-limit :test 1 (t/seconds 1))
158+
first-limit (ip-rate-limit :test 1 (Duration/ofSeconds 1))
157159
first-config {:storage storage
158160
:limit first-limit}
159-
second-limit (->MethodRateLimit #{:get} 1 (t/seconds 1))
161+
second-limit (->MethodRateLimit #{:get} 1 (Duration/ofSeconds 1))
160162
second-response-builder (partial too-many-requests-response
161163
{:headers
162164
{"Content-Type" "text/plain"}

test/io/staticweb/rate_limit/limits_test.clj

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
(ns io.staticweb.rate-limit.limits-test
2-
(:require [clj-time.core :as t]
3-
[clojure.test :refer :all]
4-
[io.staticweb.rate-limit.limits :refer :all]))
2+
(:use clojure.test
3+
io.staticweb.rate-limit.limits)
4+
(:import java.time.Duration))
55

66
(deftest ^:unit test-ip-rate-limit
77
(testing "IpRateLimit"
8-
(let [limit (->IpRateLimit :test 100 (t/seconds 10))
8+
(let [limit (->IpRateLimit :test 100 (Duration/ofSeconds 10))
99
req {:remote-addr "127.0.0.1"}]
1010
(is (= (get-key limit req)
11-
"io.staticweb.rate-limit.limits.IpRateLimit:test-127.0.0.1"))
11+
"io.staticweb.rate_limit.limits.IpRateLimit:test-127.0.0.1"))
1212
(is (= (get-quota limit req) 100))
13-
(is (= (get-ttl limit req) (t/seconds 10))))))
13+
(is (= (get-ttl limit req) (Duration/ofSeconds 10))))))
1414

1515
(deftest ^:unit test-nil-rate-limit
1616
(testing "nil as a RateLimit"

test/io/staticweb/rate_limit/middleware_test.clj

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
(ns io.staticweb.rate-limit.middleware-test
2-
(:require [clj-time.coerce :as c]
3-
[clojure.test :refer :all]
4-
[io.staticweb.rate-limit.limits :as l]
5-
[io.staticweb.rate-limit.middleware :refer :all]
2+
(:use clojure.test
3+
io.staticweb.rate-limit.middleware
4+
io.staticweb.rate-limit.test-utils)
5+
(:require [io.staticweb.rate-limit.limits :as l]
66
[io.staticweb.rate-limit.responses :as r]
7-
[io.staticweb.rate-limit.storage :as s]
8-
[io.staticweb.rate-limit.test-utils :refer :all]))
7+
[io.staticweb.rate-limit.storage :as s])
8+
(:import java.time.Duration))
99

1010
(defrecord MockStorage [counters timeouts]
1111
s/Storage
@@ -37,7 +37,7 @@
3737
[counters & body]
3838
`(binding [*storage* (->MockStorage (atom {}) (atom {}))]
3939
(doseq [[k# v# e#] ~counters]
40-
(set-counter k# v# (c/from-date e#)))
40+
(set-counter k# v# e#))
4141
~@body))
4242

4343
(defn make-request
@@ -66,17 +66,17 @@
6666
(is (= (s/counter-expiry *storage* :mock-limit-key) :mock-ttl)))))
6767

6868
(testing "with exhausted limit"
69-
(with-counters [[:mock-limit-key 10 #inst "2014-12-31T12:34:56Z"]]
69+
(with-counters [[:mock-limit-key 10 (Duration/ofMinutes 5)]]
7070
(let [limit (->MockRateLimit 10 :mock-limit-key :mock-ttl)
7171
rsp (make-request wrap-stacking-rate-limit limit)]
7272
(is (= (:status rsp) 429))
7373
(is (= (::r/rate-limit-applied rsp) {:key :mock-limit-key
7474
:quota 10
7575
:remaining 0}))
76-
(is (= (retry-after rsp) "Wed, 31 Dec 2014 12:34:56 GMT")))))
76+
(is (= (retry-after rsp) "300")))))
7777

7878
(testing "with custom 429 reponse"
79-
(with-counters [[:mock-limit-key 10 #inst "2014-12-31T12:34:56Z"]]
79+
(with-counters [[:mock-limit-key 10 (Duration/ofSeconds 42)]]
8080
(let [limit (->MockRateLimit 10 :mock-limit-key :mock-ttl)
8181
custom-response-handler (fn [key retry-after]
8282
{:status 418
@@ -111,7 +111,7 @@
111111
(is (= (s/counter-expiry *storage* :first-limit-key) :first-ttl))))))
112112

113113
(testing "with exhausted first rate limit"
114-
(with-counters [[:first-limit-key 1000 #inst "2014-12-31T12:34:56Z"]]
114+
(with-counters [[:first-limit-key 1000 (Duration/ofMillis 15000)]]
115115
(let [first-limit (->MockRateLimit 1000 :first-limit-key :first-ttl)
116116
second-limit (->MockRateLimit 10 :second-limit-key :second-ttl)
117117
handler (-> default-response
@@ -125,12 +125,12 @@
125125
(is (= (::r/rate-limit-applied rsp) {:key :first-limit-key
126126
:quota 1000
127127
:remaining 0}))
128-
(is (= (retry-after rsp) "Wed, 31 Dec 2014 12:34:56 GMT"))
128+
(is (= (retry-after rsp) "15"))
129129
(is (= (s/get-count *storage* :first-limit-key) 1000))
130130
(is (= (s/get-count *storage* :second-limit-key) 0))))))
131131

132132
(testing "with exhausted second rate limit"
133-
(with-counters [[:second-limit-key 10 #inst "2014-12-31T12:34:56Z"]]
133+
(with-counters [[:second-limit-key 10 (Duration/ofHours 3)]]
134134
(let [first-limit (->MockRateLimit 1000 :first-limit-key :first-ttl)
135135
second-limit (->MockRateLimit 10 :second-limit-key :second-ttl)
136136
handler (-> default-response
@@ -144,6 +144,6 @@
144144
(is (= (::r/rate-limit-applied rsp) {:key :second-limit-key
145145
:quota 10
146146
:remaining 0}))
147-
(is (= (retry-after rsp) "Wed, 31 Dec 2014 12:34:56 GMT"))
147+
(is (= (retry-after rsp) "10800"))
148148
(is (= (s/get-count *storage* :first-limit-key) 0))
149149
(is (= (s/get-count *storage* :second-limit-key) 10)))))))

0 commit comments

Comments
 (0)