Skip to content

Commit d0424e4

Browse files
committed
Handle functions that return EAGAIN.
Do a naive immediate retry for now. Smarter retry patterns can come later.
1 parent 1cb67e8 commit d0424e4

File tree

7 files changed

+115
-21
lines changed

7 files changed

+115
-21
lines changed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright (c) 2015, Conor McDermottroe
1+
Copyright (c) 2015-2016, Conor McDermottroe
22
All rights reserved.
33

44
Redistribution and use in source and binary forms, with or without

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ TODO. :)
5151

5252
## License
5353

54-
Copyright (c) 2015, Conor McDermottroe
54+
Copyright (c) 2015-2016, Conor McDermottroe
5555
All rights reserved.
5656

5757
Redistribution and use in source and binary forms, with or without

src/clj_libssh2/agent.clj

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
(ns clj-libssh2.agent
2-
(:require [clj-libssh2.error :refer [handle-errors]]
2+
(:require [clj-libssh2.error :refer [handle-errors with-timeout]]
33
[clj-libssh2.libssh2 :as libssh2]
44
[clj-libssh2.libssh2.agent :as libssh2-agent])
55
(:import [com.sun.jna.ptr PointerByReference]))
@@ -9,10 +9,13 @@
99
the first entry."
1010
[session ssh-agent previous]
1111
(when (nil? previous)
12-
(handle-errors session (libssh2-agent/list-identities ssh-agent)))
12+
(handle-errors session
13+
(with-timeout :agent
14+
(libssh2-agent/list-identities ssh-agent))))
1315
(let [id (PointerByReference.)
1416
ret (handle-errors session
15-
(libssh2-agent/get-identity ssh-agent id previous))]
17+
(with-timeout :agent
18+
(libssh2-agent/get-identity ssh-agent id previous)))]
1619
(case ret
1720
0 (.getValue id)
1821
1 nil
@@ -25,18 +28,23 @@
2528
(when (nil? ssh-agent)
2629
(throw (Exception. "Failed to initialize agent.")))
2730
(try
28-
(handle-errors session (libssh2-agent/connect ssh-agent))
31+
(handle-errors session
32+
(with-timeout :agent
33+
(libssh2-agent/connect ssh-agent)))
2934
(when-not (loop [success false
3035
previous nil]
3136
(if success
3237
success
3338
(if-let [id (get-identity session ssh-agent previous)]
3439
(recur
35-
(= 0 (libssh2-agent/userauth ssh-agent username id))
40+
(= 0 (with-timeout :agent
41+
(libssh2-agent/userauth ssh-agent username id)))
3642
id)
3743
false)))
3844
(throw (Exception. "Failed to authenticate with the agent.")))
3945
true
4046
(finally
41-
(handle-errors session (libssh2-agent/disconnect ssh-agent))
47+
(handle-errors session
48+
(with-timeout :agent
49+
(libssh2-agent/disconnect ssh-agent)))
4250
(libssh2-agent/free ssh-agent)))))

src/clj_libssh2/authentication.clj

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
(:require [clojure.java.io :refer [file]]
33
[clj-libssh2.libssh2.userauth :as libssh2-userauth]
44
[clj-libssh2.agent :as ssh-agent]
5-
[clj-libssh2.error :refer [handle-errors]])
5+
[clj-libssh2.error :refer [handle-errors with-timeout]])
66
(:import clojure.lang.PersistentArrayMap))
77

88
(defprotocol Credentials
@@ -38,19 +38,21 @@
3838
(when-not (.exists (file keyfile))
3939
(throw (Exception. (format "%s does not exist." keyfile)))))
4040
(handle-errors session
41-
(libssh2-userauth/publickey-fromfile (:session session)
42-
(:username credentials)
43-
(:public-key credentials)
44-
(:private-key credentials)
45-
(:passphrase credentials)))
41+
(with-timeout :auth
42+
(libssh2-userauth/publickey-fromfile (:session session)
43+
(:username credentials)
44+
(:public-key credentials)
45+
(:private-key credentials)
46+
(:passphrase credentials))))
4647
true)
4748

4849
(defmethod authenticate PasswordCredentials
4950
[session credentials]
5051
(handle-errors session
51-
(libssh2-userauth/password (:session session)
52-
(:username credentials)
53-
(:password credentials)))
52+
(with-timeout :auth
53+
(libssh2-userauth/password (:session session)
54+
(:username credentials)
55+
(:password credentials))))
5456
true)
5557

5658
(defmethod authenticate PersistentArrayMap

src/clj_libssh2/error.clj

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
[clj-libssh2.libssh2.session :as libssh2-session])
44
(:import [com.sun.jna.ptr IntByReference PointerByReference]))
55

6+
(def timeouts (atom {:agent 10000
7+
:auth 10000
8+
:session 10000}))
9+
610
(def error-messages
711
"All of the error codes that are documented for libssh2 except for
812
LIBSSH2_ERROR_SOCKET_NONE which despite its name is a generic error and
@@ -92,3 +96,33 @@
9296
`(let [res# (do ~@body)]
9397
(maybe-throw-error (:session ~session) res#)
9498
res#))
99+
100+
(defn get-timeout
101+
"Get a timeout value."
102+
[name-or-value]
103+
(or (get @timeouts name-or-value) name-or-value 1000))
104+
105+
(defn set-timeout
106+
"Update a named timeout value."
107+
[timeout-name millis]
108+
(swap! timeouts assoc timeout-name millis))
109+
110+
(defmacro with-timeout
111+
"Run some code that could return libssh2/ERROR_EAGAIN and if it does, retry
112+
until the timeout is hit.
113+
114+
`timeout` can be either a number of milliseconds or a keyword referring to a
115+
named timeout set using `set-timeout`.
116+
117+
This will *not* interrupt a blocking function as this is usually used with
118+
native functions which probably should not be interrupted."
119+
[timeout & body]
120+
`(let [start# (System/currentTimeMillis)
121+
timeout# (get-timeout ~timeout)]
122+
(loop [timedout# false]
123+
(if timedout#
124+
(throw (Exception. "Timeout!"))
125+
(let [r# (do ~@body)]
126+
(if (= r# libssh2/ERROR_EAGAIN)
127+
(recur (< timeout# (- (System/currentTimeMillis) start#)))
128+
r#))))))

src/clj_libssh2/session.clj

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
(:require [clj-libssh2.libssh2 :as libssh2]
33
[clj-libssh2.libssh2.session :as libssh2-session]
44
[clj-libssh2.authentication :refer [authenticate]]
5-
[clj-libssh2.error :refer [handle-errors]]
5+
[clj-libssh2.error :refer [handle-errors with-timeout]]
66
[clj-libssh2.socket :as socket]))
77

88
(def sessions (atom {}))
@@ -22,14 +22,20 @@
2222
([^Session session]
2323
(destroy-session session "Shutting down normally." false))
2424
([^Session {session :session} ^String reason ^Boolean raise]
25-
(handle-errors nil (libssh2-session/disconnect session reason))
26-
(handle-errors nil (libssh2-session/free session))
25+
(handle-errors nil
26+
(with-timeout :session
27+
(libssh2-session/disconnect session reason)))
28+
(handle-errors nil
29+
(with-timeout :session
30+
(libssh2-session/free session)))
2731
(when raise
2832
(throw (Exception. reason)))))
2933

3034
(defn- handshake
3135
[^Session {session :session socket :socket :as s}]
32-
(handle-errors s (libssh2-session/handshake session socket)))
36+
(handle-errors s
37+
(with-timeout :session
38+
(libssh2-session/handshake session socket))))
3339

3440
(defn- session-id
3541
"Generate the session ID that will be used to pool"

test/clj_libssh2/test_error.clj

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,47 @@
3939
(is (thrown-with-msg? Exception
4040
#".+"
4141
(error/maybe-throw-error nil -10000)))))
42+
43+
(deftest with-timeout-works
44+
(let [test-func (fn [& args]
45+
(let [state (atom (vec args))]
46+
(fn []
47+
(when (empty? @state)
48+
(throw (Exception. "Called too many times!")))
49+
(let [result (first @state)
50+
new-state (rest @state)]
51+
(reset! state new-state)
52+
(if (instance? clojure.lang.Fn result)
53+
(result)
54+
result)))))]
55+
(testing "test-func behaves as expected"
56+
(let [f (test-func 1 (constantly 2) 3)]
57+
(is (= 1 (f)))
58+
(is (= 2 (f)))
59+
(is (= 3 (f)))
60+
(is (thrown? Exception (f)))))
61+
(testing "with-timeout doesn't retry successful function calls"
62+
(let [f (test-func 0)]
63+
(is (= 0 (error/with-timeout 10000 (f))))))
64+
(testing "with-timeout doesn't retry failed function calls"
65+
(let [f (test-func libssh2/ERROR_ALLOC)]
66+
(is (= libssh2/ERROR_ALLOC (error/with-timeout 10000 (f))))))
67+
(testing "with-timeout retries when it sees EAGAIN"
68+
(let [f (test-func libssh2/ERROR_EAGAIN 0)]
69+
(is (= 0 (error/with-timeout 10000 (f))))))
70+
(testing "with-timeout doesn't retry exceptions"
71+
(let [f (test-func #(throw (Exception. "")) 0)]
72+
(is (thrown-with-msg? Exception #"" (error/with-timeout 10000 (f))))
73+
(is (= 0 (error/with-timeout 10000 (f))))))
74+
(testing "with-timeout obeys the timeout"
75+
(let [f (test-func #(do (Thread/sleep 20)
76+
libssh2/ERROR_EAGAIN)
77+
0)]
78+
(is (thrown? Exception (error/with-timeout 10 (f))))))
79+
(testing "with-timeout can deal with fast-returning functions."
80+
(let [f (constantly libssh2/ERROR_EAGAIN)]
81+
(is (thrown? Exception (error/with-timeout 100 (f))))))
82+
(testing "with-timeout can use symbolic times"
83+
(with-redefs [error/timeouts (atom {:sym 5000})]
84+
(let [f (test-func libssh2/ERROR_EAGAIN 1)]
85+
(is (= 1 (error/with-timeout :sym (f)))))))))

0 commit comments

Comments
 (0)